- How webhooks work
- Registering your endpoint
- Verifying the signature
- The payload envelope
- The customer object
- Event catalog
- Responding to a webhook
- Retries, delivery logs and resending
- Testing your integration
- Dashboard UI: adding Resend to the delivery log
- Best practices
How webhooks work
- You register a single HTTPS URL on your account. Every event is sent to that one URL.
- When an event occurs, Yativo sends an
HTTP POSTrequest to your URL with a JSON body describing the event. - Your endpoint verifies the request signature, processes the event, and returns a
2xxstatus code quickly.
Note: the signature header is namedSignature, notX-SignatureorX-Webhook-Signature.
Registering your endpoint
All webhook management endpoints live under/v1/business/webhook and require your API credentials:
Create or replace your webhook
200 OK
- One endpoint per account. Calling
POST /v1/business/webhookagain replaces your URL and issues a new secret. It doesn’t add a second endpoint. - Save the
secretstraight away; you need it to verify signatures. Every endpoint below also returns it. - Every event goes to your URL. There is no per-event subscription, so ignore
event.typevalues you don’t handle. - Right after you save a webhook, Yativo sends a
webhook_updatedtest event to the new URL.
List your webhook
data array.
Update your webhook URL
Rotate your signing secret
secret. The previous secret stops working immediately, so deploy the new one to your endpoint right away.
Delete your webhook
Error responses
All endpoints above return this shape on failure:Verifying the signature
Every webhook request carries aSignature header:
PHP
Node.js (Express)
Python (Flask)
The payload envelope
Almost every event uses this two-key envelope:event.typeidentifies the event, for exampledeposit.created.payloadholds the event data, documented per event below.
payload (such as id, event_id, created_at or status) for idempotency and ordering.
Gift card events are the one exception. They have noevent.type/payloadwrapper. Seegiftcard.status_changed.
The customer object
Every event includes acustomer object, so you never need a second API call to find out whose event it is. It sits inside payload (at the top level for gift card events), next to customer_id:
customer is null when the event isn’t about one of your customers, for example user.updated, webhook_updated or a platform-wide method change. Only these five fields are sent; KYC documents and other sensitive data are never included.
Changed on 5 October 2026. Thecustomerobject is now included on every event, always with the five fields above. Events that previously sent a shortercustomerobject (virtual_account.*,virtual_account.deposit,endorsement.updated) now send the full one.
Event catalog
Changed on 21 October 2026.user.updated,customer.created,customer.updatedanddeposit.*now send only the fields documented below.user.updatedandcustomer.updatedare no longer sent for changes to fields you don’t receive (for example signing in).endorsement.updatedno longer includes_webhook_id.virtual_account.deletedis new.
deposit.created / deposit.updated
Sent when a fiat deposit (pay-in) is created and whenever its status changes.
status is one of pending, processing, success, incomplete, failed, cancelled or expired.
amount is the checkout amount that was requested. Arc Onramp settlements also include:
Those two extra fields are only sent once the payment has settled. The settlement webhook is the one that carries them; a processing update is not sent for that hop.
Deposits with a currency conversion also include
currency_from, currency_to, amount_from, amount_to and deposit_currency. Only these fields and the ones above are sent.
payout.updated
Sent whenever a payout changes status. There is no payout.created event; the first one you see is usually pending or processing.
status follows the payout lifecycle: pending, processing, completed, failed, cancelled or refunded. Fields on beneficiary vary slightly by payout rail and currency.
reference is the payment reference you sent when creating the payout, or null if you didn’t send one. It is what was requested; the recipient’s bank may show a shortened version, depending on the rail.
customer.created / customer.updated
Sent when a customer is created on your account, and when one of the fields below or the customer’s KYC status changes.
virtual_account.created / virtual_account.updated
Sent when a virtual account (a dedicated bank account number) is provisioned or updated.
customer is null when the account belongs to your business rather than a customer.
virtual_account.deleted
Sent when you delete a virtual account with DELETE /v1/business/virtual-account/delete-virtual-account/{account_id}. The account stops accepting funds. The payload has the same fields as virtual_account.created.
virtual_account.deposit
Sent when funds land in one of your virtual accounts.
amount_receviedis spelled that way in the payload, so map it as-is.amountis the net amount credited;amount_receviedis the gross amount received before fees.
metadata.created / metadata.updated
Sent when a key/value metadata tag is attached to, or edited on, one of your transactions.
tracking.created
Sent when a status update is posted against one of your transactions.
endorsement.updated
Sent when a customer’s KYC endorsement status changes, for example when they’re approved for a new payment rail.
endorsements is the customer’s full current list, not just the one that changed. Compare it with your stored copy to see what changed.
RFI events (rfi.created, rfi.updated, rfi.response_required)
Sent when Yativo records a compliance request for information on a customer or transaction tied to your account. Payloads do not include upstream partner names, external reference ids, or third-party dashboard URLs.
A new RFI that already needs your action may send
rfi.created and rfi.response_required in quick succession. Use payload.id (Yativo RFI id) for idempotency.
rfi.created
rfi.updated
Same payload shape as above. status may be under_review or closed; response_required becomes false when no action is needed from you.
rfi.response_required
Same payload shape; sent when response_required is true and the RFI newly needs your team (often together with rfi.created).
Reply once with every requested item via PUT /api/v1/business/rfis/{id}/draft-responses (or POST …/responses). Replies are final until Yativo reopens the RFI. Yativo reviews before anything is sent upstream.
Sandbox: POST /v1/demo/webhook/rfi (optional body event: rfi.created, rfi.updated, rfi.response_required).
user.updated
Sent when your account’s name, email, membership id or KYC status changes. Logins, security settings and other account activity don’t send it.
business holds only the fields above (each is present when it’s set on your account). Only these fields are sent; documents, directors, shareholders and verification results are never included.
webhook_updated
Sent once, immediately, to your new URL after POST /v1/business/webhook, so you can confirm your endpoint receives events and your signature check works.
This payload contains your webhook secret in plain text. Handle it like the registration response and don’t log it.
payin_method.enabled / payin_method.disabled
A platform-wide notice, sent to every business with a webhook, when a pay-in method is turned on or off.
payout_method.enabled / payout_method.disabled
The same as above, for payout methods.
crypto.wallet.created
Sent when a crypto deposit address is created.
is_customer is false the wallet belongs to your business, and customer is null.
crypto.deposit.completed
Sent when a crypto deposit lands. Listen for this one.
transaction_hash is the on-chain transaction hash, which you can look up on a block explorer. Use it rather than transaction_id to match a deposit to its on-chain transfer: transaction_id is our reference for the deposit and isn’t always the on-chain hash. transaction_hash is null if the hash isn’t available.
crypto_deposit
An older event name for a completed crypto deposit, still sent by some flows. It has the same payload as crypto.deposit.completed, plus user_id. Handle both names until you’ve confirmed you only receive the newer one.
wallet.deleted
Sent when a crypto wallet is deleted. Funds sent to it afterwards can’t be recovered.
giftcard.status_changed (different envelope)
Gift card events have no event.type/payload wrapper. The event name is under event, and customer_id and customer are at the top level:
event.type for every other event, event for gift cards.
Virtual card events (virtualcard.*)
On sandbox you can send any of these events with POST /api/v1/demo/webhook/virtualcard or POST /api/v1/demo/webhook/{event} — see Testing your integration on sandbox. To simulate a charge that is stored on the card, use Testing on sandbox.
Every virtual card payload includes:
event_id: unique per event and identical on every redelivery of it. Use it to deduplicate.card_id: the card id returned when you created the card, which you pass to the card endpoints.customer_idandcustomer: the cardholder (see The customer object).
4.5 means $4.50).
Changed on 5 October 2026. Virtual card events used to arrive with no envelope, with amounts in cents, and with camelCase fields such ascardId. They now use the standard envelope, snake_case fields, decimal amounts and thecustomerobject. The success events were also renamed:.successis now.completed(.completefor KYC). If you parsed the old shape, update your handler.
KYC
kyc_status is under_review, approved or rejected. Show rejection_details[].message to your customer and branch your own logic on code.
Card lifecycle
When you terminate a card through the API, the remaining balance reaches your wallet when virtualcard.terminated.refund arrives, not in the terminate response.
Funding
reference is the reference returned by your top-up or withdrawal request, so you can match the event to it.
Card transactions
These events report spending on the card balance. Apart from the fees noted below, they don’t move money in your wallet.
All of them share this payload:
Some events fire with several
transaction_type values. Branch on event.type first, then on transaction_type.
Cross-border purchases send virtualcard.transaction.crossborder twice, with the same reference:
transaction_type: cross_border_pending: the cross-border fee.fee_amountis included, and that fee is debited from your USD wallet.statusstayspendinguntil the purchase settles.transaction_type: cross_border_settled: the purchase amount leaves the card balance. There is no wallet movement.
Declines
Declined and failed authorizations count toward Yativo’s limit of 2 failed charges. When a card reaches it, the card is frozen and you receive
virtualcard.deactivated:
Business spend card events (business_spend_card.*)
Events for the Business Spend Card, the corporate card product (separate from the virtualcard.* events above). card_id is always your own card id; pass it back to /v1/spend/cards/*. Events not tied to one of your customers carry "customer": null.
On sandbox you can send any of these events with POST /api/v1/demo/webhook/spendcard or POST /api/v1/demo/webhook/{event} — see Testing your integration on sandbox. To simulate an authorization that is stored on the card, use Testing on sandbox.
business_spend_card.created: a card was issued, through the API or by an employee in the employee portal. The payload is the card object the card endpoints return:
business_spend_card.funded / business_spend_card.withdrawn: money moved into or out of an employee’s card balance (POST /v1/corporate-card/employees/balance) or a card’s wallet (POST /v1/spend/cards/wallet). For employee balances, card_id is null and employee_id is set, because the balance is shared by all of that employee’s cards:
business_spend_card.terminated: a card was closed, including the automatic termination after a full-balance withdrawal. The payload has card_id, reason (for example balance_withdrawn_in_full), customer_id and customer.
business_spend_card.pin_updated / business_spend_card.limits_updated: a card’s PIN or spend limits changed. The PIN is never included. limits_updated carries the limits as submitted:
business_spend_card.transaction.approved / .declined / .refunded / .reversed: a card swipe or ATM authorization. Each is also saved to your transaction history (see below); transaction_id is that record’s id, which you need to attach notes or invoices.
business_spend_card.transaction.disputed: you raised a dispute on a transaction (POST /v1/business/spend-card/transactions/{id}/dispute or POST /v1/spend/cards/transactions/{transaction_id}/dispute). The dispute fee has already been charged. The payload has transaction_id, card_id, amount, currency, disputed_at and customer.
business_spend_card.status_changed: a card’s status changed, for example activated, suspended or replaced:
Transaction history, notes and invoices
Everybusiness_spend_card.transaction.* event is backed by a transaction record you can fetch, annotate and attach receipts to:
422 and never stored. Files are kept in private storage, and GET .../invoices/{id}/download returns one only after confirming you own the transaction.
Card Checkout events (card_checkout.order.*)
Events for Card Checkout orders. The payload is the order object (id, customer_id, status, amounts). See the Card Checkout guide for the full list and payload.
virtual_card.deposit (sandbox only)
Only sent by the sandbox demo endpoint POST /api/v1/demo/webhook/virtual_card_deposit. Nothing in production sends it, so don’t build on it.
Responding to a webhook
- Return a
2xxstatus as soon as you’ve safely stored or queued the event. Do slow work (calling other APIs, sending emails) afterwards. - We don’t read your response body; an empty
200 OKis enough. - A non-
2xxstatus, a connection error or a timeout counts as a failed delivery.
Retries, delivery logs and resending
Failed deliveries are retried with exponential backoff, a few times, after which the event is dropped. Some events are delivered inline with the action that triggered them, so respond quickly (well under a few seconds); a slow endpoint can slow down the API call behind the event. Every delivery attempt, successful or not, is logged:GET /v1/business/events/show/{eventId} returns 404 with Webhook event not found when the ID isn’t in your log.
Resending an event
If your endpoint was down or rejected an event, you can redeliver it yourself. To put a Resend button on the dashboard delivery log, see Webhook resend — dashboard UI.{eventId} is the id from the delivery log above, not an id from inside the event’s payload.
- The original payload is resent, signed with your current secret.
- It goes to the URL you have registered now, even if you’ve changed it since.
- Fields that events no longer carry are left out.
- The resend appears as a new entry in the delivery log.
- You can only resend events from your own account.
- We record who resent it, when, and the original device IP (not a Cloudflare hop). That object is on the delivery log as
resendand is not sent to your webhook URL.
via is dashboard, api_key, or admin. On the original log, resend is the latest redelivery of that event; on the new log row it describes this attempt.
Errors
Testing your integration on sandbox
To settle a real sandbox payin or payout, credit a virtual account, or simulate a virtual-card / spend-card charge — so the record exists afterwards — use the helpers in Testing on sandbox. You can also send sample events to your registered URL without creating real records:{type} can be:
- A legacy slug:
payin,payout,virtual_account,virtual_account_created,virtual_account_updated,virtual_account_deleted,virtual_card_deposit,customer,customer_updated,endorsement,metadata_created,metadata_updated,webhook_updated,giftcard,user,tracking,payin_method_enabled,payin_method_disabled,payout_method_enabled,payout_method_disabled,crypto_wallet,crypto_deposit,wallet_deleted. - A documented event name, for example
deposit.updated,virtual_account.deleted,virtualcard.topup.completed,business_spend_card.transaction.approved,card_checkout.order.paid. - A family alias, which sends one representative event from that family:
virtualcard→virtualcard.topup.completedspendcard(alsospend_cardorbusiness_spend_card) →business_spend_card.createdcard_checkout→card_checkout.order.paid
event:
virtualcard.transaction.contactless, .refund and .crossborder, you can also pass transaction_type (for example cross_border_pending or contactless_pending) to choose the variant.
payin sends deposit.created followed by deposit.updated. Demo events use sample data, so their customer may be null. When this account already has a matching record (a virtual card, spend card, customer or checkout order), the sample reuses that id.
Best practices
- Verify every signature before trusting the contents. See Verifying the signature.
- Respond fast, process later. Return
2xximmediately and process the event in a background job. - Be idempotent. The same event can arrive more than once. Deduplicate on
event_idwhere present, or on the record’s id (id,transaction_id,deposit_id). - Don’t assume ordering. Events for the same record can arrive out of order, for example
deposit.createdafterdeposit.updated. Usestatusand timestamps, not arrival order. - Ignore unknown events. New
event.typevalues are added over time; skip ones you don’t recognize instead of failing. - Keep your secret safe. Store it in a secrets manager, never in source control, and rotate it with
regenerate-secretif it may have leaked. - Use HTTPS for your webhook URL.

