Describing an integration: the API contract from the analyst’s side
At the seam between two systems it is not the code that fails but the agreement. Each side builds its part correctly — the way it understood it.
Two teams agreed in a meeting: “we send you the order, you send back the status”. Each built its part, both passed their own tests. At the seam it turns out that “status” is a code to one side and a text to the other, and that a resend of the same order is treated by the second system as a new order — and shipped twice.
Neither team got its own part wrong. The mistake is that there was no contract between them — only a spoken agreement about the happy path.
What a contract describes that a specification does not
A machine-readable API description — OpenAPI, a message schema — answers the question of shape: which fields, which types, which address. It says nothing about meaning: what each field signifies, when to call, what to do when there is no answer. Developers own the shape; the analyst owns the meaning. Neither works alone: shape without meaning gives a seam that compiles but behaves wrongly.
Six sections of an integration description
- Purpose and initiator. Who calls, on which event, whether they wait for an answer or learn the result later.
- Data. Fields with meaning and mandatoriness. If a field is in the data model, a reference to the dictionary is enough — no retelling.
- Responses and errors. Which outcomes are possible and what the caller does in each.
- Retries and duplicates. What happens if the same request arrives twice.
- Limits. Rate, volume, response timeout — the non-functional requirements of the seam.
- Versions. How the contract changes and how long an old version lives.
Plus an example request and response — a real one, not a schematic. Both sides check themselves against the example, and it is the first thing to expose a difference in understanding.
Errors: what the caller does
A list of response codes on its own gives nothing. The useful part is the right-hand column — the decision about what to do with each outcome.
| Outcome | Example | What the caller does |
|---|---|---|
| Invalid data | A mandatory field is missing | Do not retry. Fix the source, tell whoever entered it |
| No access | The key has expired | Do not retry. Raise an alarm: it will not fix itself |
| Already exists | An order with this number was already accepted | Decide in advance: a successful retry or a conflict |
| Temporarily unavailable | The service is overloaded | Retry with a pause that grows between attempts; after N attempts — into a queue for review |
| Outcome unknown | The request went out, no answer came back | The most dangerous case — see below |
Unknown outcome and idempotency
When no answer arrives, the caller does not know the one thing that matters: was the operation performed or not. Retrying risks doing it twice. Not retrying risks not doing it at all. For an order or a payment both mistakes cost money.
The way out is to agree that a retry is safe. An operation that can be performed twice with the same result as once is called idempotent. Usually the caller sends a request key, and the receiver remembers it and, on a retry with the same key, returns the earlier result without performing the operation again.
POST /orders
Idempotency-Key: 7f3c2a90-5d1e-4b8a-9c1f-0e2d4a6b8c10
{ "external_id": "SHOP-118204", "items": [ ... ], "total": 1249.90 }
first call → 201, order_id 55120
retry → 200, order_id 55120 (no second order created)
The analyst does not need to choose the mechanism. What is needed is the requirement: “resending the same order does not create a second order” — and a statement of what makes an order “the same”.
An asynchronous seam duplicates by design. When the exchange goes through a queue or events, delivery is usually guaranteed “at least once”, and order is not guaranteed at all. The receiver has to tolerate duplicates and messages that arrive out of sequence. That is a requirement on the receiver, and it is written into the contract explicitly.
Versions and change
The contract will change. So that a change does not break the neighbours, it helps to agree two rules up front:
- A compatible change — adding an optional field. Safe if the receiver ignores unknown fields; worth writing down as a requirement on the receiver.
- An incompatible one — renaming a field, making it mandatory, changing its meaning. Such a change ships as a new version, and the old one lives for an agreed period.
The most dangerous change is one of meaning without shape: the field has the same name and type, but “total” suddenly includes delivery. The schema will not notice. Only the data dictionary will — if there is one.
The sign there is no contract: the neighbouring system’s behaviour is learned from its error logs.
A fifteen-minute check
Take one integration and ask both teams a single question: what happens if the request went out and no answer came back? If the answers differ — or there are none — that is where the contract needs finishing.
What next
The description is done, development is under way — and midway a change arrives. It has to be made so that it reaches the code, the contract and the neighbouring team. That is the next stage — development.