Webhooks
Webhooks allow you to receive real-time notifications about important events related to outbound and inbound transactions in your FV Merchant account, eliminating the need to continuously poll APIs.
📡 How Webhooks Work
When an event occurs (e.g., payment status update or deposit confirmation), FV Bank sends an HTTP POST request to your configured webhook URL with event details.
Event Occurs → FV System → Webhook POST → Your Server → Process Event
⚙️ Webhook Setup
To receive webhooks, you must provide a publicly accessible HTTPS endpoint.
- The endpoint must accept POST requests
- It should return a 200 OK response to acknowledge successful receipt
- Respond quickly (acknowledge first, process asynchronously)
📦 Payload Structure
Every webhook body has the same envelope:
json
{
"Date": "2025-01-14T09:12:33.000Z",
"Event": "TRANSACTION_STATUS_UPDATED",
"Id": "7bdeb64f-50b2-4624-87ea-4fa111282e8e",
"Message": "The member transaction status changed",
"Data": {
"TransactionNumber": "FV000006187",
"Status": "COMPLETE",
"Amount": "1000.00",
"Currency": "USD"
…| Field | Description |
|---|---|
Id | Unique delivery ID. Use this to deduplicate — the same Id may arrive more than once |
Event | Event type (e.g. TRANSACTION_CREATED, TRANSACTION_STATUS_UPDATED, DEPOSIT_RECEIVED) |
Date | Time the event was queued |
Message | Human-readable description of the event |
Data | Event payload. This object alone is what the signature covers |
🔐 Webhook Security (Signature Verification)
Each webhook request includes an HMAC signature header:
x-signature: <hex-encoded HMAC-SHA256>| Property | Value |
|---|---|
| Header name | x-signature |
| Algorithm | HMAC-SHA256 |
| Encoding | Lowercase hex |
| Secret | Your API client secret — the same secret you use to sign the X-AUTH-TOKEN JWT. There is no separate webhook secret |
| Signed bytes | JSON.stringify(body.Data) — the serialized Data object only, not the full request body and not the raw request bytes |
Important for non-Node.js integrations: the signature is computed over a re-serialization of the
Dataobject, not over the raw bytes we transmit. To verify, parse the JSON body, then re-serializebody.Datausing a serializer that preserves the original key order and uses compact separators (no extra whitespace). In languages where JSON object key order is not preserved by default, you must preserve insertion order explicitly, otherwise the computed HMAC will not match.
How to verify:
- Take your API client secret
- Compute the HMAC-SHA256 of the re-serialized
Dataobject - Compare it to the
x-signatureheader using a constant-time comparison
Example (Node.js):
javascript
const crypto = await import('crypto');
const signature = req.headers['x-signature'];
const hmac = crypto.createHmac('sha256', CLIENT_SECRET);
const expectedSignature = hmac.update(JSON.stringify(req.body?.Data)).digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expectedSignature);
if (a.length === b.length && crypto.timingSafeEqual(a, b)) {
console.log('Matched');
} else {
console.log('Signature Not Matched');
…Replay protection
Webhook requests do not include a timestamp header, and the signature does not cover a timestamp or nonce. A captured request stays valid indefinitely if replayed. Because of this:
- Always serve your webhook endpoint over HTTPS
- Deduplicate on
Idand ignore any delivery you have already processed - Treat the webhook as a notification, not as the source of truth: re-read the authoritative state with
GET /v2/transactions/details/{transactionNumber}before acting on high-value changes - Optionally restrict your endpoint to FV Bank source IPs (contact support for the current list)
🔁 Delivery and Recovery
Each event is delivered on a single attempt when it is generated. If your endpoint is unreachable or returns a non-2xx response, the delivery is recorded as failed and is not automatically retried in production. Plan for this explicitly:
- Reconcile by polling. Use
GET /v2/transactions/details/{transactionNumber}for a known transaction, orPOST /v2/transactions/historyandPOST /v2/transactions/history/completedto sweep a date range after an outage. - Re-request a callback.
POST /v2/transactions/send-webhookre-sends a webhook for a specific transaction. It delivers the current state of that transaction, not the individual event you missed — if several status changes occurred while you were down, you receive only one callback reflecting the latest state. - Make your handler idempotent. Deduplicate on
Id, and make status transitions safe to apply more than once.
Ordering is not guaranteed. Do not assume webhooks arrive in the order the underlying status changes occurred; always trust the latest state read from the API over the arrival order of callbacks.
📌 Best Practices
- Verify the
x-signaturebefore processing - Store processed
Idvalues to prevent duplicate processing - Use webhooks for real-time updates and the transaction APIs for reconciliation
- Always return
200 OKquickly, then process asynchronously - Alert on gaps: if you expect a terminal status and never receive one, poll rather than wait
📌 Summary
Webhooks provide a low-latency signal for:
Payments → Status Updates
Deposits → Confirmation Events
They are the fastest way to learn about a change, but they are best-effort. A correct integration pairs webhooks with periodic reconciliation against the transaction APIs.
API reference