Webhooks
Webhooks let you receive real-time HTTP POST notifications when events occur in your project. Configure webhook endpoints in the dashboard under Project → Webhooks, or manage them programmatically with the API/SDK (requires the webhooks:manage scope): GET /api/v1/webhooks, POST /api/v1/webhooks, and DELETE /api/v1/webhooks/:id (SDK: helmdesk.webhooks.list() / .create() / .delete()). The signing secret is returned once, on create. Webhooks are available on Starter and up; on the Free plan the create call returns a 403.
Which project?
Every webhook belongs to one project. A key limited to one project implies it — send nothing extra. A key that reaches several projects (the kind you give a coding agent) names the project on each request with the Helmdesk-Project header (slug or id) or ?project=; without it the API answers 400 project.required with the projects it could have meant. Every object in a response carries projectId and projectSlug, so you can always tell what you are looking at.
curl https://helmdesk.dev/api/v1/webhooks \
-H "Authorization: Bearer sk_live_..." \
-H "Helmdesk-Project: ledgerly"// Pin the project once — or set HELMDESK_PROJECT and omit it.
const helmdesk = new HelmdeskClient({ apiKey, project: 'ledgerly' })
await helmdesk.webhooks.list()See Authentication for how keys and projects fit together.
Available events
| Event | Fired when |
|---|---|
ticket.created | A new ticket is submitted (via portal, API, or SDK) |
ticket.resolved | A ticket is marked as resolved |
message.received | A new message is added to a ticket |
feedback.submitted | A customer submits a post-resolution satisfaction rating |
email.bounced | A transactional email bounces (recipient is auto-suppressed) |
customer.created | A customer enters the directory (via API, or their first ticket/feedback) |
customer.updated | A customer's profile is updated |
Payload format
Each webhook delivery sends a JSON POST request with the following structure:
{
"event": "ticket.created",
"timestamp": "2025-01-15T10:30:00.000Z",
"data": {
"ticket": {
"id": "uuid",
"number": 42,
"subject": "Can't reset password",
"status": "open",
"priority": "high"
},
"customer": {
"email": "user@example.com",
"name": "Jane Smith"
}
}
}Headers
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Helmdesk-Event | The event type (e.g. ticket.created) |
X-Helmdesk-Signature | HMAC-SHA256 signature of the request body |
Verifying signatures
Every delivery is signed with your webhook secret using HMAC-SHA256. Always verify the signature to ensure the request came from Helmdesk.
import { createHmac } from 'crypto'
function verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
const expected = createHmac('sha256', secret)
.update(body)
.digest('hex')
return signature === expected
}
// Express / Next.js API route example
export async function POST(req: Request) {
const body = await req.text()
const signature = req.headers.get('x-helmdesk-signature') ?? ''
if (!verifyWebhookSignature(body, signature, WEBHOOK_SECRET)) {
return new Response('Invalid signature', { status: 401 })
}
const payload = JSON.parse(body)
switch (payload.event) {
case 'ticket.created':
// Handle new ticket
break
case 'ticket.resolved':
// Handle resolved ticket
break
case 'message.received':
// Handle new message
break
}
return new Response('OK', { status: 200 })
}Slack integration
Enable Slack format when creating a webhook to receive payloads formatted as Slack Block Kit messages. Point the webhook URL at a Slack incoming webhook to get formatted notifications in your channel.
// Slack-formatted payload example
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*New Ticket Created*\n> *Subject:* Can't reset password\n> *#42*\n> *Customer:* user@example.com\n> *Priority:* high"
}
}
]
}Managing endpoints over the API
Everything the dashboard's Webhooks page does is available to a key with the webhooks:manage scope — including the two questions that come up after you register an endpoint: is it receiving? and what did it receive?
Update an endpoint
Change the URL, the subscribed events (the full new list, not a diff), the active flag, or Slack formatting — at least one field. Setting active: false pauses deliveries without losing the endpoint or its secret. The secret itself cannot be changed: delete and recreate for a new one.
curl -X PATCH https://helmdesk.dev/api/v1/webhooks/<webhookId> \
-H "Authorization: Bearer sk_live_..." \
-H "Helmdesk-Project: ledgerly" \
-H "Content-Type: application/json" \
-d '{"events": ["ticket.created", "customer.created"], "active": true}'// SDK — the response is the endpoint without its secret
const webhook = await helmdesk.webhooks.update(webhookId, { active: false })Test an endpoint
Sends a synthetic ticket.created payload (ticket number 0, an all-zero id, data.test: true) to this one endpoint and waits for the answer — the same delivery the dashboard's Test button makes. It goes out regardless of the endpoint's active flag or subscriptions, and is recorded like any other delivery.
curl -X POST https://helmdesk.dev/api/v1/webhooks/<webhookId>/test \
-H "Authorization: Bearer sk_live_..." \
-H "Helmdesk-Project: ledgerly"// SDK
const result = await helmdesk.webhooks.test(webhookId)
// {
// success: true, // a 2xx response
// responseStatus: 200, // null when nothing answered (timeout, DNS, refused)
// responseBody: "ok", // the endpoint's body or the transport error, ≤ 1 KB
// durationMs: 184,
// deliveryId: "…", // the row in the deliveries list
// event: "ticket.created", webhookId, url, projectId, projectSlug
// }Recent deliveries
The endpoint's delivery log, newest first — every attempt with its event, success flag, HTTP status, response body, and time. Default 20, max 100. Filter with ?success=false to see only failures; add ?includePayload=true to see exactly what was sent.
curl "https://helmdesk.dev/api/v1/webhooks/<webhookId>/deliveries?success=false&limit=10" \
-H "Authorization: Bearer sk_live_..." \
-H "Helmdesk-Project: ledgerly"// SDK
const { deliveries } = await helmdesk.webhooks.deliveries(webhookId, {
success: false,
includePayload: true,
})
// deliveries[0] -> { id, event, success, responseStatus, responseBody, attemptCount, deliveredAt, payload }Delivery behavior
- • Webhooks are delivered asynchronously (fire-and-forget) and do not block API responses
- • Deliveries time out after 5 seconds
- • All deliveries (successes and failures) are logged — visible in the dashboard and via
GET /webhooks/:id/deliveries - • Use the Test button in the dashboard, or
POST /webhooks/:id/test, to send a sample delivery to your endpoint