Data interchange in APIs: negotiation and versioning

Content-Type and Accept negotiation, envelope versus bare payloads, how to version a format without breaking clients, and an error contract worth keeping.

Negotiating the representation

The same resource can be returned in several representations. The client states a preference with Accept, the server declares what it actually sent with Content-Type, and a Vary header keeps caches honest.

GET /v1/orders/42 HTTP/1.1
Accept: application/json;q=1.0, application/vnd.acme.order+json;q=0.9, */*;q=0.1

HTTP/1.1 200 OK
Content-Type: application/vnd.acme.order+json; charset=utf-8
Vary: Accept
Cache-Control: private, max-age=60

{"id":42,"total":{"amount":"19.99","currency":"EUR"}}
  • A vendor media type such as application/vnd.acme.order+json versions the schema without touching the URL.
  • Return 406 Not Acceptable when no offered type can be produced — silently returning JSON is worse.
  • Always state charset=utf-8; for JSON it is the only valid choice, and saying so removes a class of parsing bugs.

Versioning and compatibility

ChangeCompatible?Notes
Add an optional response fieldYesClients must ignore unknown fields
Add a required request fieldNoBreaking: old clients never send it
Rename a fieldNoEmit both names for a deprecation window
Widen a type (int to string)NoBreaks typed clients; introduce a new field
Tighten validationNoRequests that used to work now 400
Remove a documented fieldNoAnnounce, measure usage, then remove
{
  "id": 42,
  "total": "19.99",
  "totalAmount": "19.99",
  "currency": "EUR"
}

The duplicate-field pattern above is the pragmatic way to rename: publish both, deprecate one in the docs and with a Warning or Deprecation header, and remove it after usage telemetry reaches zero.

Envelope or bare payload, and the error contract

{
  "data": { "id": 42, "total": "19.99" },
  "meta": { "requestId": "b7f1", "durationMs": 12 },
  "errors": []
}

An envelope costs bytes and one level of nesting, and buys a place to put request IDs, pagination cursors and partial-failure details. Use it for list endpoints and any API with partial success; skip it for single-resource reads where the payload is the resource.

⚠️
Agree on the error shape before you need it. A consistent body — a machine-readable code, a human message, and a field path — turns a 400 into a self-service fix. Error formats invented per endpoint are the main reason clients resort to string matching.

FAQ

Should I version in the URL or the media type?
URL versioning (/v2/) is simpler to route and cache and is what most teams can operate. Media-type versioning is more precise and allows per-representation versions, but needs stricter client discipline.
Is a 204 response with no body better than a 200 with an envelope?
For deletes and updates with nothing to return, 204 is clearer and saves bytes. The envelope earns its place only when you have metadata to convey.

Choosing a format: size, speed, readability and tooling Schema definition and validation

Last refreshed 2026-09-18.