Skip to content
Docs
opendesk.shOpen app

Webhooks

Receive signed HTTP callbacks for opendesk events, verify them and handle retries.

A webhook sends an HTTP POST to your endpoint each time an event happens in your workspace. Use webhooks to sync tickets to a warehouse, notify a chat tool, or trigger your own workflows.

In Settings → Webhooks (admin), or with the API, add:

  • a URL. It must be HTTPS, and must not resolve to a private, loopback or link-local address.
  • the events to receive, or * for all of them
  • an optional description

The signing secret is shown once, when the endpoint is created. It is encrypted at rest.

Send a test event with Send test or POST /v1/webhooks/{id}/test. It delivers a webhook.test event.

The body is the event envelope, without topics:

{
"id": "0192f0c4-8f3a-7c1e-9a4e-3c1d2e5f6a7b",
"seq": 1042,
"workspace_id": "0192f0aa-...",
"type": "ticket.created",
"visibility": "agents",
"data": { "ticket": { "id": "...", "number": 1042, "subject": "Can't sign in", "status": "open" } },
"occurred_at": "2026-10-11T09:00:00Z"
}

data holds the same objects the REST API returns. See the events catalog for every type.

Header Value
OD-Signature t=<unix seconds>,v1=<hex signature>
OD-Event-Id The event id. Use it to deduplicate.
OD-Event-Type The event type, such as ticket.created.
Content-Type application/json

The signature is HMAC-SHA256 of <t>.<body> with your endpoint secret, hex encoded:

v1 = hex( HMAC-SHA256( key = secret, message = t + "." + raw_body ) )

To verify:

  1. Read the raw request body, before any JSON parsing.
  2. Split OD-Signature on , and read t and v1.
  3. Compute the expected signature and compare it with v1 in constant time.
  4. Reject the request if t is too far from your clock. We recommend a tolerance of 5 minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
const TOLERANCE_SECONDS = 300;
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
const t = Number(parts.t);
if (!parts.v1 || !Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
app.post('/hooks/opendesk', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body.toString('utf8'), req.get('OD-Signature') ?? '', process.env.OPENDESK_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200); // acknowledge first, then do the work
handle(event);
});
  • Respond with any 2xx status to acknowledge a delivery. Respond quickly and do slow work in the background.
  • Any other status, a timeout or a connection error counts as a failure, and the delivery is retried with exponential backoff.
  • Each delivery records its status (pending, succeeded, failed), number of attempts, response code and duration. List them with GET /v1/webhooks/{id}/deliveries or in the webhook’s settings page.
  • Deliveries can arrive more than once, for example after a retry. Deduplicate on OD-Event-Id.
  • Deliveries can arrive out of order. Every event carries seq, which is contiguous per workspace, so you can order them, and occurred_at.
  • If you need to recover events your endpoint missed, use GET /v1/events?since=<seq>, which keeps events for 7 days.

Set enabled: false to pause an endpoint without deleting it.

Edit this page on GitHub