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.created

A /v1 payment was created.

payment.succeeded

A /v1 payment succeeded.

payment.failed

A /v1 payment failed or was stopped. Includes the failure reason code and message.

payout.created

A /v1 payout was created.

payout.succeeded

A /v1 payout succeeded.

payout.failed

A /v1 payout failed or was stopped.

refund.created

A /v1 refund was created.

refund.succeeded

A /v1 refund succeeded.

refund.failed

A /v1 refund failed or was stopped.

Register an endpoint

The response includes the endpoint's signing secret. Store it server-side.

register-endpoint.httphttp
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.

payment.succeeded.httphttp
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
  }
}
verify.tsts
// 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.