Skip to main content

Webhooks

Receive signed HTTP callbacks when ingestion, detection, or research events complete.

Create a webhook endpoint#

Subscribe a URL to event types and get back a signing secret.

Webhooks are managed via REST today — there are no SDK helpers yet. The POST response includes the signing `secret` exactly once; capture it then and store it as an environment variable on the receiving service.

// POST /v1/orgs/:orgId/projects/:projectId/webhooks
const res = await fetch(
  `https://api.meetdewey.com/v1/orgs/${orgId}/projects/${projectId}/webhooks`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.DEWEY_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://api.example.com/hooks/dewey',
      events: ['document.ready', 'document.error', 'research.completed'],
      description: 'Production ingestion hook',
    }),
  },
)
const endpoint = await res.json()

// Store endpoint.secret immediately — it's only returned now.
console.log(endpoint.id, endpoint.secret)

Response

{
  "id": "whe_abc123",
  "url": "https://api.example.com/hooks/dewey",
  "events": ["document.ready", "document.error", "research.completed"],
  "secret": "whsec_b7f2c1d4...e9a3",
  "description": "Production ingestion hook",
  "active": true,
  "createdAt": "2026-05-14T18:21:09.412Z"
}

See also

Verify webhook signatures#

Validate the X-Dewey-Signature header with a timing-safe HMAC comparison.

Every delivery carries an `X-Dewey-Signature` header — `sha256=<hex>` of the raw request body, computed with your endpoint's signing secret. Always use a constant-time comparison; a non-timing-safe `===` leaks information about partial matches. Two more useful headers ship alongside: `X-Dewey-Event-Id` (the delivery UUID) and `X-Dewey-Event-Type`.

import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyWebhook(
  rawBody: string,
  signature: string,
  secret: string,
): boolean {
  const expected =
    'sha256=' +
    createHmac('sha256', secret).update(rawBody).digest('hex')
  return (
    signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  )
}

// Express / Next.js example — use raw body, not parsed JSON
app.post(
  '/hooks/dewey',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['x-dewey-signature'] as string
    if (!verifyWebhook(req.body.toString(), sig, process.env.DEWEY_WEBHOOK_SECRET!)) {
      return res.sendStatus(401)
    }
    const event = JSON.parse(req.body.toString())
    // Ack fast, process async
    res.sendStatus(200)
    queue.enqueue(event)
  },
)

See also

Inspect and retry deliveries#

List recent deliveries, see response codes, and retry failed ones.

Dewey retries failed deliveries up to 10 times with exponential backoff. Anything still failing after the last attempt is parked in the deliveries list with the response body and status code captured. Manually retrying re-runs the delivery from scratch and resets the attempt count.

// GET /v1/orgs/:orgId/projects/:projectId/webhooks/:endpointId/deliveries
const res = await fetch(
  `https://api.meetdewey.com/v1/orgs/${orgId}/projects/${projectId}/webhooks/${endpointId}/deliveries`,
  { headers: { Authorization: `Bearer ${process.env.DEWEY_API_KEY}` } },
)
const deliveries = await res.json()

for (const d of deliveries) {
  console.log(d.id, d.status, d.attempts, '—', d.eventType)
}

// POST /v1/orgs/:orgId/projects/:projectId/webhooks/:endpointId/deliveries/:id/retry
await fetch(
  `https://api.meetdewey.com/v1/orgs/${orgId}/projects/${projectId}/webhooks/${endpointId}/deliveries/${deliveries[0].id}/retry`,
  {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.DEWEY_API_KEY}` },
  },
)