Down Payment Webhook
When a plan requires a down payment, FINA collects it on a hosted checkout page. Once that payment settles and the order authorises, FINA POSTs a signed confirmation to your webhook_url — the server-to-server source of truth that the down payment was received.
Fulfil on the webhook, not the browser redirect
A buyer can close the tab before your success_redirection_url ever loads. Treat the redirect as browser UX only; confirm the payment via this webhook, or pull the state on demand with Reconcile Prepaid Payment if a confirmation is delayed or missed.
When it fires
Once per order, when the FINA-collected down payment is paid and the order moves to AUTHORISED. Delivery is at-least-once — you may receive the same confirmation more than once, so make your handler idempotent (see below). Configure the destination via webhook_url and webhook_secret in the Initiate Checkout request body.
Handling the webhook
Four things your handler must do. Each is expanded in the sections below.
| To determine | Use |
|---|---|
| Payment completion | Check payment_status == "COMPLETED". Only then is the down payment actually collected — do not treat receipt of the webhook alone as completion. |
| Authenticity | Verify X-Fina-Signature against the raw HTTP body exactly as received. Never re-serialize the parsed JSON before hashing. |
| Idempotency & dedup | Key off event_id. It is deterministic and identical across retries, so process each one once. |
| Acknowledgement | Return a 2xx as soon as you have durably stored the event. Anything else is treated as a failed delivery and retried. |
Request & headers
POSTwithContent-Type: application/json, ~30s timeout per attempt.- Redirects are not followed — your endpoint must return a terminal
2xxitself. Any non-2xx (including 3xx) or a timeout counts as a failed delivery and is retried. - Return
2xxas soon as you have durably accepted the event; do fulfilment work asynchronously.
Signature headers are present only when a webhook_secret was configured:
| Header | Description |
|---|---|
X-Fina-Signature | Lowercase-hex HMAC-SHA256 over "<timestamp>.<raw_body>" keyed with your webhook_secret. |
X-Fina-Timestamp | Unix time (seconds) when the request was signed; also part of the signed string, so it cannot be tampered with. |
No secret means an unsigned webhook
If you did not set a webhook_secret, no signature headers are sent and you cannot verify authenticity — rely on TLS and treat the payload cautiously. Setting a secret is strongly recommended.
Body
| Field | Type | Description |
|---|---|---|
event_id | string | Deterministic id for this event — use as your idempotency key; retries carry the same event_id. |
order_reference_id | string | Your order_reference_id from Initiate Checkout. |
order_number | string | FINA's order id (the order_id returned by Initiate Checkout). |
order_status | string | The order's lifecycle status at confirmation — AUTHORISED once the down payment is confirmed. |
payment_status | string | Same value space as Reconcile Prepaid Payment's payment_status — COMPLETED for a confirmed down payment. |
amount | number | Down-payment amount confirmed. |
payment_method_code | string | Payment method used (omitted if unavailable). |
event_timestamp | string | RFC 3339 UTC timestamp, e.g. 2026-07-15T04:30:58Z. |
{
"event_id": "e3bb4ee574e355513ac2025d4fef13781ee119cb4e33a70d80ae865bf7a31e6a",
"order_reference_id": "ORD-2026-0001",
"order_number": "F26070910AB3XKQ70",
"order_status": "AUTHORISED",
"payment_status": "COMPLETED",
"amount": 26.25,
"payment_method_code": "E-PAYMENT mada",
"event_timestamp": "2026-07-15T04:30:58Z"
}Verifying the signature
Recompute the HMAC over the exact raw request bytes — do not re-serialize the parsed JSON, since key order and whitespace would differ and break the check.
signed_string = X-Fina-Timestamp + "." + <raw request body bytes>
expected = hex( HMAC_SHA256( webhook_secret, signed_string ) )
valid = constant_time_equals( expected, X-Fina-Signature )const crypto = require("crypto");
// Give your handler the RAW body (e.g. express.raw()), not a re-parsed object.
function verifyFinaWebhook(rawBody, headers, secret) {
const ts = headers["x-fina-timestamp"];
const sig = headers["x-fina-signature"];
if (!ts || !sig) return false;
const expected = crypto.createHmac("sha256", secret)
.update(ts + "." + rawBody) // rawBody = exact bytes received
.digest("hex");
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300; // replay window
return ok && fresh;
}Reliability & idempotency
- At-least-once delivery. FINA retries failed deliveries (non-2xx, timeout, network error) up to 5 attempts with backoff — the same confirmation can arrive more than once.
- Idempotency. Dedupe on
event_id— it is deterministic and identical across retries. Process each event once; ack duplicates with 2xx. - Replay protection. The signature covers
X-Fina-Timestamp; reject requests whose timestamp falls outside a freshness window (e.g. ±5 minutes). - Ordering. Don't assume ordering — key all logic off
event_id/order_reference_id. - If a confirmation is delayed, dropped, or never received, pull the authoritative state on demand via Reconcile Prepaid Payment.