StatusHub.
One receiver in front of every payment provider.

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.

The problem

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.

What it is not

StatusHub never decides what a transaction means.

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.

How it works

Providers6 built-in
Receiververify + store
Postgresraw bytes
Normalisecanonical
Dispatchordered
Your endpointone shape

The store happens before anything tries to understand the payload. That ordering is the product.

Seeing it

statushubctl simulate paystack charge.success
  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" }

In practice

BeforeAfter
Integrating a new provider1–2 weeksunder a day, or zero for a built-in
Webhook lines in your codebase400–800~40, against one schema
Events lost during your deployunboundedzero — buffered and replayable
Replaying a historical eventnot possibleone API call
Signature defectsrecurring, per providertested once per provider

Decisions that shaped it

Persist, then acknowledge ADR-001

The raw bytes hit Postgres before parsing. A payload we cannot understand is still a payload we did not lose.

unknown is first-class

An unrecognised status is never coerced into a guess. Your code is told we do not know.

Integer minor units

Per-currency exponents, decimal-text arithmetic. No float64 touches an amount, ever.

Ordering per transaction ADR-003

Sharded delivery keyed on transaction_ref, so two events for one transaction never overtake each other.

DNS re-resolved at delivery

The SSRF check runs inside the dialler, after the lookup the connection will use. No rebinding window.

Hash-chained audit

Length-prefixed, gapless per tenant, with ed25519 signed checkpoints. Verifiable from the dashboard.

Getting started

Run it
make up && make demo
Point a provider at it
https://your-host/v1/hooks/{tenant}/{provider}/{env}/{token}