Skip to main content
Webhooks notify your own HTTPS callback about your account’s events, cryptographically signed.

Create a subscription

  • event_type: one of the events below, or * for all.
  • callback_url: HTTPS required; localhost and private IPs are rejected — for local development use an HTTPS tunnel.
  • secret: at least 16 characters; used to sign every delivery. Stored encrypted and cannot be retrieved.
The subscription receives your account’s events. You can list active subscriptions at any time:

Disabling and reactivating a subscription

When a callback is no longer used, disable it instead of deleting it (subscriptions are never deleted: they stay disabled and you can reactivate them at any time):
To reactivate it, same call with { "status": "active" }.
  • The toggle governs future events: a disabled subscription stops receiving new events, but deliveries already queued are still sent.
  • It is idempotent: repeating the current status answers 200 with no change.
  • You can only touch your account’s subscriptions: a subscription of another account answers 404 (it is indistinguishable from an inexistent one).

Events

Payload of each event

For account-scoped subscriptions, ip and user_agent are intentionally redacted. Organization-wide security views retain the forensic user_agent and the resolved actor IP fields (actor_ip, actor_ip_source) for admin audit; those admin-only fields are not part of the public webhook contract. In payout_status_changed, a technical-screening update can be pending before any debit. Final updates are completed or failed (with failed, the debit has already been refunded by the time you receive the event). crypto_withdrawal_status_changed uses completed or failed. For payout_status_changed, status_code and status_message are the normalized outcome reported by the core. status_message may be empty when the rail provides no additional detail; it is scrubbed before delivery and never contains an upstream provider identity.

Pre-debit terminal failures

If screening fails before the debit path, the event can be terminal status: "failed" with predebit_failure: true and funds_debited: false; receipt_url is omitted. A post-debit failure is different: its exact debit is refunded before the terminal event. The platform can also persist non-terminal transition revisions, so one payout may produce more than one payout_status_changed delivery; deduplicate by X-Webhook-Event-ID and do not create a second payout.

Delivery format

Every delivery is a JSON POST with these headers:

Verify the signatures

CBPay sends both signature headers on every platform webhook delivery. V1 remains valid and continues to be sent, so adopting V2 is not a breaking change. New integrations should verify V2 first and keep the V1 fallback while migrating existing receivers. Both inputs use the X-Webhook-Timestamp, X-Webhook-Event and X-Webhook-Event-ID values from the same request where applicable. The result is lowercase hexadecimal HMAC-SHA256 using the subscription secret. V2 binds the event type and event ID to the signature, so do not take those values from a re-serialized JSON payload. X-Webhook-Delivery-ID is for delivery traceability; deduplicate by X-Webhook-Event-ID.
Compute the HMAC over the raw body (bytes as received), not over re-serialized JSON. Reject old timestamps (> 5 minutes) to prevent replay.

Retries and idempotency

  • Your endpoint must respond 2xx within the timeout; anything else is retried.
  • Up to 5 attempts with incremental backoff:
  • Use X-Webhook-Event-ID to deduplicate: the same event can arrive more than once (at-least-once delivery).
  • If all 5 attempts fail the event is not resent — recover the state with the resource’s GET (which is why no flow should depend ONLY on the webhook).

Your idempotency key comes back in the payload

When the event comes from an operation you created with an idempotency_key (a payout, a card payin, a crypto withdrawal, a segregated-wallet send, a refund…), the payload carries it back as idempotency_key — reconcile the notification against your own reference without an extra GET.
  • It is optional: present only when the original request carried a non-empty key, and omitted otherwise. Operations created by the platform itself never expose an internal key.
  • Covered events: payout_status_changed, payin_credited, payin_settlement_scheduled, payin_expired, payin_refunded, crypto_withdrawal_status_changed and wallet_send_status_changed. transfer_received does not carry it.
  • The payin resource also returns it on GET /v1/payins/{id}.

Best practices

  • Respond 200 immediately and process in the background.
  • Log the X-Webhook-Delivery-ID for traceability.
  • Don’t rely solely on webhooks for critical states: you can always query the object by API (GET /v1/payouts/{id}, etc.).

banking_virtual_iban_inbound

This event reports an inbound EUR transfer received by an owned virtual IBAN. The event is normalized before delivery; consumers should deduplicate by the event ID and fetch the banking operation or virtual account when they need the latest state. The event does not expose an upstream provider identity.

admin_announcement

This account-scoped event is emitted when an organization administrator’s approved broadcast reaches the account notification channel. It is plain localized text, never HTML:
The event is delivered only to the account selected by the campaign. It can be received through an account webhook subscription and the account event stream; deduplicate deliveries by X-Webhook-Event-ID.

dispute_status_changed

See the disputes guide for the signed payload and transitions. Local-fiat payins use the existing webhook family: payin_credited carries credit_asset/fiat_credited; payin_refunded carries debited_asset/debited_amount.
Last modified on October 3, 2026