Webhooks

Introduction

Bitnob emits real-time webhook events that inform your system of all important activities and lifecycle events related to your virtual cards.

Each webhook provides structured data about:

Transaction outcomes (approved, failed, reversed, refunded),

Card lifecycle changes (created, terminated, frozen, regularized, expired),

Operational risk events (failed authorizations, frozen-card declines, cross-border activity).

Webhooks are delivered to your registered callback URL via HTTPS POST requests.


Webhook Structure

All webhook payloads share the same top-level envelope:

Webhook Payload Structure
Conventions

All JSON keys and identifier values use snake_case. The event name itself stays dot-delimited (e.g. virtualcard.transaction.debit) for routing. Amounts are integers in micro-units — 1 USD = 1,000,000. Transaction events also include display_amount as the same value expressed as a major-unit float.


Wire Conventions for Transaction Events

transaction_type carries Bitnob's internal classification (stable). Branch on this for routing. Values: authorization, settlement, pre_auth_approved, authorization_declined, refund_authorization, refund_settled, reversal_settled, cross_border_pending, cross_border_settled, cross_border_reversal, contactless_pending, contactless_settled, verification.

status carries the wire-level outcome. completed for settled debits / credits / reversals / verifications. pending for cross-border auth still awaiting settlement. decline for declines. terminated for card-terminated declines.

reference is the local Bitnob reference (CARD_*-prefixed for backend-generated ones). It's the join key against the matching ledger entry.

event_id is unique per delivery — use it as the idempotency key when persisting received webhooks. Retries reuse the same event_id.


Webhook Categories

category
description
Card Lifecycle
When a card is created, fails to create, is terminated, regularized, or expires
Top-Ups & Withdrawals
When a card is loaded or funds are withdrawn
Transaction Activity
Authorizations, settlements, pre-auth holds, contactless, refunds, reversals, cross-border, verification
Declines & Risk Events
Generic declines, decline-rule violation fees, frozen-card declines, terminated-card declines, authorization failures, issuer expiration
KYC & Activation
Customer KYC outcomes and contactless activation
Refunds on Terminated Cards
Per-transaction refunds posted as part of card termination cleanup

1

Card Lifecycle Webhooks

virtualcard.created.completed

Fired when a virtual card is successfully created at the provider and is active.

virtualcard.created.completed Payload

virtualcard.created.failed

Fired when the provider rejects the card creation attempt.

virtualcard.created.failed Payload

virtualcard.terminated.refund

Fired when a card is terminated and any remaining balance is refunded.

virtualcard.regularized

Card moved out of a degraded or indeterminate state into a stable status.

virtualcard.expiration

Card has reached its expiry date and can no longer be used.


2

Card Funding Webhooks

virtualcard.topup.completed

Card top-up (load funds) succeeded.

virtualcard.topup.completed Payload

virtualcard.topup.failed

Top-up failed (e.g. insufficient balance, card status invalid). Payload shape matches the success event with status: "failed".


3

Card Withdrawal Webhooks

virtualcard.withdrawal.completed

A successful withdrawal was made from the card.

virtualcard.withdrawal.completed Payload

virtualcard.withdrawal.failed

Withdrawal attempt failed (e.g. below minimum balance, card status invalid). Payload shape matches the success event with status: "failed".


4

Transaction Webhooks

