# Webhooks

GetItDone outbound webhooks (Pro+): the task event catalog, Standard Webhooks signature verification, delivery retries, and secret rotation.

Register an HTTPS endpoint and receive signed events when tasks change.
Available on **Pro and Team** plans.

## Event types

These event types are exactly what the delivery worker emits:

- `task.created`
- `task.updated`
- `task.archived`

## Example signed delivery

```http
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](https://www.standardwebhooks.com/)
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.

```ts title="With the TypeScript SDK"
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 (published on npm —
[source](https://github.com/DevinoSolutions/getitdone-sdk)).

```js title="Runtime-agnostic (HMAC-SHA256)"
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.

A disabled endpoint receives nothing, whether it was disabled automatically,
by you through the API or the console, or paused because the account that set
it up had its email address verified (`disabled_reason` says which).
Deliveries already queued for it are held, not sent, and resume only if you
re-enable the endpoint (a take-back pause sets its queued deliveries aside for
good instead).

## 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`](/docs/api/problems/webhook-url-rejected).
