Payout Webhooks

When a payout order has a callback_url, GifQ POSTs JSON events to it as each recipient moves through the crypto payout lifecycle. Every request carries a unique delivery ID so you can deduplicate retries.

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

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.

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.

  • Up to 8 attempts. Failed deliveries are retried with exponential backoff. After the final attempt, the event is marked permanently failed and can be re-sent from the GifQ dashboard.
  • Ordering is not guaranteed. Use occurred_at on the body to resolve out-of-order arrivals.
  • Terminal events supersede intermediate ones. Once payout.terminal_state is sent for a external_send_id, any pending payout.status_changed for the same send is dropped — you will not see stale intermediate states after the terminal event.

Common body fields

Every event body carries these fields. Event-specific additions are listed under each event below.

NameTypeValue
eventStringThe event name. One of payout.quote_created, payout.quote_confirmed, payout.quote_canceled, payout.status_changed, payout.terminal_state.
payout_order_uuidStringUUID of the payout order this event belongs to. Stable across retries.
recipient_emailStringEmail of the recipient whose payout this event describes.
statusStringCurrent status of the underlying send. See Status values.
occurred_atStringISO-8601 timestamp at which the event was generated.
external_send_idStringIdentifier for the underlying crypto send. A single recipient can produce multiple external_send_ids if a quote expires and is reopened — treat each one as a separate lifecycle.
tx_hashString (optional)Present on payout.terminal_state once a completed send is confirmed on chain. Omitted otherwise.
failure_reasonString (optional)Present on payout.terminal_state when status == "failed". Omitted otherwise.
dataHash (optional)Full send-request snapshot — FX rate, fees, ledger account, beneficiary wallet, blockchain transactions. Present on payout.quote_* and payout.terminal_state events; omitted on payout.status_changed. See data shape.

Events

payout.quote_created

The recipient has selected a currency and network and an FX quote has been issued. This is the earliest event you receive for a given external_send_id.

NameTypeValue
common fieldsSee Common body fields.
dataHashQuote details. See data shape.
{
  "event": "payout.quote_created",
  "status": "in_progress",
  "occurred_at": "2026-05-25T10:00:00Z",
  "recipient_email": "[email protected]",
  "external_send_id": "123456",
  "payout_order_uuid": "11111111-2222-3333-4444-555555555555",
  "data": {
    "id": 123456,
    "status": "in_progress",
    "purpose": "Gifq payout to [email protected]",
    "created_at": "2026-05-25T10:00:00.000Z",
    "input_amount": "100.00",
    "input_currency": {
      "id": 33,
      "kind": "crypto",
      "title": "USDC",
      "symbol": "USDC",
      "enabled": true,
      "disabled_message": null
    },
    "sending_amount": "100.00",
    "sending_currency": {
      "id": 33,
      "kind": "crypto",
      "title": "USDC",
      "symbol": "USDC",
      "enabled": true,
      "disabled_message": null
    },
    "input_to_sending_rate": "1.0",
    "balance_debit_amount": "0.047778",
    "balance_debit_currency": {
      "id": 5,
      "kind": "crypto",
      "title": "Ethereum",
      "symbol": "ETH",
      "enabled": true,
      "disabled_message": null
    },
    "sending_to_balance_debit_rate": "2092.89",
    "fees": {
      "service_fee": {
        "amount": "0.00056141",
        "currency": {
          "id": 5,
          "kind": "crypto",
          "title": "Ethereum",
          "symbol": "ETH",
          "enabled": true,
          "disabled_message": null
        }
      },
      "conversion_fee": {
        "amount": "0.000478",
        "currency": {
          "id": 5,
          "kind": "crypto",
          "title": "Ethereum",
          "symbol": "ETH",
          "enabled": true,
          "disabled_message": null
        }
      }
    },
    "ledger_account": {
      "id": "01EXAMPLELEDGER0000000000000",
      "title": "Ethereum main account",
      "status": "active",
      "balance": "10.000000000000000000",
      "account_type": "main",
      "currency": {
        "id": 5,
        "kind": "crypto",
        "title": "Ethereum",
        "symbol": "ETH",
        "enabled": true,
        "disabled_message": null
      }
    },
    "beneficiary_payout_setting": {
      "id": 999,
      "beneficiary_id": 888,
      "created_at": "2026-05-25T09:59:59.000Z",
      "currency": {
        "id": 33,
        "kind": "crypto",
        "title": "USDC",
        "symbol": "USDC",
        "enabled": true,
        "disabled_message": null
      },
      "platform": {
        "id": 21,
        "title": "Solana",
        "id_name": "solana"
      },
      "crypto_address": "ExampleSolanaWalletAddress00000000000000000",
      "crypto_address_metadata": null
    },
    "blockchain_transactions": [],
    "requires_2fa_confirmation": false
  }
}
📘

