Вебхуки (Приём)
При возникновении событий в вашем проекте SupDesk исходящие вебхуки отправляют HTTP POST-запросы на ваши настроенные эндпоинты. Эта страница документирует формат полезной нагрузки, механизм подписи и поведение повторных попыток, чтобы вы могли создавать надёжные приёмники вебхуков.
Формат полезной нагрузки
Каждая доставка вебхука использует стандартную оболочку:
{
"event": "post.created",
"timestamp": "2026-07-15T12:00:00.000Z",
"project_id": "550e8400-e29b-41d4-a716-446655440000",
"data": { ... }
}| Поле | Тип | Описание |
|---|---|---|
event | string | Событие, вызвавшее доставку |
timestamp | string | Временная метка ISO 8601 момента возникновения события |
project_id | string | UUID проекта, в котором произошло событие |
data | object | Полезная нагрузка для конкретного события (см. ниже) |
События
post.created
Отправлен новый пост обратной связи.
{
"event": "post.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode",
"type": "feature",
"status": "open",
"author_type": "end_user",
"body": "It would be great to have a dark mode option.",
"created_at": "2026-07-15T12:00:00Z"
}
}post.updated
Пост обратной связи был отредактирован.
{
"event": "post.updated",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode",
"type": "feature",
"status": "open",
"author_type": "end_user",
"body": "It would be great to have a dark mode option.",
"created_at": "2026-07-15T12:00:00Z"
}
}post.deleted
Пост обратной связи был удалён.
{
"event": "post.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode",
"type": "feature",
"status": "open",
"author_type": "end_user",
"body": "It would be great to have a dark mode option.",
"created_at": "2026-07-15T12:00:00Z"
}
}post.status_changed
Статус поста был изменён.
{
"event": "post.status_changed",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode",
"type": "feature",
"old_status": "open",
"new_status": "planned",
"author_type": "member",
"body": "We're planning this for Q3.",
"created_at": "2026-07-15T12:00:00Z"
}
}comment.created
Добавлен новый комментарий к посту.
{
"event": "comment.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"post_id": "550e8400-e29b-41d4-a716-446655440001",
"post_title": "Add dark mode",
"author_type": "end_user",
"body": "Any update on this?",
"created_at": "2026-07-15T12:00:00Z"
}
}comment.updated
Комментарий был отредактирован.
{
"event": "comment.updated",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"post_id": "550e8400-e29b-41d4-a716-446655440001",
"post_title": "Add dark mode",
"author_type": "end_user",
"body": "Any update on this?",
"created_at": "2026-07-15T12:00:00Z"
}
}comment.deleted
Комментарий был удалён.
{
"event": "comment.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"post_id": "550e8400-e29b-41d4-a716-446655440001",
"post_title": "Add dark mode",
"author_type": "end_user",
"body": "Any update on this?",
"created_at": "2026-07-15T12:00:00Z"
}
}message.created
Отправлено новое приватное сообщение.
{
"event": "message.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"thread_id": "550e8400-e29b-41d4-a716-446655440001",
"thread_subject": "Help needed",
"sender": "end_user",
"body": "I can't find the settings page.",
"via": "web",
"created_at": "2026-07-15T12:00:00Z"
}
}message.updated
Приватное сообщение было отредактировано.
{
"event": "message.updated",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"thread_id": "550e8400-e29b-41d4-a716-446655440001",
"thread_subject": "Help needed",
"sender": "end_user",
"body": "I can't find the settings page.",
"via": "web",
"created_at": "2026-07-15T12:00:00Z"
}
}message.deleted
Приватное сообщение было удалено.
{
"event": "message.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"thread_id": "550e8400-e29b-41d4-a716-446655440001",
"thread_subject": "Help needed",
"sender": "end_user",
"body": "I can't find the settings page.",
"via": "web",
"created_at": "2026-07-15T12:00:00Z"
}
}beta_feedback.created
Бета-тестировщик отправил отзыв о бета-программе.
{
"event": "beta_feedback.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"program": "Acme 2.0 Beta",
"category": "bug",
"severity": "high",
"title": "Crash on export",
"body": "The app closes when I export a large file.",
"created_at": "2026-07-15T12:00:00Z"
}
}severity может быть null.
beta_feedback.deleted
Отзыв о бете был удалён.
{
"event": "beta_feedback.deleted",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"program": "Acme 2.0 Beta",
"category": "bug",
"severity": "high",
"title": "Crash on export",
"body": "The app closes when I export a large file.",
"created_at": "2026-07-15T12:00:00Z"
}
}waitlist_signup.created
Посетитель присоединился к списку ожидания.
{
"event": "waitlist_signup.created",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jamie@example.com",
"status": "waiting",
"position": 42,
"referral_count": 0,
"referral_code": "a1b2c3",
"source": "portal",
"created_at": "2026-07-15T12:00:00Z",
"invited_at": null,
"joined_at": null
}
}position равно null для записей invited и joined.
waitlist_signup.invited
Запись была приглашена из списка ожидания.
{
"event": "waitlist_signup.invited",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jamie@example.com",
"status": "invited",
"position": null,
"referral_count": 0,
"referral_code": "a1b2c3",
"source": "portal",
"created_at": "2026-07-15T12:00:00Z",
"invited_at": "2026-07-15T12:00:00Z",
"joined_at": null
}
}waitlist_signup.joined
Приглашённая запись приняла приглашение и присоединилась.
{
"event": "waitlist_signup.joined",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jamie@example.com",
"status": "joined",
"position": null,
"referral_count": 0,
"referral_code": "a1b2c3",
"source": "portal",
"created_at": "2026-07-15T12:00:00Z",
"invited_at": "2026-07-15T12:00:00Z",
"joined_at": "2026-07-15T12:05:00Z"
}
}csat_survey.completed
Опрос удовлетворённости CSAT завершён с оценкой.
{
"event": "csat_survey.completed",
"data": {
"thread_id": "550e8400-e29b-41d4-a716-446655440000",
"rating": 5,
"comment": "Support was fast and friendly.",
"created_at": "2026-07-15T12:00:00Z"
}
}rating — целое число от 1 до 5; comment может быть null.
Проверка подписей
Каждая доставка включает заголовок X-SupDesk-Signature:
X-SupDesk-Signature: sha256=a1b2c3d4e5f6...Значение после sha256= — это шестнадцатеричный HMAC-SHA256 от тела запроса,
вычисленный с использованием секрета подписи вашего вебхука.
Шаги проверки
- Прочитайте тело запроса (не парсите JSON сначала)
- Вычислите
HMAC-SHA256(секрет_подписи, тело_запроса) - Сравните шестнадцатеричный дайджест со значением в заголовке подписи
- Используйте сравнение постоянного времени для предотвращения атак по времени
Пример на Node.js
import { createHmac, timingSafeEqual } from "crypto";
function verifySupDeskWebhook(secret, rawBody, signatureHeader) {
const expected = createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = signatureHeader.replace("sha256=", "");
return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
// Express example
app.post("/webhooks/supdesk", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-supdesk-signature"];
if (!verifySupDeskWebhook(SECRET, req.body, sig)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
// Process event...
res.status(200).json({ ok: true });
});Пример на Python
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your-signing-secret"
def verify_signature(secret: str, body: bytes, header: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
received = header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
@app.route("/webhooks/supdesk", methods=["POST"])
def handle_webhook():
sig = request.headers.get("X-SupDesk-Signature", "")
if not verify_signature(WEBHOOK_SECRET, request.data, sig):
return jsonify(error="Invalid signature"), 401
event = request.get_json()
# Process event...
return jsonify(ok=True), 200Пример на Ruby
require "openssl"
require "sinatra"
require "json"
WEBHOOK_SECRET = "your-signing-secret"
def verify_signature(secret, body, header)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
received = header.sub("sha256=", "")
Rack::Utils.secure_compare(expected, received)
end
post "/webhooks/supdesk" do
body = request.body.read
sig = request.env["HTTP_X_SUPDESK_SIGNATURE"] || ""
unless verify_signature(WEBHOOK_SECRET, body, sig)
halt 401, { error: "Invalid signature" }.to_json
end
event = JSON.parse(body)
# Process event...
status 200
{ ok: true }.to_json
endПовторные попытки
При неудачной доставке вебхука SupDesk повторяет попытки с экспоненциальной задержкой:
| Попытка | Задержка |
|---|---|
| 1 | Немедленно |
| 2 | 1 секунда |
| 3 | 2 секунды |
Условия повторных попыток: ответы 5xx, сетевые ошибки, тайм-ауты (10 секунд).
Не повторяется: ответы 4xx (ошибки клиента указывают на проблему конфигурации).
Ответ на вебхуки
- Верните код статуса 2xx для подтверждения получения
- Отвечайте в течение 10 секунд, чтобы избежать повторных попыток из-за тайм-аута
- Обрабатывайте событие асинхронно, если ваша логика занимает больше времени
- Возвращайте тот же ответ для дублирующих доставок (идемпотентность)