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.

http
Authorization: Bearer flw_test_••••

Endpoint

http
POST https://api.flowyn.dev/v1/events

Request

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.

Response
{
  "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

  1. POST /v1/events stores the event and returns HTTP 202.
  2. Flowyn signs the exact body with each endpoint's secret and POSTs it. The response is attempt 1.
  3. A failed attempt schedules the next one within the policy.
  4. A 2xx marks the delivery delivered. Attempts stay attached to the event.
  5. POST /v1/events/{id}/replay sends it again as a new delivery without touching history.

Status values

StatusMeaning
acceptedStored, no delivery yet.
deliveringDelivery is in progress or a retry is scheduled.
deliveredA 2xx was received.
failedEvery attempt failed. Replay is available.
cancelledCancelled 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

StatustypeMeaning
400invalid_requestThe body is not valid JSON, a field has the wrong type, or the body is over 1 MiB (body_too_large).
401unauthorizedMissing, malformed, or revoked API key.
402billing_errorThe monthly credit allowance is used (credit_limit_exceeded). Reads keep working.
403forbiddenThe key lacks the ability this call needs (ability_required).
404not_foundThe id does not exist in this environment.
409conflictFor example idempotency_mismatch: the Idempotency-Key was used with a different body.
422invalid_requestA field value is out of range, for example invalid_timeout.
429rate_limitedToo many requests (rate_limit_exceeded). Wait for Retry-After seconds.
5xxserver_errorFlowyn failed. The body carries an error_id to quote to support.

Limits

  • payload must 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.

js
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.

bash
curl https://api.flowyn.dev/v1/events/0f8e4a52-3c1d-4e7b-9a26-5d1b7c3e9f10/attempts \
  -H "Authorization: Bearer $FLOWYN_API_KEY"

Next step

Configure a destination in Integrations.