No duplicates on retry

Idempotent retries of the underlying quote action do not produce duplicate payout.quote_created events for the same external_send_id.

payout.quote_confirmed

The recipient confirmed the FX exchange on a cross-currency quote. Fires only when an FX exchange actually occurred — same-currency quotes do not produce this event.

NameTypeValue
common fieldsSee Common body fields.
dataHash{ "remote": <confirm response> }.

payout.quote_canceled

The recipient canceled an open quote. The same external_send_id cannot be re-confirmed; a subsequent quote produces a new external_send_id and a fresh payout.quote_created.

NameTypeValue
common fieldsSee Common body fields.
dataHash{ "remote": <cancel response> }.

payout.status_changed

A non-terminal status change on the underlying send. You may see one or more of these between payout.quote_created and payout.terminal_state. Treat occurred_at as authoritative for ordering.

NameTypeValue
common fieldsSee Common body fields.
{
  "event": "payout.status_changed",
  "status": "processing",
  "occurred_at": "2026-05-25T10:01:32Z",
  "recipient_email": "[email protected]",
  "external_send_id": "123456",
  "payout_order_uuid": "11111111-2222-3333-4444-555555555555"
}

payout.terminal_state

The underlying send has reached a terminal status (completed, failed, expired, or canceled). You receive at most one payout.terminal_state per external_send_id.

🚧

tx_hash may arrive on a follow-up event

If a send is completed but the on-chain transaction has not yet confirmed at dispatch time, tx_hash is omitted. A second payout.terminal_state for the same external_send_id may follow once the hash lands — treat the later event as authoritative for tx_hash.

NameTypeValue
common fieldsSee Common body fields.
tx_hashString (optional)Present when status == "completed" and the on-chain confirmation has landed.
failure_reasonString (optional)Present when status == "failed".

Completed example:

{
  "event": "payout.terminal_state",
  "status": "completed",
  "occurred_at": "2026-05-25T10:04:17Z",
  "recipient_email": "[email protected]",
  "external_send_id": "123456",
  "payout_order_uuid": "11111111-2222-3333-4444-555555555555",
  "tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
  "data": {
    "id": 123456,
    "status": "completed",
    "purpose": "Gifq payout to [email protected]",
    "created_at": "2026-05-25T10:00:00.000Z",
    "input_amount": "100.00",
    "input_currency": {
      "id": 33,
      "kind": "crypto",
      "title": "USDC",
      "symbol": "USDC",
      "enabled": true,
      "disabled_message": null
    },
    "sending_amount": "100.00",
    "sending_currency": {
      "id": 33,
      "kind": "crypto",
      "title": "USDC",
      "symbol": "USDC",
      "enabled": true,
      "disabled_message": null
    },
    "input_to_sending_rate": "1.0",
    "balance_debit_amount": "0.047778",
    "balance_debit_currency": {
      "id": 5,
      "kind": "crypto",
      "title": "Ethereum",
      "symbol": "ETH",
      "enabled": true,
      "disabled_message": null
    },
    "sending_to_balance_debit_rate": "2092.89",
    "fees": {
      "service_fee": {
        "amount": "0.00056141",
        "currency": {
          "id": 5,
          "kind": "crypto",
          "title": "Ethereum",
          "symbol": "ETH",
          "enabled": true,
          "disabled_message": null
        }
      },
      "conversion_fee": {
        "amount": "0.000478",
        "currency": {
          "id": 5,
          "kind": "crypto",
          "title": "Ethereum",
          "symbol": "ETH",
          "enabled": true,
          "disabled_message": null
        }
      }
    },
    "blockchain_transactions": [
      {
        "id": 7000001,
        "txid": "0x1111111111111111111111111111111111111111111111111111111111111111",
        "amount": "100.00",
        "status": "confirmed",
        "network_confirmations": 1,
        "currency": {
          "id": 33,
          "title": "USDC",
          "symbol": "USDC",
          "platform": {
            "id": 21,
            "title": "Solana"
          }
        }
      }
    ],
    "requires_2fa_confirmation": false
  }
}

