Resources · 33

Evolve API contracts without losing clients

Describe requests, responses and errors; test versions and make webhooks safe to replay.

· 8 min

Fictional empty API assurance bench with a network gateway, abstract request and response diagram, burgundy binder and brass lamp

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

  1. 01

    Inventory consumers

    List operations, clients, usages, versions and owners. Separate observed use from assumptions before changing a field.

    Deliverable: contract dependency map.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

IndicatorWhat it measuresFirst action
ContractOperations described and matching service behaviorFix divergence
CompatibilityRepresentative clients tested before changeAdd missing cases
ErrorsDocumented types without sensitive dataRevise messages and schemas
MigrationOld-version use with an ownerAssist 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.test

Response 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-store
4. 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.test

Response 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.

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

Five members: role and client-side reading
MemberExpected typeRoleAcceptance question
typestring · URI referenceIdentifies the problem type; defaults to about:blank if absent.Does the client decide using the URI rather than a translated title?
statusnumber · HTTP statusRestates the status for this occurrence; advisory.Do body and response status agree?
titlestringHuman-readable summary of the problem type.Does localisation change only person-facing text?
detailstringHuman-readable explanation of this occurrence.Does the client avoid parsing prose as a machine code?
instancestring · URI referenceIdentifies 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.

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

Six registered types: preserve exact URIs and references
Type URI and IANA titleRecommended statusReference as publishedRegistration date
about:blank

See HTTP Status Code

N/A

rfc · rfc9457

2023-05-02
https://iana.org/assignments/http-problem-types#date

Date Not Acceptable

400

rfc · rfc9458 · §6.5.2

2023-05-02
https://iana.org/assignments/http-problem-types#ohttp-key

Oblivious HTTP key configuration not acceptable

400

rfc · rfc9458 · §5.3

2023-05-02
https://iana.org/assignments/http-problem-types#digest-unsupported-algorithms

Unsupported 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-values

Invalid 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-values

Mismatched 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

Evolve an error without breaking its contract
Proposed changePractical client riskRecommended CIE exercise
Localise title or detailA 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 URIA new problem identity may replace the old one.Test existing consumers and document migration, tolerance and fallback.
Add an extensionAn 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 extensionA client action may depend on the documented field.Check the type-specific contract, observed use and compatibility plan.
Change the HTTP statusA proxy, cache or SDK may handle the response differently.Compare raw response and body through generation and intermediaries.
Add technical detailInternal 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.