An API that moves money is judged on what it does when things go wrong, not on what it does when they go right. The happy path is straightforward. Everything below is about the rest.
Idempotency is the first requirement, not a refinement
Networks time out. A client that does not receive a response does not know whether the request succeeded, and it will retry. Without idempotency, that retry is a second payment.
The pattern is a client-supplied idempotency key on every state-changing request. The server records the key with the result, and a repeat of the same key returns the original result rather than performing the action again. Keys are stored long enough to outlive any plausible retry, which is longer than most implementations assume.
Money is not a float
Floating point cannot represent decimal fractions exactly, so arithmetic on it drifts. In a financial system that drift eventually becomes a reconciliation problem that nobody can explain.
Store amounts as integers in the smallest unit, or as a fixed-precision decimal. Store the currency alongside every amount, always, even in a system that handles only one today. Retrofitting currency into a schema that assumed it is worse than carrying the column from the start.
Record events, not just current state
A balance field tells you what is true now and nothing about how it got there. An append-only record of transactions tells you both, and the balance becomes something you derive rather than something you overwrite.
This is what makes the system auditable, and auditability is the requirement that separates financial software from everything else. It also makes reconciliation possible, because you can replay what happened rather than argue about it.
Failing safely against systems you do not control
Payment providers, banks and verification services will be slow or unavailable. The API has to have an answer for each case.
- Timeouts on every outbound call, set deliberately. A call with no timeout will eventually hold a worker until the process is restarted.
- A circuit breaker, so a provider having a bad hour stops taking your whole system down with it.
- Clear separation between “declined” and “unknown”. A declined payment is an answer. An unknown one needs reconciliation, and treating it as declined loses money.
What PHP 8 changes about this
Typed properties, enums and readonly classes let you make illegal states unrepresentable rather than merely tested. An amount that cannot be constructed without a currency, and a status that can only ever be one of the values you defined, remove a whole category of runtime bug at the point of writing the code.
That is a modest improvement in expressiveness and a large one in confidence, which in this sector is the thing being bought.
The boundary
None of this is compliance. We build to the requirements your compliance team sets and expect them to review the result. We hold no financial certification and do not claim any.