Webhooks deliver reports to any endpoint you control. Where integrations cover the tools we support, webhooks cover everything else.
Webhooks require a Pro or Ultra plan.
Events
Event | Fires when |
|---|---|
| A new report is submitted |
One event today, and the one that matters — the moment a bug enters your system is the moment you want to act.
Creating a webhook
In your project, go to Webhooks and add one. You provide the destination URL and select the events to subscribe to. Creating and managing webhooks requires OWNER or ADMIN.
Each webhook gets its own signing secret, generated when you create it. Copy it then — afterwards it is displayed masked (a1b2****f9e8).
Your endpoint must accept POST and should respond quickly.
What we send
A POST with a JSON body and these headers:
Header | Value |
|---|---|
|
|
|
|
| the event name, e.g. |
We wait up to 30 seconds for a response. Any 2xx counts as success; anything else is a failure and will be retried.
The body wraps the event's own payload in an envelope:
{
"event": "report.created",
"timestamp": "2026-09-09T12:34:56.789Z",
"data": {
"reportId": "6f1c…",
"projectId": "03bc…",
"projectSlug": "your-project",
"taskId": "284e…",
"taskPublicId": 42,
"publicId": 42,
"taskKey": "BD-42",
"priority": "high",
"title": "Checkout total is wrong",
"description": "…",
"reporterName": "…",
"reporterEmail": "…",
"pageUrl": "https://example.com/checkout",
"createdAt": "2026-09-09T12:34:56.000Z"
}
}
taskPublicId is the task's number within its project, and publicId is the same value under its original name. taskKey carries the display form, BD-42, built from the project's key. All three are null when a report did not reach a task.
Task numbers count per project, so a number is only meaningful together with projectSlug or projectId. See Task IDs and project keys.
Verifying the signature
Your endpoint is a public URL, so anyone can POST to it. Always verify the signature before acting on a request.
The signature is an HMAC-SHA256 of the raw request body, keyed with your webhook's secret, hex-encoded and prefixed with sha256=.
import crypto from 'node:crypto';
function verify(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader ?? '');
const computed = Buffer.from(expected);
return (
received.length === computed.length &&
crypto.timingSafeEqual(received, computed)
);
}
Two details matter:
Use the raw body, exactly as received. If your framework parses JSON before you see it, re-serializing produces different bytes and the signature will not match. In Express, use express.raw({ type: 'application/json' }) on this route.
Compare in constant time with timingSafeEqual rather than ===, so the comparison does not leak information about the expected value.
Reject anything that fails verification. An unverified handler is a way for a stranger to create tickets in your tracker.
Delivery and retries
Deliveries run through a background queue rather than inline with the report submission. Two consequences worth relying on:
A slow endpoint never slows the widget. The report is accepted immediately regardless of how long your server takes.
Failed deliveries are retried — up to 5 attempts with exponential backoff. A brief outage on your side does not silently drop events.
Design your handler to be idempotent. A retry after a timeout means you may receive the same event twice, and the correct response is to notice and ignore the duplicate rather than create a second ticket.
Delivery attempts are recorded, so you can see the response status and body we received for each one.
Rotating a secret
A webhook's secret can be rotated. Doing so invalidates the old one immediately — deploy the new secret to your endpoint promptly, or verification will start failing.
Rotate if the secret may have been exposed, for example in a log or a committed config file.
Disabling without deleting
A webhook can be made inactive. Inactive webhooks are skipped at delivery time, which is a cleaner way to pause during maintenance than deleting and recreating (recreating issues a new secret).
Good uses
Route by severity. Parse the console log for uncaught exceptions and page on-call for those, while everything else goes to a channel.
Enrich from your own data. The report includes the reporter's email when they provided one. Look it up and attach their plan, account age, or support history before creating a ticket.
Deduplicate your own way. Fingerprint on the top stack frame and increment a counter on an existing ticket instead of opening a new one.
Post somewhere unsupported. Any internal tool with an HTTP API becomes a destination.
Troubleshooting
Nothing is delivered. Confirm the plan supports webhooks, the webhook is active, and the URL is publicly reachable — localhost will not work from our servers. Use a tunneling tool for local development.
Signature verification always fails. Almost always the raw-body problem above. Confirm you are hashing the exact bytes received, not a re-serialized object.
Duplicates arrive. Expected on retry. Make the handler idempotent.
Deliveries stop after a while. Persistent failures from your endpoint are the usual cause. Check that it returns a 2xx promptly and does not exceed the 30-second timeout.
See also
Connecting integrations
Task IDs and project keys
Managing API keys safely