apihttpcontent-negotiationjson

Content Negotiation for JSON APIs — The Accept Header, Explained

·7 min read·APIs

The header that decides the response shape before any code runs

Content negotiation is how a single URL can serve different representations of the same resource — JSON to one client, HTML to a browser navigating directly, XML to a legacy integration — based entirely on what the client says it can accept, via the Accept request header. Most JSON APIs only ever serve one representation, which is exactly why this header is easy to ignore until something depends on it.

The basic mechanism

bash
curl -H "Accept: application/json" https://api.example.com/users/42
curl -H "Accept: text/html" https://api.example.com/users/42

A server that honors content negotiation inspects Accept, picks a representation it can produce that the client claims to understand, and reflects that choice back in the response's Content-Type header — which is a *different* header, describing what was actually sent, not what was requested. A client sending Accept: application/json but receiving Content-Type: text/html back (a login redirect page, an error page from a proxy) is precisely the failure mode that turns into a confusing "Unexpected token <" JSON parse error — the client tried to JSON.parse an HTML page because the server ignored its Accept header, or something in front of the API (a load balancer, an auth proxy) intercepted the request before it ever reached the real handler.

Vendor-specific media types

A plain application/json accept header says "I understand generic JSON." APIs that need finer-grained content negotiation define their own vendor media type, often folding an API version into it:

bash
curl -H "Accept: application/vnd.github+json" https://api.github.com/users/octocat
curl -H "Accept: application/vnd.api+json" https://api.example.com/orders/42     # JSON:API's own media type
curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/users/octocat  # version embedded

This is the mechanism behind header-based API versioning — the version lives inside the media type string itself, and a server can dispatch to entirely different serialization logic based on which vendor type was requested, all while the URL stays identical. JSON:API formalizes its own vendor type (application/vnd.api+json) specifically so a client can assert "I understand the JSON:API document structure," not just "I understand JSON in general."

Quality values: expressing a preference, not a requirement

Accept supports weighted preferences via q values, letting a client rank multiple acceptable formats in one header instead of picking exactly one:

text
Accept: application/json, application/xml;q=0.8, text/html;q=0.5

This says "JSON preferred (default weight 1.0), XML acceptable as a fallback, HTML as a last resort" — a server should pick the highest-q option it's actually capable of producing. In practice, most JSON-only APIs skip implementing full weighted negotiation and simply check "does the Accept header contain application/json or */*, and if not, return 406 Not Acceptable" — which covers the common case without the complexity of a full negotiation algorithm.

Accept vs. Content-Type: the two headers people mix up

HeaderDirectionMeaning
AcceptRequest"Here's what representations I can understand"
Content-TypeRequest (with a body)"The body I'm sending you is encoded like *this*"
Content-TypeResponse"The body I'm sending back is encoded like *this*"

A POST request needs *both*, for different reasons: Content-Type: application/json on the request tells the server how to parse the request body you're sending; Accept: application/json tells it what format to send the *response* back in. Missing Content-Type on a JSON POST body is one of the most common causes of a server treating the payload as unparseable — and returning a 415 Unsupported Media Type error, a distinct failure mode from an Accept-header mismatch.

Frequently asked questions

Most JSON-only APIs do exactly this in practice — they always return JSON regardless of what Accept says, since JSON is their only representation anyway. This is a pragmatic simplification, not a spec violation exactly, but it does mean a client requesting an unsupported format gets JSON back instead of a clean 406 telling it no acceptable representation exists.

Some APIs do implement real content negotiation and serve HTML to a browser's default Accept: text/html,application/xhtml+xml,... header specifically so navigating to the URL directly shows a human-readable page, while an explicit Accept: application/json from a script gets the raw JSON — GitHub's API does this for its HTML-preview endpoints.

Indirectly — a custom Accept value (a vendor media type, for instance) can turn a simple cross-origin GET request into one that triggers a CORS preflight OPTIONS request first, since only a small set of header values qualify as a "simple request" under the CORS spec. This is a common, non-obvious cause of unexpected preflight requests showing up in the network tab.

Either works, but a vendor media type in Accept is more aligned with HTTP's actual content-negotiation design (it participates in caching's Vary mechanism correctly), whereas a custom header like X-API-Version is simpler to implement and log but isn't part of any standardized negotiation — see the versioning post above for the fuller tradeoff.

Try JSONKit — JSON Developer Tools

Format, validate, convert and diff JSON. Instant, accurate and 100% private.