Browse docs
previewThese docs describe OpenIntents as it is being built. Endpoints and payloads may change before launch.

Webhooks

Instead of polling, register an HTTPS endpoint in the dashboard and OpenIntents will POST an event every time an intent changes status.

Events

EventSent when
intent.runningA browser agent has started.
intent.requires_paymentPaused at checkout; data.payment.payment_url is set.
intent.completedPaid and confirmed; data.result is set.
intent.failedThe intent could not be completed.
intent.cancelledThe 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"));
}