Skip to content

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
curl https://helmdesk.dev/api/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: ledgerly"
SDK
// 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

EventFired when
ticket.createdA new ticket is submitted (via portal, API, or SDK)
ticket.resolvedA ticket is marked as resolved
message.receivedA new message is added to a ticket
feedback.submittedA customer submits a post-resolution satisfaction rating
email.bouncedA transactional email bounces (recipient is auto-suppressed)
customer.createdA customer enters the directory (via API, or their first ticket/feedback)
customer.updatedA 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:

HeaderDescription
Content-TypeAlways application/json
X-Helmdesk-EventThe event type (e.g. ticket.created)
X-Helmdesk-SignatureHMAC-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