StatusHub verifies each provider's signature, normalises every payload into one canonical transaction schema, and forwards it to your endpoint with ordering, retries and replay.
The handler returns 500. Some providers retry and some do not.
Transactions get stuck in a state nobody notices until a customer complains.
A fintech of any size integrates three to five providers. Each delivers webhooks differently, and the differences are not cosmetic — payload shape, amount units, success values, signature schemes, timestamps and retry policy all vary. So the webhook handler becomes a 600-line switch on provider name that exactly one engineer understands, and that engineer eventually leaves.
It normalises, forwards and records; your system decides what to do. When a provider sends a status StatusHub does not recognise, the canonical status is unknown — not a guess — and your code is told plainly that we do not know.
That boundary is load-bearing. The alternative, mapping an unrecognised SUCCESS to failed because failure looks like the safe default, is how a fintech reverses a payment that actually completed and charges the customer for a refund of money they received.
It is not a payment provider and holds no card data. Any 13–19 digit string passing a Luhn check is replaced before storage — enforced, not asserted.
The store happens before anything tries to understand the payload. That ordering is the product.
POST /v1/hooks/acme/paystack/live/rcv_7f3a… → 200 accepted and stored (1.4 ms) normalised 6 ms later: { "event_type": "payment", "status": "succeeded", "amount_minor": 12500000, "currency": "NGN", "transaction_ref": "TXN-88213" }
| Before | After | |
|---|---|---|
| Integrating a new provider | 1–2 weeks | under a day, or zero for a built-in |
| Webhook lines in your codebase | 400–800 | ~40, against one schema |
| Events lost during your deploy | unbounded | zero — buffered and replayable |
| Replaying a historical event | not possible | one API call |
| Signature defects | recurring, per provider | tested once per provider |
The raw bytes hit Postgres before parsing. A payload we cannot understand is still a payload we did not lose.
unknown is first-classAn unrecognised status is never coerced into a guess. Your code is told we do not know.
Per-currency exponents, decimal-text arithmetic. No float64 touches an amount, ever.
Sharded delivery keyed on transaction_ref, so two events for one transaction never overtake each other.
The SSRF check runs inside the dialler, after the lookup the connection will use. No rebinding window.
Length-prefixed, gapless per tenant, with ed25519 signed checkpoints. Verifiable from the dashboard.
make up && make demo
https://your-host/v1/hooks/{tenant}/{provider}/{env}/{token}