Work · API product

Payment Truth

What should a merchant do when a payment looks complete but has not been verified? I designed and built an API reference project that separates provider observations from the decision to release an order.

Stage
Reference build
Evidence
Simulated validation
Last updated
24 September 2026
For
Online merchants on Paystack, and the developers who integrate their checkout and fulfilment.
My role
Sole product owner and builder. I scoped the problem, defined the decision rules and states, and built the API, simulator, merchant and operator views, OpenAPI contract and failure tests with AI-assisted coding. No merchant or external team contributed.
Latest decision
Keep the card-payment build as a reference and turn the next investigation toward bank-transfer confirmation.

The problem

A buyer returns from checkout and the page says the payment worked. Paystack's own guidance says a callback does not prove payment: the merchant must verify status and amount before releasing value. When confirmation is delayed, a webhook is repeated or forged, or the amount is wrong, the merchant has to decide whether to deliver anyway.

I chose digital access as the test case because delivery is immediate and hard to reverse. That segment is a hypothesis; I have not observed a merchant losing money to this problem.

The context

Paystack already provides a Verify endpoint. A separate API only earns its place if it makes the decision and the recovery clearer than merchant code calling that endpoint directly. The buyer needs a message that stops them paying twice; the operator needs a queue of what is stuck and why.

The important decision

Hold until the provider confirms the reference, a successful status, the exact amount in kobo and the currency. Review any mismatch. Release only when every check passes. A callback or webhook triggers verification but never releases an order on its own.

Provider observation is stored separately from the merchant's decision. A release is never rewritten: if the provider later contradicts it, the system opens an incident instead of silently changing history. The cost of this choice is slower confirmation in ambiguous cases, which the buyer message has to absorb.

How the system works

Six endpoints under /v1 register an expected payment, report the current decision, queue a fresh lookup, accept signed webhooks, hand a fulfilment worker a stable decision ID and list exceptions for an operator. Decisions are made in one pure function of the registered expectation and the provider observation; every state change is appended to a transition log with its evidence source.

Webhooks are matched to a merchant before the HMAC signature is checked and are stored once before acknowledgement. Provider secrets are encrypted at rest, API keys are stored as hashes, amounts are integer kobo only, and another merchant's check returns 404.

  1. TriggerCheckout callback or signed webhook
  2. CheckLook up the provider's own record
  • HoldPending, not found yet or provider unavailable. Retry with backoff.
  • ReleaseReference, successful status, exact amount and currency all match.
  • ReviewMismatch, malformed response or unrecognised status. No automatic release.

A later contradiction opens an incident. The earlier release is not rewritten.

Payment Truth's decision model, from the repository's state documentation. Reference build tested against simulated payment records.

What a developer integrates

The API contract is part of the product. A developer needs to know what to send, what the response establishes, and what to do when the result is uncertain.

Payment Truth makes those choices inspectable through an OpenAPI contract, explicit decision states, a simulator and a fault walkthrough. The integration story includes what happens after the first successful request: verification, retries, tenant boundaries and recovery. Those details shape whether another team can use the product with confidence.

GET /v1/payment-checks/{id}

{
  "payment_check_id": "pc_…",
  "merchant_order_id": "order_demo_01",
  "payment_state": "pending",
  "fulfilment_decision": "hold",
  "reason": "provider_verification_pending",
  "decision_id": null,
  "expected_amount_kobo": 250000,
  "currency": "NGN"
}
HTTP status says whether the request was handled. fulfilment_decision says what the merchant may do: hold, release or review. A decision_id exists only for a release, so a fulfilment worker can deliver once. Excerpt from the project's API documentation, with simulated values.

What happens when it goes wrong

Delayed confirmation, a provider timeout, a missing webhook, a duplicate or forged webhook, an amount mismatch and a later contradiction each have a defined path: stay on hold with bounded retries, reach the same decision through scheduled reconciliation, move to review, or open an incident. A fulfilment worker that asks repeatedly gets the same decision ID, so it can deliver once.

What I checked

Automated tests cover the decision table, the seven failure scenarios and the response contract; all 42 passed when I re-ran them on 14 October 2026. They run against a deterministic simulator with fictional orders: no real money, no live provider and no customer data.

What changed my mind

Desk research after the build suggested that official card integrations mostly verify payments correctly already, while bank transfers — a large share of Nigerian online payments — have no comparable hold, release or review decision that I could find. I kept the card build as a reference and turned the next investigation toward transfer confirmation. Completing the build gave me evidence for a different decision about what to pursue.

What remains open

No independent merchant or developer has used it, and no Paystack test-mode adapter is connected. Nothing here measures how often the problem happens or what a merchant would pay to avoid it. The next step is to observe recent transfer exceptions with online merchants before building further.

What happened

Against a scripted, Paystack-shaped simulator, the decision table and seven failure scenarios produced no false releases and no duplicate demo deliveries. This is behaviour under simulation, not a reduction in real merchant losses.

Next question

Do online merchants struggle more with confirming and matching bank transfers than with card verification, and would a separate service earn its place beside their gateway?

Artifacts

Look closer

Want to discuss the decisions behind this?