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": { ... }
}| Field | Type | Description |
|---|---|---|
event | string | The event that triggered the delivery |
timestamp | string | ISO 8601 timestamp of when the event occurred |
project_id | string | UUID of the project where the event occurred |
data | object | Event-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
- Read the raw request body (do not parse JSON first)
- Compute
HMAC-SHA256(signing_secret, raw_body) - Compare the hex digest with the value in the signature header
- 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), 200Ruby 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
endRetries
When a webhook delivery fails, SupDesk retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 second |
| 3 | 2 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)