Webhooks (Recepción)
Cuando ocurren eventos en su proyecto de SupDesk, los webhooks salientes envían solicitudes HTTP POST a sus endpoints configurados. Esta página documenta el formato del payload, el mecanismo de firma y el comportamiento de reintentos para que pueda crear receptores de webhooks confiables.
Formato del payload
Cada entrega de webhook usa un sobre estándar:
{
"event": "post.created",
"timestamp": "2026-07-15T12:00:00.000Z",
"project_id": "550e8400-e29b-41d4-a716-446655440000",
"data": { ... }
}| Campo | Tipo | Descripción |
|---|---|---|
event | string | El evento que desencadenó la entrega |
timestamp | string | Marca de tiempo ISO 8601 de cuándo ocurrió el evento |
project_id | string | UUID del proyecto donde ocurrió el evento |
data | object | Payload específico del evento (ver más abajo) |
Eventos
post.created
Se envió un nuevo post de feedback.
{
"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
Un post de feedback fue editado.
{
"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
Un post de feedback fue eliminado.
{
"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
El estado de un post fue cambiado.
{
"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
Se agregó un nuevo comentario a un post.
{
"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
Un comentario fue editado.
{
"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
Un comentario fue eliminado.
{
"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
Se envió un nuevo mensaje privado.
{
"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
Un mensaje privado fue editado.
{
"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
Un mensaje privado fue eliminado.
{
"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
Un tester beta envió feedback sobre un programa beta.
{
"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 puede ser null.
beta_feedback.deleted
Se eliminó un elemento de feedback de la beta.
{
"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
Un visitante se unió a la lista de espera.
{
"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 es null para las inscripciones invited y joined.
waitlist_signup.invited
Se invitó a una inscripción desde la lista de espera.
{
"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
Una inscripción invitada aceptó y se unió.
{
"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
Se completó una encuesta de satisfacción CSAT con una calificación.
{
"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 es un número entero entre 1 y 5; comment puede ser null.
Verificación de firmas
Cada entrega incluye un header X-SupDesk-Signature:
X-SupDesk-Signature: sha256=a1b2c3d4e5f6...El valor después de sha256= es el HMAC-SHA256 codificado en hexadecimal del
cuerpo raw de la solicitud, calculado usando el secreto de firma de su webhook.
Pasos de verificación
- Lea el cuerpo raw de la solicitud (no parsee JSON primero)
- Calcule
HMAC-SHA256(secreto_de_firma, cuerpo_raw) - Compare el digest hexadecimal con el valor en el header de firma
- Use una comparación de tiempo constante para prevenir ataques de temporización
Ejemplo en 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 });
});Ejemplo en 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), 200Ejemplo en 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
endReintentos
Cuando una entrega de webhook falla, SupDesk reintenta con backoff exponencial:
| Intento | Retraso |
|---|---|
| 1 | Inmediato |
| 2 | 1 segundo |
| 3 | 2 segundos |
Condiciones de reintento: respuestas 5xx, errores de red, timeouts (10 segundos).
No se reintenta: respuestas 4xx (los errores de cliente indican un problema de configuración).
Responder a webhooks
- Devuelva un código de estado 2xx para confirmar la recepción
- Responda dentro de 10 segundos para evitar reintentos por timeout
- Procese el evento de forma asíncrona si su lógica tarda más
- Devuelva la misma respuesta para entregas duplicadas (idempotencia)