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.
Disabling and reactivating a subscription
When a callback is no longer used, disable it instead of deleting it (subscriptions are never deleted: they staydisabled and you can
reactivate them at any time):
{ "status": "active" }.
- The toggle governs future events: a
disabledsubscription stops receiving new events, but deliveries already queued are still sent. - It is idempotent: repeating the current status answers
200with 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
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 terminalstatus: "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 JSONPOST 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.
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-IDto 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 anidempotency_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_changedandwallet_send_status_changed.transfer_receiveddoes not carry it. - The payin resource also returns it on
GET /v1/payins/{id}.
Best practices
- Respond
200immediately and process in the background. - Log the
X-Webhook-Delivery-IDfor 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:
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.