Webhook delivery
Every webhook Flint sends follows one contract: the same headers, the same envelope, the same retry schedule. To build and register a handler, follow the Webhooks guide. For the list of event types and their data payloads, see the event catalog.
Delivery contract#
| Property | Behavior |
|---|---|
| Protocol | HTTPS POST with a JSON body |
| User agent | Flint-Webhooks/1.0 |
| Timeout | 10 seconds per attempt |
| Redirects | Never followed |
| Success | Any 2xx response from your server |
| Failure | Anything else: a non-2xx status, a redirect, a timeout, or a connection error |
| Guarantees | At-least-once delivery, in no guaranteed order |
- event created moves to delivery attempted on signed HTTP POST
- delivery attempted moves to delivered on 2xx within 10 seconds
- delivery attempted moves to waiting to retry on anything else
- waiting to retry moves to delivery attempted on backoff, up to 8 more attempts over 72 hours
Webhook URLs must be HTTPS and must resolve to a publicly routable address. Flint rejects localhost, private IPs, and internal hostnames both when you register the URL and again at delivery time.
Delivery headers#
POST /webhooks/flint HTTP/1.1
Content-Type: application/json
User-Agent: Flint-Webhooks/1.0
X-Flint-Event-Type: payment_intent.succeeded
X-Flint-Webhook-ID: whev_1kmn0aExample
X-Flint-Webhook-Endpoint-ID: whep_1kmn0aExample
X-Flint-Signature: t=1783090410,v1=d2eb22206ed2470e912f9e4465586273c16748df664b5ee4ba2700a4694d92a9
webhook-id: whev_1kmn0aExample
webhook-timestamp: 1783090410
webhook-signature: v1,PLz/mlH/ybZHvKsfeKbKu8qA7eC6OjCoXuXAWqb4WFM=
| Header | Purpose |
|---|---|
webhook-id, webhook-timestamp, webhook-signature | The preferred identity and signature headers for new integrations, in Standard Webhooks format. See Verify with a standard webhooks library. |
X-Flint-Signature | Legacy proof the delivery came from Flint. Verified in Step 4 of the Webhooks guide. |
X-Flint-Webhook-ID | Legacy event ID (whev_...). Identical on every retry and resend of the same event, which makes it your deduplication key. |
X-Flint-Event-Type | The event type, so you can route the delivery without parsing the body. |
X-Flint-Webhook-Endpoint-ID | The endpoint (whep_...) this delivery was sent to. |
The user agent is always Flint-Webhooks/1.0, which is useful when your infrastructure filters inbound traffic. Pick one signature scheme and verify it completely. There is no security benefit to checking both; they authenticate the same delivery with the same secret.
Event body#
{
"webhook_event_id": "whev_1kmn0aExample",
"event_type": "payment_intent.succeeded",
"payload_version": 1,
"mode": "test",
"merchant_id": "mer_1kmn0aExample",
"created_at": "2026-07-03T14:53:30Z",
"request": {
"id": "2f7c5a9d-6e4b-4f8a-9b73-1d8e4c6a0f25",
"idempotency_key": "confirm-payment-1kmn0a-001"
},
"data": {
"payment_intent": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "succeeded",
"amount_money": {"amount": 2500, "currency": "USD"},
"order_id": "ord_1kmn0aExample"
}
}
}
| Field | Description |
|---|---|
webhook_event_id | Unique event ID. Identical on every retry and resend of this event; also sent as webhook-id and legacy X-Flint-Webhook-ID. |
event_type | What happened. Every type is listed in the event catalog. |
payload_version | Envelope format version, currently 1. |
mode | test or live. An endpoint only receives events for the mode of the API key that created it. |
merchant_id | The merchant the event belongs to. |
created_at | When the event was created (RFC 3339, UTC). |
request | Always present. null when the event has no originating API request. Otherwise an object containing both the request id and idempotency_key. |
test | Present, and true, only on synthetic events from the test-events endpoint (Step 5 of the Webhooks guide). Real events in your sandbox carry "mode": "test" instead. |
data | The event payload. Its shape depends on the event type; the example above is payment_intent.succeeded. |
Two forward-compatibility rules keep integrations from breaking as the platform grows: new event types appear over time (endpoints subscribed to everything receive them automatically), and existing payloads gain new fields. Ignore fields you don't recognize, ignore event_type values you don't handle, and return 2xx for those events so Flint does not retry them.
Retry schedule#
A delivery attempt fails on a non-2xx status, a redirect, a connection error, or a 10-second timeout. Failed deliveries retry on a fixed backoff schedule, up to 9 total attempts spanning about three days:
| Attempt | Delay after previous failure | Approximate elapsed time |
|---|---|---|
| 1 | none (immediate) | 0 |
| 2 | 30 seconds | 30 seconds |
| 3 | 2 minutes | 2.5 minutes |
| 4 | 10 minutes | 13 minutes |
| 5 | 1 hour | 1.2 hours |
| 6 | 4 hours | 5 hours |
| 7 | 12 hours | 17 hours |
| 8 | 24 hours | 41 hours |
| 9 | 30 hours | 71 hours |
Each delay gets up to 20 percent of random jitter added, so treat the elapsed times as approximate. Every attempt is signed fresh with a new timestamp, so retries always pass your tolerance check.
While retries remain, the event's status is pending with a next_retry_at. After the ninth failure it becomes failed and automatic delivery stops, but the event is never silently lost: it stays queryable, and you can resend it manually after fixing your handler.
Delivery order#
Events fan out independently, and a failed delivery retries while newer events keep flowing. A sequence like this is normal:
happened on Flint arrived at your endpoint
------------------ -------------------------
1 subscription.created 1 subscription.payment_succeeded
2 subscription.payment_ 2 subscription.activated
succeeded 3 subscription.created
3 subscription.activated (first delivery failed; this is its retry)
Treat a webhook as a signal that something changed, not as the change itself. When your logic needs current state, fetch the resource: a GET /v1/orders/{order_id} reflects everything that has happened to that order regardless of what has arrived at your endpoint. The envelope's created_at lets you sequence events locally if you must, but fetching fresh state is simpler and harder to get wrong.
Lifecycle payloads include the resource ID, current public status, and resource_updated_at or an equivalent freshness timestamp. Some subresource events also include event-specific timestamps such as payment_attempt_updated_at; for invoice payment-attempt terminal events, resource_updated_at matches the payment-attempt transition time. If you mirror state locally, compare those timestamps before overwriting a newer row, and fetch the resource before irreversible work when stale data would be costly.
Related#
- Webhooks guide: register an endpoint, verify signatures, and build a handler that survives retries.
- Webhook events: every event type Flint delivers, with payload schemas.
- Webhooks API: endpoints for managing endpoints, listing deliveries, and resending.
