Writing

The API Design Checklist I Wish I Had at Integration #1

August 3, 2026 · Amit Solanki

For twenty years, every API I built had a silent assumption baked into it: I could see, and mostly control, what was calling it. My own frontend. My own admin panel. A partner I’d had a kickoff call with, whose engineers I could message directly when something looked wrong.

Across two decades of building production integrations — POS and delivery-platform systems for a restaurant-tech integrations organization, SMS gateway routing spanning two continents, and everything in between — that assumption broke the same way, over and over. What started as a handful of point-to-point integrations always grew into a longer list of independent callers: code I’d never see, written by teams I’d never meet, running in places I’d never visit. By the time the count crossed 200 production integrations, the pattern was unmistakable. Not demos — production traffic, real money, systems depending on a contract I couldn’t renegotiate after the fact.

Getting those APIs ready for that many independent callers turned out to be its own engineering discipline. Here’s what it taught me.

The assumption you don’t know you’ve made

An API designed for a small, known set of callers gets away with an enormous amount of sloppiness, because you can always call up whoever wrote the client and ask what they meant.

Your error message says "Something went wrong, please try again later"? When it’s your own app, a developer on your team reads that, shrugs, adds a retry with backoff, moves on. When it’s a POS vendor’s integration team three time zones away, running firmware you don’t have access to, that same message becomes an unanswerable support ticket — or an undocumented retry loop nobody at your company knows exists until it’s misfiring in production.

Your menu model distinguishes “Large” from “LG” from “Lrg” across three POS generations? When it was just your own app, nobody noticed — the UI rendered whatever string it got and a person read it. A new integration partner mapping that data into their own system doesn’t have a person in the loop to notice; their code either matches the string or it silently doesn’t, and orders come through wrong until a restaurant manager complains.

The pattern underneath both: when you can’t see or influence the caller’s code, every assumption you never wrote down becomes a bug someone else finds — usually in production, usually with money attached.

What actually breaks

Across 200+ integrations, the failures clustered in ten places:

1. Missing idempotency. Your own app mostly doesn’t double-submit; a POS terminal on flaky restaurant wifi does, constantly. If your order-placement endpoint doesn’t accept an idempotency key and dedupe on it, a terminal retrying an ambiguous timeout will feed someone twice and charge them twice. The consequences aren’t abstract: a duplicate order means a restaurant kitchen preps and wastes real food for an order that shouldn’t exist, and worse, a duplicate charge means a customer disputing a payment. This is the single highest-stakes fix on the list, and it’s the one every new POS integration eventually needs.

2. One authentication scheme stretched to cover every kind of caller. A single static API key model handled everything early on — inbound webhooks and outbound partner data access alike. That meant no scoping (one leaked key could reach more than the one partner it belonged to), no clean way to revoke a single partner’s access without breaking others sharing the same key generation, and no way for a receiving endpoint to actually verify a payload’s origin beyond trusting whatever shared secret showed up in a header. The fix was matching the mechanism to the calling pattern instead of reusing one: OAuth 2.0 for external systems that need scoped, revocable access to data on someone’s behalf, and HMAC-signed payloads for webhooks, so the receiving side can verify a request genuinely came from the sender without a live handshake.

3. Outbound calls with no cushion when a dependency wobbles. This cuts both ways: the integrations I built also called out to POS systems, delivery platforms, payment providers, and our own internal microservices. Without exponential backoff, a partner’s brief outage turned into a retry storm from my side — piling load onto a system that was already struggling, and turning their bad five minutes into mine too. And without a fallback or a cache to fall back on, one slow or unavailable internal service didn’t just degrade — it took down everything that called it, and everything that called that, one domino at a time. Traffic spikes have a way of finding whichever dependency has no cushion; without one, that’s how a quiet Monday turns into an all-hands incident.

4. Webhook payloads processed inline, then lost the moment a step threw. A webhook is a delivery I don’t get to ask for again on my own schedule. Running validation, transformation, and business logic synchronously inside the request handler was fine — until one of those steps threw an exception partway through, and the payload that triggered it was gone, with nothing local left to reprocess. The fix wasn’t cleverer error handling, it was structural: persist the raw payload the moment it lands, acknowledge receipt, and process it asynchronously from there. A downstream exception becomes a job to retry instead of data that just vanished.

5. Failure modes that only spoke to humans. Prose error messages are useless to code deciding what to do next, and a doc page showing one successful request-response pair tells a new integration partner nothing about the other twenty ways the call can come back. Both failures come from the same gap: designing for the one happy path a human tester exercises, not the full space of responses a partner’s code will eventually hit. Send a partner a payload with a validation failure and no structured detail about what failed, and their integration has no way to act on it — someone on their team has to notice, guess, and reach out to yours, burning time and support cycles a machine-readable error would have made unnecessary. What a new integration partner actually needs is what good API partners always secretly wanted — stable, machine-readable error codes with a clear retryable/non-retryable distinction, and every status code the endpoint can actually return, 2xx and 4xx/5xx alike, documented with what it means and what to do next. Skip either half and partners find out by trial and error, in production.

