Docs menu

Docs Tutorials / Recall alerts with webhooks

Recall alerts with webhooks

Register an HTTPS address, tell RecallDB what to watch, and check the signature on every message you receive.

You need a free key (get one; it takes about a minute) in the environment variable RECALLDB_KEY. Keep the key on a server or your own machine, never in a web page.

On a free key you get 1 watchlist of up to 10 values and 1 webhook, and messages are sent together once an hour (each still its own signed POST). Delivery within minutes is planned for Pro.

1. Register, watch, test

webhook-setup.sh
# 1. Register your HTTPS address. The answer includes the signing secret, shown only once.
curl -X POST https://recalldb.app/v1/webhooks \
  -H "Authorization: Bearer $RECALLDB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/recalls"}'

# 2. Watch some makes and brands (a free key: 1 watchlist of up to 10 values). Use the webhook id from step 1.
curl -X POST https://recalldb.app/v1/watchlists \
  -H "Authorization: Bearer $RECALLDB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Our fleet and brands", "makes": ["Ford", "Toyota"], "brands": ["Graco"], "webhook_id": "wh_REPLACE_ME"}'

# 3. Ask for a test message, then look at the delivery log.
curl -X POST https://recalldb.app/v1/webhooks/wh_REPLACE_ME/test -H "Authorization: Bearer $RECALLDB_KEY"
curl https://recalldb.app/v1/webhooks/wh_REPLACE_ME/deliveries -H "Authorization: Bearer $RECALLDB_KEY"

Keep the secret from step 1: it is shown only once. Test messages are sent within about 5 minutes.

2. Receive and verify

Every message has X-RecallDB-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your secret. Check it on the raw body before parsing, and refuse old timestamps.

webhook-receiver.mjs
// A minimal receiver for RecallDB webhooks that checks the signature before trusting a message.
//   WEBHOOK_SECRET=whsec_... node webhook-receiver.mjs      (listens on port 8787; put it behind HTTPS)
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'

const SECRET = process.env.WEBHOOK_SECRET
const TOLERANCE_SECONDS = 300 // refuse messages older than 5 minutes (replays)

export function verify(rawBody, header, secret, now = Date.now()) {
  // X-RecallDB-Signature looks like: t=1763035200,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
  const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!t || !parts.v1 || Math.abs(now / 1000 - t) > TOLERANCE_SECONDS) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

if (process.argv[1]?.endsWith('webhook-receiver.mjs')) {
  createServer((req, res) => {
    let raw = ''
    req.on('data', (chunk) => (raw += chunk))
    req.on('end', () => {
      if (!verify(raw, req.headers['x-recalldb-signature'], SECRET)) {
        res.writeHead(401).end('bad signature')
        return
      }
      const event = JSON.parse(raw)
      if (event.type === 'test') console.log('Test message received:', event.id)
      else {
        const { recall, matched } = event.data
        console.log(`${event.type}: ${recall.title} (${recall.source.agency} ${recall.source.official_id})`)
        console.log(`  matched: ${matched.flatMap((m) => m.terms.map((t) => `${t.kind} ${t.value}`)).join(', ')}`)
        console.log(`  official notice: ${recall.source.official_url}`)
      }
      res.writeHead(200).end('ok') // answer 2xx quickly; slow work belongs in a queue
    })
  }).listen(8787, () => console.log('Listening on http://localhost:8787'))
}

3. Handle retries

  • Answer 2xx within 10 seconds. Anything else is retried up to 6 times (after 1, 5, 30, 120 and 360 minutes).
  • The same event can arrive twice: use the event id to skip ones you already handled.
  • A match means the notice mentions your value. It does not say your own item is affected; link the official notice.

Reference: webhooks, watchlists, delivery log.