All transaction events include transaction_type (Bitnob's stable enum), status, display_amount, and merchant_* fields where applicable.

virtualcard.transaction.debit

Generic settled debit shape. The transaction_type will be one of settlement, authorization, pre_auth_approved, contactless_settled, or contactless_pending. The dedicated event names below are what is actually emitted on the wire — branch on event first.

virtualcard.transaction.debit Payload

virtualcard.transaction.authorization

Authorization hold placed by merchant; settlement pending. transaction_type: authorization.

virtualcard.transaction.settlement

Merchant settled a previously authorized transaction. transaction_type: settlement.

virtualcard.transaction.pre-auth.approved

Pre-authorization hold (e.g., hotel, gas pump). transaction_type: pre_auth_approved.

virtualcard.transaction.contactless

Contactless / tap-to-pay payment. Same event string fires for both the pending authorization (transaction_type: contactless_pending) and the final settled state (transaction_type: contactless_settled) — distinguish via transaction_type.

virtualcard.transaction.credit

Generic credit posting (refund, adjustment).

virtualcard.transaction.refund

Merchant-initiated refund. Same event string fires for the refund authorization (transaction_type: refund_authorization) and the refund settlement (transaction_type: refund_settled).

virtualcard.transaction.reversed

Authorization reversed before settlement. Also emitted for cross-border reversals — transaction_type is reversal_settled for auth reversals and cross_border_reversal for cross-border.

virtualcard.transaction.verification

Zero-dollar verification authorization (e.g., Uber, Netflix card-on-file checks). transaction_type: verification, amount: 0.

virtualcard.transaction.crossborder

Cross-border transactions emit this event twice with different transaction_type values:

transaction_type
where the money came from
cross_border_pending
Company wallet.
cross_border_settled
Card balance.

Correlate the two via the reference field (CARD_CROSSBORD_*).

Verification-API-down variant

If Miden's verification API was unreachable when the settlement webhook arrived, the row is recorded at pending and the wire status is "pending" instead of "completed". The transaction_type stays cross_border_settled. Recovery is via the awaiting-verification reconciler.

virtualcard.transaction.terminated.refund

Per-transaction refund posted as part of card termination cleanup. Funds returned to the float or master account.


5

Declines & Risk Event Webhooks

virtualcard.transaction.declined

Generic decline (e.g., insufficient funds, CVV mismatch). status: "declined" with a reason string.

virtualcard.transaction.declined.charge

Decline-rule violation fee charged to your USD wallet after repeated declines. Includes fee_amount and violation_count. See the Decline Rule Policy for full details.

virtualcard.transaction.declined.charge Payload

virtualcard.transaction.declined.frozen

Authorization declined because the card is frozen (usually a fraud rule).

virtualcard.transaction.declined.terminated

Authorization attempted on a terminated card. Includes balance_before_termination.

virtualcard.transaction.authorization.failed

Authorization rejected by the issuer (e.g., bad PAN/CVV, AVS failure). Includes reason.

virtualcard.transaction.issuerexpiration

Transaction declined because the card has expired at the issuer.


6

KYC & Activation Webhooks

virtualcard.user.kyc.completed

KYC verification succeeded for a customer. kyc_passed: true.

virtualcard.user.kyc.failed

KYC verification failed. kyc_passed: false with a reason.

virtualcard.contactless.activation

Customer KYC + card activation for contactless support completed.


Best Practices for Handling Webhooks

practice
why it matters
Acknowledge with HTTP 200
Prevent retries and duplication. Wand retries up to 3 times with exponential backoff on non-2xx responses.
Deduplicate by event_id
Retries reuse the same event_id — store it and reject duplicates to keep handlers idempotent.
Branch on event first, then transaction_type
Several events (contactless, crossborder, refund, reversed) share an event string but carry different transaction_type values.
Verify the HMAC signature
Each delivery is signed hmac_sha256(secret, body) and sent in the X-Bitnob-Signature header.
Ignore unknown fields
Event names are stable but new optional fields may be added to data without a version bump.
Log and reconcile
Persist every event for audit. Cross-check against /api/cards/:cardId/transactions for periodic reconciliation.

Virtual Card Webhooks – Reference Table

event name
category
description
virtualcard.created.completed
Card Lifecycle
Card was successfully created at the provider and is now active.
virtualcard.created.failed
Card Lifecycle
Card creation rejected by provider. Check `reason` in payload.
virtualcard.terminated.refund
Card Lifecycle
Card was terminated; remaining balance refunded.
virtualcard.regularized
Card Lifecycle
Card moved out of a degraded state into a stable status.
virtualcard.expiration
Card Lifecycle
Card reached its expiry date.
virtualcard.topup.completed
Top-Up
Card was funded successfully.
virtualcard.topup.failed
Top-Up
Card funding attempt failed.
virtualcard.withdrawal.completed
Withdrawal
Withdrawal from card completed.
virtualcard.withdrawal.failed
Withdrawal
Withdrawal failed (e.g. limit, balance, status).
virtualcard.transaction.debit
Transaction
Generic settled debit shape (settlement / authorization / pre_auth_approved / contactless).
virtualcard.transaction.credit
Transaction
Generic credit posting on the card.
virtualcard.transaction.authorization
Transaction
Auth approved by provider; settlement pending.
virtualcard.transaction.settlement
Transaction
Previously-authorized transaction now settled.
virtualcard.transaction.verification
Transaction
Zero-amount card-on-file verification authorization.
virtualcard.transaction.pre-auth.approved
Transaction
Pre-authorization hold (hotel, gas pump, etc.).
virtualcard.transaction.reversed
Transaction
Authorization reversed before settlement. Also fires for cross-border reversals.
virtualcard.transaction.refund
Transaction
Merchant refund. Fires for both refund authorization and settlement — branch on `transaction_type`.
virtualcard.transaction.crossborder
Transaction
Cross-border activity. `cross_border_pending` = wallet fee debit; `cross_border_settled` = card-balance debit.
virtualcard.transaction.contactless
Transaction
Contactless / tap-to-pay. Fires for both pending and settled — branch on `transaction_type`.
virtualcard.transaction.terminated.refund
Transaction
Per-transaction refund posted during card termination cleanup.
virtualcard.transaction.declined
Decline
Generic decline. See `reason` in payload.
virtualcard.transaction.declined.charge
Decline
Decline-rule violation fee charged to USD wallet.
virtualcard.transaction.declined.frozen
Decline
Authorization declined because the card is frozen.
virtualcard.transaction.declined.terminated
Decline
Authorization attempted on a terminated card.
virtualcard.transaction.authorization.failed
Decline
Authorization rejected (bad PAN/CVV, AVS failure, etc.).
virtualcard.transaction.issuerexpiration
Decline
Authorization declined because card expired at the issuer.
virtualcard.user.kyc.completed
KYC
Customer KYC verification succeeded.
virtualcard.user.kyc.failed
KYC
Customer KYC verification failed.
virtualcard.contactless.activation
Activation
Customer activated for contactless / NFC support.

Next Steps

You can test webhook delivery via the Bitnob API or by manually triggering test events from your dashboard.

Subscribe to sandbox events first to validate your implementation against the new payload shape.

For production use, log all failed deliveries and monitor retry queues.


Share on
Did you find this page useful?

Join our Discord