6. Rate limiting arriving too late, and shaped wrong once it does. It’s tempting to skip rate limiting until real external traffic justifies it — but “real traffic” usually means a misbehaving partner integration already in production, discovered the hard way. And the limits that do get built almost always assume “there’s one predictable caller here”: session lengths, request cadence, and retry behavior tuned for internal use. A new partner’s traffic trips them constantly during onboarding and testing, and the failure mode is ugly — legitimate orders silently throttled. The fix is limits in place before the first outside caller shows up, tuned per partner rather than for a single predictable shape.

7. A schema that didn’t state what was actually true. This showed up in two recurring forms. Type: your own framework might treat the number 1 and the string "1" as interchangeable, or quietly coerce null, nil, and an empty array into whatever felt convenient at the boundary — a partner’s framework doesn’t share those instincts, and won’t make the same silent conversion. Requiredness: a field marked optional in the schema but actually required by the business logic behind it is worse than one marked required outright — a partner’s integration validates cleanly, submits the request, and only finds out from a downstream error, or a silently wrong result, that the field was never truly optional. Both come from the same habit: writing the schema to match what your own language and your own internal caller happened to send, instead of stating explicitly what’s actually true — the real type for every field, and an honest required-or-optional flag matching what the endpoint actually enforces.

8. Semantics that live in tribal knowledge. Every long-lived API accumulates conventions that exist only in onboarding calls and Slack threads — fields that are “always” set together, statuses that “never” happen in practice. A new partner’s engineering team wasn’t on those calls. Anything that isn’t in the schema or the docs effectively doesn’t exist to them, and anything ambiguous in the schema will be exercised in every way it technically permits — usually by whichever partner read the spec most literally.

9. Data modeled for rendering, not meaning. Menus — like most catalog data — were structured to be displayed: modifier groups nested the way the app’s own UI nested them, names written for screens rather than for another system to reason about. A POS or delivery-platform integration needs the data to carry meaning: what’s actually a size, what’s a required choice versus an upsell, what “no onions” is allowed to apply to. I worked with years of data shaped by “the app renders it fine” — true, right up until it needed to mean something to somebody else’s system too.

10. No runnable starting point, so every new partner rebuilt the same first hour by hand. Docs describe an API; they don’t let someone fire the first request in thirty seconds. Without a ready-to-import Postman collection — auth pre-wired, every endpoint with a real example payload — a new partner’s engineer spends their first day translating prose into working requests before writing a line of integration code. That’s an hour of friction repeated once per partner, forever, for a problem that only needed solving once.

What I changed — the checklist

The fixes are unglamorous, which is the good news: this is engineering, not research.

  • Idempotency keys on every mutating endpoint, honored end-to-end — including through the layers that talk to a POS.
  • Auth matched to calling pattern: OAuth 2.0 for external systems that need scoped, revocable access to data, HMAC-signed payloads for webhooks — not one scheme stretched to cover both.
  • Backoff and a fallback on every outbound call: exponential backoff for partner and internal dependencies alike, plus a cache or fallback response so one slow or unavailable service doesn’t cascade into everything that depends on it.
  • Async webhook processing: persist every inbound payload the moment it lands, acknowledge receipt, then process it asynchronously — a downstream exception becomes a retryable job, not lost data.
  • Structured, fully documented responses: stable error codes with a retryable: true/false flag, and every status code an endpoint can actually return — 2xx and 4xx/5xx alike — documented with what it means and what to do next. Keep the prose for the humans debugging it; add the contract for the systems calling it.
  • Rate limiting from day one, tuned per partner: build limits in before the first external caller shows up, and tag/dashboard each partner’s traffic separately — you can’t tune for a shape you can’t distinguish.
  • A schema that states what’s actually true: the real type for every field (a number is never a numeric string, null isn’t an empty array) and an honest required-or-optional flag — both matching what the endpoint actually enforces, not what your own language’s coercion rules or your one internal caller happened to make true.
  • Schema tightening: make illegal states unrepresentable instead of assuming a value “never” happens. Enums over free strings. Required-together fields actually required together.
  • Semantic data review: audit your most-consumed resources (menus, in my case) for places where meaning lives in convention rather than structure.
  • A maintained Postman collection: auth pre-wired, every endpoint with a real example payload, kept in sync with the API itself — so a new partner’s first request happens in minutes, not their first day.
  • Explicit confirmation semantics: a clear point of no return in the flow, so a partner’s system — and the human behind it — knows exactly when money moves.
  • Guardrails sized for partners you don’t control: order-value caps, quantity sanity checks, and a way to flag “this needs a human.” Boring, until a new partner’s integration misparses a request during their first week live.

Notice what’s on that list: it’s just good API design, applied with unusual seriousness. Growing past 200 integrations didn’t create new requirements so much as raise the price of every corner I’d been quietly cutting for a decade. The integrations that handled scale best were the ones that had always been strict.

Your APIs are next

If your product has an API and more than a couple of consumers, this is coming for you — not as a partnership you negotiate once and forget, but as a growing list of systems you’ll never fully see inside. The only question is whether your API meets each new one with contracts or with charm.

The checklist above is where I’d start. And if you want a second set of eyes on whether your API is ready for its next integration partner — that’s exactly the kind of review I do now.

Working through something similar?

← All writing