Delivery Confirmation Webhook
With confirmation_type SMS_LINK, Confirm Delivery leaves the order OUT_FOR_DELIVERY until the buyer confirms receipt via the SMS link. This webhook is the signed server-to-server signal that the delivery completed — the order moves to DELIVERED and invoicing proceeds.
Part of the buyer confirmation flow
Until the buyer confirms, the order stays OUT_FOR_DELIVERY and invoicing is held. Key your post-delivery bookkeeping off this webhook — there is no browser redirect in this flow, so it is the only completion signal your servers receive.
When it fires
Once per order, when the delivery completes:
- The buyer confirms via the SMS link FINA sent when Confirm Delivery was called, or
- a buyer dispute is resolved (approved) by FINA operations — the callback is identical to a direct confirmation.
A raised dispute sends nothing — the webhook only arrives once the dispute is resolved, and invoicing is held in between.
Configure the destination via delivery_callback_webhook_url and delivery_callback_webhook_secret on the Confirm Delivery request. The URL must be HTTPS and its origin allowlisted with FINA at onboarding. If no callback URL was supplied, no webhook is sent — you opted out.
Request & headers
POSTwithContent-Type: application/json.- Return
2xxas soon as you have durably accepted the event; do bookkeeping work asynchronously. Anything else counts as a failed delivery and is retried.
Signature headers are present only when a delivery_callback_webhook_secret was supplied:
| Header | Description |
|---|---|
X-Fina-Signature | Lowercase-hex HMAC-SHA256 over "<timestamp>.<raw_body>" keyed with your delivery_callback_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. |
Body
| Field | Type | Description |
|---|---|---|
order_number | string | FINA's order id (the order_id returned by Initiate Checkout). Also your idempotency key — there is no event_id on this webhook. |
status | string | Always "DELIVERY_CONFIRMED" — the webhook is only sent when the delivery completes. |
event_timestamp | string | RFC 3339 UTC timestamp, e.g. 2026-07-15T06:12:41Z. |
{
"order_number": "F26070910AB3XKQ70",
"status": "DELIVERY_CONFIRMED",
"event_timestamp": "2026-07-15T06:12:41Z"
}Verifying the signature
The scheme is exactly the same as the Down Payment Webhook's — HMAC-SHA256 over the exact raw request bytes, keyed with your delivery_callback_webhook_secret. The samples below work unchanged.
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. Failed deliveries are retried — the same confirmation can arrive more than once.
- Idempotency. There is no
event_idon this webhook — dedupe onorder_number.statusis alwaysDELIVERY_CONFIRMED, so a repeat for the same order is always a duplicate. - Replay protection. The signature covers
X-Fina-Timestamp; reject requests whose timestamp falls outside a freshness window (e.g. ±5 minutes). - There is no reconcile-style pull API for delivery state. If a webhook is missed, recover by checking the order against your own records — or contact FINA support to confirm the order's status.