FinaDOCS

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 determineUse
Payment completionCheck payment_status == "COMPLETED". Only then is the down payment actually collected — do not treat receipt of the webhook alone as completion.
AuthenticityVerify X-Fina-Signature against the raw HTTP body exactly as received. Never re-serialize the parsed JSON before hashing.
Idempotency & dedupKey off event_id. It is deterministic and identical across retries, so process each one once.
AcknowledgementReturn a 2xx as soon as you have durably stored the event. Anything else is treated as a failed delivery and retried.

Request & headers

  • POST with Content-Type: application/json, ~30s timeout per attempt.
  • Redirects are not followed — your endpoint must return a terminal 2xx itself. Any non-2xx (including 3xx) or a timeout counts as a failed delivery and is retried.
  • Return 2xx as soon as you have durably accepted the event; do fulfilment work asynchronously.

Signature headers are present only when a webhook_secret was configured:

HeaderDescription
X-Fina-SignatureLowercase-hex HMAC-SHA256 over "<timestamp>.<raw_body>" keyed with your webhook_secret.
X-Fina-TimestampUnix 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

FieldTypeDescription
event_idstringDeterministic id for this event — use as your idempotency key; retries carry the same event_id.
order_reference_idstringYour order_reference_id from Initiate Checkout.
order_numberstringFINA's order id (the order_id returned by Initiate Checkout).
order_statusstringThe order's lifecycle status at confirmation — AUTHORISED once the down payment is confirmed.
payment_statusstringSame value space as Reconcile Prepaid Payment's payment_status — COMPLETED for a confirmed down payment.
amountnumberDown-payment amount confirmed.
payment_method_codestringPayment method used (omitted if unavailable).
event_timestampstringRFC 3339 UTC timestamp, e.g. 2026-07-15T04:30:58Z.
Example delivery
{
  "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.

Signature scheme
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.