Webhooks
Receive events such as reservation confirmations and delivery updates.
A reservation is not always confirmed at the moment you create it. Create reservation answers confirmed when the venue approves instantly, but it answers pending when the venue must approve, and that decision lands minutes or hours later. Chasing it by polling Get reservation burns calls and still lags behind. Webhooks invert the direction: the moment the state changes, APIFuse sends the event to your registered endpoint.
The same principle runs through the whole catalogue. Delivery orders move through kitchen and courier states, payments approve asynchronously, and a ride keeps updating while the car moves. Every one of these changes arrives as a push, in one envelope shape, signed the same way.
You register a single HTTPS endpoint in the APIFuse console and pick the event types it subscribes to. All subscribed events for the account arrive at that one endpoint, so you branch on the type field in the body instead of registering one URL per service. When registration completes, the console shows the endpoint secret exactly once. It starts with whsec_ and is what you use to verify signatures, so store it in the same secret store as your managed key.
Every delivery is a POST request with a JSON body, and the body always has the same shape.
{
"id": "evt_01J8QH5R7T",
"type": "reservation.confirmed",
"created_at": "2026-09-01T10:15:27Z",
"data": {
"object": {
"id": "rsv_01J8QDGT2K",
"status": "confirmed",
"restaurant_id": "res_01J8Q9W3TE",
"starts_at": "2026-09-02T19:00:00+09:00",
"party_size": 2
}
}
}data.object has exactly the shape the API returns for the resource. For reservation.confirmed it is the same reservation object that returns, field for field, as documented in the . You never need a second fetch to hydrate an event.
Event types follow the <domain>.<event> convention. The domain names the resource, not the service, so a reservation.confirmed from Catch Table and one from Tabelog share the same envelope structure. Representative events:
| Event | Fires when | Services |
|---|---|---|
reservation.confirmed | The venue confirms a reservation | , tabelog |
reservation.cancelled | The venue or the caller cancels a reservation | , tabelog |
waitlist.called | A queued party is called in | |
order.updated | A delivery order changes state | baemin |
payment.approved | A payment is approved | toss-pay |
ride.updated | A requested ride changes state | kakao-t |
Frism-Event-Id header, which stays stable across redeliveries.const seenEventIds = new Set<string>();
const queue: unknown[] = [];
export async function handleFrismEvent(request: Request): Promise<Response> {
const rawBody = await request.text();
// 1. Verify the signature first, always. The code lives in the signatures guide.
// 2. Dedupe on Frism-Event-Id: delivery is at-least-once.
const eventId = request.headers.get("Frism-Event-Id");
if (eventId === null) return new Response(null, { status: 400 });
if (seenEventIds.has(eventId)) return new Response(null, { status: 200 });
seenEventIds.add(eventId);
// 3. Enqueue only, then return 2xx right away. A worker does the heavy part.
queue.push(JSON.parse(rawBody));
return new Response(null, { status: 200 });
}Save the envelope example above to a file and you can rehearse the handler locally.
# See the local testing example in the signatures guide for computing a real signature value.
curl -X POST http://localhost:3000/webhooks/frism \
-H "Content-Type: application/json" \
-H "Frism-Event-Id: evt_01J8QH5R7T" \
-H "Frism-Signature: t=1788257727,v2=d7e16839f487db479fa277fb11c2d6139225ea837c672df8a68e0452fd7ea772" \
--data @reservation-confirmed.jsonEverything above rests on the assumption that the request really came from APIFuse. That assumption only holds when you of every payload, so read that guide before you deploy the endpoint. If you need a call that produces your first event, the reservation flow in the is a good fit.