Webhooks
Receiver examples
Implementation examples for verifying and storing webhook deliveries.
Receiver examples
Examples below show safe minimum flow.
Node example
import { createHmac, timingSafeEqual } from 'node:crypto'
import express from 'express'
const app = express()
app.use('/webhooks/openpoly', express.raw({ type: 'application/json' }))
const processedEventIds = new Set<string>()
const secret = process.env.OPENPOLY_WEBHOOK_SECRET!
const maxSkewMs = 5 * 60_000
function sign(timestamp: string, rawBody: Buffer) {
const payload = `${timestamp}.${rawBody.toString('utf8')}`
const digest = createHmac('sha256', secret).update(payload).digest('hex')
return `v1=${digest}`
}
app.post('/webhooks/openpoly', async (req, res) => {
const eventId = String(req.header('x-polynion-event-id') || '')
const timestamp = String(req.header('x-polynion-timestamp') || '')
const signature = String(req.header('x-polynion-signature') || '')
const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.from('')
if (!eventId || !timestamp || !signature) {
return res.status(401).json({ error: 'MISSING_SIGNATURE_HEADERS' })
}
const parsedTs = Date.parse(timestamp)
if (!Number.isFinite(parsedTs) || Math.abs(Date.now() - parsedTs) > maxSkewMs) {
return res.status(401).json({ error: 'TIMESTAMP_SKEW_TOO_LARGE' })
}
const expected = sign(timestamp, rawBody)
const expectedBuf = Buffer.from(expected)
const gotBuf = Buffer.from(signature)
if (expectedBuf.length !== gotBuf.length || !timingSafeEqual(expectedBuf, gotBuf)) {
return res.status(401).json({ error: 'INVALID_SIGNATURE' })
}
const payload = JSON.parse(rawBody.toString('utf8'))
if (payload.event_id !== eventId) {
return res.status(400).json({ error: 'EVENT_ID_MISMATCH' })
}
if (processedEventIds.has(eventId)) {
return res.status(409).json({ error: 'DUPLICATE_EVENT_ID' })
}
await persistIncomingEvent(payload)
await applyBusinessSideEffects(payload)
processedEventIds.add(eventId)
return res.status(200).json({ ok: true })
})
Replace in-memory dedupe with DB table or durable cache.
Generic pseudocode
read raw_body
read event_id, timestamp, signature headers
if any missing -> 401
if timestamp older/newer than allowed skew -> 401
expected = hmac_sha256_hex(secret, "{timestamp}.{raw_body}")
if "v1={expected}" != signature using constant-time compare -> 401
payload = parse_json(raw_body)
if payload.event_id != header event_id -> 400
if event_id already processed -> 409
begin durable transaction
store inbound event log
apply business effect
mark event_id processed
commit
return 200
Response behavior
- return
200or other2xxafter durable success - return
401for missing or bad signature - return
409for duplicate already-processed event - return
500for transient internal failure -> OpenPoly retries
Common mistakes
- parsing JSON before reading raw body
- rebuilding JSON string before signature verification
- deduping by
aggregate_idinstead ofevent_id - returning
200before durable commit - deleting dedupe record too early
