Card issuer events and general crypto events (deposits, swaps, transactions) are delivered through the same webhook service. Register once at
POST /v1/webhook/create-webhook and subscribe to any combination of event types.How It Works
Your server exposes a public HTTPS URL. Yativo sends a signedPOST request to that URL each time an event occurs in your issuer program. Your server verifies the signature, acknowledges receipt with HTTP 200, and processes the event.
Step 1 — Build Your Webhook Endpoint
Your endpoint must:- Accept
POSTrequests at a public HTTPS URL - Capture the raw request body before JSON-parsing it (required for signature verification)
- Return HTTP
2xxwithin 30 seconds - Process event logic asynchronously after returning 200
- TypeScript (Express)
- Python (Flask)
- PHP
Step 2 — Register Your Webhook
Register your endpoint at the unified webhook endpoint. You can subscribe to card issuer events, general crypto events, or any mix.Response
"events": ["*"] to receive every event type. You can narrow the list later via PUT /v1/yativo-card/webhooks/:webhookId.
Step 3 — Verify Signatures
Every webhook POST includes four security headers:| Header | Description |
|---|---|
X-Yativo-Signature | sha256=<HMAC-SHA256 hex> of the signed string |
X-Yativo-Timestamp | Unix timestamp (seconds) when the event was dispatched |
X-Yativo-Event | The event type (e.g. customer.funded) |
X-Yativo-Delivery-Id | Unique delivery ID — used to retry individual deliveries |
<raw JSON body> is the exact bytes received — not re-serialized. This is why you must capture the raw body before JSON-parsing.
Replay attack protection: Also check that the timestamp is within 5 minutes of your server clock. Discard requests that are older.
The Event Lifecycle
The following diagram shows the full lifecycle of a card issuer program, and where webhook events fire at each stage.Master wallet deposit → settled
When you send funds to your master wallet deposit address, Yativo firesmaster_wallet.deposit events as the deposit progresses. The number of events depends on the deposit path:
- Two-step deposits (funds routed through an intermediate settlement): a
processingevent fires first when the deposit is detected, followed by asettledevent when the funds land in your spendable balance. - One-step deposits (funds that settle directly): only a single
settledevent fires — there is no precedingprocessingevent.
Always act on
status: "settled" — this is the authoritative confirmation that funds are available. If you receive only a settled event with no prior processing event, that is expected for one-step deposit paths.Fund customer card
When you callPOST /v1/card-issuer/fund-customer, two events fire in tandem: master_wallet.customer_funded (your master wallet was debited) and customer.funded (the customer’s card wallet was credited).
customer.funding.failed fires.
Card transaction lifecycle
When your customer uses their card, events fire at each stage of the authorization → settlement cycle.transaction.declined instead of transaction.authorized. A reversal fires transaction.reversed.
Card status changes
Card lifecycle events fire whenever a card’s status changes — whether triggered by your API, by the customer, or by a compliance action.Event Reference
Amount fields — Every monetary amount is provided in two formats:
amount(float) — human-readable decimal, e.g.12.50amount_minor(integer) — minor currency units (cents), e.g.1250
null when the amount is unavailable.Envelope
All events share this structure. Thedata object is event-specific.
| Field | Type | Description |
|---|---|---|
id | string | Unique event ID — use as idempotency key |
type | string | Event type |
created_at | ISO 8601 | When the event was generated |
data | object | Event-specific payload (see below) |
master_wallet.deposit
Fires when a deposit is detected or settles into your spendable balance. One-step deposits fire only settled. Two-step deposits fire processing first, then settled.
All amount fields are provided as both a decimal float and an integer in minor units (cents). For example, 500.00 USD → amount: 500.00, amount_minor: 50000.
Processing
Settled — regular deposit
Settled — withdrawal credit
master_wallet.deposit fires for two distinct scenarios: (1) a new deposit credited to your master wallet, and (2) funds returning to your master wallet when a card withdrawal settles. In the withdrawal case the payload includes withdrawal_id and yativo_card_id so you can correlate it with the originating card.withdrawal.settled and master_wallet.withdrawal events. When withdrawal_id is absent the event is a regular deposit.| Field | Type | Description |
|---|---|---|
source_currency | string | Currency/chain of the incoming deposit (e.g. USDC_SOL, USDC_XDC). Present on processing events only. |
source_amount | float | Amount sent by the depositor, in source_currency units. Present on processing events. |
source_amount_minor | integer | source_amount in minor units (cents). |
deposit_amount | float | Amount that arrived in your wallet after settlement. Present on settled events. |
deposit_amount_minor | integer | deposit_amount in minor units (cents). |
wallet_balance | float | null | Total wallet balance after this credit. Present on settled events. |
wallet_balance_minor | integer | null | wallet_balance in minor units (cents). |
currency | string | Wallet currency (e.g. USD, EUR, GBP). Present on settled events. |
settlement_id | string | null | Internal settlement or bridge transaction reference. |
tx_hash | string | null | On-chain transaction hash of the final settlement. |
solana_tx_hash | string | null | Original Solana deposit transaction signature. Present for USDC_SOL deposits; null for all other paths including withdrawal credits. |
withdrawal_id | string | null | Present only when this event is a withdrawal credit. Matches the withdrawal_id on the corresponding card.withdrawal.settled and master_wallet.withdrawal events. |
yativo_card_id | string | null | Present only when this event is a withdrawal credit. Identifies the card the funds were withdrawn from. |
master_wallet.swap
Fired when a currency swap is submitted from your master wallet (e.g. USD → EUR).
master_wallet.customer_funded
Fired alongside customer.funded whenever your master wallet is debited. Includes remaining_balance when available and tx_hash with the on-chain transaction hash of the funding transfer.
customer.funded
Fired when a customer’s card wallet has been successfully credited.
customer.funding.failed
Fired when a funding transfer to a customer’s card wallet fails.
wallet.deposit.confirmed
Fired when a customer’s account wallet balance increases — i.e. a deposit has arrived. This is the primary event for detecting incoming funds to a customer card wallet.
| Field | Type | Description |
|---|---|---|
amount | float | Amount deposited (balance delta) |
amount_minor | integer | amount in minor units (cents) |
new_balance | float | Customer’s new total balance after the deposit |
new_balance_minor | integer | new_balance in minor units (cents) |
currency | string | null | Card currency (USD, EUR, GBP) |
A
customer.balance.updated event fires for the same balance change. wallet.deposit.confirmed fires only when the balance increased; customer.balance.updated fires for all balance changes including spend and reversal.customer.balance.updated
Fired after any card balance change — top-up, purchase authorization, settlement, reversal, or periodic reconciliation.
| Field | Type | Description |
|---|---|---|
ledger_balance | float | Total balance in the customer’s account wallet |
ledger_balance_minor | integer | ledger_balance in minor units (cents) |
available_balance | float | Spendable balance after pending authorizations |
available_balance_minor | integer | available_balance in minor units (cents) |
pending_balance | float | null | Held for authorized but not yet settled transactions. null when no pending authorizations |
pending_balance_minor | integer | null | pending_balance in minor units (cents) |
currency | string | null | USD, EUR, or GBP |
balance_source | string | null | "reconciliation" when fired by the background reconciliation job; absent or null for live card events |
trigger | string | null | What we determined caused this balance change: "funding", "withdraw", or "transaction" (a card purchase). null if we couldn’t match it to a known operation within a 10-minute window. |
transfer_id | string | null | Our own reference for the underlying operation — matches the transfer_id you already received on customer.funded/master_wallet.customer_funded (funding), the withdrawal_id from the withdrawal endpoints (withdrawal), or the transaction reference (card purchase). Use this to reconcile a customer.balance.updated event back to the operation that caused it. |
tx_hash | string | null | On-chain hash for the underlying operation, when one exists (funding/withdrawal). null for card purchases, which don’t have a dedicated on-chain leg at this stage. |
pending_balance / pending_balance_minor are always null when balance_source is "reconciliation" — the reconciliation job reads the on-chain settled balance and cannot determine pending authorizations. Use available_balance for spendable funds.A single top-up typically produces two
customer.balance.updated events a couple of minutes apart — the first when the ledger reflects the incoming funds, the second when they become spendable (available_balance catches up). Both carry the same trigger/transfer_id, since both stem from the same underlying operation — use transfer_id to recognize them as the same event rather than treating the second as a new, unrelated top-up.trigger/transfer_id/tx_hash are inferred by matching this balance change against our own records of your recent funding/withdrawal/transaction activity for this card, not supplied directly by the card network (which doesn’t include a correlator on this event type). If nothing matched within the 10-minute window, all three come back null.card.created
Fired when a new virtual card is issued to a customer. card_id is the card token identifier. type and status reflect values from the upstream card provider and may be null depending on the event payload received.
card.activated
Fired when a physical card is activated by the cardholder. Virtual cards do not fire this event.
card.frozen / card.unfrozen
For card.frozen the status field reflects the upstream frozen status string. For card.unfrozen the status field is "active" — the upstream provider reports the card status returning to active when a freeze is lifted.
card.frozen
card.unfrozen
The
card_id in webhook events is the card token identifier and may differ from the card_id returned by the Get Customer API. Use yativo_card_id as the stable cross-reference between events and API responses.card.lost / card.stolen / card.voided / card.cancelled / card.deactivated
Same shape as card.frozen — the envelope type identifies the event and data.status reflects the upstream status string for that change.
Transaction event field reference
Alltransaction.* events share the same data shape. Fields are populated from card network data and some may be null depending on the transaction type or network.
| Field | Type | Notes |
|---|---|---|
yativo_card_id | string | Stable card identifier — use this as the cross-reference key |
card_id | string | null | Card token from the card network |
transaction_id | string | null | Network transaction ID. Use this as the key for deduplication. Falls back to the webhook event id when not provided by the card network. |
blockchain_tx_hash | string | null | Hash of the on-chain settlement transaction that debited the card’s wallet. Populated on transaction.authorized (we wait for this to be available before dispatching — see note below). May be null on other event types where no on-chain leg exists yet (e.g. transaction.declined, or a reversal that never settled on-chain). |
type | string | null | Transaction type as reported by the card network (e.g. "Payment", "Refund") |
amount | float | null | Transaction amount as a decimal number, in the merchant’s local currency |
amount_minor | integer | null | amount in minor units (cents) |
currency | string | null | ISO 4217 alpha-3 currency code of amount — the merchant’s local currency (e.g. "EUR", "USD", "BRL") |
billing_amount | float | null | The amount actually debited from the card’s own settlement currency. Present on every transaction, not only foreign-currency ones — on a same-currency purchase it simply equals amount. |
billing_amount_minor | integer | null | billing_amount in minor units (cents) |
billing_currency | string | null | ISO 4217 alpha-3 currency code of billing_amount — the card’s own currency. Equals currency when the purchase was made in the card’s native currency. |
fx_rate | float | null | Local-currency units per 1 unit of billing currency: fx_rate = amount / billing_amount. Reads as “1 = ” — e.g. 924.3827 means 1 USD = 924.3827 CLP. Equals 1 on a same-currency purchase. Provided so you don’t have to compute it, but if you ever need to derive billing_amount yourself: billing_amount = amount / fx_rate. |
merchant | string | null | Merchant name — null for contactless/tap or when not provided by the network |
merchant_category | string | null | Merchant category code (MCC) |
merchant_city | string | null | Merchant city — null when not provided |
merchant_country | string | null | ISO 3166 alpha-2 country code (e.g. "FR", "BR") — null when not provided |
status | string | null | Raw status string from the card network — values vary (e.g. "Approved", "Declined") |
timestamp | ISO 8601 | When the card network event occurred |
transaction.authorized
Fired once per card purchase, when the authorization clears (not at the earlier point-of-sale authorization moment — we intentionally wait for clearing so this event always carries both transaction_id and blockchain_tx_hash together, rather than firing an incomplete notification first). A customer.balance.updated event follows immediately.
Use
data.transaction_id (not event.id) as your reconciliation key. If transaction_id is null, fall back to event.id.This card settles in EUR and the purchase was also in EUR, so
billing_amount/billing_currency simply match amount/currency and fx_rate is 1 — no conversion took place. See the transaction.settled example below for a foreign-currency purchase, where they differ.transaction.settled
Fired when the merchant batch clears and the authorization is finalized.
This example shows a foreign-currency purchase: a USD-denominated card used at a Brazilian merchant.
amount/currency are the merchant’s local currency (BRL, what was charged at the point of sale); billing_amount/billing_currency are what was actually debited from the card (USD). fx_rate (5.0124) reads as “1 USD = 5.0124 BRL” — check: 101 / 5.0124 ≈ 20.15, which matches billing_amount.blockchain_tx_hash on transaction.settled is not guaranteed the way it is on transaction.authorized — treat it as best-effort here.merchant and merchant_city may come back null on some networks/transaction stages even when populated on the corresponding transaction.authorized event for the same transaction_id — treat them as best-effort, not guaranteed on every event.transaction.declined
Fired when a card transaction is declined.
transaction.reversed
Fired when an authorization is reversed (e.g. a cancelled hold before settlement).
transaction.refund.created
Fired when a merchant issues a refund.
Managing Webhooks
List all subscriptions
Get one subscription
Update URL, events, or enabled state
Rotate signing secret
Delete a subscription
Delivery history
Retry a failed delivery
Retry Policy
If your endpoint returns a non-2xx response, times out (> 30 s), or is unreachable, Yativo retries using exponential backoff:| Attempt | Delay after previous attempt |
|---|---|
| 1 (initial) | — |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
| 7 | 24 hours |
POST /v1/yativo-card/webhooks/deliveries/:deliveryId/retry.
If your endpoint fails 100 consecutive deliveries, Yativo automatically disables the subscription to protect your program’s event queue. Re-enable it with PUT /v1/yativo-card/webhooks/:webhookId and { "enabled": true } after resolving the issue.
Best Practices
Return 200 first, process after. Yativo waits up to 30 seconds for a response. Enqueue the event to a job queue (BullMQ, Celery, SQS) before returning — don’t block on database writes or downstream calls. Deduplicate withevent.id. Yativo deduplicates deliveries server-side so the same upstream event is not queued twice, but network-level retries can still cause your endpoint to receive the same delivery more than once. Store processed event IDs in a database table with a unique constraint and skip duplicates.
Do not return 200 speculatively. Only acknowledge when you have successfully enqueued the event. If your queue is unavailable, return 500 to trigger a retry.
Handle unknown event types gracefully. Log and ignore event types you don’t recognise rather than throwing an error. Yativo adds new event types over time — unknown types should not break your handler.
Subscribe to the events you need. Use a focused event list rather than "*" in production to reduce noise and make your handler logic explicit.
Idempotent Handler Pattern
Testing
Use the sandbox environment to test your webhook handler without real funds:- Sandbox base URL:
https://crypto-sandbox.yativo.com/api/v1/ - Register webhooks the same way as production — use the same endpoint
POST /v1/webhook/create-webhook - Expose your local server using ngrok or Hookdeck during development

