Overview

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_url must 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 2xx status code to acknowledge receipt. Any other response (including 3xx) is treated as a failure and retried.

Headers

Every webhook request carries these headers:

HeaderValue
Content-Typeapplication/json
User-AgentGifQ-Webhooks/1.0
X-Gifq-EventThe event name. Also present as event in the body.
X-Gifq-DeliveryUUID unique to this delivery attempt. Use this to deduplicate retries.
X-Gifq-TimestampUnix 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.

FieldTypeScopeValue
eventStringuniversalThe event name, formatted <domain>.<action> (e.g. payout.terminal_state). Mirrors the X-Gifq-Event header.
occurred_atStringuniversalISO-8601 timestamp at which the event was generated. Authoritative for ordering.
statusStringuniversalCurrent status of the underlying resource. Each domain defines its allowed status values.
dataHash (optional)event-specificFull 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_uuidStringdomain identifierIdentifier of the parent resource. Each domain carries its own identifier(s) in this slot.
recipient_emailStringdomain identifierThe subject the event describes.
external_send_idStringdomain identifierStable identifier for the underlying lifecycle. A single subject can produce multiple external_send_ids over time — treat each as a separate lifecycle.
tx_hashString (optional)event-specificPromoted to the top level by specific events when present.
failure_reasonString (optional)event-specificPromoted 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, and data are present across all domains and never change meaning.
  • Identifiers are domain-scoped. Each domain declares which *_uuid / external_*_id / subject fields it sends. Payouts send payout_order_uuid, recipient_email, and external_send_id.
  • data is the per-event extension point. New events introduce new data shapes; they never alter the envelope.
  • Ignore unknown fields and unknown event values. 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 as order); the action is a state or past-tense verb (quote_created, status_changed, terminal_state).
  • The event body field and the X-Gifq-Event header are always identical.
  • Lifecycle domains use <domain>.status_changed for non-terminal transitions and <domain>.terminal_state for terminal ones.
  • Evolution is additive. We add new optional data fields, 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 unrecognized status values as non-terminal until a *.terminal_state event arrives.

Delivery guarantees

🚧

At-least-once delivery

A 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 4xx responses are treated as permanent and are not retried; 5xx responses 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_at on the body to resolve out-of-order arrivals.
  • Terminal events supersede intermediate ones. Once a *.terminal_state event is sent for an external_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:

  1. Deduplicate on X-Gifq-Delivery.
  2. Handle the event.
  3. Acknowledge with 2xx only 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 strings

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