Guides
Guides/Webhooks

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:

code

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, no Data field)
javascript
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-signature header. Hash the body (there is no Data field); 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 ApplicationId arrive 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:

json
{
  "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

FieldTypeNotes
EventstringName of the event — see Event reference
BrokerReferenceIdstringYour own reference from application create. Present only when the application was created with one (optional)
ApplicationIdstringApplication id. Pass it to GET /application/detail/{applicationId}
ApplicationNostringHuman-readable application number (e.g. IP-260903-B)
RecordTypestringRecord family that changed: form, kyc, document, comment, ask, or account
RecordIdstringId 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)
StatusLabelstringState the record moved to: Missing, Approved, Rejected, or Created
QuestionstringFull question text, on ask.created only. Copy it verbatim into POST /application/ask/answer (optional)
CreatedAtnumberEvent time as a Unix timestamp in seconds (e.g. 1788418161)

Event reference

EventWhen it is sentBroker action required?
form.missingThe 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 itYes — fix everything in State=missing, answer open asks, and resubmit the form
form.approvedThe form passed reviewNo — account creation continues
form.rejectedThe form was rejected. This is a decision, not a request to resubmitNo — read the reason from detail. If another attempt is allowed, form.missing follows
kyc.missingAn individual has to complete or redo identity verificationYes — send every Kyc[] item in State=missing to its Link
kyc.approvedIdentity verification passedNo
kyc.rejectedIdentity verification failedNo — if another attempt is allowed, kyc.missing follows
document.missingA document was not accepted, or is still requiredYes — match RecordId in detail, read its remark, and upload a replacement
document.approvedThe document was acceptedNo
document.rejectedThe document was rejectedNo — if a replacement is required, document.missing follows for the same RecordId
comment.createdA supervisor commented on the application form. System and applicant comments do not emitNo — informational only; the comment text is not returned by any broker endpoint
ask.createdA reviewer added a question to the application. A reviewer save commonly sends ask.created followed by form.missingYes — copy Question verbatim into POST /application/ask/answer, then refresh detail
account.createdFires 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 itNo — 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.