Failed example:

{
  "event": "payout.terminal_state",
  "status": "failed",
  "occurred_at": "2026-05-25T10:04:17Z",
  "recipient_email": "[email protected]",
  "external_send_id": "123456",
  "payout_order_uuid": "11111111-2222-3333-4444-555555555555",
  "failure_reason": "insufficient_funds"
}

Status values

statusMeaning
in_progressThe send request has been accepted. No funds have moved yet.
processingThe send is being processed. An FX exchange may be in flight or the chain broadcast is pending.
completedThe send is finalized. If tx_hash is present, funds are confirmed on chain.
failedThe send did not go through. failure_reason carries the cause.
expiredThe send was not actioned within its lifetime window.
canceledThe send was canceled. No funds moved.

completed, failed, expired, and canceled are terminal — no further events fire for that external_send_id once a terminal event has been sent.

data shape

data is present on payout.quote_* events and on payout.terminal_state events. It carries the full send-request snapshot at the moment the event was generated.

FieldTypeNotes
idIntegerSend-request ID. Equals external_send_id parsed as integer.
statusStringCurrent status of the underlying send. Mirrors the top-level status.
purposeStringHuman-readable description of the payout.
created_atStringISO-8601 timestamp when the send request was created.
input_amountStringAmount the recipient is sending, in input_currency.
input_currencyHashSee Currency object.
sending_amountStringAmount the recipient will receive, in sending_currency.
sending_currencyHashSee Currency object.
input_to_sending_rateStringFX rate used to convert input_amount to sending_amount. "1.0" for same-currency sends.
balance_debit_amountStringAmount debited from the merchant's ledger, in balance_debit_currency.
balance_debit_currencyHashSee Currency object.
sending_to_balance_debit_rateStringFX rate used to convert from sending_currency to balance_debit_currency.
fees.service_feeHash{ amount, currency }. amount is a decimal string; currency follows the Currency object shape.
fees.conversion_feeHash{ amount, currency }. Only present on cross-currency sends.
ledger_accountHash{ id, title, status, balance, account_type, currency }. The merchant ledger account funds were debited from.
beneficiary_payout_settingHash{ id, beneficiary_id, created_at, currency, platform, crypto_address, crypto_address_metadata }. The destination wallet for this send.
blockchain_transactionsArrayEmpty [] until the chain broadcast lands. Once populated, each entry is { id, txid, amount, status, network_confirmations, currency }. The txid of the first entry is surfaced at the top level as tx_hash on payout.terminal_state.
requires_2fa_confirmationBooleanWhether the recipient must complete 2FA before the send proceeds.

Currency object

input_currency, sending_currency, balance_debit_currency, and the currency nested under fees.* / ledger_account / beneficiary_payout_setting all share the same shape:

FieldTypeNotes
idIntegerInternal currency ID.
kindString"fiat" or "crypto".
titleStringFull currency name (e.g. "Ethereum", "USDC").
symbolStringTicker symbol (e.g. "ETH", "USDC").
enabledBooleanWhether the currency is currently enabled for new sends.
disabled_messageString (optional)Reason a disabled currency is unavailable. null when enabled: true.

BlockchainTransaction object

Entries in data.blockchain_transactions[] describe individual on-chain transactions backing the send. The array is empty until the chain broadcast lands; once populated, the txid of the first entry is surfaced at the top level as tx_hash on payout.terminal_state.

FieldTypeNotes
idIntegerInternal transaction ID.
txidStringOn-chain transaction hash.
amountStringDecimal amount transferred in this transaction.
statusStringTransaction status (e.g. "confirmed").
network_confirmationsIntegerNumber of confirmations the transaction has received.
currencyHashA thinner variant of the Currency object: { id, title, symbol, platform: { id, title } }. The nested platform identifies the chain the transaction was broadcast on (e.g. "Base", "Solana", "Ethereum").

Example:

{
  "id": 7000001,
  "txid": "0x1111111111111111111111111111111111111111111111111111111111111111",
  "amount": "12.0",
  "status": "confirmed",
  "network_confirmations": 1,
  "currency": {
    "id": 113,
    "title": "USDC",
    "symbol": "USDC",
    "platform": {
      "id": 42,
      "title": "Base"
    }
  }
}
📘

Decimal amounts are strings

All amounts are decimal strings — parse with your language's arbitrary-precision decimal type, not floats. Additional fields may appear in data; treat unknown fields as informational.

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