Webhooks
Receive real-time notifications for application onboarding, transactions, deposits, counterparties, instruments, and debit cards.
Webhooks
FV Bank sends an HTTP POST to the webhookUrl configured for your broker whenever something changes for one of your clients. Events come in two families:
- Application lifecycle — onboarding progress on broker-managed applications (
form.*,kyc.*,document.*,comment.created,ask.created,account.created). - Transaction, deposit, counterparty, instrument, and card — payment and account activity.
Signature Verification
Every request includes a signature header:
x-signature: <HMAC-SHA256 hex>HMAC-SHA256 over the signed payload using your Broker Client Secret:
- Transaction, deposit, counterparty, instrument, and card —
JSON.stringify(req.body.Data) - Application lifecycle —
JSON.stringify(req.body)(flat envelope, noDatafield)
const crypto = require('crypto');
const payload = req.body.Data ?? req.body;
const expected = crypto.createHmac('sha256', BROKER_CLIENT_SECRET)
.update(JSON.stringify(payload))
.digest('hex');
const valid = crypto.timingSafeEqual(
Buffer.from(req.headers['x-signature']),
Buffer.from(expected)
);Always verify the signature before processing any webhook.
Application lifecycle webhooks
Onboarding notifications for broker-managed applications. Twelve event names cover the application form, KYC, documents, reviewer comments, reviewer questions, and account opening.
An event is sent when the application belongs to a broker and that broker has a webhookUrl configured. Nothing else is filtered.
A webhook tells you that a record changed, not what it now says — it carries no reason text, remark, comment, or document content. On every event, call GET /application/detail/{applicationId} and act on the action items it returns.
Delivery
- Signature — every delivery carries the
x-signatureheader. Hash the body (there is noDatafield); see Signature Verification. - Acknowledgement — return any
2xx. A non-2xx response, transport error, or timeout marks the delivery failed and it is not retried, so a missed event has to be replayed by hand. Timeouts are 10s connect, 30s read, and every attempt is recorded in the HTTP log. - Ordering — events for the same
ApplicationIdarrive serially, in the order they were produced. Different applications are delivered in parallel. - Duplicates — there is no delivery id to deduplicate on, and the same event name can legitimately arrive twice for one logical change. Keep handlers idempotent and treat every event as "this record may have changed".
Application payload envelope
Every application lifecycle webhook has this top-level structure — a flat, PascalCase object:
{
"Event": "<EVENT_NAME>",
"ApplicationId": "<application id>",
"ApplicationNo": "<application number>",
"RecordType": "<form | kyc | document | comment | ask | account>",
"RecordId": "<id of the record that changed>",
"StatusLabel": "<Missing | Approved | Rejected | Created>",
"CreatedAt": "<unix seconds>"
}Application field reference
| Field | Type | Notes |
|---|---|---|
Event | string | Name of the event — see Event reference |
BrokerReferenceId | string | Your own reference from application create. Present only when the application was created with one (optional) |
ApplicationId | string | Application id. Pass it to GET /application/detail/{applicationId} |
ApplicationNo | string | Human-readable application number (e.g. IP-260903-B) |
RecordType | string | Record family that changed: form, kyc, document, comment, ask, or account |
RecordId | string | Id of the record that changed — the application id for form.* and account.created, the identity verification id for kyc.*, the matching Documents[].RecordId for document.*, the comment id for comment.created. Not sent on ask.created unless the ask has an id (optional) |
StatusLabel | string | State the record moved to: Missing, Approved, Rejected, or Created |
Question | string | Full question text, on ask.created only. Copy it verbatim into POST /application/ask/answer (optional) |
CreatedAt | number | Event time as a Unix timestamp in seconds (e.g. 1788418161) |
Event reference
| Event | When it is sent | Broker action required? |
|---|---|---|
form.missing | The form needs work — a reviewer marked it incomplete, or pushed a submitted form back to draft. A new application starting in draft does not emit it | Yes — fix everything in State=missing, answer open asks, and resubmit the form |
form.approved | The form passed review | No — account creation continues |
form.rejected | The form was rejected. This is a decision, not a request to resubmit | No — read the reason from detail. If another attempt is allowed, form.missing follows |
kyc.missing | An individual has to complete or redo identity verification | Yes — send every Kyc[] item in State=missing to its Link |
kyc.approved | Identity verification passed | No |
kyc.rejected | Identity verification failed | No — if another attempt is allowed, kyc.missing follows |
document.missing | A document was not accepted, or is still required | Yes — match RecordId in detail, read its remark, and upload a replacement |
document.approved | The document was accepted | No |
document.rejected | The document was rejected | No — if a replacement is required, document.missing follows for the same RecordId |
comment.created | A supervisor commented on the application form. System and applicant comments do not emit | No — informational only; the comment text is not returned by any broker endpoint |
ask.created | A reviewer added a question to the application. A reviewer save commonly sends ask.created followed by form.missing | Yes — copy Question verbatim into POST /application/ask/answer, then refresh detail |
account.created | Fires once, when account opening completes. For Individual Plus that is the account-status scheduler; for a business application it is later than the main account being activated — officers, entities, and any prime upgrade have to finish first. RecordId is the application id. There is no failure counterpart. Only broker-managed applications emit it | No — informational; poll GET /application/detail/{applicationId} |
Individual Plus applications emit the same form.* names as business applications, and RecordType is form even when the detail response's ApplicationState reads RecordType=individual.
Samples
One example per event name, plus two variants that produce an already-listed name from a different trigger.
form.missing — the form needs work
RecordId is the application id itself. Read GET /application/detail/{applicationId}: ApplicationState.Remark carries the reason, and the asks, documents, and KYC items in State=missing say what to fix.
{
"Event": "form.missing",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "form",
"RecordId": "6a9914ea4b27b870e9132da8",
"StatusLabel": "Missing",
"CreatedAt": 1788418161
}form.missing — a submitted form was sent back to draft
Same event name, different trigger: a reviewer pushed a submitted form back to draft. The payload is indistinguishable from the example above — both mean the application needs work again. A brand-new application sitting in draft does not emit this event.
{
"Event": "form.missing",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a3a2f324c2e4b093dce834b",
"ApplicationNo": "BP-260624-A",
"RecordType": "form",
"RecordId": "6a3a2f324c2e4b093dce834b",
"StatusLabel": "Missing",
"CreatedAt": 1782280200
}form.approved — the form passed review
{
"Event": "form.approved",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "form",
"RecordId": "6a9914ea4b27b870e9132da8",
"StatusLabel": "Approved",
"CreatedAt": 1788418918
}form.rejected — the form was rejected
{
"Event": "form.rejected",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "form",
"RecordId": "6a9914ea4b27b870e9132da8",
"StatusLabel": "Rejected",
"CreatedAt": 1788419618
}form.missing — Individual Plus application
Individual Plus applications use the same form.* names as business applications. RecordType is form here even though the detail response's ApplicationState may read RecordType=individual.
{
"Event": "form.missing",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a3b1c884c2e4b093dce8361",
"ApplicationNo": "IP-260624-B",
"RecordType": "form",
"RecordId": "6a3b1c884c2e4b093dce8361",
"StatusLabel": "Missing",
"CreatedAt": 1782280500
}kyc.missing — an individual must complete or redo KYC
RecordId is the identity verification id and does not appear in the detail response, and the event carries no individual id — so it does not tell you which individual. Call GET /application/detail/{applicationId} and act on every Kyc[] item reading State=missing.
{
"Event": "kyc.missing",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "kyc",
"RecordId": "6a9914f94b27b870e9132db9",
"StatusLabel": "Missing",
"CreatedAt": 1788420727
}kyc.approved — identity verification passed
{
"Event": "kyc.approved",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "kyc",
"RecordId": "6a9914f94b27b870e9132db9",
"StatusLabel": "Approved",
"CreatedAt": 1788420321
}kyc.rejected — identity verification failed
{
"Event": "kyc.rejected",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a3a2f324c2e4b093dce834b",
"ApplicationNo": "BP-260624-A",
"RecordType": "kyc",
"RecordId": "6a3a37a24c2e4b093dce834f",
"StatusLabel": "Rejected",
"CreatedAt": 1782280800
}document.missing — a document was not accepted, or is still required
RecordId matches RecordId on the corresponding Documents[] item of the detail response. Read that item's remark for the reason, then upload a replacement through POST /application/document/upload using its DocumentType and Individual_id.
{
"Event": "document.missing",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "document",
"RecordId": "6a9915794b27b870e9132dbe",
"StatusLabel": "Missing",
"CreatedAt": 1788417841
}document.approved — the document was accepted
{
"Event": "document.approved",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "document",
"RecordId": "6a9915794b27b870e9132dbe",
"StatusLabel": "Approved",
"CreatedAt": 1788418634
}document.rejected — the document was rejected
{
"Event": "document.rejected",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "document",
"RecordId": "6a9915794b27b870e9132dbe",
"StatusLabel": "Rejected",
"CreatedAt": 1788417695
}comment.created — a reviewer commented on the application
Sent only for a comment posted by a supervisor on the application form. RecordId is the comment id, StatusLabel is always Created, and the comment text is not on the event. This example also shows BrokerReferenceId omitted — what an application created without a broker reference looks like.
{
"Event": "comment.created",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "comment",
"RecordId": "6a9917c24b27b870e9132df1",
"StatusLabel": "Created",
"CreatedAt": 1788417986
}ask.created — a reviewer asked a question that needs an answer
One event per question added to the application. Question must be copied verbatim into POST /application/ask/answer — it is the only thing that identifies the ask, because ask items on the detail response carry no id. RecordId is normally omitted and StatusLabel is always Created.
{
"Event": "ask.created",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a9914ea4b27b870e9132da8",
"ApplicationNo": "IP-260903-B",
"RecordType": "ask",
"StatusLabel": "Created",
"Question": "Please confirm the registered business address.",
"CreatedAt": 1788418161
}account.created — account opening completed
Fires once, when account opening completes. For an Individual Plus application that is the account-status scheduler; for a business application it is the point the account-opening flow reaches its completed stage, which is later than the main account being activated — officers, entities, and any prime upgrade have to finish first. RecordId is the application id. There is no failure counterpart. Only broker-managed applications emit it.
{
"Event": "account.created",
"BrokerReferenceId": "third-party-reference",
"ApplicationId": "6a3a2f324c2e4b093dce834b",
"ApplicationNo": "BP-260624-A",
"RecordType": "account",
"RecordId": "6a3a2f324c2e4b093dce834b",
"StatusLabel": "Created",
"CreatedAt": 1782281400
}Transaction and deposit webhooks
FV Bank sends an HTTP POST to your configured callback URL whenever a transaction event occurs for one of your clients.
Payload Envelope
Transaction, deposit, counterparty, instrument, and card webhooks have Event, Id, Message, and Data. Counterparty, card lifecycle, and card transaction webhooks also include a top-level Date (ISO 8601). Fiat payment and deposit webhooks do not send Date. Application lifecycle webhooks use the flat envelope documented above.
Treat Date as optional so consumers accept both shapes.
{
"Date": "<ISO 8601 — counterparty and card events only>",
"Event": "<EVENT_TYPE>",
"Id": "<unique delivery UUID>",
"Message": "<human-readable summary>",
"Data": { ... }
}Data Field Reference
| Field | Type | Notes |
|---|---|---|
CreatedDate | ISO 8601 | Timestamp of when the transaction was originally created (e.g. 2026-06-29T08:59:45.000Z) |
TransactionNumber | string | Unique FV Bank transaction reference (e.g. FV000726845). Use this to reconcile with your internal records |
Type | string | Identifies the payment or deposit rail (ACH, Wire, International Wire, Cross-Border, Stablecoin, etc.) — see Type Reference |
Amount | string | Transaction amount as a decimal string (e.g. "20.00") |
Balance | number | Account balance at the time the webhook was delivered, reflecting the effect of this transaction (e.g. 397885.60) |
From | string | Display name of the sender or originating party |
FromUserId | string | Id of the sender or originating party |
To | string | Display name of the recipient or beneficiary |
ToUserId | string | Id of the recipient or beneficiary |
Currency | string | Currency code for the transaction (e.g. "USD") |
Description | string | Human-readable transaction description. Not present on all event types (optional) |
AdditionalData | array | Rail-specific metadata as label/value pairs — e.g. IMAD/OMAD for wire transfers, document IDs, stablecoin network info. Format: [{ "label": "...", "value": "..." }] (optional) |
OldStatus | string | Previous transaction status before this change. Present only in TRANSACTION_STATUS_UPDATED events |
NewStatus | string | Updated transaction status. Possible values: IN PROCESS, BEGIN PROCESSING, COMPLETED, RETURNED, FAILED. Present only in TRANSACTION_STATUS_UPDATED events |
Fiat Payment Webhooks
Covers Domestic ACH, Domestic Wire, International Wire, FVNet, and Stablecoin Withdraw transactions.
Event Flow
TRANSACTION_CREATED
→ TRANSACTION_AUTHORIZED (compliance approved)
→ TRANSACTION_STATUS_UPDATED (status changes during bank processing)
OR
→ TRANSACTION_DENIED
→ TRANSACTION_CANCELLED
→ TRANSACTION_EXPIRED
→ TRANSACTION_RETURNEDTRANSACTION_CREATED
Triggered when a payment is initiated. Pending compliance review — funds have not moved.
{
"Event": "TRANSACTION_CREATED",
"Id": "7d026990-7f3a-4e15-b00e-9af258b22309",
"Message": "Transaction created",
"Data": {
"CreatedDate": "2026-06-26T10:36:26.687Z",
"TransactionNumber": "FV000726845",
"Type": "Payment - Domestic Wire",
"Amount": "20.00",
"Balance": 397905.60,
"From": "Your Company Name",
"FromUserId": "<user-id>",
…TRANSACTION_AUTHORIZED
Triggered when compliance approves the payment. Processing begins.
{
"Event": "TRANSACTION_AUTHORIZED",
"Id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"Message": "Transaction authorized",
"Data": {
"CreatedDate": "2026-06-29T08:59:45.000Z",
"TransactionNumber": "FV000726845",
"Type": "Payment - Domestic Wire",
"Amount": "20.00",
"Balance": 397885.60,
"From": "Your Company Name",
"FromUserId": "<user-id>",
…TRANSACTION_STATUS_UPDATED
Triggered when the payment status changes after funds movement begins. May fire multiple times.
OldStatus and NewStatus are added to Data for this event only.
NewStatus | Meaning |
|---|---|
IN PROCESS | Ready for the bank to process the transfer |
BEGIN PROCESSING | Bank started processing your request |
COMPLETED | Transfer successfully settled |
RETURNED | Returned by the receiving bank |
FAILED | Failed during processing |
{
"Event": "TRANSACTION_STATUS_UPDATED",
"Id": "a7b8c9d0-e1f2-3456-0123-567890123456",
"Message": "The member transaction status changed",
"Data": {
"CreatedDate": "2026-06-29T08:59:45.000Z",
"TransactionNumber": "FV000726845",
"Type": "Payment - Domestic Wire",
"Amount": "20.00",
"Balance": 397885.60,
"From": "Your Company Name",
"FromUserId": "<user-id>",
…TRANSACTION_DENIED / CANCELLED / EXPIRED / RETURNED
Terminal states — no further events follow.
| Event | Message | When |
|---|---|---|
TRANSACTION_DENIED | Transaction denied | Compliance rejected |
TRANSACTION_CANCELLED | Transaction cancelled | Cancelled before processing |
TRANSACTION_EXPIRED | Transaction expired | Auth window elapsed |
TRANSACTION_RETURNED | Transaction returned | Reversed by receiving bank |
These events share the same payload shape as TRANSACTION_CREATED.
Fiat Deposit Webhooks
Covers Wire, ACH, International Wire, and Custodial Transfer deposits with full lifecycle events.
Stablecoin deposits only emit
DEPOSIT_RECEIVED. No further lifecycle events (authorized, denied, etc.) fire for stablecoin.
Event Flow
DEPOSIT_RECEIVED
→ DEPOSIT_AUTHORIZED (funds credited) ← success
OR
→ DEPOSIT_DENIED (compliance rejected) ← terminal
→ DEPOSIT_EXPIRED (auth window elapsed) ← terminal
→ DEPOSIT_CANCELLED (cancelled OR chargeback/reversal) ← terminalDEPOSIT_RECEIVED
Triggered when a deposit arrives. Pending compliance review — funds are not yet credited.
{
"Event": "DEPOSIT_RECEIVED",
"Id": "d0e1f2a3-b4c5-6789-3456-890123456789",
"Message": "Transaction created",
"Data": {
"CreatedDate": "2026-06-29T10:59:45.000Z",
"TransactionNumber": "FV000726900",
"Type": "Deposit Domestic Wire",
"Amount": "50000.00",
"Balance": 75000.00,
"From": "Sender Bank Name",
"To": "Your Company Name",
…Stablecoin Deposit
Only
DEPOSIT_RECEIVEDfires for stablecoin deposits — no authorized, denied, or cancelled events follow.
{
"Event": "DEPOSIT_RECEIVED",
"Id": "f8a9b0c1-d2e3-4567-1234-678901234567",
"Message": "Transaction created",
"Data": {
"CreatedDate": "2026-06-29T16:59:50.000Z",
"TransactionNumber": "FV000726903",
"Type": "Stablecoin Deposit",
"Amount": "1000.00",
"Balance": 26000.00,
"From": "FV Bank",
"To": "Your Company Name",
…DEPOSIT_AUTHORIZED
Triggered when compliance approves the deposit. Funds are now credited. Balance reflects the updated amount.
{
"Event": "DEPOSIT_AUTHORIZED",
"Id": "a3b4c5d6-e7f8-9012-6789-123456789012",
"Message": "Transaction authorized",
"Data": {
"CreatedDate": "2026-06-29T10:59:45.000Z",
"TransactionNumber": "FV000726900",
"Type": "Deposit Domestic Wire",
"Amount": "50000.00",
"Balance": 125000.00,
"From": "Sender Bank Name",
"To": "Your Company Name",
…DEPOSIT_DENIED / EXPIRED / CANCELLED
Terminal states — same payload shape as DEPOSIT_RECEIVED.
| Event | Message | When |
|---|---|---|
DEPOSIT_DENIED | Transaction denied | Compliance rejected the deposit |
DEPOSIT_EXPIRED | Transaction expired | No compliance action within the allowed timeframe |
DEPOSIT_CANCELLED | Transaction cancelled | Cancelled by compliance before processing |
DEPOSIT_CANCELLED | Transaction returned | Deposit reversed or charged back after receipt |
DEPOSIT_CANCELLEDfires for two distinct scenarios — use theMessagefield to distinguish between a cancellation and a chargeback/reversal.
Cross-Border Payment Webhooks
Covers all cross-border rails (SEPA, Wire, Faster Payments, CLABE, etc.).
Event Flow
TRANSACTION_CREATED
→ TRANSACTION_AUTHORIZED (compliance approved)
→ TRANSACTION_STATUS_UPDATED (status changes during processing)
OR
→ TRANSACTION_DENIED ← terminal
→ TRANSACTION_CANCELLED ← terminal
→ TRANSACTION_EXPIRED ← terminalCross-border payments do not emit
TRANSACTION_RETURNED. Chargebacks apply to domestic rails only.
Currency is USD. The destination currency is identified from Data.Type.
TRANSACTION_CREATED (Cross-Border)
{
"Event": "TRANSACTION_CREATED",
"Id": "e741c4bc-a6b0-466a-bf23-f4f20b799f39",
"Message": "Transaction created",
"Data": {
"CreatedDate": "2026-06-26T08:26:44.531Z",
"TransactionNumber": "FV000726696",
"Type": "Payment - Cross Border - Clabe",
"Amount": "1.16",
"Balance": 736.16,
"From": "Your Company Name",
"FromUserId": "<user-id>",
…TRANSACTION_STATUS_UPDATED (Cross-Border)
Same structure as fiat — includes OldStatus and NewStatus in Data.
{
"Event": "TRANSACTION_STATUS_UPDATED",
"Id": "a5b6c7d8-e9f0-1234-8901-345678901234",
"Message": "The member transaction status changed",
"Data": {
"CreatedDate": "2026-06-29T08:59:50.000Z",
"TransactionNumber": "FV000726950",
"Type": "Payment - Cross Border - SEPA",
"Amount": "2000.00",
"Balance": 17500.00,
"From": "Your Company Name",
"FromUserId": "<user-id>",
…Counterparty & Instrument Status Webhooks
These webhooks are triggered when a Counterparty record status changes to Active or Awaits Approval (Rejected), and when a Counterparty Instrument record status changes to Active or Rejected.
The payload for these webhooks includes a top-level
Datefield (ISO 8601 timestamp) in addition to the standardEvent,Id,Message, andDatafields.
Data Field Reference
| Field | Type | Notes |
|---|---|---|
BrokerId | string | Internal identifier for the GMA partner broker |
UserId | string | Identifier of the customer user associated with the record |
OldStatus | string | Status of the record before this change |
NewStatus | string | Status of the record after this change |
RecordId | string | Unique identifier of the counterparty or instrument record |
RecordType | string | Type of the record — COUNTERPARTY or COUNTERPARTY_INSTRUMENT |
Events
| Event | Message | When |
|---|---|---|
COUNTERPARTY_ACTIVATED | Counterparty activated | Counterparty status changed to Active |
COUNTERPARTY_INSTRUMENT_ACTIVATED | Counterparty Instrument activated | Instrument status changed to Active |
COUNTERPARTY_AWAITING_ACTIVATION | Counterparty awaiting activation | Counterparty status changed to Awaits Approval (Rejected) |
COUNTERPARTY_INSTRUMENT_REJECTED | Counterparty Instrument rejected | Instrument status changed to Rejected |
Sample — COUNTERPARTY_AWAITING_ACTIVATION
{
"Event": "COUNTERPARTY_AWAITING_ACTIVATION",
"Id": "244badfa-64fb-4436-b00a-b43789f8e6ee",
"Message": "Counterparty awaiting activation",
"Date": "2026-07-15T06:14:23",
"Data": {
"BrokerId": "<broker-id>",
"UserId": "<user-id>",
"OldStatus": "Pending",
"NewStatus": "Awaits approval",
"RecordId": "<record-id>",
"RecordType": "COUNTERPARTY"
…Card Order Webhooks
These webhooks are triggered when a card order is created, when the card order status changes, and when tracking information is updated.
Sent only for GMA broker-managed customers that have a main broker. These webhooks do not send email or SMS. Shipment email/SMS still run only when tracking fields actually change.
Envelope
{
"Date": "2026-09-09T10:30:00.000Z",
"Event": "<EVENT_NAME>",
"Message": "<human readable message>",
"Id": "<callback-uuid>",
"Data": {}
}Shared field notes
| Field | Notes |
|---|---|
UserId | Masked managed customer id (same as GET /cards/{userId}) |
CardId | Card reference id (same as GET /cards/{cardId}/...). Omitted on CARD_ORDER_CREATED |
MaskedPan | Masked PAN. Omitted on CARD_ORDER_CREATED |
CARD_ORDER_CREATED
Triggered when a card order record is created.
Message: Card order created
| Field | Type | Notes |
|---|---|---|
UserId | string | Masked customer id |
OrderStatus | string | Current order status |
CardId and MaskedPan are omitted (they are not available yet).
{
"Date": "2026-09-09T10:30:00.000Z",
"Event": "CARD_ORDER_CREATED",
"Message": "Card order created",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"OrderStatus": "Applied"
}
}CARD_ORDER_STATUS_CHANGED
Triggered when card order status changes.
Message: Card order status changed
| Field | Type | Notes |
|---|---|---|
UserId | string | Masked customer id |
CardId | string | Card reference id |
MaskedPan | string | Masked PAN |
OldOrderStatus | string | Previous order status |
NewOrderStatus | string | New order status |
{
"Date": "2026-09-09T10:31:00.000Z",
"Event": "CARD_ORDER_STATUS_CHANGED",
"Message": "Card order status changed",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "<card-reference-id>",
"MaskedPan": "<masked-pan>",
"OldOrderStatus": "Applied",
"NewOrderStatus": "Approved"
}
…CARD_TRACKING_UPDATED
Triggered when shipment tracking fields change.
Message: Card order tracking updated
The payload contains new values only (no Old* fields).
| Field | Type | Notes |
|---|---|---|
UserId | string | Masked customer id |
CardId | string | Card reference id |
MaskedPan | string | Masked PAN |
TrackingNumber | string | Current tracking number |
CourierServiceName | string | Current courier |
TrackingUrl | string | Current tracking URL |
{
"Date": "2026-09-09T10:45:00.000Z",
"Event": "CARD_TRACKING_UPDATED",
"Message": "Card order tracking updated",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "34a5ca34-7e01-4be7-b4d3-a35dc23e6e8e",
"MaskedPan": "XXXXXXXXXXX2302",
"TrackingNumber": "1Z999AA10123456784",
"CourierServiceName": "UPS",
"TrackingUrl": "https://www.ups.com/track"
…Card Status Webhooks
Triggered when a debit card's status changes for a GMA customer. OldStatus and NewStatus reflect the update.
| Event | When |
|---|---|
CARD_STATUS_CHANGED | Debit card status changed |
Includes top-level Date.
Data fields: BrokerId, UserId, Date, OldStatus, NewStatus, RecordId, RecordType (Card Data), CardReferenceId, MaskedCardNumber.
Message: Card status changed.
{
"Date": "2026-09-07T10:54:37",
"Event": "CARD_STATUS_CHANGED",
"Id": "579d6ed2-0da0-475b-87a0-0291283a7a47",
"Message": "Card status changed",
"Data": {
"BrokerId": "-8197785738920153257",
"UserId": "-8193282139292782761",
"Date": "2026-09-07T10:54:37",
"OldStatus": "Hot - Fraud",
"NewStatus": "Pending Activation",
"RecordId": "3232350115346165697",
…Status only — not spend, block reason, limits, or tracking. Call GET /cards/{userId} for the full card object.
Card Data Webhooks
These webhooks are triggered when card data is created, and when spend, ATM usage, or limits change later. Status updates are under Card Status Webhooks.
Sent only for GMA broker-managed customers that have a main broker. On card-data create, only CARD_DATA_CREATED is sent — status, suspend, ATM, and limits events do not fire on that first save. Initial state is on CARD_DATA_CREATED only.
There is no separate CARD_BLOCKED webhook. POST /cards/{cardId}/block does not itself send a webhook; when the card is actually cancelled or blocked, partners receive CARD_STATUS_CHANGED.
CARD_DATA_CREATED
Triggered once when card data is created.
Message: Card data created
CardStatus is the raw status value. AtmEnabled and TransactionSuspended are the strings "true" or "false".
| Field | Type | Notes |
|---|---|---|
UserId | string | Masked customer id |
CardId | string | Card reference id |
MaskedPan | string | Masked PAN |
CardStatus | string | Current card status |
AtmEnabled | string | "true" or "false" |
TransactionSuspended | string | "true" or "false" |
{
"Date": "2026-09-16T10:00:00.000Z",
"Event": "CARD_DATA_CREATED",
"Message": "Card data created",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "<card-reference-id>",
"MaskedPan": "<masked-pan>",
"CardStatus": "Pending Activation",
"AtmEnabled": "true",
"TransactionSuspended": "false"
…CARD_SUSPENDED
Triggered when card transactions are suspended.
Message: Card transactions suspended
{
"Date": "2026-09-16T10:05:00.000Z",
"Event": "CARD_SUSPENDED",
"Message": "Card transactions suspended",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "<card-reference-id>",
"MaskedPan": "<masked-pan>",
"TransactionSuspended": "true"
}
}CARD_RESUMED
Triggered when card transactions are resumed.
Message: Card transactions resumed
{
"Date": "2026-09-16T10:06:00.000Z",
"Event": "CARD_RESUMED",
"Message": "Card transactions resumed",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "<card-reference-id>",
"MaskedPan": "<masked-pan>",
"TransactionSuspended": "false"
}
}CARD_ATM_ENABLED
Triggered when ATM usage is enabled.
Message: Card ATM usage enabled
{
"Date": "2026-09-16T10:07:00.000Z",
"Event": "CARD_ATM_ENABLED",
"Message": "Card ATM usage enabled",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "<card-reference-id>",
"MaskedPan": "<masked-pan>",
"AtmEnabled": "true"
}
}CARD_ATM_DISABLED
Triggered when ATM usage is disabled.
Message: Card ATM usage disabled
{
"Date": "2026-09-16T10:08:00.000Z",
"Event": "CARD_ATM_DISABLED",
"Message": "Card ATM usage disabled",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "<card-reference-id>",
"MaskedPan": "<masked-pan>",
"AtmEnabled": "false"
}
}CARD_LIMITS_CHANGED
Triggered when one or more effective limits change.
Limit amounts are numbers with 2 decimal places (e.g. 17 or 17.5, not "17.000000000"). Missing or invalid values are null.
Message: Card limits changed
Purchase: DailyPurchaseLimit / OldDailyPurchaseLimit, WeeklyPurchaseLimit / OldWeeklyPurchaseLimit, MonthlyPurchaseLimit / OldMonthlyPurchaseLimit, YearlyPurchaseLimit / OldYearlyPurchaseLimit, PerTransactionLimit / OldPerTransactionLimit
ATM: DailyAtmLimit / OldDailyAtmLimit, WeeklyAtmLimit / OldWeeklyAtmLimit, MonthlyAtmLimit / OldMonthlyAtmLimit, PerAtmWithdrawalLimit / OldPerAtmWithdrawalLimit
{
"Date": "2026-09-16T10:09:00.000Z",
"Event": "CARD_LIMITS_CHANGED",
"Message": "Card limits changed",
"Id": "a1b2c3d4-....",
"Data": {
"UserId": "<masked-user-id>",
"CardId": "<card-reference-id>",
"MaskedPan": "<masked-pan>",
"DailyPurchaseLimit": 17.5,
"OldDailyPurchaseLimit": 17,
"WeeklyAtmLimit": 500,
…Card Transaction Webhooks
FV Bank sends an HTTP POST with a JSON body to your configured callback URL when a card related transaction is created or when its status is updated. Whenever a card fee is charged, it is also created as a transaction, and a webhook is sent for it.
| Event | Message | When |
|---|---|---|
CARD_TRANSACTION_CREATED | Card transaction created | A card transaction is created |
CARD_TRANSACTION_AUTHORIZED | Card transaction authorized | The card transaction is authorized |
CARD_TRANSACTION_DENIED | Card transaction denied | The card transaction is denied |
CARD_TRANSACTION_CANCELLED | Card transaction cancelled | The card transaction is cancelled |
CARD_TRANSACTION_EXPIRED | Card transaction expired | The card transaction authorization expires |
A purchase normally produces CARD_TRANSACTION_CREATED followed by CARD_TRANSACTION_AUTHORIZED, CARD_TRANSACTION_DENIED, CARD_TRANSACTION_CANCELLED, or CARD_TRANSACTION_EXPIRED. Use TransactionNumber to link events for the same transaction, and Id to detect duplicate deliveries.
Payload Envelope
| Field | Type | Description |
|---|---|---|
Id | string (UUID) | Unique webhook ID |
Event | string | One of the events above |
Message | string | Event description |
Date | string (ISO 8601, UTC) | When the event occurred |
Data | object | Card transaction details |
Data Fields
| Field | Type | Description |
|---|---|---|
UserId | string | FV Bank user ID of the cardholder |
CardId | string | null | FV Bank card ID (same ID returned by the Cards API) |
TransactionNumber | string | FV Bank transaction number |
Amount | number | Transaction amount (up to 2 decimal places) |
Currency | string | ISO currency code, e.g. USD |
TransactionType | string | Transaction type, e.g. CardPurchaseTransfer |
Status | string | null | Authorization status of the transaction, e.g. Authorized. May be null on CARD_TRANSACTION_CREATED |
MerchantName | string | null | Merchant name |
MerchantCategory | string | null | Merchant category |
Mcc | string | null | Merchant Category Code |
AuthorizationCode | string | null | Authorization code |
CardLastFourDigits | string | null | Last 4 digits of the card |
Direction | string | SEND (money leaving the cardholder's account) or RECEIVE (money coming in, such as returns and credits) |
CardId, MerchantCategory, Mcc, AuthorizationCode, and CardLastFourDigits are populated for card purchase, ATM, cashback, return, and credit transactions. They may be null for card fees.
Sample Card Transaction Webhook
Card Transaction Created
{
"Id": "1596410c-8abe-427c-b8e0-ecb63a971cdb",
"Event": "CARD_TRANSACTION_CREATED",
"Message": "Card transaction created",
"Date": "2026-10-05T10:15:33",
"Data": {
"UserId": "-8202289338547523753",
"CardId": "55d0c52f-79ee-4d38-acf4-a16c07957898",
"TransactionNumber": "FV000721206",
"Amount": 21,
"Currency": "USD",
"TransactionType": "CardPurchaseTransfer",
…Card Transaction Authorized
{
"Id": "c4b8e2f1-7a36-4d90-9e15-6f0a3c8d2b47",
"Event": "CARD_TRANSACTION_AUTHORIZED",
"Message": "Card transaction authorized",
"Date": "2026-10-05T10:15:33",
"Data": {
"UserId": "-8202289338547523753",
"CardId": "55d0c52f-79ee-4d38-acf4-a16c07957898",
"TransactionNumber": "FV000721206",
"Amount": 21,
"Currency": "USD",
"TransactionType": "CardPurchaseTransfer",
…Delivery
- Respond with HTTP
2xxto acknowledge receipt. - Make your handler idempotent using
Id. - Events can arrive close together and are not guaranteed to be in order. Use
EventandStatusto determine the latest state.
Type Reference
Fiat Payments
| Data.Type |
|---|
| Payment - Domestic ACH |
| Payment - Domestic Wire |
| Payment - International Wire |
| FVNet Payment |
| Stable Coin Withdraw |
Fiat Deposits
| Data.Type |
|---|
| BUS ACH Deposit |
| Credit IAT ACH |
| Deposit Domestic Wire |
| Deposit Bridge Wire |
| Business Deposit |
| Deposit Third Party Custodial Transfer |
| Stablecoin Deposit |
Cross-Border Payments
| Data.Type | Destination Currency |
|---|---|
| Payment - Cross Border - AED Domestic Payments | AED |
| Payment - Cross Border - AED Wire Payments | AED |
| Payment - Cross Border - ARS Domestic Payments | ARS |
| Payment - Cross Border - BRL Domestic Payments | BRL |
| Payment - Cross Border - CAD Domestic Payments | CAD |
| Payment - Cross Border - CAD Wire Payments | CAD |
| Payment - Cross Border - CLP Domestic Payments | CLP |
| Payment - Cross Border - COP Domestic Payments | COP |
| Payment - Cross Border - DKK Domestic Payments | DKK |
| Payment - Cross Border - SEPA | EUR |
| Payment - Cross Border - EUR Wire Payments | EUR |
| Payment - Cross Border - Faster Payments | GBP |
| Payment - Cross Border - GBP Wire Payments | GBP |
| Payment - Cross Border - HKD Domestic Payments | HKD |
| Payment - Cross Border - HKD Wire Payments | HKD |
| Payment - Cross Border - IDR Domestic Payments | IDR |
| Payment - Cross Border - JPY Domestic Payments | JPY |
| Payment - Cross Border - JPY Wire Payments | JPY |
| Payment - Cross Border - Clabe | MXN |
| Payment - Cross Border - MXN Wire Payments | MXN |
| Payment - Cross Border - PEN Domestic Payments | PEN |
| Payment - Cross Border - SGD Domestic Payments | SGD |
| Payment - Cross Border - SGD Wire Payments | SGD |
| Payment - Cross Border - ZAR Domestic Payments | ZAR |
| Payment - Cross Border - ZAR Wire Payments | ZAR |
Best Practices
- Verify signatures on every request before processing (same for Card Order, Card Data, Card Transaction, and counterparty webhooks)
- Application lifecycle — return 2xx (failed deliveries are not retried) and keep handlers idempotent; there is no delivery
Idto deduplicate on - Deduplicate transaction-family events using
Id— retries use the sameId - Use
TransactionNumberto reconcile with your internal records - Use
DEPOSIT_AUTHORIZED(notDEPOSIT_RECEIVED) to credit funds — the deposit is not confirmed until authorized - Handle terminal states (
DENIED,CANCELLED,EXPIRED,RETURNED) as failure paths requiring attention - Card Order Webhooks — use
CARD_ORDER_CREATED,CARD_ORDER_STATUS_CHANGED, andCARD_TRACKING_UPDATEDfor order and shipment updates. Tracking payloads contain new values only (noOld*fields) - Card Data Webhooks —
CARD_DATA_CREATEDcarries initial state. Later updates useCARD_STATUS_CHANGED,CARD_SUSPENDED/CARD_RESUMED,CARD_ATM_ENABLED/CARD_ATM_DISABLED, andCARD_LIMITS_CHANGED. There is noCARD_BLOCKEDevent - Card Transaction Webhooks — use
TransactionNumberto link events for the same transaction andIdto detect duplicate deliveries. A purchase normally sendsCARD_TRANSACTION_CREATEDfollowed byCARD_TRANSACTION_AUTHORIZED,CARD_TRANSACTION_DENIED,CARD_TRANSACTION_CANCELLED, orCARD_TRANSACTION_EXPIRED. Events can arrive close together and are not guaranteed to be in order; useEventandStatusfor the latest state