Webhooks
Webhooks
Receive events when orders, payments, refunds and payouts change, verify signatures, and handle retries safely.
Webhooks tell your server when something happens in your account: an order is paid, a refund succeeds, a payout fails. Use them to fulfil orders and update your records. Don't rely on browser redirects, which shoppers can close.
This is preview documentation for a prototype. Endpoints, fields and keys are illustrative.
Register an endpoint
Add an endpoint in the dashboard under Developers → Webhooks, or with the API:
curl https://api.international-orders.com/v1/webhook_endpoints \
-u sk_test_51Hx...: \
-d url="https://example.com/webhooks/io" \
-d "events[]=order.paid" \
-d "events[]=refund.succeeded" \
-d "events[]=payout.failed"
The response includes a signing secret starting with whsec_. Store it with your other secrets.
The event object
Every event has the same shape. The changed object is in data.object.
{
"id": "evt_1Nb7cY4fH8qA",
"type": "order.paid",
"created": "2026-10-01T09:14:22Z",
"livemode": false,
"data": {
"object": {
"id": "ord_7Hc2kQ9xL4mP",
"object": "order",
"status": "paid",
"amount": 12900,
"currency": "EUR"
}
}
}
Common events
| Event | When it's sent |
|---|---|
order.paid | The order's payment is captured. Fulfil now |
order.cancelled | The order was cancelled |
payment.authorized | Funds are held (manual capture) |
payment.failed | A payment attempt failed |
refund.succeeded | The network confirmed a refund |
dispute.opened | A shopper disputed a payment |
payout.paid | A payout reached the bank |
payout.failed | A payout was rejected |
Browse every event and its payload in the webhook explorer.
Verify signatures
Each request carries an IO-Signature header with a timestamp and an HMAC-SHA256 signature of the raw body. Verify it before you trust the event.
import { InternationalOrders } from "@international-orders/node";
const io = new InternationalOrders(process.env.IO_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("io-signature") ?? "";
let event;
try {
event = io.webhooks.verify(body, signature, process.env.IO_WEBHOOK_SECRET!);
} catch {
return new Response("Invalid signature", { status: 400 });
}
if (event.type === "order.paid") {
await fulfil(event.data.object.id);
}
return new Response(null, { status: 200 });
}
Use the raw request body. Parsing and re-serialising JSON changes the bytes and breaks the signature. The SDK also rejects events more than five minutes old, which protects against replays.
Retries
Respond with a 2xx status within 10 seconds. Anything else counts as a failure, and we retry with exponential backoff for up to three days.
| Attempt | Delay after previous |
|---|---|
| 2 | 1 minute |
| 3 | 10 minutes |
| 4 | 1 hour |
| 5 onwards | Every 6 hours, until 3 days have passed |
If an endpoint keeps failing, we email you and eventually disable it.
Handle events safely
- Return quickly. Acknowledge the event, then do slow work in a background job.
- Expect duplicates. Retries can deliver the same event twice. Store
evt_IDs you've processed and skip repeats. - Don't assume order. Events can arrive out of sequence. If you need the latest state, retrieve the object from the API.
- Filter. Subscribe only to the events you use.
Test locally
Send a test event to any endpoint from the dashboard, or forward events to your machine with the CLI:
io listen --forward-to localhost:3000/webhooks/io
io trigger order.paid