Skip to main content
Webhooks let your integration react in real time to events on your Yativo account, such as deposits landing, payouts settling, customers being created, virtual accounts receiving funds and virtual cards being charged, instead of polling our API. This guide covers:

How webhooks work

  1. You register a single HTTPS URL on your account. Every event is sent to that one URL.
  2. When an event occurs, Yativo sends an HTTP POST request to your URL with a JSON body describing the event.
  3. Your endpoint verifies the request signature, processes the event, and returns a 2xx status code quickly.
Note: the signature header is named Signature, not X-Signature or X-Webhook-Signature.

Registering your endpoint

All webhook management endpoints live under /v1/business/webhook and require your API credentials:

Create or replace your webhook

Response 200 OK
  • One endpoint per account. Calling POST /v1/business/webhook again replaces your URL and issues a new secret. It doesn’t add a second endpoint.
  • Save the secret straight 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.type values you don’t handle.
  • Right after you save a webhook, Yativo sends a webhook_updated test event to the new URL.

List your webhook

Returns the same webhook object as above, inside a data array.

Update your webhook URL

Updating the URL keeps your secret unchanged.

Rotate your signing secret

Returns the webhook object with a new 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 a Signature header:
It is computed as:
Recompute it over the exact raw request body. Don’t re-serialize the parsed JSON, because whitespace or key-order differences will break the match. Compare using a constant-time comparison. The signed payload has no timestamp, so pair signature checks with idempotency handling (see Best practices).

PHP

Node.js (Express)

Python (Flask)


The payload envelope

Almost every event uses this two-key envelope:
  • event.type identifies the event, for example deposit.created.
  • payload holds the event data, documented per event below.
The envelope has no delivery id or timestamp, so use fields inside payload (such as id, event_id, created_at or status) for idempotency and ordering.
Gift card events are the one exception. They have no event.type/payload wrapper. See giftcard.status_changed.

The customer object

Every event includes a customer 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. The customer object is now included on every event, always with the five fields above. Events that previously sent a shorter customer object (virtual_account.*, virtual_account.deposit, endorsement.updated) now send the full one.

Event catalog

Changed on 21 October 2026. user.updated, customer.created, customer.updated and deposit.* now send only the fields documented below. user.updated and customer.updated are no longer sent for changes to fields you don’t receive (for example signing in). endorsement.updated no longer includes _webhook_id. virtual_account.deleted is 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.
Only the fields above are sent. KYC documents and identity numbers are never included.

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_recevied is spelled that way in the payload, so map it as-is. amount is the net amount credited; amount_recevied is 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.
Only metadata meant for you is sent.

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.
When 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:
Route on the top-level key: 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_id and customer: the cardholder (see The customer object).
Amounts are decimal USD (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 as cardId. They now use the standard envelope, snake_case fields, decimal amounts and the customer object. The success events were also renamed: .success is now .completed (.complete for 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:
  1. transaction_type: cross_border_pending: the cross-border fee. fee_amount is included, and that fee is debited from your USD wallet. status stays pending until the purchase settles.
  2. 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

Every business_spend_card.transaction.* event is backed by a transaction record you can fetch, annotate and attach receipts to:
Invoice uploads accept JPG, PNG or PDF up to 10 MB. Every file is virus-scanned; a file that fails is rejected with 422 and never stored. Files are kept in private storage, and GET .../invoices/{id}/download returns one only after confirming you own the transaction.
See the Corporate Card Integration guide for disputes, fees and error codes.

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 2xx status 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 OK is enough.
  • A non-2xx status, 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:
To find the delivery for a specific transaction (for example to resend it):
Logged payloads are shown with the fields events carry today, so older entries may show fewer fields than were sent at the time. 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 resend and 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.completed
    • spendcard (also spend_card or business_spend_card) → business_spend_card.created
    • card_checkout → card_checkout.order.paid
To pick a specific event when using a family alias, pass event:
For virtualcard.transaction.contactless, .refund and .crossborder, you can also pass transaction_type (for example cross_border_pending or contactless_pending) to choose the variant.
The sample events are signed like real ones. For example, 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 2xx immediately and process the event in a background job.
  • Be idempotent. The same event can arrive more than once. Deduplicate on event_id where 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.created after deposit.updated. Use status and timestamps, not arrival order.
  • Ignore unknown events. New event.type values 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-secret if it may have leaked.
  • Use HTTPS for your webhook URL.