Resources · 33
Evolve API contracts without losing clients
Describe requests, responses and errors; test versions and make webhooks safe to replay.
· 8 min
What this guide helps achieve
- Write the contract
- Clarify errors
- Test compatibility
- Plan retirement
Quick check
- Which clients use each operation?
- Do errors have stable types and coherent statuses?
- Can an old client tolerate the change?
- Can retries duplicate an action?
- How will retirement be announced and verified?
Step-by-step method
- 01
Inventory consumers
List operations, clients, usages, versions and owners. Separate observed use from assumptions before changing a field.
Deliverable: contract dependency map.
- 02
Describe requests and responses
Maintain an OpenAPI description with schemas, realistic examples, success and failure responses. Check it against the running service.
Deliverable: versioned contract and conformance tests.
- 03
Stabilize errors
Use HTTP statuses according to their semantics and, where helpful, a documented problem type under RFC 9457. Do not expose secrets in details.
Deliverable: tested error catalog.
- 04
Classify the change
Inspect required fields, types, values, behavior, timeouts and pagination. Test old clients against the new version with representative cases.
Deliverable: compatibility matrix and version decision.
- 05
Make notifications robust
For webhooks, plan authentication, unordered delivery, duplicates and retries. Make processing idempotent where repetition is possible.
Deliverable: replay and acknowledgement scenario.
- 06
Release with an exit path
Publish migration notes, coexistence period and contacts. Measure old-version use and retire only after checking affected consumers.
Deliverable: migration plan and retirement evidence.
Management indicators
| Indicator | What it measures | First action |
|---|---|---|
| Contract | Operations described and matching service behavior | Fix divergence |
| Compatibility | Representative clients tested before change | Add missing cases |
| Errors | Documented types without sensitive data | Revise messages and schemas |
| Migration | Old-version use with an owner | Assist remaining clients |
Common pitfalls
- Assuming a version number alone preserves compatibility
- Documenting only 200 responses
- Returning an internal trace or secret in an error
- Assuming webhooks arrive once and in order
Frequently asked questions
Does OpenAPI replace testing?
No. Compare the description with real responses and consumer needs.
Does every change require a new version?
No. Decide from client impact and contract behavior; incompatible changes need an explicit plan.
Why typed errors?
They give clients a stable way to distinguish problems and choose an action.
ETag, HTTP caching and concurrent updates
Two clients, three versions: make a conflict visible
In this fictional scenario A and B edit the same representation. The server emits a strong ETag for each newly stored version. If-Match rejects a modification based on a version that is no longer current. These five explanatory excerpts make no connection and perform no real write.
1. A reads the initial version
A receives representation v1 and keeps "v1" with the data being edited. A copied tag without its representation is insufficient to understand a conflict.
Request excerpt
GET /demo/resource HTTP/1.1
Host: api.example.testResponse excerpt
HTTP/1.1 200 OK
Date: Thu, 22 Oct 2026 00:00:00 GMT
Cache-Control: no-store
ETag: "v1"2. B saves before A
B has also read v1. Its PUT satisfies If-Match: "v1". The exact new representation is stored as "v2"; this server returns 204 with the new validator.
Request excerpt
PUT /demo/resource HTTP/1.1
Host: api.example.test
If-Match: "v1"Response excerpt
HTTP/1.1 204 No Content
Date: Thu, 22 Oct 2026 00:00:00 GMT
Cache-Control: no-store
ETag: "v2"3. A attempts a stale write
The current version is now v2. A still sends If-Match: "v1": 412, without applying its modification. Preserve local work for comparison.
Request excerpt
PUT /demo/resource HTTP/1.1
Host: api.example.test
If-Match: "v1"Response excerpt
HTTP/1.1 412 Precondition Failed
Date: Thu, 22 Oct 2026 00:00:00 GMT
Cache-Control: no-store4. A reads again and resolves the conflict
A retrieves v2 and compares its original data, local edits and the current version. A person or an explicit business rule chooses which changes to retain.
Request excerpt
GET /demo/resource HTTP/1.1
Host: api.example.testResponse excerpt
HTTP/1.1 200 OK
Date: Thu, 22 Oct 2026 00:00:00 GMT
Cache-Control: no-store
ETag: "v2"5. A submits a reconciled modification
After that decision, A sends the revised proposal with If-Match: "v2". It succeeds in this scenario and creates v3. Another concurrent writer could still require another cycle.
Request excerpt
PUT /demo/resource HTTP/1.1
Host: api.example.test
If-Match: "v2"Response excerpt
HTTP/1.1 204 No Content
Date: Thu, 22 Oct 2026 00:00:00 GMT
Cache-Control: no-store
ETag: "v3"Fictional HTTP/1.1 excerpts: request and response bodies are omitted even where allowed. HEAD, 204 and 304 have no response content. The api.example.test domain and fixed date serve only to explain this scenario.
The protocol detects a stale version; it does not decide how to merge content. Document what the interface retains after 412, who resolves the conflict and when a fresh read is needed. If-Match: * does not replace comparison of a specific strong ETag.
Explore ETag comparisons and the simulator →
Primary references and limits
The standards define HTTP conditions and caching. The ten cases, laboratory and two-client walkthrough are original illustrations. Displayed results are calculated by this local model, not measurements of this site or your infrastructure.
- IETF · RFC 9110 — HTTP Semantics · June 2022 · STD 97
§8.8.3.2; §13.1.1–13.1.2; §13.2; §15.3.5; §15.4.5 - IETF · RFC 9111 — HTTP Caching · June 2022 · STD 98
§4.1; §4.3; §5.2.2.4–5.2.2.5; §5.2.2.7 - IETF · RFC 6585 — Additional HTTP Status Codes · April 2012 · Standards Track
§3 · 428 Precondition Required
References reviewed
Retry-After: waiting, budgets and retry decisions
Specify permission to repeat before retry timing
Retry-After describes waiting. The contract must also define replayable operations, keys, versions and unknown-result states.
- State whether the field is seconds or a date and how clients handle absent or invalid values.
- Name the retry controller and global budget to prevent overlapping attempts.
- Retain parameters, context and the idempotency window without generalising Stripe to every API.
Explore Retry-After and the retry model →
References reviewed · RFC 9110 · §9.2.2, §10.2.3 · RFC 6585 · §4 · AWS
OAuth/OIDC: audience and access
An API contract must name its audience and permissions
Bind the route to an expected resource, explicit scopes and an object rule. Signature verification alone does not establish this contract.
- Document accepted profiles and separate access tokens, ID tokens and opaque tokens.
- Test a token intended for another API, missing scope and another tenant’s object.
- Return a controlled denial and keep traces free of codes and tokens.
See token recipients and the ten OAuth/OIDC cases →
Sources reviewed on · IETF · RFC 9700 · OAuth 2.0 Security BCP · IETF · RFC 9068 · JWT access token profile · OpenID Foundation · OpenID Connect Core 1.0 · IETF · RFC 8725 · JWT Best Current Practices · IETF · RFC 6749 · OAuth 2.0 framework
Webhooks: signatures and replay
The contract must define the exact authenticated bytes
Stripe and GitHub do not use the same HMAC message. Record the header, raw body, trusted keys and freshness policy without generalising one provider’s profile.
- Test a reformatted body with its original signature.
- Separate cryptographic validation, freshness and duplicate recognition.
- Document durable acknowledgement and business status with distinct evidence.
See webhook signatures, digests and the ten cases →
Sources reviewed on · Stripe · Webhooks · Stripe · Manage webhook endpoints · GitHub · Validating webhook deliveries · GitHub · Best practices for using webhooks · GitHub · Handling failed webhook deliveries
API errors: types, statuses and evidence
A useful API error connects a stable problem type, an occurrence and a possible client action. RFC 9457, published in July 2023 to replace RFC 7807, supplies a common format. It does not turn an error message into authorisation evidence, replay instructions or a complete security diagnosis.
Five explained standard members, six problem types in the IANA snapshot updated 26 June 2026, and eight fictional acceptance cases. Sources consulted 23 October 2026. Examples and proposed tests are original CIE material.
IETF · M. Nottingham, E. Wilde, S. Dalal · RFC 9457 · July 2023 · obsoletes RFC 7807 · §3.1, §3.2, §4.2, §5
Five members: role and client-side reading
| Member | Expected type | Role | Acceptance question |
|---|---|---|---|
| type | string · URI reference | Identifies the problem type; defaults to about:blank if absent. | Does the client decide using the URI rather than a translated title? |
| status | number · HTTP status | Restates the status for this occurrence; advisory. | Do body and response status agree? |
| title | string | Human-readable summary of the problem type. | Does localisation change only person-facing text? |
| detail | string | Human-readable explanation of this occurrence. | Does the client avoid parsing prose as a machine code? |
| instance | string · URI reference | Identifies a particular occurrence. | Does the reference support follow-up without exposing internal details? |
These members are optional. Consumers must ignore a member with an unexpected JSON type. They must also tolerate unrecognised extensions. The specified JSON media type is application/problem+json.
Reading workshop
Select one of eight examples, then optionally change the assumed received HTTP status. The body and media type remain those of the chosen case. This workshop compares limited facts; it validates neither all URIs, business schemas, authorisation nor a real service. The media-type name is compared without validating all its parameters. HTTP method and other transport rules still need checking.
Only the fictional examples displayed below are used. URIs are shown as text: none is automatically visited or resolved. No production log or message is requested.
Enter three digits from 100 to 599.
Comparison result
Content-Type: application/problem+json
Fictional JSON body
{
"type": "https://api.example.net/problems/invalid-window",
"title": "Invalid time window",
"status": 422,
"detail": "The end must follow the start.",
"instance": "/problems/events/test-17",
"errors": [
{
"field": "end",
"code": "before-start"
}
]
}Six registered types: preserve exact URIs and references
This common-type registry is not a list of every problem an API can define. N/A for about:blank is not an HTTP status. The three digest entries retain a draft-kind reference as published in this IANA snapshot; this is not rewritten as a final RFC number.
IANA · HTTP Problem Types · Registry updated 2026-06-26 · consulted 2026-10-23
| Type URI and IANA title | Recommended status | Reference as published | Registration date |
|---|---|---|---|
about:blankSee HTTP Status Code | N/A | rfc · rfc9457 | 2023-05-02 |
https://iana.org/assignments/http-problem-types#dateDate Not Acceptable | 400 | rfc · rfc9458 · §6.5.2 | 2023-05-02 |
https://iana.org/assignments/http-problem-types#ohttp-keyOblivious HTTP key configuration not acceptable | 400 | rfc · rfc9458 · §5.3 | 2023-05-02 |
https://iana.org/assignments/http-problem-types#digest-unsupported-algorithmsUnsupported Hashing Algorithms | 400 | draft · RFC-ietf-httpapi-digest-fields-problem-types-06 · §3.1 | 2026-06-26 |
https://iana.org/assignments/http-problem-types#digest-invalid-valuesInvalid Digest Values | 400 | draft · RFC-ietf-httpapi-digest-fields-problem-types-06 · §3.2 | 2026-06-26 |
https://iana.org/assignments/http-problem-types#digest-mismatched-valuesMismatched Digest Values | 400 | draft · RFC-ietf-httpapi-digest-fields-problem-types-06 · §3.3 | 2026-06-26 |
IANA · Protocol Registries licensing · CC0 1.0 · Revised 2021-11-10 · CIE commentary and fictional examples identified separately
Eight cases and acceptance decisions
P01 · An explicit business problem type
HTTP 422 · Content-Type: application/problem+json
{
"type": "https://api.example.net/problems/invalid-window",
"title": "Invalid time window",
"status": 422,
"detail": "The end must follow the start.",
"instance": "/problems/events/test-17",
"errors": [
{
"field": "end",
"code": "before-start"
}
]
}Reading : Body and response statuses agree. The errors array is an extension specific to this example, not a universal standard member.
Test to add : Test the stable before-start code and field association; do not parse detail prose to automate correction.
P02 · Type and status omitted
HTTP 404 · Content-Type: application/problem+json
{
"title": "Ressource introuvable",
"instance": "/support/test-18"
}Reading : The five members are not an all-required checklist. Without a usable type, about:blank applies; the received HTTP status remains 404.
Test to add : Check that the client accepts this minimal response and a localised title without inventing a body status.
P03 · Body says 403 but HTTP says 200
HTTP 200 · Content-Type: application/problem+json
{
"type": "https://api.example.net/problems/access-denied",
"title": "Access denied",
"status": 403
}Reading : Two layers disagree. The advisory JSON status does not replace the status received by generic HTTP clients.
Test to add : Investigate generation and intermediaries; preserve both values rather than declare success from HTTP 200 alone.
P04 · Five members with the wrong JSON type
HTTP 422 · Content-Type: application/problem+json
{
"type": 17,
"title": false,
"status": "422",
"detail": [
"Invalid"
],
"instance": null,
"traceRef": "test-19"
}Reading : type, title, status, detail and instance have unexpected types. RFC consumer handling is to ignore them rather than coerce the string "422" into a number.
Test to add : Report the producer defect and test a tolerant client. traceRef remains an extension; about:blank is the default after type is ignored.
P05 · Relative URI and unknown extensions
HTTP 409 · Content-Type: application/problem+json
{
"type": "/problems/conflict",
"title": "Version conflict",
"status": 409,
"instance": "?case=test-20",
"requestRef": "test-20",
"retryable": false
}Reading : The type uses a relative path and instance a query reference. This workshop displays them without resolution; the real document base would be required.
Test to add : Define the contract base, prefer an absolute type and test ignored unrecognised extensions. The word retryable alone does not grant universal replay safety.
P06 · An exact registered URI
HTTP 400 · Content-Type: application/problem+json
{
"type": "https://iana.org/assignments/http-problem-types#date",
"title": "Date Not Acceptable",
"status": 400,
"instance": "/support/test-21"
}Reading : This type appears in the IANA snapshot. Its recommended status is 400. Comparing only #date or title does not identify the type.
Test to add : Refer the client to this type’s definition in its applicable context; registration does not certify an implementation.
P07 · A similar object with another media type
HTTP 503 · Content-Type: application/json
{
"type": "https://api.example.net/problems/maintenance",
"title": "Service maintenance",
"status": 503,
"detail": "Try the documented recovery path."
}Reading : The body resembles Problem Details, but the received media-type name is application/json. Structural reading does not verify negotiation or the response contract.
Test to add : Compare Content-Type, documentation and client. This example is not a retry rule: Retry-After and replay safety remain separate checks.
P08 · A number is not sufficient for an HTTP status
HTTP 400 · Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Bad Request",
"status": 700,
"instance": "/support/test-22"
}Reading : status is a JSON number, but 700 lies outside the HTTP three-digit range 100–599. This differs from having the wrong JSON type.
Test to add : Repair generation and preserve the received HTTP status, 400 here. A data-type-only check would have missed this anomaly.
IETF · RFC 9110 · HTTP Semantics · June 2022 · §6.4.1, §15, §15.3.6 · status range and response content
Evolve an error without breaking its contract
| Proposed change | Practical client risk | Recommended CIE exercise |
|---|---|---|
| Localise title or detail | A client parsing the phrase may take a different branch. | Keep the same type and replay two languages with the same expected machine outcome. |
| Change the type URI | A new problem identity may replace the old one. | Test existing consumers and document migration, tolerance and fallback. |
| Add an extension | An overly strict client may reject a previously readable response. | Add an unknown member to the consumer test and check the intended tolerance. |
| Remove or rename a business extension | A client action may depend on the documented field. | Check the type-specific contract, observed use and compatibility plan. |
| Change the HTTP status | A proxy, cache or SDK may handle the response differently. | Compare raw response and body through generation and intermediaries. |
| Add technical detail | Internal paths, secrets or account information may leak. | Test the public message, occurrence reference and access-controlled logs separately. |
Keep a raw response example for each refusal, its type definition, the consumer test outcome and contract version. Two matching numbers do not demonstrate that the server chose the correct refusal or that another attempt has no side effects.
Provenance and snapshot limits
IANA · 6 types · registry 2026-06-26 · reviewed 2026-10-23 · CC0 1.0
IANA XML SHA-256: 2b6a7ae5a63a5f6afccd8884658393d5268d16b63d624e883fd5fe5dcf5aa57b
RFC 9457 TXT SHA-256: f2b3db92fb0bf3489cb3841a0da0c0d88dff40797b64d40b6123085183886c7b
GraphQL: errors, lists and partial data
GraphQL contract: errors do not always erase data
Define what a client may retain after resolver failure: the contract includes data shape, null scope and error paths.
- Compare absent data, partial data and data: null.
- Keep the original path even when a parent becomes null.
- Test permissions, quotas and interface states separately.
Explore GraphQL responses and nullability →
GraphQL Specification · September 2025 · Published edition · September 2025
API deprecation and retirement
Plan retirement beyond the version number
A published version does not say when consumers need to migrate. Connect deprecation announcements, retirement dates and existing-client tests.
- Distinguish Deprecation, Sunset and policy links.
- Check that Sunset does not precede Deprecation when both dates are provided.
- Measure remaining consumers and validate the replacement before cutover.
Compare deprecation and retirement announcements →
IETF · RFC 9745 · Deprecation · Standards Track · March 2025 · §2–5 · consulted 2026-10-23
IETF · RFC 8594 · Sunset · Informational · May 2019 · §3–5, §8 · consulted 2026-10-23
Official references
References consulted: . The method and worksheet propose checks to adapt to your context; they do not constitute certification.
- OpenAPI Specification 3.1.1
- IETF RFC 9457 — Problem Details for HTTP APIs
- IETF · RFC 9110 — HTTP Semantics
- IETF · RFC 9111 — HTTP Caching
- IETF · RFC 6585 — Additional HTTP Status Codes
- IETF · RFC 9700 · OAuth 2.0 Security BCP
- IETF · RFC 9068 · JWT access token profile
- OpenID Foundation · OpenID Connect Core 1.0
- IETF · RFC 8725 · JWT Best Current Practices
- IETF · RFC 6749 · OAuth 2.0 framework
- Stripe · Webhooks
- Stripe · Manage webhook endpoints
- GitHub · Validating webhook deliveries
- GitHub · Best practices for using webhooks
- GitHub · Handling failed webhook deliveries
- IETF · RFC 9745 · Deprecation
- IETF · RFC 8594 · Sunset
- GraphQL Specification · September 2025
- IANA · HTTP Problem Types
- IETF · RFC 9110 · HTTP Semantics
- AWS Builders’ Library · Timeouts, retries, and backoff with jitter
- Stripe · Idempotent requests
- Stripe · API v2 overview — Idempotency
- Stripe · Advanced error handling
