Webhooks

Learn how to listen for events that happen on your virtual cards.

A webhook is a URL on your server where we send payloads for card-related events. For example, when a customer's card is successfully topped up, we immediately deliver a virtualcard.topup.completed event to your endpoint. Whenever you receive a webhook from us, return a 200 OK to avoid retries.

All payloads share the same envelope:

Webhook Payload Envelope

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 include display_amount as the same value expressed as a major-unit float.


Verifying Events

Verifying that these events come from Bitnob is necessary to avoid acting on a forged event.

To verify events, validate the x-bitnob-signature header sent with the event. The HMAC SHA512 signature is the raw event body signed with your secret key.

Notification Retries

When delivering notifications we expect a 200 response. If the response is not 200, we retry up to 3 times with exponential backoff. Use the top-level event_id to deduplicate retries.

Don't rely on webhooks entirely

Note

We recommend that you also poll the card transactions endpoint as a safety net in case webhook delivery fails.

Testing Webhooks

Webhook endpoints must be publicly accessible. While developing locally, use a tunnelling tool like ngrok or localtunnel and point your test webhook URL at the tunnel.

Only use tunnels in test environments to avoid leaking data publicly.

Verify Bitnob Webhook (Node.js - Express)

Wire Conventions for Transaction Events

Transaction events carry three fields that you should branch on:

transaction_type — Bitnob's stable internal classification. 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 — wire-level outcome. completed for settled debits / credits / reversals / verifications. pending for cross-border auth still awaiting settlement. declined for declines. terminated for card-terminated declines. failed for failed top-ups / withdrawals / authorizations.

reference — local Bitnob reference (CARD_*-prefixed for backend-generated ones). Use it as the join key against the matching ledger entry.

Some event strings (virtualcard.transaction.contactless, virtualcard.transaction.crossborder, virtualcard.transaction.refund, virtualcard.transaction.reversed) fire with multiple transaction_type values — always branch on event first, then transaction_type.


Virtual Cards Webhooks

Virtual card webhooks are fired when a customer performs any virtual card action.

Virtual Card Created Successfully

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

1
event_idUUID

Unique identifier for this delivery. Stable across retries — use it as the idempotency key.

2
company_idUUID

Top-level company identifier. Also repeated inside `data` for convenience.

3
eventString

Webhook event name. Value: 'virtualcard.created.completed'.

4
data.idUUID

Unique identifier of the newly created virtual card. Use this to retrieve card details or reference the card in support cases.

5
data.statusString

Current status of the card. Typically 'active' immediately after a successful creation.

6
data.referenceString

Unique reference string for the card creation request. Use for deduplication, auditing, and reconciliation.

7
data.created_statusString

Outcome of the creation request. Value 'completed' confirms the card was issued without errors.

8
data.reissueBoolean

Whether this card was created as a reissue of an earlier card. `false` for net-new cards.

virtualcard.created.completed

Virtual Card Creation Failed

Fired when the provider rejects a card creation attempt.

1
eventString

Webhook event name. Value: 'virtualcard.created.failed'.

2
data.idUUID

Unique identifier for the failed virtual card creation attempt.

3
data.reasonString

Machine-readable reason code describing why creation failed (e.g. 'card_operation_failed'). Use for debugging.

4
data.amountNumber

Intended initial funding amount in micro-units (1 USD = 1,000,000).

5
data.referenceString

Unique reference string used to identify the creation request across systems.

6
data.created_statusString

Final outcome of the creation request. Value: 'failed'.

7
data.customer_emailString

Email of the customer the card was being created for.

virtualcard.created.failed

Virtual Card Terminated Refund

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

1
data.card_idUUID

Identifier of the terminated card.

2
data.idString

Unique transaction identifier for the termination refund.

3
data.amountNumber

Refund amount in micro-units.

4
data.referenceString

Reference for the termination refund. Joins to the matching ledger entry.

5
data.narrativeString

Description of the refund (typically 'card_termination_refund').

6
data.timestampISO 8601 String

