API Design for Integrations: Building Something Other People Can Depend On
An API other systems rely on is a promise. Predictable shapes, careful versioning, pagination, and idempotency decide whether integrators trust it or work around it.
Once another system depends on your API, it stops being an implementation detail and becomes a promise. Every field name, every status code, and every behaviour you did not think about is something someone has built on top of. Changing it later costs them, and they will remember.
That changes how an API should be designed. The aim is not elegance for its own sake. It is to be predictable, hard to misuse, and safe to change, because the people calling it are not in the room when you make decisions, and they cannot see your intent, only your behaviour.
Design from the caller’s side
It is easy to build an API that mirrors your internal structure: the tables, the services, the way your code happens to be organised. That makes it simple to build and awkward to use, because the caller has to understand your internals to get anything done.
Start instead from what the caller is trying to accomplish. What are the common tasks, and what is the shortest, clearest path through each? Design the resources and operations around those tasks, and keep the internal arrangement hidden behind them. The test is whether someone can do the common thing from the documentation alone, without asking you anything.
Be consistent to the point of boredom
Consistency is most of what makes an API easy to learn. If dates are in one format in one place and another elsewhere, if one list endpoint paginates and another does not, if errors look different depending on the route, every inconsistency is a small tax on every integrator.
Pick conventions and apply them everywhere: naming, casing, date and time formats, identifiers, how collections are returned, how filters and sorting are expressed. Once a convention exists, a new endpoint should be guessable from the ones before it.
Make errors useful
When a call fails, the caller needs to know what happened and what to do. Use the status codes in their ordinary meanings, and return a consistent body with a stable machine-readable error code, a human-readable message, and the field at fault where relevant.
Distinguish clearly between errors the caller caused and can fix, such as invalid input, and those that are temporary, such as a timeout or an overloaded service, since the right response to each is different. Callers building retry logic depend on that distinction, which is the same classification of transient and permanent failures described in dead-letter queues for automation.
Plan for versioning from the start
You will need to change the API. The question is whether changes break callers. Separate the two kinds: additive changes, such as a new optional field or a new endpoint, are generally safe, and breaking changes, such as renaming a field or changing a meaning, are not.
Give breaking changes a path: a version in the address or a header, a clear deprecation policy, a stated notice period, and a period where the old and new run side by side. That is the same parallel-running discipline as in migrating off legacy software, applied to your own interface. And tell callers when things are changing, not after.
Tolerant behaviour on both sides helps. Callers should ignore fields they do not recognise, and you should avoid removing or repurposing fields without a version bump.
Paginate every collection
Any endpoint that returns a list will eventually return too many items to send at once. Add pagination from the beginning, because adding it later breaks callers who assumed they would get everything.
Prefer cursor-based pagination for collections that change, since offset-based paging can skip or repeat items when records are added or removed between requests. Return a clear indicator of whether more results exist, and set a sensible default and maximum page size.
Make writes safe to repeat
Networks fail, and callers retry. If a create request times out, the caller cannot know whether it succeeded. Without protection, retrying creates a duplicate.
Support idempotency keys on any operation that creates something or has a side effect: the caller supplies a unique key, and a repeated request with the same key returns the original result instead of acting again. It is the mechanism explained in idempotent automation, and offering it is one of the most considerate things an API can do for its integrators, particularly for anything involving money, as in payments and billing architecture.
Authenticate, authorise, and limit
Decide how callers prove who they are, and what each is allowed to do. Issue credentials that can be scoped and revoked individually, so one compromised key does not expose everything. Apply the least privilege: a key for reading should not be able to write.
Document rate limits and enforce them predictably, returning a clear response and a wait time when exceeded. Callers who know the limits can pace themselves, as covered in rate limits and backoff, and you protect the service from a single misbehaving client.
Offer events, not just requests
Integrators often need to know when something changes. Polling your API repeatedly is wasteful for them and costly for you. Offer webhooks for important events, and design them with the care described in webhooks vs polling: signed payloads, retries, unique event identifiers, and a way to fetch current state to recover from missed events.
Document it like a product
The documentation is part of the API. Provide a clear reference, worked examples for the common tasks, error explanations, and a changelog. Make it easy to try the API safely with a sandbox or test credentials. An integrator who can make a successful call in minutes is far more likely to trust and keep using it.
When to keep it simple
An API used by one internal system, maintained by the same team, does not need the full apparatus of versioning policy and public documentation. But even there, consistency, useful errors, and safe retries pay for themselves. As soon as someone outside your control depends on it, treat it as the promise it has become.
We design and build the interfaces other systems depend on. See how we work on applications, or read about multi-tenant SaaS architecture.