A payment integration begins where the API ends
Provider changes that pass request tests can still alter webhook meaning and break settlement reconciliation, leaving a compiling integration with corrupted financial history.
8 min read
A payment API can accept a well-formed request and still leave the integration with its hardest questions unanswered. Did a timed-out request create a charge? Does authorized permit fulfilment? Is a second attempt a retry or a second financial instruction? Which record wins when a webhook, a status query and a settlement file disagree?
Those questions are the integration. The endpoint describes how to ask for a payment operation; production contracts define the operation’s identity, allowed state transitions, retry meaning, evidence and owner. Until those contracts exist, a successful API call is a demo, not an operable payment system.
Give the financial intent an identity
Start with the failure that exposes weak integrations fastest. The client sends a payment request, the provider processes it, and the response disappears when the connection times out. Silence does not reveal whether the side effect occurred. Retrying with a new identity can create a second operation; abandoning the request can leave a completed payment recorded as failed.
Provider idempotency helps, but only inside a declared scope. Stripe stores the first result for a key, including a 500, and requires the same parameters on replay; its API v1 documentation permits keys to be removed after they are at least 24 hours old.1 Adyen scopes keys to a company account for at least seven days and says duplicate detection does not cross regional endpoints.2 “Supports idempotency” is therefore not a complete guarantee. The integration must know the key’s namespace, retention window, parameter-matching rule, concurrency behaviour and replay response.
The durable model needs at least three identities: the commercial payment intent, an attempt to fulfil it, and the provider operation created by that attempt. Write the attempt and its idempotency key before making the network call. If the process crashes, recovery can replay the same attempt. If the amount, currency, credential or beneficiary changes, create a new attempt instead of smuggling a new financial instruction under an old key.
This local identity must outlive a provider’s deduplication window. It must also span routing decisions. A provider can prevent a duplicate within its own boundary; it cannot decide whether an uncertain operation elsewhere makes a new route unsafe.
Model transitions, not a success flag
The response to the first call is rarely the full payment state. Cardholder authentication, authorization, capture and reversal are separate mechanisms. EMV 3-D Secure exchanges transaction, payment-method and device information between merchant and issuer so the issuer can authenticate the consumer, sometimes through a challenge.3 Authorization can then reserve funds; capture is a follow-on instruction linked to that authorization; a reversal releases an authorization hold that will not be used.4
Collapsing these transitions into success creates operational ambiguity. An authenticated payment may still be declined. An authorized payment may still need capture. A capture request may remain unresolved. A partially captured authorization still has an unused amount. A reversed authorization must not later be captured accidentally.
Represent the payment as a state machine with amounts, not as a string copied from the latest response. Record authorized, captured, reversed and refunded amounts independently. Make each transition declare its prerequisites: a capture names the authorization it consumes; a reversal names the remaining authorized amount; fulfilment policy names the state it requires.
Also separate provider state from business state. AUTHORIZED is evidence about a payment operation. “Safe to ship” is a business decision that may additionally depend on capture policy, fraud review and the goods being sold. If the two are one field, changing a fulfilment rule rewrites financial history.
Turn errors into actions
An HTTP status is a transport or API result, not a complete payment diagnosis. Visa Acceptance documentation notes that an unsuccessful request can still return 201 because a transaction resource was created but the transaction is under review, declined or failed; it describes 502 as a server error or timeout that can be retried.5 The integration therefore cannot map every 2xx to “paid” or every 5xx to “nothing happened.”
Issuer declines need even finer treatment. Visa’s public response categories distinguish a category that should never be retried from categories that may be retried within a defined limit, including one that requires the payment data to be revalidated first.6 Throwing all of them into payment_failed discards the instruction needed by checkout, retry automation and support.
Build an error taxonomy by decision layer: request validation, authentication, transport, provider processing, network or issuer decision, and local persistence. Then map each normalized reason to one action: correct the request, ask the customer to authenticate, replay the same attempt, wait and query status, create a new attempt, or stop. The retry budget belongs to the payment attempt, not to whichever worker happens to receive the exception. Otherwise a queue retry, an operator replay and a customer refresh can each believe they own the first retry.
Unknown deserves its own state. It should carry a next check, a deadline and an escalation path. Treating uncertainty as failure is how duplicate charges are born.
Use events for speed and reconciliation for truth
The synchronous response is one observation. Webhooks provide later observations, but delivery itself has failure semantics: Stripe documents that events can arrive more than once and are not guaranteed to arrive in generation order; it recommends recording processed event IDs and retrieving related objects when required.7
A production receiver should therefore authenticate the message, insert it into a durable inbox under a uniqueness constraint, acknowledge it, and process it asynchronously. Applying an event means evaluating an allowed transition, not overwriting the payment with the last message dequeued. When a late event conflicts with a newer state, query the provider and converge deliberately.
Even a perfect webhook consumer cannot prove that money reached the expected bank payout. Settlement reporting closes a different boundary. Adyen’s public Settlement details report contains settled payments and their transaction-level costs specifically for reconciliation.8 A reconciliation job should join the internal payment and attempt IDs to provider transaction references, captures, settlement entries and payout batches. Missing joins become work items, not spreadsheet folklore.
The resulting design has three loops. The request path starts work. The event path reduces latency as external state changes. The reconciliation path finds what both missed or misinterpreted. Each loop must be replayable without repeating a financial effect.
Preserve lineage without preserving secrets
When an incident crosses checkout, a provider and a webhook worker, “the API returned an error” is not enough. W3C Trace Context standardizes traceparent and tracestate headers so request traces can propagate across distributed services.9 That trace is useful for latency and causality, but it is not the payment’s financial identity and may end at an external boundary.
Keep a structured payment journal alongside tracing: payment intent ID, attempt ID, idempotency key reference, provider request and transaction references, event ID, amount and currency, previous and next state, normalized reason, and timestamps. These identifiers let an operator reconstruct decisions without searching raw payload dumps.
That restraint is a security contract too. PCI DSS prohibits storing sensitive authentication data such as card verification codes and PIN blocks after authorization, even when encrypted.10 Debug logging cannot be an accidental archive of full payment requests. Logs should record the decision and safe references, while sensitive fields are excluded at the serializer and transport boundaries.
Operational measures can now describe broken contracts rather than generic uptime: attempts stuck in unknown, authorizations aging without capture or reversal, webhook processing lag, illegal transition counts, and settlement lines without an internal match. An alert should identify the payment cohort, the violated invariant and the safe recovery action.
Make change part of the contract
An integration is not finished at launch. API versions can affect accepted parameters, response shapes and webhook objects; Stripe explicitly documents all three and recommends pinning the version used by code rather than relying on an account default.11 A schema change in an asynchronous event can otherwise reach a code path that the synchronous integration test never exercised.
Pin versions where the provider permits it. Keep captured response and event fixtures with secrets removed. Test unknown enum values and additive fields. Replay historical events against the new consumer, then run old and new parsing or state-mapping logic in shadow before changing production decisions. A version upgrade is complete only when reconciliation still closes, not when the new SDK compiles.
The most useful go-live review is therefore a contract review. For every operation, name its durable identity, allowed transitions, ambiguous outcomes, retry rules, asynchronous evidence, reconciliation source, sensitive-data boundary, metrics and owner. The API call will occupy a few lines of code. These agreements determine whether those lines can be trusted at 3 a.m., after a timeout, during a provider incident, or at the end of a settlement day.
The API ends with a response. The payment integration begins with everything the system must still remember, decide and prove.
Sources
- “Idempotent requests (opens in a new tab).” Stripe Documentation.
- “API idempotency (opens in a new tab).” Adyen Docs.
- “EMV® 3-D Secure (opens in a new tab).” EMVCo.
- “Payments (opens in a new tab).” Visa Acceptance Solutions.
- “Transaction response codes (opens in a new tab).” Visa Acceptance Developer Portal.
- “Declined Authorizations (opens in a new tab).” Visa Acceptance Solutions.
- “Receive Stripe events in your webhook endpoint (opens in a new tab).” Stripe Documentation.
- “Settlement details report (opens in a new tab).” Adyen Docs.
- “Trace Context (opens in a new tab).” World Wide Web Consortium.
- “For PCI DSS, why is storage of sensitive authentication data (SAD) after authorization not permitted even when there are no primary account numbers (PANs) in an environment? (opens in a new tab).” PCI Security Standards Council.
- “API upgrades (opens in a new tab).” Stripe Documentation.