When the refund was processed.

7
data.statusString

Final status. Value: 'completed'.

8
data.reasonString

Why the card was terminated (e.g. 'customer_requested_termination').

virtualcard.terminated.refund

Virtual Card Regularized

Fired when a card moves out of a degraded or indeterminate state into a stable status (typically active).

1
data.card_idUUID

Identifier of the card that was regularized.

2
data.statusString

New status after regularization (e.g. 'active').

3
data.referenceString

Reference for the regularization event.

virtualcard.regularized

Virtual Card Expiration

Fired when a card reaches its expiry date and can no longer be used.

1
data.card_idUUID

Identifier of the expired card.

2
data.expired_atISO 8601 String

Expiration timestamp.

virtualcard.expiration

Virtual Cards Top-Up

Virtual Card Top-up Completed

Fired when a top-up to a virtual card succeeds.

1
data.card_idUUID

Identifier of the card that was funded.

2
data.idString

Unique transaction identifier for the top-up.

3
data.amountNumber

Top-up amount in micro-units (1 USD = 1,000,000).

4
data.referenceString

Unique reference for the top-up. Joins to your wallet ledger entry.

5
data.narrativeString

Short description of the top-up.

6
data.statusString

Final status. Value: 'completed'.

7
data.initiated_fromString

Where the top-up originated (e.g. 'wallet').

8
data.is_terminatedBoolean

Whether the card was terminated after this top-up. Usually `false`.

virtualcard.topup.completed

Virtual Card Top-up Failed

Fired when a virtual card top-up fails.

Note

The payload shape matches Virtual Card Top-up Completed with status set to "failed". Refer to that section for field explanations.

virtualcard.topup.failed

Virtual Cards Withdrawal

Virtual Card Withdrawal Completed

Fired when a withdrawal on a virtual card succeeds.

1
data.card_idUUID

Identifier of the card the funds were withdrawn from.

2
data.idString

Unique transaction identifier for the withdrawal.

3
data.amountNumber

Withdrawal amount in micro-units.

4
data.referenceString

Unique reference for the withdrawal.

5
data.narrativeString

Short description of the withdrawal.

6
data.statusString

Final status. Value: 'completed'.

7
data.initiated_fromString

Where the withdrawal originated (e.g. 'card').

8
data.is_terminatedBoolean

Whether the card was terminated as a result of this withdrawal.

virtualcard.withdrawal.completed

Virtual Card Withdrawal Failed

Fired when a withdrawal on a virtual card fails.

Note

The payload shape matches Virtual Card Withdrawal Completed with status set to "failed".

virtualcard.withdrawal.failed

Virtual Cards Transaction

Virtual Card Transaction - Debit

Generic settled debit shape. Branch on transaction_type to distinguish settlement, authorization, pre_auth_approved, contactless_settled, and contactless_pending. The dedicated event names below are what is actually emitted on the wire — branch on event first.

1
data.card_idUUID

Identifier of the card that was charged.

2
data.idString

Unique identifier for this transaction.

3
data.amountNumber

Debit amount in micro-units (1 USD = 1,000,000).

4
data.display_amountNumber

Same value expressed as a major-unit float (e.g. 1.599 for $1.599).

5
data.currencyString

ISO 4217 currency code, lowercase (e.g. 'usd').

6
data.transaction_typeString

Bitnob's stable internal classification (e.g. 'settlement', 'authorization', 'pre_auth_approved', 'contactless_settled', 'contactless_pending').

7
data.referenceString

Local Bitnob reference. Joins to the matching ledger entry.

8
data.narrativeString

Human-readable transaction description.

9
data.timestampISO 8601 String

When the transaction occurred at the network.

10
data.statusString

Wire-level outcome (e.g. 'completed').

11
data.reasonString or null

Additional context if the network supplied it. Often null for successful debits.

12
data.merchant_nameString

Merchant or acceptor name (e.g. 'netflix_com').

13
data.merchant_mccString

ISO 18245 Merchant Category Code (e.g. '4899').

