Webhooks let GIFQ notify your backend when something happens, instead of you polling our API. GIFQ POSTs a JSON event to a URL you control as resources move through their lifecycle. Every event shares the same transport, the same delivery guarantees, and the same envelope — only the event-specific data differs.
This page describes how webhooks work for every event. For the list of event types see Events reference. For payout-specific events see Payout events.
Subscribing
You subscribe per resource by supplying a callback_url. For payouts, pass callback_url when creating a payout order (POST /api/payout-orders). Without a callback_url, no webhooks fire for that resource.
callback_urlmust be an HTTPS URL.- A single payout order emits one event stream per recipient as each recipient's payout progresses.
Endpoint contract
We POST to your registered callback_url:
POST <your callback_url>
Content-Type: application/json
- We send JSON only.
- We never include a query string.
- We do not follow redirects.
- Respond with any
2xxstatus code to acknowledge receipt. Any other response (including3xx) is treated as a failure and retried.
Headers
Every webhook request carries these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | GifQ-Webhooks/1.0 |
X-Gifq-Event | The event name. Also present as event in the body. |
X-Gifq-Delivery | UUID unique to this delivery attempt. Use this to deduplicate retries. |
X-Gifq-Timestamp | Unix epoch seconds at dispatch time. |
The event envelope
Every webhook body — in every domain — shares the same top-level envelope. Event pages only document their own data shape; everything else below is constant.
| Field | Type | Scope | Value |
|---|---|---|---|
event | String | universal | The event name, formatted <domain>.<action> (e.g. payout.terminal_state). Mirrors the X-Gifq-Event header. |
occurred_at | String | universal | ISO-8601 timestamp at which the event was generated. Authoritative for ordering. |
status | String | universal | Current status of the underlying resource. Each domain defines its allowed status values. |
data | Hash (optional) | event-specific | Full resource snapshot at the moment the event was generated. The only part that varies per event; its shape is documented on each event page. Omitted on some events. |
payout_order_uuid | String | domain identifier | Identifier of the parent resource. Each domain carries its own identifier(s) in this slot. |
recipient_email | String | domain identifier | The subject the event describes. |
external_send_id | String | domain identifier | Stable identifier for the underlying lifecycle. A single subject can produce multiple external_send_ids over time — treat each as a separate lifecycle. |
tx_hash | String (optional) | event-specific | Promoted to the top level by specific events when present. |
failure_reason | String (optional) | event-specific | Promoted to the top level by specific events when present. |
Scalability contract
When you build a webhook consumer, rely on these invariants so new event types never break you:
event,occurred_at,status, anddataare present across all domains and never change meaning.- Identifiers are domain-scoped. Each domain declares which
*_uuid/external_*_id/ subject fields it sends. Payouts sendpayout_order_uuid,recipient_email, andexternal_send_id. datais the per-event extension point. New events introduce newdatashapes; they never alter the envelope.- Ignore unknown fields and unknown
eventvalues. This is what lets GIFQ add new fields, statuses, and event types without breaking your integration.
Event naming & versioning
- Event names are
<domain>.<action>— lowercase, dot-separated. The domain is the resource family (payout, and future families such asorder); the action is a state or past-tense verb (quote_created,status_changed,terminal_state). - The
eventbody field and theX-Gifq-Eventheader are always identical. - Lifecycle domains use
<domain>.status_changedfor non-terminal transitions and<domain>.terminal_statefor terminal ones. - Evolution is additive. We add new optional
datafields, new status values, and new event types over time. We do not mutate the meaning of an existing payload; a breaking change ships as a new event name. Treat unrecognizedstatusvalues as non-terminal until a*.terminal_stateevent arrives.
Delivery guarantees
At-least-once deliveryA single event may be delivered more than once if your acknowledgement is delayed or fails. Always deduplicate on
X-Gifq-Delivery.
- Retries. Up to 8 attempts with polynomial backoff. Merchant
4xxresponses are treated as permanent and are not retried;5xxresponses and connection/timeout failures are retried. After the final attempt the event is marked permanently failed and can be re-sent from the GIFQ dashboard. - Timeouts. Connect timeout 5s, read timeout 10s. Acknowledge fast — do heavy work asynchronously.
- Ordering is not guaranteed. Use
occurred_aton the body to resolve out-of-order arrivals. - Terminal events supersede intermediate ones. Once a
*.terminal_stateevent is sent for anexternal_send_id, any pending non-terminal event for the same lifecycle is dropped — you will not see stale intermediate states after the terminal event.
Receiving in your handler
Recommended order:
- Deduplicate on
X-Gifq-Delivery. - Handle the event.
- Acknowledge with
2xxonly after durable persistence.
post '/gifq/webhooks' do
body = JSON.parse(request.body.read)
delivery_id = request.env['HTTP_X_GIFQ_DELIVERY']
return status 204 if already_handled?(delivery_id)
case body['event']
when 'payout.quote_created' then on_quote_created(body)
when 'payout.terminal_state' then on_terminal(body)
# ...
end
record_handled(delivery_id)
status 204
end
Decimal amounts are stringsAll monetary amounts in webhook bodies are decimal strings — parse with your language's arbitrary-precision decimal type, not floats. Additional fields may appear over time; treat unknown fields as informational.