json
{
  "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.

json
{
  "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

json
{
  "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

json
{
  "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.

json
{
  "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.

json
{
  "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

json
{
  "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

json
{
  "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.

json
{
  "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

json
{
  "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

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "Date":    "<ISO 8601 — counterparty and card events only>",
  "Event":   "<EVENT_TYPE>",
  "Id":      "<unique delivery UUID>",
  "Message": "<human-readable summary>",
  "Data":    { ... }
}

Data Field Reference

FieldTypeNotes
CreatedDateISO 8601Timestamp of when the transaction was originally created (e.g. 2026-06-29T08:59:45.000Z)
TransactionNumberstringUnique FV Bank transaction reference (e.g. FV000726845). Use this to reconcile with your internal records
TypestringIdentifies the payment or deposit rail (ACH, Wire, International Wire, Cross-Border, Stablecoin, etc.) — see Type Reference
AmountstringTransaction amount as a decimal string (e.g. "20.00")
BalancenumberAccount balance at the time the webhook was delivered, reflecting the effect of this transaction (e.g. 397885.60)
FromstringDisplay name of the sender or originating party
FromUserIdstringId of the sender or originating party
TostringDisplay name of the recipient or beneficiary
ToUserIdstringId of the recipient or beneficiary
CurrencystringCurrency code for the transaction (e.g. "USD")
DescriptionstringHuman-readable transaction description. Not present on all event types (optional)
AdditionalDataarrayRail-specific metadata as label/value pairs — e.g. IMAD/OMAD for wire transfers, document IDs, stablecoin network info. Format: [{ "label": "...", "value": "..." }] (optional)
OldStatusstringPrevious transaction status before this change. Present only in TRANSACTION_STATUS_UPDATED events
NewStatusstringUpdated 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

code

TRANSACTION_CREATED
  → TRANSACTION_AUTHORIZED      (compliance approved)
  → TRANSACTION_STATUS_UPDATED  (status changes during bank processing)
     OR
  → TRANSACTION_DENIED
  → TRANSACTION_CANCELLED
  → TRANSACTION_EXPIRED
  → TRANSACTION_RETURNED

TRANSACTION_CREATED

Triggered when a payment is initiated. Pending compliance review — funds have not moved.

json
{
  "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.

json
{
  "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.

NewStatusMeaning
IN PROCESSReady for the bank to process the transfer
BEGIN PROCESSINGBank started processing your request
COMPLETEDTransfer successfully settled
RETURNEDReturned by the receiving bank
FAILEDFailed during processing
json
{
  "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.

EventMessageWhen
TRANSACTION_DENIEDTransaction deniedCompliance rejected
TRANSACTION_CANCELLEDTransaction cancelledCancelled before processing
TRANSACTION_EXPIREDTransaction expiredAuth window elapsed
TRANSACTION_RETURNEDTransaction returnedReversed 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

code

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) ← terminal

DEPOSIT_RECEIVED

Triggered when a deposit arrives. Pending compliance review — funds are not yet credited.

json
{
  "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_RECEIVED fires for stablecoin deposits — no authorized, denied, or cancelled events follow.

json
{
  "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.

json
{
  "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.

EventMessageWhen
DEPOSIT_DENIEDTransaction deniedCompliance rejected the deposit
DEPOSIT_EXPIREDTransaction expiredNo compliance action within the allowed timeframe
DEPOSIT_CANCELLEDTransaction cancelledCancelled by compliance before processing
DEPOSIT_CANCELLEDTransaction returnedDeposit reversed or charged back after receipt

DEPOSIT_CANCELLED fires for two distinct scenarios — use the Message field 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

code

TRANSACTION_CREATED
  → TRANSACTION_AUTHORIZED      (compliance approved)
  → TRANSACTION_STATUS_UPDATED  (status changes during processing)
     OR
  → TRANSACTION_DENIED          ← terminal
  → TRANSACTION_CANCELLED       ← terminal
  → TRANSACTION_EXPIRED         ← terminal

Cross-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)

json
{
  "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.

json
{
  "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 Date field (ISO 8601 timestamp) in addition to the standard Event, Id, Message, and Data fields.

Data Field Reference

FieldTypeNotes
BrokerIdstringInternal identifier for the GMA partner broker
UserIdstringIdentifier of the customer user associated with the record
OldStatusstringStatus of the record before this change
NewStatusstringStatus of the record after this change
RecordIdstringUnique identifier of the counterparty or instrument record
RecordTypestringType of the record — COUNTERPARTY or COUNTERPARTY_INSTRUMENT

Events

EventMessageWhen
COUNTERPARTY_ACTIVATEDCounterparty activatedCounterparty status changed to Active
COUNTERPARTY_INSTRUMENT_ACTIVATEDCounterparty Instrument activatedInstrument status changed to Active
COUNTERPARTY_AWAITING_ACTIVATIONCounterparty awaiting activationCounterparty status changed to Awaits Approval (Rejected)
COUNTERPARTY_INSTRUMENT_REJECTEDCounterparty Instrument rejectedInstrument status changed to Rejected

Sample — COUNTERPARTY_AWAITING_ACTIVATION

json
{
  "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

json
{
  "Date": "2026-09-09T10:30:00.000Z",
  "Event": "<EVENT_NAME>",
  "Message": "<human readable message>",
  "Id": "<callback-uuid>",
  "Data": {}
}

Shared field notes

FieldNotes
UserIdMasked managed customer id (same as GET /cards/{userId})
CardIdCard reference id (same as GET /cards/{cardId}/...). Omitted on CARD_ORDER_CREATED
MaskedPanMasked PAN. Omitted on CARD_ORDER_CREATED

CARD_ORDER_CREATED

Triggered when a card order record is created.

Message: Card order created

FieldTypeNotes
UserIdstringMasked customer id
OrderStatusstringCurrent order status

CardId and MaskedPan are omitted (they are not available yet).

json
{
  "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

FieldTypeNotes
UserIdstringMasked customer id
CardIdstringCard reference id
MaskedPanstringMasked PAN
OldOrderStatusstringPrevious order status
NewOrderStatusstringNew order status
json
{
  "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).

FieldTypeNotes
UserIdstringMasked customer id
CardIdstringCard reference id
MaskedPanstringMasked PAN
TrackingNumberstringCurrent tracking number
CourierServiceNamestringCurrent courier
TrackingUrlstringCurrent tracking URL
json
{
  "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.

EventWhen
CARD_STATUS_CHANGEDDebit card status changed

Includes top-level Date.

Data fields: BrokerId, UserId, Date, OldStatus, NewStatus, RecordId, RecordType (Card Data), CardReferenceId, MaskedCardNumber.

Message: Card status changed.

json
{
  "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".

FieldTypeNotes
UserIdstringMasked customer id
CardIdstringCard reference id
MaskedPanstringMasked PAN
CardStatusstringCurrent card status
AtmEnabledstring"true" or "false"
TransactionSuspendedstring"true" or "false"
json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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.

EventMessageWhen
CARD_TRANSACTION_CREATEDCard transaction createdA card transaction is created
CARD_TRANSACTION_AUTHORIZEDCard transaction authorizedThe card transaction is authorized
CARD_TRANSACTION_DENIEDCard transaction deniedThe card transaction is denied
CARD_TRANSACTION_CANCELLEDCard transaction cancelledThe card transaction is cancelled
CARD_TRANSACTION_EXPIREDCard transaction expiredThe 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

FieldTypeDescription
Idstring (UUID)Unique webhook ID
EventstringOne of the events above
MessagestringEvent description
Datestring (ISO 8601, UTC)When the event occurred
DataobjectCard transaction details

Data Fields

FieldTypeDescription
UserIdstringFV Bank user ID of the cardholder
CardIdstring | nullFV Bank card ID (same ID returned by the Cards API)
TransactionNumberstringFV Bank transaction number
AmountnumberTransaction amount (up to 2 decimal places)
CurrencystringISO currency code, e.g. USD
TransactionTypestringTransaction type, e.g. CardPurchaseTransfer
Statusstring | nullAuthorization status of the transaction, e.g. Authorized. May be null on CARD_TRANSACTION_CREATED
MerchantNamestring | nullMerchant name
MerchantCategorystring | nullMerchant category
Mccstring | nullMerchant Category Code
AuthorizationCodestring | nullAuthorization code
CardLastFourDigitsstring | nullLast 4 digits of the card
DirectionstringSEND (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

json
{
  "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

json
{
  "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 2xx to acknowledge receipt.
  • Make your handler idempotent using Id.
  • Events can arrive close together and are not guaranteed to be in order. Use Event and Status to 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.TypeDestination Currency
Payment - Cross Border - AED Domestic PaymentsAED
Payment - Cross Border - AED Wire PaymentsAED
Payment - Cross Border - ARS Domestic PaymentsARS
Payment - Cross Border - BRL Domestic PaymentsBRL
Payment - Cross Border - CAD Domestic PaymentsCAD
Payment - Cross Border - CAD Wire PaymentsCAD
Payment - Cross Border - CLP Domestic PaymentsCLP
Payment - Cross Border - COP Domestic PaymentsCOP
Payment - Cross Border - DKK Domestic PaymentsDKK
Payment - Cross Border - SEPAEUR
Payment - Cross Border - EUR Wire PaymentsEUR
Payment - Cross Border - Faster PaymentsGBP
Payment - Cross Border - GBP Wire PaymentsGBP
Payment - Cross Border - HKD Domestic PaymentsHKD
Payment - Cross Border - HKD Wire PaymentsHKD
Payment - Cross Border - IDR Domestic PaymentsIDR
Payment - Cross Border - JPY Domestic PaymentsJPY
Payment - Cross Border - JPY Wire PaymentsJPY
Payment - Cross Border - ClabeMXN
Payment - Cross Border - MXN Wire PaymentsMXN
Payment - Cross Border - PEN Domestic PaymentsPEN
Payment - Cross Border - SGD Domestic PaymentsSGD
Payment - Cross Border - SGD Wire PaymentsSGD
Payment - Cross Border - ZAR Domestic PaymentsZAR
Payment - Cross Border - ZAR Wire PaymentsZAR

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 Id to deduplicate on
  • Deduplicate transaction-family events using Id — retries use the same Id
  • Use TransactionNumber to reconcile with your internal records
  • Use DEPOSIT_AUTHORIZED (not DEPOSIT_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, and CARD_TRACKING_UPDATED for order and shipment updates. Tracking payloads contain new values only (no Old* fields)
  • Card Data Webhooks — CARD_DATA_CREATED carries initial state. Later updates use CARD_STATUS_CHANGED, CARD_SUSPENDED / CARD_RESUMED, CARD_ATM_ENABLED / CARD_ATM_DISABLED, and CARD_LIMITS_CHANGED. There is no CARD_BLOCKED event
  • Card Transaction Webhooks — use TransactionNumber to link events for the same transaction and Id to detect duplicate deliveries. A purchase normally sends CARD_TRANSACTION_CREATED followed by CARD_TRANSACTION_AUTHORIZED, CARD_TRANSACTION_DENIED, CARD_TRANSACTION_CANCELLED, or CARD_TRANSACTION_EXPIRED. Events can arrive close together and are not guaranteed to be in order; use Event and Status for the latest state

Search guide books, endpoints, paths, or parameters

↑↓navigate↵openEscclose