14
data.merchant_countryString

ISO 3166-1 alpha-2 country code of the merchant, lowercase (e.g. 'us').

virtualcard.transaction.debit

Virtual Card Transaction - Authorization

Fired when an authorization hold is approved by the provider; settlement is pending.

Note

Same payload shape as Virtual Card Transaction - Debit with transaction_type: "authorization".

virtualcard.transaction.authorization

Virtual Card Transaction - Settlement

Fired when a previously-authorized transaction is settled by the merchant.

Note

Same payload shape as Virtual Card Transaction - Debit with transaction_type: "settlement". Match against the earlier authorization using reference.

virtualcard.transaction.settlement

Virtual Card Transaction - Pre-Auth Approved

Fired when a pre-authorization hold is placed (e.g. hotel deposit, gas-pump pre-auth).

Note

Same payload shape as Virtual Card Transaction - Debit with transaction_type: "pre_auth_approved".

virtualcard.transaction.pre-auth.approved

Virtual Card Transaction - Contactless

Contactless / tap-to-pay payment. The 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.

Note

Same payload shape as Virtual Card Transaction - Debit.

virtualcard.transaction.contactless (settled)

Virtual Card Transaction - Credit

Generic credit posting on the card (e.g. refund credit, adjustment).

Note

Same payload shape as Virtual Card Transaction - Debit. transaction_type will reflect the kind of credit (e.g. refund_settled).

virtualcard.transaction.credit

Virtual Card Transaction - Refund

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

Note

Same payload shape as Virtual Card Transaction - Debit. Branch on transaction_type.

virtualcard.transaction.refund

Virtual Card Transaction - Reversed

Fired when an authorization is reversed before settlement. Same event string is also emitted for cross-border reversals — transaction_type is reversal_settled for auth reversals and cross_border_reversal for cross-border.

1
data.card_idUUID

Identifier of the card whose transaction was reversed.

2
data.idString

Unique identifier for the reversal transaction.

3
data.amountNumber

Amount returned to the card in micro-units.

4
data.display_amountNumber

Same value as a major-unit float.

5
data.currencyString

Lowercase ISO 4217 currency code.

6
data.transaction_typeString

'reversal_settled' for auth reversals; 'cross_border_reversal' for cross-border reversals.

7
data.referenceString

Reference for the reversal; correlates to the original authorization.

8
data.narrativeString

Human-readable description, often referencing the original merchant.

9
data.timestampISO 8601 String

When the reversal occurred at the network.

10
data.statusString

Final status. Value: 'completed'.

11
data.reasonString or null

Why the reversal occurred (e.g. 'authorization_expired').

12
data.merchant_nameString

Merchant from the original transaction.

virtualcard.transaction.reversed

Virtual Card Transaction - Verification

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

1
data.card_idUUID

Identifier of the card being verified.

2
data.idString

Unique identifier for the verification transaction.

3
data.amountNumber

Always 0 for verification.

4
data.display_amountNumber

Always 0.0 for verification.

5
data.currencyString

Lowercase ISO 4217 currency code.

6
data.transaction_typeString

Value: 'verification'.

7
data.referenceString

Reference for the verification authorization.

8
data.narrativeString

Short description of the verification (e.g. 'card_verification_at_uber').

9
data.timestampISO 8601 String

When the verification occurred.

10
data.statusString

Value: 'completed'.

11
data.merchant_nameString

Merchant initiating the verification.

12
data.merchant_mccString

ISO 18245 MCC.

13
data.merchant_countryString

Lowercase ISO 3166-1 alpha-2 country code.

virtualcard.transaction.verification

Virtual Card Transaction - Cross-border (Pending)

Fired when a cross-border fee is debited from the company wallet during the auth phase of an international transaction. The amount on this payload is the FEE, not the purchase principal — the purchase principal lands later as cross_border_settled against the card balance.

Two money paths

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

  • cross_border_pending → company wallet fee debit (this event)
  • cross_border_settled → card balance purchase principal debit

