Architecture
APIs: Contracts Between the Parts of a System
An API is a contract about operations, data, errors, permissions and retries, and changing it safely means knowing its consumers, checking access to…
An API (application programming interface) describes how one part of a program asks another part to perform an operation or return data. In a web application that interface is often an HTTP API, but the idea of an API is broader than a web address or any single protocol.
Throughout this unit a booking service serves as the running example. Its identifiers, users, dates, tokens and results are invented; they do not describe a real service or any measured performance.
A data model and its transactions define what a system stores and how it protects changes (see Data Architecture). As soon as parts of the system, or outside clients, start exchanging requests, they need a contract that tells each side what it can expect from the other.
By the end of this unit you will be able to:
- separate an API's contract from its internal implementation;
- take apart a request, a response, an error, the permissions involved and the limits on retrying;
- judge whether changing a field or a value is compatible;
- describe an HTTP API in OpenAPI and understand what such a schema does not prove;
- check consumers and recovery paths before changing a contract.
The contract is a promise to the consumer
An API contract describes an available operation and how it behaves: who may call it, what input it needs, what success means, which errors are possible, whether the operation can take a long time, and whether a retry is allowed. A contract is only useful when the server actually implements the described behaviour and consumers know where its limits lie.
An API is not the same thing as a URL (uniform resource locator). HTTP (Hypertext Transfer Protocol) APIs are a common choice for web applications, but the protocol and the address still do not describe the whole contract. A request to /v1/bookings might create a booking with one method and return a list with another; the URL itself says nothing about parameters, permissions or consequences.
| Part of the contract | What the team has to define |
|---|---|
| Operation | Which action is allowed |
| Input | Fields, types, format, limits and defaults |
| Result | Status, response fields, identifier and completion time |
| Failure | The error code and error shape a program can handle |
| Access | Which user, role or object is entitled to the action |
| Concurrency | What happens if the resource changed while the request was in flight |
| Retry | Whether retrying is safe and how to find the earlier result |
| Compatibility | How long old clients and a new server will keep working together |
The contract has to name the business result. A response that says "request accepted" may only mean the server put a job in a queue. If bookings are created asynchronously, the client needs an operation identifier, a way to check its state, and the meaning of each state. Otherwise the consumer cannot tell whether it is allowed to show the customer a confirmation.
Sources for this section (checked 23 September 2026): RFC (Request for Comments, the series of Internet technical documents) 9110, HTTP Semantics, defines the semantics of methods and responses; the OpenAPI Specification 3.1.1 describes a machine-readable contract for an HTTP API. The list of what a contract must cover is a teaching synthesis; the specific promises are set by the API's owner.
Resources, endpoints and methods
A resource is the thing an API stores or reports on: a slot or a booking. An endpoint is a resource's address together with a request method, for example GET /v1/bookings/{booking_id}. The method expresses the consumer's intent, but it is the server that must carry out the matching semantics.
HTTP has general rules for safety and idempotency. A safe request does not ask for any change of state. An idempotent request has the same intended effect on the resource whether it is sent once or several times in an identical form; that does not promise byte-for-byte identical responses, nor that the server writes nothing to its own service log.
| Method | Typical purpose | Safe | Idempotent under HTTP semantics |
|---|---|---|---|
| GET | Read a representation | Yes | Yes |
| POST | Create something or start an action | No | Usually not |
| PUT | Replace a resource's representation | No | Yes |
| PATCH | Apply a partial change | No | Not necessarily |
| DELETE | Remove a resource | No | Yes |
An implementation must not turn GET into "confirm booking": a browser, a cache or a crawler may read the link again. A PUT that rewrites the same desired representation should, by design, not create a second booking. PATCH can be idempotent when the contract sets an absolute value, and not idempotent when the command means "increase the counter by one". POST can support safe repetition through a separate key, but HTTP itself adds no such protection.
For the booking service, sensible endpoints are GET /v1/slots?date=… to read available slots, POST /v1/bookings to create a booking and PATCH /v1/bookings/{booking_id} for permitted changes. Cancelling does not have to mean a physical DELETE: if audit, refunds or history matter, the contract can move the booking to a cancelled state through a separate domain operation.
Sources for this section (checked 23 September 2026): the methods section of RFC 9110 defines "safe" and "idempotent"; MDN: HTTP request methods summarises them for GET, POST, PUT, PATCH and DELETE. The table states what the standard defines; it does not guarantee that any particular server behaves correctly.
The parts of a request and the shape of data
A request can contain a path, query parameters, headers and a body. In the response the server returns a status code, headers and, when needed, a body. The types of these parts must be explicit: a slot identifier is a string, a number of seats is an integer, and a list of bookings is an array of objects.
POST /v1/bookings HTTP/1.1
Host: api.example.test
Authorization: Bearer <example-token>
Accept: application/json
Content-Type: application/json
Idempotency-Key: demo-booking-8f31
{
"slot_id": "slot_demo_204",
"note": null
}
A key in the path, such as /v1/bookings/bkg_demo_550, usually identifies a specific resource. Query parameters after the question mark, such as ?limit=20&after=…, often control a list or a filter. Headers carry metadata about format, authentication or tracing; the body carries the data the server has to validate. A header value does not become trustworthy just because a program sent it.
JSON (JavaScript Object Notation) is a common body format. In the request above, note is sent with the value null. If the field were absent, the body would look like this:
{
"slot_id": "slot_demo_204"
}
The contract has to say what a missing field means: apply a default, keep the previous value, or reject the request. That is not necessarily the same as null or an empty string; replacing a missing field with empty text can change the meaning. In OpenAPI 3.1, a field that accepts either text or null can be described as type: [string, "null"]; older versions use different syntax, so check a schema's version before reading it.
A schema checks shape: whether slot_id is present, whether a string is within the allowed length, whether note stays under its limit. A business check answers a different question: does this booking belong to this customer, is the slot still available, does the rule allow this record to be changed? Valid JSON does not prove that its content is acceptable to the product.
Sources for this section (checked 23 September 2026): the OpenAPI Specification 3.1.1 describes parameters, request bodies and the Schema Object, and uses the JSON Schema model; JSON Schema 2020-12 defines types, objects and arrays. The example requests are invented.
Responses, status codes and the shape of errors
The HTTP status code gives an API a general category for the result. The contract has to match it to the action:
| Code | Typical meaning in the example |
|---|---|
| 200 | The request succeeded and the response contains a representation |
| 201 | A new booking was created; the response may contain its identifier |
| 202 | The job was accepted, but its result is not complete yet |
| 204 | The action succeeded and there is no response body |
| 400 | The request does not match the API's format or rules |
| 401 | There is no valid proof of identity; the response must include an authentication challenge in WWW-Authenticate |
| 403 | The identity is recognised, but it has no access to the action |
| 404 | The resource was not found, or the API deliberately does not reveal that it exists |
| 409 | The resource's state conflicts with the request, for example the slot is already taken |
| 422 | The body is well-formed, but the instruction cannot be carried out because of its content |
| 429 | The request rate limit has been reached |
| 5xx | The server or one of its dependencies could not complete the request |
A success status does not always mean the business result is complete. In particular, 202 Accepted means the server accepted the request for further processing, not that the booking is already confirmed. For an asynchronous process the contract can return an operation_id and a link to its state, then report queued, running, completed or failed.
The response to the booking request shown earlier can confirm that the resource was created and return its address:
HTTP/1.1 201 Created
Location: /v1/bookings/bkg_demo_550
X-Request-ID: req_demo_84a1
Content-Type: application/json
{
"id": "bkg_demo_550",
"slot_id": "slot_demo_204",
"status": "confirmed",
"created_at": "2026-09-23T12:00:00Z"
}
Status 201 means the resource was created, and Location points to it. The body contains the identifier the server issued, the confirmed state and the creation time. The example values are invented.
So that programs do not have to parse free-form error text, an API can use RFC 9457, Problem Details for HTTP APIs. Here is an example conflict response:
{
"type": "https://api.example.test/problems/slot-unavailable",
"title": "This time is already taken",
"status": 409,
"detail": "Choose another available slot.",
"instance": "/requests/req_demo_84a1",
"request_id": "req_demo_84a1"
}
In Problem Details, type is a URI (uniform resource identifier) identifying the kind of problem and title gives a short description. status repeats the HTTP status, but the status of the response itself remains authoritative. detail explains this particular occurrence and instance identifies it. request_id here is an extra field defined by this API; it is useful for searching logs, but it is neither proof of authenticity nor a grant of access. An error must not reveal private data, an internal stack trace or secrets. How the server chooses between 401, 403 and a privacy-preserving 404 has to follow its access policy (see authentication and authorization).
Walking through the booking creation. The example request sends a POST to the bookings collection. The bearer token establishes who is making the request, and the server determines that identity itself: the client must not be able to assign someone else's booking through an attendee_id field. Content-Type describes the body's format, Accept the preferred response format, and Idempotency-Key switches on retry rules that the API itself has to define. slot_id selects the slot; note: null means "no note" unless the contract says otherwise. All the data in the example is invented.
The 201 response means the resource was created. Location gives the address of the new booking, id is the identifier the server issued, and status is the confirmed state. In this example X-Request-ID helps locate the operation in the logs, and created_at records the time in UTC (Coordinated Universal Time). A request identifier is useful for support, but it does not confirm identity or access rights.
If a user is signed in but has no right to an action or resource, the server can return 403 with a general reason, without revealing anything from someone else's record:
{
"type": "https://api.example.test/problems/booking-forbidden",
"title": "Action not allowed",
"status": 403,
"detail": "You do not have permission to perform this operation.",
"instance": "/requests/req_demo_84a2"
}
403 means access is forbidden; 409 means a state conflict, such as a taken slot. When a token is invalid or missing, servers usually return 401 with WWW-Authenticate, for example with the Bearer scheme. A privacy-aware policy may use 404 so as not to reveal that someone else's resource exists.
How to recover depends on the response. After a 400, fix the request: resending the same invalid date will not help. After a 409, re-read availability and offer a new slot. After a 429, respect Retry-After. If the result is unclear because of a timeout or a 5xx, first find out the state using the operation key instead of assuming the server did nothing (covered in detail under timeouts, retries and idempotency keys).
Sources for this section (checked 23 September 2026): RFC 9110: status codes defines the status classes and their semantics; RFC 9457 describes a machine-readable problem-details format for HTTP APIs. Where the standard does not prescribe a specific code or text, the exact choice is a decision for the contract.
Authentication, authorization and object-level access
Authentication establishes who the requester is, or which service is calling the API. Authorization checks whether this particular action is allowed for this requester on this particular resource. A bearer token in the Authorization header can grant access to a protected API, but it is a secret: anyone who obtains it can try to use it.
A role or scope often defines a general capability, such as reading bookings. That does not replace checking the object itself. If the path contains /v1/bookings/bkg_731, the server has to verify that the current user, or their organisation, really may read or change bkg_731 specifically. Do not rely on the identifier being hard to guess, or on the endpoint being hidden from the interface.
A worked example of an access boundary. Marta has a valid token and may read her own bookings. She sends GET /v1/bookings/bkg_demo_550; the server checks the record's owner and returns 200. Then Marta replaces the final identifier with bkg_demo_731, which belongs to Oleh. The server must reject this, or, under an agreed policy, answer 404 without revealing that the other record exists. The check "the token is valid" does not, on its own, permit returning Oleh's booking.
In teaching examples use text placeholders such as <example-token>, never a real key. Do not put a token in a URL, a screenshot, a log or an error message. Write permissions, organisation boundaries, changes to someone else's record and administrative roles all have to be part of the server-side contract.
Sources for this section (checked 23 September 2026): RFC 6750, Bearer Token Usage, describes sending such a token in the Authorization header and the risks involved; the Open Worldwide Application Security Project (OWASP), API Security Top 10 2023, API1: Broken Object Level Authorization, explains checking permissions on every object the API accesses. The example identifiers and users are invented.
Lists, filters and pages
For lists, the contract has to define the permitted filters, the ordering, the maximum response size and how to move to the next page. An unbounded "return all bookings" request can turn into a huge data transfer or an overly expensive operation. A filter also does not relieve the server of checking permissions on every result.
Pagination by offset and limit is easy to understand:
GET /v1/bookings?limit=20&offset=40&order=created_at
But if a new record is inserted ahead of the first row between page requests, positions shift: the next response may repeat a record or skip one. The risk is most visible in lists that change often.
Cursor pagination passes an opaque continuation token instead:
GET /v1/bookings?limit=20&after=opaque_cursor_a9f3k2
The server can continue after the last sort key it returned. For a reproducible order, sorting by a non-unique date is not enough; add a stable tie-breaker, for example created_at together with booking_id. A cursor does not, however, create a snapshot of the data by itself. If a filtered field changes between requests, a record can cross the boundary of the result set; a new record whose key sorts after the cursor can appear on the next page. If you need an unchanging view across all pages, the contract has to specify a snapshot token, a result-set version or another verified mechanism.
A request for the next page must keep the same filters and sort order, and the server checks the user's permissions again. A cursor is not an access token.
A worked example. An operator reviews a week of bookings sorted by creation time and identifier. The first response contains 20 records and a next_cursor. When the client asks for the next page, the server keeps the same filters, checks the user again and continues after the pair of keys. If a new booking has a later creation time, it may appear on the next page, and the API should document that property. A cursor is not an access token: a forged or stolen cursor does not override authorization.
Sources for this section (checked 23 September 2026): Google's Pagination guidance (AIP-158) requires defining a page size and page token, stresses that the token is not an access grant, and describes how page parameters interact. The composite-sort rule and the changing-list example are teaching explanations; each server has to document its own behaviour.
Versions and contract compatibility
Compatibility has at least three dimensions: an old client can still load its library, can still send a request of the right shape, and still receives the business meaning it expects. A /v1 in the path guarantees none of these. A field can keep the same type and still change its unit, rounding or meaning, and that alone can break a client.
A complete example: changing price. In the first version, a booking returns:
{
"price": 19.9,
"currency": "EUR"
}
The client treats price as a JSON number and calculates a total. If a new server replaces it with the string "19.90", a strict client may reject the response, while another may concatenate text with a number and show an error. The fact that JSON allows both types does not make them compatible.
The migration options depend on the consumers and on the rules for money. The team could add a new money field with an explicitly defined format, keep returning the old price in the old version for a while, move clients over and only after checking declare the old field deprecated. Or it could release a new major version of the contract and support both versions during the migration. For a monetary value, precision, currency, rounding and unit all need defining separately. The number of minor units is not the same for every currency, and a decimal string also needs a documented format.
Adding an optional field is often compatible for a client that ignores unknown fields, but it breaks a strict decoder that rejects unfamiliar names. Adding a new required field to a request breaks old clients that do not send it. Changing the list of status values can break code with an exhaustive switch. Find out how consumers actually behave first, then choose the change.
A transition plan has to name the old consumers, the date or condition for ending support, a usage metric, the behaviour after the version is retired, and a rollback path. Versioning is a way to organise incompatible contracts, not permission to skip telling consumers.
Sources for this section (checked 23 September 2026): Google's backwards-compatibility guidance (AIP-180) distinguishes source, wire and semantic compatibility and classes a change of field type as incompatible; the OpenAPI Specification 3.1.1 defines field types and descriptions in a schema. The price example is for teaching; the exact representation of money is set by each API.
Timeouts, rate limits, retries and idempotency keys
A timeout means the client stopped waiting; it does not prove the server did nothing. POST /v1/bookings may have created the resource and the response may have been lost in the network. A fresh request sent without checking can create a second booking.
An example of an uncertain result. The client sends a booking creation with the key idempotency-key-demo-552. Within a single local transaction the server reserves the slot and stores the result under that key. The 201 response is lost. The client does not know whether the action happened, so it repeats the same logical request with the same key, from the same client, with the same parameters. Under the contract, the server returns the identifier it stored earlier and does not create a second booking.
An idempotency key is not a general standard HTTP header. The contract has to define the scope in which the key is unique (per client or per operation), how long it is kept, how to handle a retry whose body differs, and what happens once the retention period ends. A retry with a different body should be rejected as a conflict, not silently executed as a new action. A local key does not guarantee a single effect in an external payment service: that service needs its own idempotency rules and a check of the result.
The right reaction depends on the response. After a 400 caused by a wrong date, fix the request; after a 409, re-read availability rather than retrying the old slot. After a 429, respect Retry-After; if the header is missing, use a documented, bounded delay that grows with each attempt and includes random jitter. Cap both the number of attempts and the total time for the operation. A refusal because of permissions, validation or a payment limit is not a temporary error to be retried blindly. A timeout or a 5xx does not prove the change failed either: look up the earlier result by its key before sending the action again, and make the final state visible to the customer or operator.
Sources for this section (checked 23 September 2026): RFC 9110 on idempotent methods explains when a client may safely repeat an HTTP request; Stripe: Idempotent requests documents Stripe's own rules for storing the response for a key; OpenAI API: Error codes shows that some 429 errors call for changing a limit, not repeating the same attempt. Stripe's or OpenAI's behaviour is not a universal contract for other providers.
Webhooks: events between services
A webhook is an HTTP message that a source server sends after an event, for example after a payment is confirmed. The receiver verifies the signature using the provider's secret, over exactly the same body bytes the sender signed, and compares it in a way that resists timing measurement. An identifier in a header does not by itself prove where the request came from.
An example of redelivery. A provider sends payment_succeeded with event_id evt_demo_44. After verifying the signature, the service records the event and updates the payment's state in a single local transaction. The deduplication key has to include both the provider's namespace and the event identifier. If the process crashes before the commit, neither the event record nor the payment change is saved, so a redelivery can safely run the local transaction again. After the commit, the service quickly returns success and hands any separate notifications to an outbox (the mechanism is covered in Data Architecture). If the process crashes before the email goes out, the job is still in the outbox log for a background worker to pick up after restart.
Do not send an email or credit a payment first and only then record that you saw the event: a crash between the external effect and the deduplication commit can repeat that effect. Likewise, do not answer 2xx before the event is durably stored, or a failure can lose it forever. If your response is lost, the provider will redeliver; agree on its rules for ordering, number of retries and retry window, because different webhook APIs behave differently.
GitHub recommends checking X-Hub-Signature-256, an HMAC (hash-based message authentication code) computed with SHA-256 (Secure Hash Algorithm 256), and comparing the signature without leaking information through execution time. X-GitHub-Delivery stays the same when a delivery is manually redelivered; it identifies the event, not an individual delivery attempt. GitHub Docs has documented the same GUID (globally unique identifier) appearing across different GitHub Apps. If you receive events from several Apps or webhook configurations, add the App's or hook's identity to the key namespace; the delivery ID alone is not necessarily unique across the whole organisation.
Sources for this section (checked 23 September 2026): GitHub: best practices for using webhooks describes the response deadline and redelivery; validating webhook deliveries recommends this algorithm and a constant-time comparison; issue github/docs#32822 and the merged pull request #33184 record the clarification of X-GitHub-Delivery semantics. The conclusion about a composite deduplication namespace is a recommendation for systems with several integrations, not a universal GitHub guarantee.
OpenAPI describes shape, not all behaviour
The OpenAPI Specification is a language-independent format for describing APIs. An OpenAPI document can describe operations, types, parameters, responses and security mechanisms, and a generator can use it to produce documentation or client code. The schema itself, however, does not implement the server, the check on who owns a booking, protection against two clients racing for a slot, or the idempotency of an external payment.
Here is a small, abridged example of the create operation:
openapi: 3.1.1
info:
title: Example booking API
version: "1.0.0"
paths:
/v1/bookings:
post:
operationId: createBooking
summary: Create a booking for the current user
security:
- bearerAuth: []
parameters:
- in: header
name: Idempotency-Key
required: false
schema:
type: string
minLength: 1
description: Repeat the same key with the same data, as the contract specifies.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateBooking"
responses:
"201":
description: Booking created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Booking"
"401":
description: Token missing or invalid; the server sends an authentication challenge
headers:
WWW-Authenticate:
description: For example, a Bearer challenge if the API uses bearer tokens
schema:
type: string
"403":
description: The current user is not allowed to perform this action
"409":
description: The slot is already taken
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
CreateBooking:
type: object
additionalProperties: false
required: [slot_id]
properties:
slot_id:
type: string
note:
type: [string, "null"]
maxLength: 500
Booking:
type: object
required: [id, slot_id, status, created_at]
properties:
id:
type: string
slot_id:
type: string
status:
type: string
enum: [confirmed, cancelled]
created_at:
type: string
format: date-time
Problem:
type: object
required: [type, title, status]
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
detail:
type: string
Before handing a file like this to consumers, check it against the implementation and the examples. The idempotency key here is optional, and its scope, retention period and behaviour on a retry with a different body are not defined; add those rules if the operation promises retry protection. security describes the authentication mechanism, but it does not check who owns a specific booking. The 409 response describes a conflict, but it does not guarantee that reserving the slot is atomic. The Location header must point to the resource actually created, and the Problem schema does not list every optional field the standard allows. A client generator checks some aspects of shape, but it does not prove that the server enforces the business conditions.
Sources for this section (checked 26 September 2026): the OpenAPI Specification 3.1.1: structure and security mechanisms is the normative description of the format; RFC 9457 defines problem details. The YAML is a teaching example, not a verified specification of a real service.
Service boundaries
A new endpoint does not by itself create a useful service boundary. If every small action requires a synchronous call to a neighbouring service, network dependencies multiply; an endpoint that is too broad ties clients to internal details. To decide on granularity, ask which part of the system owns the booking, the price or the notification, and what result another service can treat as sufficient.
If a client needs several calls in sequence, define the partial outcomes: the first call may succeed and the second fail. A network boundary does not turn them into one transaction. Record who owns the canonical state, and the rules for retrying and recovering (reliable event delivery is covered in Data Architecture). These are working trade-offs between coupling and the distribution of responsibility, and they are best considered together with the data model.
These questions connect API design with the architecture of the whole system. Software Architecture: The Hard Parts examines the trade-offs between granularity, coupling and distributed responsibility; Designing Data-Intensive Applications, 2nd Edition gives wider context on serialisation, schema evolution, retries and the uncertainty of network communication. They are bibliographic pointers for further reading, not a claim that we have checked specific pages or chapters of the full texts.
Sources for this section (bibliographic details and publisher pages checked 26 September 2026): Software Architecture: The Hard Parts; Designing Data-Intensive Applications, 2nd Edition (O'Reilly); the book's website; Martin Kleppmann's website. The full texts of the books were not used.
Two current model APIs are two separate contracts
When comparing the OpenAI and Anthropic APIs, read each provider's documentation on its own first. Similar concepts such as "input" or "message" do not mean identical fields, states, headers or guarantees.
OpenAI's official documentation describes creating a response with the Responses API through POST /v1/responses. The request includes, among other things, model and input; the response includes an identifier, a status and an array of outputs. Below is an abridged form, not the API's full JSON schema:
{
"model": "<model-id>",
"input": "Explain the status of booking bkg_demo_550"
}
Anthropic's documentation describes the Messages API at POST /v1/messages. Its example includes a versioned anthropic-version header, the key in an x-api-key header, and the fields model, max_tokens and messages:
POST /v1/messages HTTP/1.1
Host: api.anthropic.com
anthropic-version: 2023-06-01
x-api-key: <placeholder-key>
Content-Type: application/json
{
"model": "<model-id>",
"max_tokens": 512,
"messages": [
{
"role": "user",
"content": "Explain the status of booking bkg_demo_550"
}
]
}
These two fragments do not define the same response format, the same set of tools or the same storage rules. Do not carry a parameter from one provider to the other without checking that provider's current contract. Anthropic's documentation describes its header-versioning policy separately; OpenAI's example response has its own set of status and output values. In the examples the key is only a placeholder, and no secret should ever end up in course material or in code.
A model API adds an external dependency with its own errors, limits and data-retention terms. Your application still has to control access, timeouts, retries, logging and what it tells the user. Text produced by a model is not proof that a business action happened; for that, check the result of your own operation or the state returned by the API itself.
Sources for this section (checked 26 September 2026): OpenAI API Reference: Create a model response describes the Responses API, its input and the structure of its response; Anthropic API Reference: Create a Message shows the endpoint, headers and request body; Anthropic: API versioning explains the mandatory version header and which changes are allowed within a version. These are examples of specific platforms, not a universal standard.
Adapting APIs for agent tools
This is an adaptation for agent tools, not a guarantee about the model. A tool schema describes a name, arguments and types. The model may propose one or more calls; the application executes them and passes the results back. A proposal is not permission: the server validates the arguments, the access rights and the invariants exactly as it would for any ordinary API client.
For example, instead of an execute_sql(text) tool, offer a narrow operation such as confirm_booking(booking_id, expected_version). The handler determines the user from a protected context, checks access to the object, checks the version and confirms the booking within a transaction. The schema the model read is not authorization; even a correctly formed booking_id may belong to someone else.
For a long-running action, return a separate operation_id and its state. After a timeout the tool should help check the earlier result instead of forcing the agent to guess whether it may retry. Allow only the operations a specific task needs, limit the size of responses, and never return tokens, private fields or an arbitrary database dump. Before a significant external action, the product owner may require confirmation from a human.
Sources for this section (checked 26 September 2026): OpenAI: Function calling describes the multi-step loop between the model, the application and the tool call; Anthropic: Create a Message documents the tools and input_schema parameters. The safeguards in this section adapt ordinary server-side authorization and validation; they are not guarantees about model behaviour.
Before you change an API
Before a change, find the current description of the operation, its examples and its real consumers: clients, integrations, agents, background jobs and partners. Determine whether only the shape of the data changes or the business meaning too; check how old and new versions will work side by side, and which errors or uncertain results a client might see.
Name the condition under which the old version can safely be retired, how you will notice a failed transition and how you will roll it back: version usage, errors, latency, and looking up an operation by its request ID. Check the OpenAPI description against the implementation and the server's actual responses; for agent tools, check that permissions are narrow and that the result of an interrupted call can be established.
If you have no data about consumers, record that unknown as a risk and gather evidence first. Do not call a change compatible just because the server compiles or the OpenAPI file passes schema validation.
- Find the current description of the operation and its examples, instead of guessing behaviour from the endpoint's name.
- List the web clients, mobile apps, integrations, agents, background jobs and partners that read or send the field.
- Distinguish a change of shape from a change of business meaning: does the type, unit, default, set of states, permission, ordering or completion time change?
- Check the failure cases: a missing field,
null, someone else's object ID, a duplicate, a conflict, a timeout after commit, and a repeated request. - Decide which old and new versions may run at the same time, how to track consumers, and when it is safe to retire the old variant.
- Check the OpenAPI text against the server implementation and example responses; do not treat client generation as a check of every business condition.
- Describe observability: request ID, contract version, error code, latency, and a safe way to find an operation whose outcome is uncertain.
- For agent use, check that the tool has narrow permissions, that the server validates its parameters, and that the result can be established after an interrupted call.
Source for this section: Google AIP-180 distinguishes source, wire-format and semantic compatibility. This is a working checklist for a team, not a universal certificate of compatibility.
Summary
An API is a contract about operations, data, errors, permissions and retries. An HTTP method sets general semantics but does not implement a business rule. A schema shows the acceptable shape but does not confirm the right to a booking. A status code describes the result of a request, but a timeout can leave the client not knowing whether anything actually changed.
A reliable contract lets a consumer tell a refusal from an uncertain result, retry only what is safe to retry, and change a client without hidden breakage. Before a release, the team checks the real consumers, the permissions on every object, the compatibility of values and the path to recovery after a partial failure.
Sources and further reading
- IETF: RFC 9110 — HTTP Semantics (2022), RFC 6750 — Bearer Token Usage (2012), RFC 9457 — Problem Details for HTTP APIs (2023).
- OpenAPI Specification 3.1.1, and Google AIP-158 Pagination and AIP-180 Backwards Compatibility; these living pages were checked on the dates given in the sections above.
- Additional primary references: MDN HTTP request methods, JSON Schema Draft 2020-12 and OpenAI Function calling; pages checked 23 September 2026.
- OWASP: API Security Top 10 2023, API1: Broken Object Level Authorization.
- GitHub: Webhook delivery best practices, signature validation, issue github/docs#32822 and the merged PR #33184.
- Stripe: Idempotent requests; Stripe's rules apply to the Stripe API specifically.
- Current platform contracts: OpenAI API errors, Responses API, Anthropic Messages API and Anthropic API versioning; pages checked 23 September 2026.
- Books: Designing Data-Intensive Applications, 2nd Edition (Martin Kleppmann and Chris Riccomini, O'Reilly, February 2026) and Software Architecture: The Hard Parts (Neal Ford and co-authors, edition as listed on the author's page). Access to the full text of these books was not confirmed; bibliographic pages and publicly available material were used, not unverified quotations from the books.