Webhooks
Register an HTTPS endpoint and receive signed events when tasks change. Available on Pro and Team plans.
Event types
These event types render from the shared contract — they are exactly what the delivery worker emits:
task.createdtask.updatedtask.archived
Example signed delivery
POST https://your-app.example.com/webhooks/getitdone
webhook-id: msg_2abc...
webhook-timestamp: 1753305600
webhook-signature: v1,g0hM9SsE9...base64signature...
{
"type": "task.created",
"data": {
"id": "T-123",
"title": "Ship the public API",
"status": "TODO"
}
}Verify the signature
Payloads are signed with the Standard Webhooks scheme. Each request carries webhook-id, webhook-timestamp and webhook-signature headers. Always verify the raw request bytes — parsing and re-serializing the body first breaks the signature.
import { verifyWebhook } from '@nowgetitdone/sdk/webhooks'
// In your route handler — pass the RAW request body bytes, never a re-parsed
// object (re-serializing breaks the signature).
const result = verifyWebhook({
headers: request.headers,
rawBody: await request.text(),
secret: process.env.GETITDONE_WEBHOOK_SECRET, // whsec_… (pass both during rotation)
})
if (!result.valid) {
return new Response('invalid signature', { status: 400 })
}The SDK ships a verifyWebhook helper (source on GitHub, npm release rolling out — source).
import { createHmac, timingSafeEqual } from 'node:crypto'
// Runtime-agnostic Standard Webhooks verification (no SDK required).
function verify(headers, rawBody, whsec) {
const id = headers['webhook-id']
const timestamp = headers['webhook-timestamp']
const signatureHeader = headers['webhook-signature'] // "v1,<base64> v1,<base64> …"
const secretBytes = Buffer.from(whsec.split('_')[1], 'base64')
const signed = `${id}.${timestamp}.${rawBody}`
const expected = createHmac('sha256', secretBytes).update(signed).digest('base64')
return signatureHeader
.split(' ')
.some(part => {
const sig = part.split(',')[1]
return (
sig !== undefined &&
sig.length === expected.length &&
timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
)
})
}Retries & auto-disable
Deliveries that fail (non-2xx, timeout, connection error) are retried with backoff. An endpoint that keeps failing is automatically disabled to protect your receiver and our delivery pipeline; re-enable it once your endpoint is healthy again. Read the delivery log per endpoint to see attempts and their outcomes.
Secret rotation
Each endpoint has a signing secret prefixed whsec_. Rotating it keeps the previous secret valid for a 24-hour grace window so in-flight deliveries still verify. Accept both secrets during rotation, then drop the old one after the window.
URL rules
Endpoint URLs must be https on port 443 and must not resolve to a private or internal address. A rejected URL answers 422 webhook_url_rejected.