Correlate via reference (CARD_CROSSBORD_*).

1
data.card_idUUID

Identifier of the card used for the cross-border transaction.

2
data.amountNumber

Cross-border FEE charged to the company wallet, in micro-units. NOT the purchase principal.

3
data.charged_amountNumber

Same as `amount` — the fee amount debited to the wallet.

4
data.display_amountNumber

Fee amount as a major-unit float.

5
data.currencyString

Lowercase ISO 4217 currency code.

6
data.transaction_typeString

Value: 'cross_border_pending'.

7
data.settlement_countryString

Country where the original transaction is being settled (ISO 3166-1 alpha-2, lowercase).

8
data.referenceString

Local reference, prefixed `CARD_CROSSBORD_*`. Use it to correlate with the matching `cross_border_settled` event.

9
data.narrativeString

Description referencing the underlying authorization id.

10
data.timestampISO 8601 String

When the fee was debited.

11
data.statusString

Value: 'pending' — reflects the purchase lifecycle, not the fee (the fee debit itself is final).

12
data.merchant_nameString

Merchant for the underlying purchase.

13
data.merchant_mccString

ISO 18245 MCC.

14
data.merchant_countryString

Lowercase ISO 3166-1 alpha-2 country code.

virtualcard.transaction.crossborder (pending)

Virtual Card Transaction - Cross-border (Settled)

Fired when the purchase principal of a cross-border transaction is deducted from the card balance. No company-wallet movement on this event — the company-wallet fee debit (if any) was emitted separately as cross_border_pending.

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 (it is the settlement event, just unverified). Recovery is via the awaiting-verification reconciler.

virtualcard.transaction.crossborder (settled)

Virtual Card Transaction - Terminated Refund

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

1
data.card_idUUID

Identifier of the terminated card.

2
data.idString

Unique identifier of the refund transaction.

3
data.amountNumber

Refunded amount in micro-units.

4
data.referenceString

Reference for the refund.

5
data.narrativeString

Description of the refund.

6
data.timestampISO 8601 String

When the refund was processed.

7
data.statusString

Value: 'completed'.

8
data.reasonString

Why the refund was issued (e.g. 'card_terminated_by_user').

virtualcard.transaction.terminated.refund

Virtual Card Declines

Virtual Card Transaction - Declined

Fired when a purchase attempt is declined (e.g. insufficient funds, CVV mismatch).

1
data.card_idUUID

Identifier of the card whose transaction was declined.

2
data.idString

Unique identifier of the decline event.

3
data.amountNumber

Attempted amount in micro-units.

4
data.display_amountNumber

Attempted amount as a major-unit float.

5
data.currencyString

Lowercase ISO 4217 currency code.

6
data.transaction_typeString

Indicates the underlying flow that was declined (e.g. 'debit').

7
data.referenceString

Reference for the declined attempt.

8
data.narrativeString

Human-readable description of the attempt.

9
data.timestampISO 8601 String

When the decline occurred.

10
data.statusString

Value: 'declined'.

11
data.reasonString

Machine-readable reason code (e.g. 'insufficient_funds').

12
data.merchant_nameString

Merchant attempting the charge.

13
data.merchant_mccString

ISO 18245 MCC.

14
data.merchant_countryString

Lowercase ISO 3166-1 alpha-2 country code.

virtualcard.transaction.declined

Virtual Card Transaction - Declined Charge

Fired when a decline-rule violation fee is charged to your USD wallet after repeated declines, or when a card is terminated due to reaching the violation threshold. See the Decline Rule Policy for full details.

1
data.card_idUUID

Identifier of the card that triggered the violation.

2
data.idString

Unique identifier of the fee transaction.

3
data.fee_amountNumber

Fee amount charged in micro-units.

4
data.violation_countNumber

Total number of decline-rule violations recorded on this card.

5
data.referenceString

Reference for the fee transaction.

6
data.statusString

Value: 'completed'.

virtualcard.transaction.declined.charge

