Webhooks
Instead of polling, register an HTTPS endpoint in the
dashboard and OpenIntents will POST an event every
time an intent changes status.
Events
| Event | Sent when |
|---|---|
intent.running | A browser agent has started. |
intent.requires_payment | Paused at checkout; data.payment.payment_url is set. |
intent.completed | Paid and confirmed; data.result is set. |
intent.failed | The intent could not be completed. |
intent.cancelled | The intent was cancelled or its payment link expired. |
Payload
json
{
"id": "evt_91c2",
"type": "intent.completed",
"created_at": "2026-10-01T08:31:12Z",
"data": {
"id": "int_7f3a2c",
"object": "intent",
"status": "completed",
"result": { "merchant": "Blue Bottle Coffee", "total": 550, "fee": 28, "currency": "usd" }
}
}Respond with any 2xx within 10 seconds. Failed deliveries are retried with
exponential backoff for 24 hours. Events can arrive more than once and out of
order: de-duplicate on id and trust data.status.
Verifying signatures
Each request carries an OpenIntents-Signature header:
text
OpenIntents-Signature: t=1790841072,v1=5f8e3a…v1 is an HMAC-SHA256, keyed with your endpoint's whsec_ secret, of
{t}.{raw request body}. Reject requests whose signature doesn't match or
whose t is more than 5 minutes old.
ts
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, "hex"));
}