Webhooks
Signed events posted to your server, with retries and a delivery log.
Add an endpoint in Settings, Webhooks, pick the events you want and copy its signing secret (whsec_...).
Events
| Event | Sent when |
|---|---|
conversation.created | A visitor or your API starts a conversation. |
conversation.assigned | Someone is assigned or unassigned. |
conversation.resolved | A conversation is resolved. |
message.created | A public message is sent by either side. Internal notes are never sent. |
csat.received | A visitor rates a resolved conversation. |
ticket.created | A bug report or request is filed. |
ticket.updated | Status, priority, type or assignee changes. |
ticket.commented | Someone comments on a ticket. |
feedback.created | Feedback or a rating arrives. |
feedback.updated | Feedback status, reply or tags change. |
contact.updated | A contact is created or their details change. |
The request
POST /your/endpoint
Content-Type: application/json
User-Agent: CorplandDesk-Webhooks/1
Desk-Event: ticket.created
Desk-Delivery: 6aca9f36b70fd74072119f84
Desk-Signature: t=1791664596,v1=5f1c...{
"id": "evt_...",
"type": "ticket.created",
"created": 1791664596,
"workspace_id": "6aca9f36b70fd74072119f84",
"data": { "_id": "...", "number": 12, "type": "bug", "title": "Export stuck at 99%" }
}Verify the signature
The signature is HMAC_SHA256(secret, "<t>.<raw body>") as hex. Use the raw body exactly as received, before any JSON parsing, and reject timestamps older than five minutes.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/webhooks/desk", express.raw({ type: "application/json" }), (req, res) => {
const header = String(req.header("Desk-Signature") || "");
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = crypto.createHmac("sha256", process.env.DESK_WEBHOOK_SECRET!).update(`${parts.t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const valid = parts.v1 && expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!fresh || !valid) return res.status(400).send("bad signature");
const event = JSON.parse(req.body.toString());
res.sendStatus(200);
handle(event);
});import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts.get("v1", ""))Retries
Answer with any 2xx within 10 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, six tries in all. Desk-Delivery stays the same across retries, so use it to ignore duplicates.
Every attempt is in the delivery log under the endpoint in Settings, Webhooks, with the status code and the first 500 characters of your response. Failed deliveries can be sent again from there, and Send a test posts a ping event.
In production, endpoints must use https and resolve to a public address.