Virtual Card Transaction Declined - Frozen

Fired when an authorization is declined because the card is frozen (usually triggered by a fraud rule).

1
data.card_idUUID

Identifier of the frozen card.

2
data.reasonString

Why the card was frozen / declined (e.g. 'card_is_frozen').

3
data.referenceString

Reference for the declined attempt.

virtualcard.transaction.declined.frozen

Virtual Card Transaction Declined - Terminated

Fired when an authorization is attempted on a terminated card.

1
data.card_idUUID

Identifier of the terminated card.

2
data.idString

Unique identifier of the decline event.

3
data.amountNumber

Attempted amount in micro-units.

4
data.display_amountNumber

Attempted amount as a major-unit float.

5
data.transaction_typeString

Underlying flow (e.g. 'debit').

6
data.referenceString

Reference for the declined attempt.

7
data.statusString

Value: 'declined'.

8
data.transaction_referenceString

Reference of the original authorization that was attempted.

9
data.balance_before_terminationNumber

Balance on the card immediately before termination (in micro-units).

10
data.merchant_nameString

Merchant attempting the charge.

11
data.merchant_mccString

ISO 18245 MCC.

12
data.merchant_countryString

Lowercase ISO 3166-1 alpha-2 country code.

virtualcard.transaction.declined.terminated

Virtual Card Transaction Authorization Failed

Fired when an authorization is rejected (e.g. bad PAN/CVV, AVS failure).

1
data.card_idUUID

Identifier of the card used in the failed authorization.

2
data.idString

Unique identifier of the failed authorization attempt.

3
data.amountNumber

Attempted amount in micro-units.

4
data.display_amountNumber

Attempted amount as a major-unit float.

5
data.currencyString

Lowercase ISO 4217 currency code.

6
data.transaction_typeString

Value: 'authorization'.

7
data.referenceString

Reference for the failed attempt.

8
data.statusString

Value: 'failed'.

9
data.reasonString

Why the authorization failed (e.g. 'avs_mismatch').

10
data.merchant_nameString

Merchant attempting the charge.

11
data.merchant_mccString

ISO 18245 MCC.

12
data.merchant_countryString

Lowercase ISO 3166-1 alpha-2 country code.

virtualcard.transaction.authorization.failed

Virtual Card Transaction Issuer Expiration

Fired when an authorization is declined because the card has expired at the issuer.

1
data.card_idUUID

Identifier of the expired card.

2
data.idString

Unique identifier of the decline event.

3
data.amountNumber

Attempted amount in micro-units.

4
data.display_amountNumber

Attempted amount as a major-unit float.

5
data.currencyString

Lowercase ISO 4217 currency code.

6
data.transaction_typeString

Value: 'authorization'.

7
data.referenceString

Reference for the declined attempt.

8
data.statusString

Value: 'declined'.

9
data.reasonString

Value: 'card_expired'.

virtualcard.transaction.issuerexpiration

Activation & KYC

Virtual Card Contactless Activation

Fired when a customer's KYC and card activation for contactless / NFC support completes.

1
data.card_idUUID

Identifier of the card that was activated for contactless.

2
data.customer_idString

Identifier of the customer that owns the card.

3
data.statusString

Value: 'activated'.

4
data.activated_atISO 8601 String

Activation timestamp.

virtualcard.contactless.activation

Virtual Card KYC Completed

Fired when KYC verification succeeds for a customer.

1
data.idString

Unique identifier of the KYC outcome.

2
data.customer_idString

Identifier of the customer that was verified.

3
data.customer_emailString

Email of the verified customer.

4
data.reasonString

Outcome detail (e.g. 'verification_passed').

5
data.kyc_passedBoolean

Value: true.

virtualcard.user.kyc.completed

Virtual Card KYC Failed

Fired when KYC verification fails for a customer.

Note

The payload shape matches Virtual Card KYC Completed with kyc_passed: false and a reason describing the failure.

virtualcard.user.kyc.failed

Did you find this page useful?

Join our Discord