Webhooks
Register an endpoint with /v1/webhook_endpoints (paid plan), and FinVeil sends a signed POST for each event you subscribe to. Webhooks cover /v1 payments, payouts and refunds.
FinVeil doesn't yet send webhooks for payment-link or Connect split payments. For those, poll GET /api/payments/by-reference/{reference}/status.
Event types
payment.createdA /v1 payment was created.
payment.succeededA /v1 payment succeeded.
payment.failedA /v1 payment failed or was stopped. Includes the failure reason code and message.
payout.createdA /v1 payout was created.
payout.succeededA /v1 payout succeeded.
payout.failedA /v1 payout failed or was stopped.
refund.createdA /v1 refund was created.
refund.succeededA /v1 refund succeeded.
refund.failedA /v1 refund failed or was stopped.
Register an endpoint
The response includes the endpoint's signing secret. Store it server-side.
POST https://api.finveil.money/v1/webhook_endpoints
X-API-Key: fvk_test_<SECRET>
Content-Type: application/json
{
"url": "https://example.com/finveil/webhooks",
"eventTypes": ["payment.succeeded", "payment.failed"]
}
→ 201 { "id": "...", "url": "...", "eventTypes": [...], "secret": "<SIGNING_SECRET>", "enabled": true, ... }Example delivery
The X-FinVeil-Signature header is an HMAC-SHA256 of the raw JSON body, keyed with the endpoint secret and hex-encoded. It has no sha256= prefix and no timestamp, so deduplicate on the event id.
POST /finveil/webhooks
Content-Type: application/json
X-FinVeil-Signature: 5d41402abc4b2a76b9719d911017c592... (lowercase hex, no prefix)
{
"id": "<EVENT_ID>",
"type": "payment.succeeded",
"created": "2026-09-14T09:41:22.531+02:00",
"data": {
"transactionId": "<TRANSACTION_ID>",
"merchantId": "<MERCHANT_ID>",
"kind": "COLLECT",
"state": "SUCCEEDED",
"amountCents": 125000,
"currency": "ZAR",
"providerId": "<PROVIDER_ID>",
"providerTransactionId": "<PROVIDER_REFERENCE>",
"failureReasonCode": null,
"failureReasonMessage": null
}
}// Node.js / TypeScript: verify before trusting the payload
import crypto from 'node:crypto';
function isValid(rawBody: string, signatureHeader: string, secret: string): boolean {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (expected.length !== signatureHeader.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}Delivery and retries
- Any 2xx response within 10 seconds counts as delivered.
- If delivery fails, FinVeil makes up to 7 attempts in total, waiting 1, 5, 25, 120, 600 and 1,440 minutes between them (about 36 hours). After that the delivery is marked dead.
- Replay a delivery with
POST /v1/webhook_endpoints/deliveries/{id}/replay.
Inbound provider webhooks
Paystack sends its events to FinVeil, not to you. FinVeil checks each one against Paystack's HMAC-SHA512 x-paystack-signature and rejects unsigned or tampered requests.