Skip to Content

Webhooks (Receiving)

When events occur in your SupDesk project, outbound webhooks deliver HTTP POST requests to your configured endpoints. This page documents the payload format, signing mechanism, and retry behavior so you can build reliable webhook receivers.

Payload format

Every webhook delivery uses a standard envelope:

{ "event": "post.created", "timestamp": "2026-07-15T12:00:00.000Z", "project_id": "550e8400-e29b-41d4-a716-446655440000", "data": { ... } }
FieldTypeDescription
eventstringThe event that triggered the delivery
timestampstringISO 8601 timestamp of when the event occurred
project_idstringUUID of the project where the event occurred
dataobjectEvent-specific payload (see below)

Events

post.created

A new feedback post was submitted.

{ "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

A feedback post was edited.

{ "event": "post.updated", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Add dark mode (updated)", "type": "feature", "status": "open", "author_type": "end_user", "body": "Updated description.", "created_at": "2026-07-15T12:00:00Z" } }

post.deleted

A feedback post was 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

A post’s status was 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

A new comment was added to a 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

A comment was edited.

{ "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": "Updated comment.", "created_at": "2026-07-15T12:00:00Z" } }

comment.deleted

A comment was 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

A new private message was sent.

{ "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

A private message was edited.

{ "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": "Updated message.", "via": "web", "created_at": "2026-07-15T12:00:00Z" } }

message.deleted

A private message was 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

A beta tester submitted feedback on a beta program.

{ "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 may be null.

beta_feedback.deleted

A beta feedback item was 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

A visitor joined the waitlist.

{ "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 is null for invited and joined entries.

waitlist_signup.invited

A waitlist entry was invited off the list.

{ "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

An invited entry accepted and 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

A CSAT satisfaction survey was completed with a rating.

{ "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 is an integer between 1 and 5; comment may be null.

Signature verification

Each delivery includes an X-SupDesk-Signature header:

X-SupDesk-Signature: sha256=a1b2c3d4e5f6...

The value after sha256= is the hex-encoded HMAC-SHA256 of the raw request body, computed using your webhook’s signing secret.

Verification steps

  1. Read the raw request body (do not parse JSON first)
  2. Compute HMAC-SHA256(signing_secret, raw_body)
  3. Compare the hex digest with the value in the signature header
  4. Use constant-time comparison to prevent timing attacks

Node.js example

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 example

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 example

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

Retries

When a webhook delivery fails, SupDesk retries with exponential backoff:

AttemptDelay
1Immediate
21 second
32 seconds

Retried conditions: 5xx responses, network errors, timeouts (10 seconds).

Not retried: 4xx responses (client errors indicate a configuration problem).

Responding to webhooks

  • Return a 2xx status code to acknowledge receipt
  • Respond within 10 seconds to avoid timeout retries
  • Process the event asynchronously if your logic takes longer
  • Return the same response for duplicate deliveries (idempotency)
Last updated on