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 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' })
}
const result = await processWebhookTransaction(async (tx) => {
const inserted = await tx.insertInboxEventIfAbsent({
eventId,
eventType: String(payload.event_type),
rawBody,
})
if (!inserted) return 'duplicate'
await applyBusinessSideEffects(tx, payload)
return 'processed'
})
return res.status(200).json({ ok: true, duplicate: result === 'duplicate' })
})
insertInboxEventIfAbsent must be backed by a unique database constraint on event_id, and it must run in the same transaction as the business effect. A duplicate is already durably handled, so acknowledge it with 2xx.
The same receiver handles both product families: branch on payload.event_type after signature verification, preserve camelCase keys inside Polyflight payload.data, and never trigger another wallet debit, payout, refund, or compensation from a retried callback. For *.pending, record an obligation, not confirmed cash; *.processed and *.compensated describe already-completed movements. feeMinor is the included purchase fee, whereas refund and compensation reverse the full stake.
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
begin durable transaction
inserted = insert inbound event_id under unique constraint
if not inserted:
commit
return 200 without applying effect
apply business effect
mark event_id processed
commit
return 200
Response behavior
- return
200or other2xxafter durable success - return
401for missing or bad signature - return
200or another2xxfor an already-committed duplicate - 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
- treating a reconstructed dashboard envelope or pretty-printed JSON as the original signed bytes
- returning
409for duplicates (any non-2xxis retried, including409)
