Content Negotiation: Accept Headers, Media Types, and Format Flexibility
HTTP was designed from the beginning to support multiple representations of the same resource. A user profile can be represented as JSON for a machine consumer or as HTML for a browser. A dataset can be returned as JSON or as CSV depending on what the client intends to do with it. An image can be served as JPEG or WebP based on what the browser supports. Content negotiation is the mechanism through which clients and servers agree on which representation to use — and understanding it is essential for building APIs that serve diverse consumers cleanly.
The Accept Header
Clients declare their content format preferences using the Accept header. The value is a comma-separated list of media types, optionally with quality values indicating relative preference:
Accept: application/json, text/csv;q=0.8, */*;q=0.1
This tells the server: “I prefer JSON. If JSON is not available, I will accept CSV with lower preference. If neither is available, I will accept anything.” The q parameter (quality value) ranges from 0 to 1; higher values indicate stronger preference. Items without an explicit quality value default to 1.0.
The server examines the Accept header, determines the best match from its available representations, and responds with the chosen format indicated in the Content-Type response header:
Content-Type: application/json; charset=utf-8
If the server cannot produce any of the requested content types, it returns 406 Not Acceptable. If the request has no Accept header, the server should default to its primary format — typically application/json for an API.
Common Media Types in API Design
application/json is the standard for structured data in REST APIs. It is widely supported, human-readable, easy to parse in every language, and the default expectation for most API consumers.
text/csv is commonly requested for data exports. CSV is not standardized beyond RFC 4180, which means quoting rules and line endings vary across implementations. Always specify the character encoding and document your CSV formatting conventions explicitly.
application/xml still appears in enterprise integrations, financial services, and any domain where XML schemas have established status (SOAP services, EDI, FIX protocol adjacent systems). Most new APIs do not support XML, but APIs targeting enterprise integration often must.
application/pdf for document endpoints that can produce PDFs on demand. image/webp, image/jpeg, image/png for image endpoints that serve different formats based on client capability.
application/problem+json (RFC 7807) is the standardized media type for API error responses. It defines a consistent structure for error representations that tools and clients can recognize across different APIs:
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "The 'email' field must be a valid email address.",
"instance": "/api/users/register"
}
Using application/problem+json for error responses rather than a custom format aligns with an emerging standard and simplifies generic error handling in client libraries.
Versioning via Media Types
Some APIs embed versioning in the media type rather than the URL or a custom header:
Accept: application/vnd.example.api.v2+json
This is the “vendor media type” approach, where vnd signals a vendor-specific type. The version is part of the content type — a different version is literally a different representation of the resource. This is technically elegant in that it respects REST’s resource-representation model: the same URL can have different representations for different versions.
In practice, vendor media type versioning is more complex for clients to implement than URL versioning and offers limited practical benefit for most APIs. GitHub’s API supports it, and it works correctly. For most new APIs, URL versioning or header versioning is simpler and delivers the same outcome.
The Vary Header and Caching
When the server responds with different content based on the Accept header, it must include Vary: Accept in the response. This tells caches that the response content depends on the Accept header value — two requests to the same URL with different Accept headers may produce different content, and the cache must store them separately.
Vary: Accept, Accept-Encoding
Forgetting Vary when using content negotiation causes caches to serve JSON to clients that requested CSV, or vice versa. The bug is difficult to diagnose because it is intermittent — only requests served from cache are affected.
Encoding Negotiation: Accept-Encoding
Parallel to content type negotiation, Accept-Encoding negotiates response compression:
Accept-Encoding: gzip, br, deflate
Brotli (br) provides better compression ratios than gzip for text content, and support is universal in modern browsers and HTTP clients. Enabling compression on large API responses — particularly JSON responses with repetitive structure — reduces transfer size substantially, sometimes by 70-80%.
The server compresses the response and indicates the applied encoding:
Content-Encoding: gzip
The response body is the compressed representation. The client decompresses before parsing. This is handled automatically by most HTTP client libraries; application code typically sees the decompressed content without knowing compression occurred.
Language Negotiation: Accept-Language
Accept-Language declares the client’s preferred language for content:
Accept-Language: fr-FR, fr;q=0.9, en;q=0.7
For APIs that serve localized content — error messages, user-facing strings, localized data — the server can use this header to select the appropriate language. Include the language in the response as Content-Language:
Content-Language: fr-FR
For APIs where all content is in a single language and locale is irrelevant, Accept-Language can be ignored. For user-facing APIs where error message language matters for usability — particularly where end users see API error content — handling Accept-Language correctly improves the experience significantly.
Implementing Content Negotiation
Most web frameworks have built-in or easily available content negotiation support. The implementation follows the same structure regardless of framework: inspect the Accept header, parse the quality values, select the best match from the available representations, serialize to that format, and respond with the appropriate Content-Type and Vary headers.
The complexity scales with how many representations you support. An API that only serves JSON has no negotiation to implement beyond defaulting to JSON and returning 406 if the client explicitly requests a format you do not support. An API that serves JSON, CSV, and PDF from the same endpoints has a more complex content selection and serialization pipeline that is worth building as a middleware layer rather than duplicating across handlers.
Content negotiation is infrastructure that enables API flexibility without proliferating endpoints. One endpoint serving multiple representations is cleaner than separate /export.json and /export.csv endpoints. The HTTP machinery for negotiating format is already there; using it well makes APIs more versatile and better aligned with how HTTP was designed to work.