Common Mistakes When Integrating a Digital-Goods Supplier API
Supplier integrations do not break where teams expect them to. The happy path — create order, receive code — takes a day to write. Everything else is the states nobody modelled in version one. Below are the eight failure modes that show up most often in this industry, each with a concrete fix.
1. Retrying without deduplication → duplicate orders
What happens. Your service sends POST /orders. The supplier creates the order and starts fulfilment, but the response is lost on the wire. Your HTTP client retries per its own policy. The supplier sees a fresh request, creates a second order and debits your deposit again. The customer got one code; you paid for two.
Many integrators assume an idempotency header protects them here. Verify that your supplier actually implements one — plenty do not perform server-side dedup, and sending the header "just in case" achieves nothing.
The fix. Deduplicate on your side, always:
- Write a local
pendingrecord with your own UUID before calling the API. - Call order creation.
- On success, store the supplier's order ID against that same record.
- On timeout or network error, do not blindly retry — query the order list and look for a match on SKU, quantity and time window.
- Create a new order only if no match exists.
The pattern is worked through in detail in our deep dive on idempotency and safe retries.
2. Treating webhooks as a guarantee
What happens. The handler is written as if each event arrives exactly once and strictly in order. In reality webhooks are lost during your own deploys, arrive twice after a sender-side retry, and overtake each other — completed can land before processing.
The result is orders stuck forever in an intermediate state, and codes delivered twice because a redelivery was processed as a new event.
The fix.
- The handler must be idempotent: a repeated event with the same ID must not change state.
- Enforce monotonicity: ignore any event describing a state earlier than the one already recorded.
- Treat the webhook as a hint, not a fact — on receipt, read the order status from the API and record what the API returned.
- Keep polling as a safety net for orders that have not reached a terminal state in a reasonable time.
If your supplier offers no webhooks at all, polling becomes the only mechanism you have — how to build it properly is covered in tracking order status.
3. Retrying without backoff or jitter
What happens. The supplier returns 429 or 503. Your client retries after a fixed one second, across ten threads, for every stuck order simultaneously. You convert a brief degradation on their side into a sustained storm, prolong it, and risk a long rate-limit ban.
The fix.
- Exponential backoff with a ceiling, plus random jitter so clients do not synchronise.
- Retry only what is safe to retry:
GETalways, order creation only under the scheme in point 1. - Distinguish error classes:
4xxfrom bad input is pointless to retry,429and5xxare worth retrying but must respectRetry-After. - Add a circuit breaker: after a run of failures, stop hammering and route traffic to an alternative source.
4. No reconciliation job
What happens. While things work, nobody compares your data to the supplier's. Discrepancies accumulate quietly: an order paid but no code stored; a code stored but the sale cancelled; a deposit debit with no successful delivery. It surfaces a month later, once logs have rotated and the supplier's claim window may have closed.
The fix. A daily automated reconciliation across three lists: your sales, supplier orders, deposit movements. The job should write mismatches to a dedicated table and raise an operator task, not simply log them. Separately, sweep for stragglers — orders that have sat outside a terminal state longer than they should.
5. Plaintext codes and codes in logs
What happens. Keys and PINs sit in the database in plaintext and leak into debug logs, APM traces and support email bodies. One database dump or one over-eager log aggregator, and the entire asset walks out.
Understand the stakes here: a digital code is a bearer instrument. You cannot un-redeem it and you cannot claw it back.
The fix.
- Encrypt at rest under a dedicated key held in a secrets manager.
- Hard-prohibit codes in logs, traces and error strings, with a redaction filter in the logger itself.
- Restrict decryption rights to a minimal service list and audit every access.
- Set a retention policy: delivered codes past the dispute window should not be kept indefinitely.
6. Ignoring partial fulfilment and stockout
What happens. Ten units ordered, seven delivered. The code treats the response as a boolean success/failure and either hands the customer seven codes while charging for ten, or marks the whole order failed and delivers nothing even though seven were already debited.
Stockout and revocation belong in the same bucket: both are normal states, not incidents.
The fix.
- Model the order per line item — status and result live at the item level, not on the order as a whole.
- Use explicit states:
fulfilled,partially_fulfilled,out_of_stock,failed,revoked. - Define a policy for partial fulfilment: deliver what arrived, refund what did not, re-order the remainder from an alternative source.
- On the storefront show an honest status instead of an error, and in the backend route to an alternative SKU or region as described in multi-source fulfilment routing.
7. No sandbox testing and no failure testing
What happens. The integration is validated with three successful orders and shipped. The first timeout, the first 429 and the first partially fulfilled order are met with live money and live customers.
The fix.
- Use the supplier's test mode if one exists, and exercise more than the happy path: failure, timeout, partial fulfilment, insufficient deposit, unavailable SKU.
- Test your own side with a mock that injects latency, dropped connections and duplicate events.
- Explicitly test what happens during a deploy while orders are in flight — the single most underrated scenario.
- The full set of states worth covering is laid out in order flow explained.
8. Hardcoded FX and assumed currency
What happens. The conversion rate is a constant in the code, or read once at application start. A month later you are selling below cost and cannot see why margin moved. The same mistake in another form: assuming API prices always arrive in one currency.
The fix.
- Make the rate a configurable parameter with an explicit source, a refresh timestamp and a buffer for movement — not a constant.
- Read the currency from the API response rather than assuming it.
- Hold all monetary values as integers in minor units; never floats for money.
- Log the rate applied to each order — without it, retrospective margin reconciliation is impossible.
- How FX folds into your final cost is covered in how reseller discounts are calculated.
Pre-launch checklist
| Check | Done? |
|---|---|
| Local pending record written before the API call | |
| Order-creation retry goes through an existing-order lookup | |
| Event handler is idempotent and ignores stale events | |
| Retries use exponential backoff, jitter and a circuit breaker | |
| Daily reconciliation of sales, orders and deposit | |
| Codes encrypted, absent from logs and traces | |
| Partial fulfilment and stockout are explicit states | |
| Failure paths tested, not just the happy path | |
| FX configurable, currency read from the response |
Where to source inventory
Half the problems on this list are decided by supplier choice rather than by code: a clear order-status model, honest states when stock runs short, and a catalogue broad enough that you are not maintaining five integrations with five different sets of bugs. FoxReload provides 900+ SKUs across keys, gift cards, top-ups, eSIM and software licences behind a single REST API with automatic delivery — one contract, one retry policy and one reconciliation job instead of a zoo.
The natural next reads are the API quickstart and the comparison of pre-purchased stock vs on-demand fulfilment, which determines how much of this list you have to implement at all.
