Events and webhooks
Stores an event, signs and delivers it to every enabled endpoint subscribed to its type, retries failed deliveries within a finite policy, records every attempt, and supports replay.
What events and webhooks does
Stores an event, signs and delivers it to every enabled endpoint subscribed to its type, retries failed deliveries within a finite policy, records every attempt, and supports replay.
When to use events and webhooks
- Order and payment notifications
- Customer-facing webhooks
- Sending an agent's results to other systems
Authentication
Send an API key as a bearer token. Keys are shown once, belong in a server environment, and are bound to one environment: flw_test_ keys act in test and flw_live_ keys in live.
Authorization: Bearer flw_test_••••Endpoint
POST https://api.flowyn.dev/v1/eventsRequest
Send an Idempotency-Key so a retried request cannot create a duplicate. The body is JSON.
curl -X POST https://api.flowyn.dev/v1/events \
-H "Authorization: Bearer $FLOWYN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123-completed" \
-d '{
"type": "order.completed",
"payload": {
"order_id": "ord_123",
"total": 4900
}
}'Response
Flowyn returns HTTP 202. The response is abridged here; the OpenAPI document lists every field.
{
"id": "0f8e4a52-3c1d-4e7b-9a26-5d1b7c3e9f10",
"environment": "test",
"type": "order.completed",
"status": "accepted",
"attempt_count": 0,
"request_id": "req_5f2a9c1e7b3d4a6f8e0c2b1d",
"created_at": "2026-09-30T14:02:11Z"
}Lifecycle
POST /v1/eventsstores the event and returns HTTP 202.- Flowyn signs the exact body with each endpoint's secret and POSTs it. The response is attempt 1.
- A failed attempt schedules the next one within the policy.
- A 2xx marks the delivery
delivered. Attempts stay attached to the event. POST /v1/events/{id}/replaysends it again as a new delivery without touching history.
Status values
| Status | Meaning |
|---|---|
| accepted | Stored, no delivery yet. |
| delivering | Delivery is in progress or a retry is scheduled. |
| delivered | A 2xx was received. |
| failed | Every attempt failed. Replay is available. |
| cancelled | Cancelled before delivery finished. |
Retries
Up to 8 attempts per delivery, waiting about 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, and 10 h between them (±20%). Repeated failures surface as an Issue.
Idempotency
Send Idempotency-Key on create. Each delivery carries webhook-id (the event id, stable across retries); deduplicate on it.
Errors
| Status | type | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not valid JSON, a field has the wrong type, or the body is over 1 MiB (body_too_large). |
| 401 | unauthorized | Missing, malformed, or revoked API key. |
| 402 | billing_error | The monthly credit allowance is used (credit_limit_exceeded). Reads keep working. |
| 403 | forbidden | The key lacks the ability this call needs (ability_required). |
| 404 | not_found | The id does not exist in this environment. |
| 409 | conflict | For example idempotency_mismatch: the Idempotency-Key was used with a different body. |
| 422 | invalid_request | A field value is out of range, for example invalid_timeout. |
| 429 | rate_limited | Too many requests (rate_limit_exceeded). Wait for Retry-After seconds. |
| 5xx | server_error | Flowyn failed. The body carries an error_id to quote to support. |
Limits
payloadmust be a JSON object; request bodies up to 1 MiB- Endpoints must be HTTPS
- One event matches at most 50 endpoints
How to verify a signed delivery
Deliveries follow the Standard Webhooks format. Each request carries webhook-id, webhook-timestamp, and webhook-signature. The signature is v1, followed by the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of your whsec_ secret. During a secret rotation the header can hold several space-separated signatures; accept the request if any one matches.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, headers, secret) {
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const signed = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${rawBody}`;
const expected = Buffer.from(createHmac("sha256", key).update(signed).digest("base64"));
return headers["webhook-signature"].split(" ").some((sig) => {
const actual = Buffer.from(sig.replace(/^v1,/, ""));
return actual.length === expected.length && timingSafeEqual(actual, expected);
});
}Reject old timestamps to limit replays, for example anything more than five minutes from your clock.
Read it back
Keep the id and read the resource whenever you need its current state.
curl https://api.flowyn.dev/v1/events/0f8e4a52-3c1d-4e7b-9a26-5d1b7c3e9f10/attempts \
-H "Authorization: Bearer $FLOWYN_API_KEY"Related
Next step
Configure a destination in Integrations.
