TS-21: HTTP APIs
This document outlines standards for designing and implementing HTTP APIs.
These standards are primarily concerned with the design of public-facing HTTP APIs ("web services"), which are made available for integration into third-party client applications ("intra-organization communication"). Such APIs may be gateways to underlying systems composed of multiple services, or they may be the primary interface to a single service.
It is expected that many of the guidelines herein will be applicable also to private HTTP APIs between services within an internal network ("intra-organization communication"). But the main focus of this technical standard is the design of HTTP APIs intended for integration into client applications that are owned and developed by third-parties.
This technical standard builds on TS-20: Network APIs, which is a protocol agnostic standard for network API requirements. It focuses on cross-cutting concerns like reliability and monitoring. TS-21 is specific to HTTP APIs.
See also TS-22: Webhooks, which covers a specific pattern implemented in some public HTTP APIs to support asynchronous event notifications to clients.
- General design principles
- Authentication and authorization
- Security
- HTTP methods
- HTTP status codes
- URLs
- Collections
- Resources
- Sub-resources and sub-collections
- Safeness and idempotency
- Actions
- Asynchronous operations
- Concurrency control
- Health check endpoints
- Headers
- Payloads
- Versioning and managing breaking changes
- Documentation and interface definition
- Common types
- Alternative HTTP API styles
- References
General design principles
In broad terms, HTTP APIs SHOULD follow the RESTful architectural style. This architecture describes the underlying design principles of the World Wide Web, particularly the HTTP protocol and HTML language.
In practice, a RESTful API is one that repurposes the semantics of the HTTP protocol, using the full capabilities of HTTP as a messaging and encoding protocol, rather than merely as a transport protocol. In simple terms, a RESTful API uses URLs to represent resources and collections of resources, HTTP methods to represent different types of CRUD-like operations to be performed on those resources, and HTTP status codes to represent outcomes from those operations.
A RESTful API is also:
- Stateless: Each request from a client to a server SHOULD contain all the information necessary for the server to understand and fulfil the request. Most client requests SHOULD NOT be dependent upon any particular stored context on the server (with exceptions for things like security measures, eg. to protect against denial of service). This design constraint will simplify the server implementation and make it more scalable.
- Uniform: A RESTful API SHOULD have a highly uniform interface. A uniform HTTP API is one that uses a consistent URL structure to represent resources and collections, uses a consistent subset of HTTP methods and status codes, uses a consistent file format and schema for message payloads, and composes data structures from a common library of types and patterns. A uniform interface gives a better user experience and supports greater reuse of code, for example through the use of libraries and software development kits (SDKs), so making client integrations quicker and easier.
- Cacheable: As much as possible, responses from a RESTful API server SHOULD be cacheable. This helps to reduce load on the server, reduce latency for clients, and support strategies for fault tolerance and availability.
- A layered system: The server application SHOULD have a layered architecture, in which cross-cutting concerns such as security and caching are implemented in discrete layers that span all endpoints of the API.
Although most HTTP APIs should be primarily resource-oriented, there are many use cases where a RESTful design is not appropriate. For example, if an API is a thin layer around a legacy system that itself is not designed around the concept of resources, or which otherwise fits uncomfortably with the RESTful model, it will probably be better to deviate from RESTful design principles and focus on creating a functional abstraction of the underlying system.
Even within an HTTP API that is predominantly RESTful in its design characteristics, it will often be beneficial to include operations that are more RPC-like in their design. For example, besides the usual create, read, update, and delete operations that are made available on a collection of resources, an HTTP API may augment these operations with additional "actions". A single action may, for example, operate on multiple different types of resources, and execute multiple CRUD-like operations on those resources. Thus, actions are a higher level of abstraction that regular resource-oriented endpoints. See the section on Actions for more information.
Important
In designing an HTTP API, developers SHOULD prioritize creating a useful interface to the operations of the system that the API abstracts, rather than trying to religiously adhere to any particular architectural style. The best API design will be the one that makes it as easy as possible for clients to interact with the underlying system.
Service design properties
The guidelines above describe the shape of the HTTP interface itself. But an HTTP API is also the public face of a service, and the way that service is designed has just as much bearing on the long-term usability of the API as the URL and payload conventions do. The following properties SHOULD be true of the service that sits behind an HTTP API.
- Loosely coupled: The API contract SHOULD NOT leak implementation details of the underlying service, eg. internal database schemas, class names, or the topology of internal systems. This allows the service’s implementation to evolve, or even be rewritten, without requiring changes to the contract that clients depend on.
- Encapsulated: A service MUST NOT expose data that it does not own. Where a service needs to expose data or functionality that belongs to another domain, it MUST do so by integrating with that domain’s own API, rather than reaching into its data store directly. This keeps ownership of each piece of data unambiguous.
- Stable: Once published, an API’s contract SHOULD remain valid for existing consumers for as long as that version of the API is supported.
- Reusable: Services SHOULD be designed for use by multiple consumers and multiple contexts, not only the first client that motivated their creation. Prefer generic, capability-oriented interfaces over ones that are narrowly tailored to a single consumer’s current requirements.
- Consistent: Services SHOULD share a common vocabulary, common data types, and common interaction patterns, eg. for pagination, filtering, and error reporting. Consistency across an organization’s portfolio of APIs reduces the learning curve for developers integrating with more than one of them.
- Easy to use: A service’s contract SHOULD be easy to discover, understand, and integrate with. This includes having clear ownership (so that consumers know who to contact about SLAs and issues), consistent identification and authentication mechanisms, and good documentation.
- Externalizable: A service SHOULD be designed once and then made available to different classes of consumers – internal teams, partners, or the public – through policy changes (eg. authentication requirements, rate limits) rather than through re-implementation or changes to the contract itself.
Input and output strictness
An HTTP API MUST be strict in the responses it produces, and SHOULD be strict in the requests it accepts.
Producing strict, well-formed output is uncontroversial: a service always knows exactly what it is sending, so there is no excuse for a response that deviates from the documented contract. Being strict about what is consumed is more nuanced, because it bears on Postel’s Law (aka. the Robustness Principle), which counsels being liberal in what a system accepts.
Postel’s Law remains good guidance for narrow, low-risk cases, eg. tolerating an optional trailing slash in a URL, or accepting a request body field in either of two equivalent forms. But it MUST be weighed against the risks of permissive parsing more broadly. An API is typically integrated with once by each client developer, who then relies on the documented contract for the lifetime of their integration. If a server silently tolerates and "corrects" malformed input, eg. guessing the intended value of a badly-typed or out-of-range field rather than rejecting it, that behavior itself becomes a de facto part of the contract, whether or not it was ever documented – and a later change to tighten validation becomes a breaking change for any client that had come to depend on the server’s leniency.
Where there is doubt about whether an input deviation is safe to tolerate,
prefer rejecting the request with a 400 Bad Request over silently guessing the
client’s intent.
Authentication and authorization
Authentication and authorization MUST be implemented for all HTTP APIs, including private (internal network) HTTP APIs.
Authentication and authorization is especially important for operations that modify data.
Every secured endpoint MUST require one of the schemes described below. All schemes rely on TLS for transport encryption; credentials and tokens MUST NOT be sent over an unencrypted connection.
OAuth 2.0
OAuth 2.0 SHOULD be used to protect endpoints that act on behalf of a user or
a third-party application, and it is the RECOMMENDED default scheme for
public-facing HTTP APIs. Clients authenticate by presenting a Bearer token in
the Authorization header:
GET /v1/vault/credit-cards HTTP/1.1
Host: api.example.com
Authorization: Bearer {token}Token issuance, refresh, scopes, and revocation are delegated to an OAuth 2.0 authorization server and are outside this standard’s scope.
HTTP Basic Authentication
HTTP Basic Authentication MAY be used for simpler, internal, or machine-to-machine use cases where the overhead of OAuth 2.0 is not justified. As with OAuth 2.0, Basic Authentication MUST only be used over TLS, since the credentials are base64-encoded, not encrypted.
API keys
API keys are a simpler alternative to OAuth 2.0 and Basic Authentication for authenticating client applications — as opposed to end users — for example where a third-party service integrates with an API server-to-server.
Where implemented, an API key SHOULD be passed using a custom HTTP header. The
header field SHOULD be named X-Api-Key.
GET /v1/vault/credit-cards HTTP/1.1
Host: api.example.com
X-Api-Key: {api_key}API keys MUST have an expiry, and it MUST be possible to revoke an active key, in case it is compromised. Where an API key is used to authenticate outbound calls to an external service, the key MUST NOT be checked into source control; store it using the secrets management practices described in TS-52: Security and secrets management.
Security
Security measures MUST be implemented for both public and private APIs. Do not assume that private/internal networks are secure.
All input MUST be validated and sanitized to prevent security vulnerabilities such as SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF). Even if that input comes from another internal system that you control and trust, you still MUST validate the input. Doing so reduces the potential blast radius of security breaches in any one service.
An error response MUST NOT expose implementation details that could assist an attacker, such as a stack trace, an internal file path, a database error message, or the name and version of internal frameworks or libraries. Attackers routinely use precisely this kind of accidental disclosure to identify known vulnerabilities in a service’s dependencies. Diagnostic detail that would help a legitimate developer belongs in server-side logs, not in the response body; see Error handling in Payloads for the corresponding error-response rule.
Personally identifiable information (PII) MUST NOT be transmitted in a URL — as a path segment or a query parameter value — since URLs are routinely logged by proxies, load balancers, and browser history, in places the API owner does not control and that are not designed to hold sensitive data. Where an endpoint’s semantics would otherwise require PII in the URL, pass it in the request body or a header instead. See TS-52: Security and secrets management and TS-53: Privacy and data protection for the organization’s broader PII handling requirements, which this rule is a specific consequence of.
HTTP methods
HTTP methods define the types of operations to be performed on a resource or a collection of resources.
HTTP methods MUST be used for their designated purpose, as specified by the HTTP standards. This will make it much easier to integrate client applications.
HTTP APIs MUST use only the following subset of standard HTTP methods:
Verb | CRUD mapping | Safe? | Idempotent? | Description |
|---|---|---|---|---|
| Read | Yes | Yes | Used to retrieve the requested resource or collection of resources. |
| Read | Yes | Yes | This behaves the same as |
| Create | No | No | Used to create a new resource. The server MUST generate a new resource identifier, and it MUST return a full representation of the newly created resource, including its new identifier and other server-generated properties. |
| Create/Update | No | Yes | Used to fully replace a resource with the request payload, or to create a new resource in scenarios where the client takes over responsibility from the server for generating a unique identifier for the resource. |
| Update | No | Yes | Used for partial updates to a resource. The request payload SHOULD contain only the fields that are being updated. |
| Delete | No | Yes | Used to delete a resource. Should be repeatable, always with a positive response even if the resource is already deleted. Clients MUST not send a body with |
| Read | Yes | Yes | Defined by RFC 10008. Used to retrieve resources using a request payload to express complex query criteria that cannot be reasonably encoded in a URL or |
Other standard HTTP methods are OPTIONS, TRACE, and CONNECT. These are
technical methods used to support the HTTP protocol itself, and are not intended
the be included in the interface definitions of HTTP APIs.
Read operations: GET versus HEAD versus POST versus QUERY
It is RECOMMENDED that all GET endpoints – for both resources and collections
– in an HTTP API also support HEAD requests. HEAD responses are identical to
GET responses, except that the server MUST NOT return a message body in the
response. This can be useful for clients that need to check the existence of a
resource without downloading its full representation.
Read operations are not always expressible as a simple GET with query
parameters, eg. where the query criteria are too complex, too large, or too
sensitive to encode in a URL. In these cases, it is tempting to reach for
POST, since request bodies are unconstrained in size and structure. However,
POST responses are never cacheable, because the HTTP caching model keys cache
entries on the request URL, not the request body.
This has a real cost for any read operation that is called frequently with the
same or similar criteria. Preferring GET (or HEAD) wherever the query
criteria can be reasonably encoded in the URL is therefore RECOMMENDED, purely
to preserve the possibility of caching – both client-side and in any
intermediary caches, proxies, or CDNs along the request path.
The QUERY method is a recent addition to the HTTP standards. It resolves the
trade-offs in using POST for complex read operations. QUERY behaves much
like POST, except its responses are cacheable Like GET. Unfortunately, as of
2026, support for QUERY is far from universal across HTTP clients, servers,
proxies, gateways, and CDNS. For the time being POST SHOULD be preferred for
complex read operations where QUERY support cannot be guaranteed along the
full request path. QUERY MAY be used in internal networks and private APIs
where end-to-end support can be guaranteed.
Asynchronous endpoints
In most real-world examples, HTTP API endpoints are implemented as synchronous operations, in which the client sends a request and waits for an immediate response from the server. However, where operations may be long-running, it is RECOMMENDED to implement the operations using asynchronous communication patterns.
The behavior of the HTTP methods, listed above, SHOULD be identical for asynchronous communication – the only differences being in the choices of response status codes, and response payloads are delivered subsequently via separate messages.
HTTP status codes
Appropriate HTTP response codes MUST be used in response messages to indicate the result of API requests. Using the correct codes in responses is not just about adhering to the HTTP protocol, but also about facilitating the correct interpretation of HTTP responses by clients.
There are many standardized HTTP status codes. Most APIs will need only a subset of the full set of standard codes. The supported subset of HTTP status codes MUST be documented as part of the API’s interface definition, and an API MUST NOT return any status code outside of this documented subset. Commonly-used status codes include:
- 1xx: Informational response codes.
100 Continue: Indicates that the initial part of the request has been received and the client should continue sending the rest of the request. This is used in the context of large payloads that cannot reasonably be transmitted in a single message.
- 2xx: Success response codes.
200 OK: Indicates that the request was successful. This is the most widely-used success response code.201 Created: Indicates that the request was successful and, as a result, a new resource has been created.202 Accepted: For asynchronous operations that will be fulfilled by the server at a later time. This signifies that the server has received the message, and has added it to a queue for processing. The outcome of that processing (whether successful or unsuccessful) is not yet known, therefore.204 No Content: Indicates that the request was successful but there is no content to return in the response message. This status code MUST be returned with an empty HTTP message body.206 Partial Content: Indicates that the response carries only a byte range of the requested resource, in response to aRangerequest header. See Partial content in Resources.
- 3xx: Redirection response codes.
301 Moved Permanently: Indicates that the requested URL has been changed permanently. The new URL MUST be specified in the response.302 Found: Indicates that the requested resource is temporarily under a different URL.
- 4xx: Client error response codes.
400 Bad Request: Indicates that the request cannot be understood or processed by the server due to a syntax error in the client’s request message.401 Unauthorized: Indicates that the request requires authentication but the client has not authenticated itself.403 Forbidden: Indicates that the server understood the request but is refusing to authorize access to the specific resource or operation requested.404 Not Found: Indicates that the server could not find the requested resource.405 Method Not Allowed: Indicates that the HTTP method used in the request is not allowed on the target resource (but the resource exists and other methods can be run on it).406 Not Acceptable: Indicates that the server cannot produce a response matching the media type(s) requested by the client in itsAcceptheader. This status code MUST be used, rather than falling back silently to a different media type, when the requested media type cannot be honored.409 Conflict: Indicates that the request could not be completed due to a conflict with the current state of a resource. Used for an idempotency key that is already locked by an in-progress request; see Idempotency-key locking in Safeness and idempotency.412 Precondition Failed: Indicates that a condition specified in a conditional request header, eg.If-Match, was not satisfied. This is used to implement optimistic concurrency control.414 URI Too Long: Indicates that the request’s target URL exceeds a length the server is willing or able to parse. See the URL length section of URLs.415 Unsupported Media Type: Indicates that the media type of the request payload, as declared in itsContent-Typeheader, is not supported by the server. This is distinct from400 Bad Request, which indicates that the payload itself, in an otherwise-acceptable media type, could not be understood.422 Unprocessable Entity: Indicates that the request was well-formed and passed basic validation, but could not be completed due to a semantic error – one that depends on factors outside of the request body itself, eg. business rules or the current state of server-side data.429 Too Many Requests: Indicates that the client has exceeded a rate limit applicable to it, eg. a limit tied to its API credentials or IP address. Responses with this status code SHOULD include aRetry-Afterheader indicating how long the client should wait before retrying.
- 5xx: Server error response codes.
500 Internal Server Error: Indicates that the server encountered a situation it doesn’t know how to handle.502 Bad Gateway: Indicates that the server, while acting as a gateway or proxy, received an invalid response from an upstream server.503 Service Unavailable: Indicates that the server is not ready to handle the request, typically due to maintenance or overload. This status is normally transient, and responses SHOULD include aRetry-Afterheader indicating when the client may usefully retry.
The Retry-After header is not exclusive to 429 and 503. It MAY also be
used with 202 Accepted responses for long-running operations (see
Asynchronous operations), to hint at a sensible polling interval, and with
any other retryable error where the server can estimate how long the
condition will persist. Its value MAY be either a number of seconds or an
HTTP-date, per RFC 7231.
Method-to-status mapping
Not every status code is meaningful for every HTTP method. The following table indicates which of the commonly-used success and error codes, listed above, are typically appropriate for each method. A check mark (✅) indicates normal, expected use. A question mark (❓) indicates a status code that is valid for the method but only in unusual circumstances, and its use SHOULD be reviewed as part of API design review.
Method | 200 | 201 | 202 | 204 | 400 | 404 | 422 | 500 |
|---|---|---|---|---|---|---|---|---|
| ✅ | ❓ | ✅ | ✅ | ❓ | ✅ | ||
| ✅ | ✅ | ❓ | ✅ | ❓ | ❓ | ✅ | |
| ❓ | ❓ | ✅ | ✅ | ✅ | ❓ | ✅ | |
| ❓ | ✅ | ✅ | ✅ | ❓ | ✅ | ||
| ❓ | ✅ | ✅ | ✅ | ❓ | ✅ |
GET: Retrieves a resource.200is the normal success response. An empty collection is still200, but a missing single resource is404.POST: Typically creates a resource (201), but is also used for actions, where200is more usual.202applies where the operation is processed asynchronously.PUT: Typically updates or creates a resource with a client-supplied identifier.204is the normal success response, since there is usually no need to echo the request back to the client.200is appropriate only where the response needs to carry server-generated fields the client could not have supplied itself.PATCH: Behaves likePUT, but200with a response body SHOULD be avoided, sincePATCHis intended for frequent, partial updates, and echoing the full resource on every call wastes bandwidth unnecessarily.DELETE:204is the normal success response, returned even where the resource was already deleted, per idempotency rules.
URLs
URLs identify resources, collections of resources, and actions.
This section covers URL design as it applies to HTTP APIs specifically — versioning, namespaces, and resource hierarchies. For general URL design principles that apply to all HTTP services (path casing, query strings, fragments, permanence), see TS-63: URL design.
Path delimiters
The forward slash (/) character is used to delimit between path segments in
URLs.
API documentation SHOULD be consistent in its use of trailing slashes. It is RECOMMENDED that trailing slashes be omitted in documentation. However, an API SHOULD accept requests with or without a trailing slash, but SHOULD NOT respond with a redirect to the canonical version.
URL length
RFC 7230 defines no length limit on the HTTP request line, but in practice client libraries, proxies, and server frameworks all impose their own limits. A URL, including its query string, SHOULD be kept under 2048 characters, to stay compatible with the most restrictive of the widely-used stacks.
Where a request’s target URL exceeds a server’s configured limit and cannot
be parsed, the server MUST respond with 414 URI Too Long. An endpoint whose
normal, valid usage risks approaching the length limit, eg. one accepting a
long comma-separated list of filter values (see the Filtering by multiple
values section of Collections), SHOULD offer a POST-based alternative –
see the Complex search actions section of Actions – rather than requiring
clients to work around the limit.
Versioning
HTTP APIs MUST be versioned, and version information SHOULD be encoded in the URL path. This pattern is widely used for its simplicity of use by clients, and compatibility with caching and proxying systems (compared to alternative designs such as header-based versioning).
HTTP APIs MUST use Semantic Versioning, as specified in TS-11: Versioning. However, only the major version number needs to be exposed in the URL schema.
It is RECOMMENDED that the major version number be the first segment of the URL
path, eg. /v1. This tends to make it easier for clients to implement
version-specific behavior, and it also tends to make it easier to maintain and
deploy multiple major versions of an API in parallel on the server side.
/v{major}/v1
See the Versioning section, below, for more guidance on HTTP API versioning and the management of breaking changes.
Namespaces
The next part of the URL path SHOULD be treated as a namespace in which related resources will be grouped.
Namespaces are used to create logical groups of resources, collections, and actions. But they do not necessarily map directly to modules or back-end services that are responsible for fulfilling requests. Namespaces SHOULD reflect the customer’s perspective of how the product works. That perspective may not necessarily reflect the internal structure of the system, or the business domains and subdomains.
/v{major}/{namespace}/v1/vault
Namespaces SHOULD be nouns but MAY be either singular or plural, as appropriate for each grouping of resources, collections, and actions.
A good practice is to open a GET endpoint for each namespace root, which
returns a list of available resources and their corresponding operations within
the namespace.
GET /v{major}/{namespace}Resources and collections
The remaining segments of a URL path are used to identify resources and collections of resources.
Consistent path components SHOULD be used to refer to the same resources, and collections of them, across different endpoints.
GET /v{major}/{namespace}/{resource}
GET /v{major}/{namespace}/{resource}/{resource_id}
POST /v{major}/{namespace}/{resource}/{resource_id}
PUT /v{major}/{namespace}/{resource}/{resource_id}
PATCH /v{major}/{namespace}/{resource}/{resource_id}
DELETE /v{major}/{namespace}/{resource}/{resource_id}Sub-resources and sub-collections MAY be supported, too.
GET /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}
GET /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
POST /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
PUT /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
PATCH /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
DELETE /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}Query parameters SHOULD NOT be used on single-resource endpoints (eg.
GET /v1/vault/credit-cards/{credit_card_id}). Query parameters exist to
restrict, search, filter, or sort a collection; a single-resource endpoint
already identifies exactly one resource via the URL path, so there is nothing
left for a query parameter to narrow down. See the Filtering section of the
Collections section, below, for the collection-scoped rule this
complements.
The {resource} and {sub_resource} components SHOULD be named using nouns.
Where there will only ever be one instance of a resource or sub-resource, the
{resource} and {sub_resource} component names SHOULD be in the singular
form. More commonly, there will be collections of each type of resource and
sub-resource, and these SHOULD be named in the plural form.
Resource-oriented endpoints SHOULD use lowercase hyphen-delimited slugs to name resources and sub-resources. Examples:
accountusersbillingcharge-pointscharge-points/{charge_point_id}/sessionscredit-cardscredit-cards/{credit_card_id}/transactions
Collections
A collection is a list of multiple resources of the same type, plus any related metadata.
Collections, and the resources they encapsulate, SHOULD be named consistently across different endpoints. This allows clients to implement generic data access handling.
The resource representations returned in collections MAY be only partial representations of the full resources. It MAY be necessary for clients to subsequently fetch individual resources to retrieve their full representations.
GET /v{major}/{namespace}/{resource}GET /v1/vault/credit-cards
{
"metadata": {
"total_items": 1,
"total_pages": 1
},
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"attributes": {
"type": "visa",
"number": "xxxxxxxxxxxx0331",
"expires": {
"month": "11",
"year": "2018",
},
"name": {
"first": "Joe",
"last": "Shopper"
}
},
"metadata": {
"create_time": "2014-01-13T07:23:15Z",
"update_time": "2014-01-13T07:23:15Z",
}
}
]
}Pagination
Any collection that could contain a large, potentially unbounded list of resources SHOULD implement pagination. The following design patterns are RECOMMENDED.
Pages of results SHOULD be referred to consistently by the query parameters
page and per_page, where per_page is a non-zero positive integer
representing the number of results per paginated response, and page is a
number of 1 or more that represents the current page of results requested.
/accounts?page={page}&per_page={per_page}The per_page query parameter SHOULD be optional. If not provided by the
client, the server should fallback to a sensible, specified default.
The page query parameter SHOULD also be optional. If not provided by the
client, the server MUST return the first page of results (ie. the default value
for page MUST be 1).
The values of both page and per_page MUST be validated by the server. A
400 Bad Request SHOULD be returned for semantically invalid values. However,
if the requested range is outside of the available range of results (eg.
page=2&per_page=100 is requested but there are only 50 results) a 200 OK
response SHOULD be returned with an empty result list, not a 404 Not Found.
In certain cases, such as querying on a large data set, in order to optimize the
query execution with pagination, it may be appropriate to retrieve the data
based on the result set of the previous page. A page_token parameter MAY be
used for this purpose. This could be, for example, an encrypted value of primary
keys to navigate to the next and previous pages, along with directions.
Additionally, responses MAY include total_items and total_pages metadata
fields. total_items indicates the total number of items in the collection, and
total_pages is the number of pages (interpolated from
total_items/per_page). This will help clients to implement better user
experiences, for example by disabling navigation to pages that are outside of
the available range. Where providing the total_items and total_pages
requires expensive queries on the server-side, the client SHOULD be able to
opt-in to receiving this information using a query parameter, for example
?include_totals=true.
Hypermedia links with rel attributes for "next", "previous", "first", and
"last" pages SHOULD be included in paginated collections, to make it easier for
clients to navigate through collections. The page and per_page query
parameters, inputted by the client, MUST be maintained for each link, to ensure
consistent client behavior. See the section on Hypermedia for further details
and examples.
Filtering
Collections MAY be filtered by default. For example, resources to which a user
is not authorized to access MUST NOT be included in a collection. If all
resources in a collection are not authorized, returning a 403 Forbidden
response would be appropriate.
Query parameters SHOULD be used only to restrict, search, or filter a collection. A resource identifier SHOULD NOT be used as a query parameter value to select a single resource out of a collection; the identifier belongs in the URL path instead, per the Resources and collections section of the URLs section, above.
Additional, optional filtering may be applied by clients using query parameters. For example, the following query parameters MAY be available for clients to filter collections by a time range:
start_timeor{property_name}_after: An ISO-8601 date and time string that represents the start of a temporal range.start_timemay be used when there is only one unambiguous time dimension, otherwise the property name should be used, eg,processed_after,uploaded_after. The property SHOULD map to a time field in the representation.end_timeor{property_name}_before: An ISO-8601 date and time string indicating the end of a temporal range.end_timemay be used when there is only one unambiguous time dimension, otherwise the property name should be used, eg.processed_before,uploaded_before. The property SHOULD map to a time field in the representation.
These query parameters SHOULD be used consistently across all endpoints that support time-based filtering.
Filtering by multiple values
It is often necessary for a client to filter a collection by more than one value
of the same field, eg. to request resources whose status is either open or
closed. Two patterns are RECOMMENDED for this, depending on context.
The default approach is to repeat the query parameter once for each value. This is natively supported by the HTTP standards and by most client and server libraries, without any special-case parsing logic.
GET /v1/{namespace}/{resource}?status=open&status=closedWhere this pattern is used, the query parameter’s documented name SHOULD be
singular (status, not statuses), since each occurrence carries a single
value.
Alternatively, a single query parameter MAY accept a comma-separated list of
values, eg. ?statuses=open,closed. This can be preferable where the number of
values is potentially large and the 2,000-character practical limit on URL
length is a concern. However, this pattern is NOT RECOMMENDED unless justified,
because it requires bespoke parsing logic on both the client and server, and it
is not universally supported by API tooling. Where this pattern is used:
- The query parameter’s documented name SHOULD be plural (
statuses), to signal to clients that comma-separated values are accepted. - The comma character MUST be used as the separator between values.
- The API’s documentation MUST specify how a client should escape a literal comma character within a value, if this is possible for the field in question.
A collection endpoint SHOULD implement only one of these two patterns for any given query parameter, not both.
Searching
Search query parameters MAY be supported on collections, to allow clients to filter collection lists based on freeform input.
The query parameter SHOULD be named q.
A single query parameters MAY be used to search across multiple fields of the resources.
Sorting
Results could be ordered according to sorting-related instructions given by the client. This includes sorting by a specific field’s value, and sorting order.
The following URL parameters SHOULD be used for this purpose:
sort_by: A dimension by which items should be sorted. The dimension SHOULD map directly to an attribute in the item’s representation.sort_order: The order, one of "asc" or "desc", indicating ascending or descending order respectively.
The default sort field and sort order MUST be documented for each collection. All collections have a default sorting, except in use cases where the order is deliberately randomized (if so, this still needs to be specified).
Bulk operations
Where a client needs to create, update, or delete a large number of resources in a single call, rather than issuing one request per resource, a collection MAY support bulk operations. Bulk operations trade off some of the simplicity of single-resource CRUD for a significant reduction in the number of round trips required.
Homogeneous bulk operations – applying the same operation (eg. create) to
many resources of the same type – are RECOMMENDED where this trade-off is
worthwhile. Heterogeneous bulk operations – combining different operations, or
different resource types, in a single call – are NOT RECOMMENDED, because they
are difficult to make atomic, difficult to report errors for consistently, and
difficult for clients to reason about. Where heterogeneous batching is genuinely
required, consider adopting an established standard such as the OData Batch
specification, rather than inventing a bespoke format.
A homogeneous bulk operation SHOULD be modelled as an action on the collection,
accepting an items array in its request body:
POST /v1/{namespace}/{resource}/actions/bulkCreate
{
"items": [
{ "...": "..." },
{ "...": "..." }
]
}The response SHOULD echo back one result per input item, in the same order, so that clients can correlate each result with its corresponding request item by index:
{
"resources": {
"{namespace}/{resource}": {
"items": [
{ "id": "...", "attributes": { "...": "..." } }
]
}
},
"metadata": {
"batch_results": [
{ "status": 201, "index": 0 },
{ "status": 422, "index": 1, "name": "VALIDATION_ERROR" }
]
}
}Where an error needs to reference a specific field within a specific item of the
request, the error’s field JSON Pointer SHOULD include the item’s index, eg.
/items/1/currency_code. Where the request items do not have a stable index –
for example, where a client-supplied identifying attribute is more meaningful
to the client than positional order – an attribute-filter form MAY be used
instead, eg. /items/@account_number=='2097094104180012047'/address_id. An API
MUST document which of the two forms it uses, and MUST use that form
consistently across all of its bulk operations.
A bulk operation MUST document whether it is atomic (all items succeed, or
none are applied) or partial (each item succeeds or fails independently). An
atomic bulk operation SHOULD use the normal single-resource status codes, since
the whole operation either succeeds or fails as one. A partial bulk operation
SHOULD normally respond with 200 OK for the operation as a whole, with the
per-item outcomes conveyed in the response body as shown above, since no single
top-level status code can represent a mix of per-item successes and failures.
Where a bulk operation is processed asynchronously, 202 Accepted SHOULD be
used instead.
Bulk operations SHOULD be atomic wherever this is practical for the underlying
system to implement. Where an operation is not atomic, this MUST be documented,
so that clients know to check the per-item results rather than assuming a 2xx
response means every item succeeded.
Bulk update and replace
Where a client needs to PUT or PATCH many resources in a single call, the
bulkCreate-style action described above is not directly reusable, because
there is no single collection URL to target the update at. Instead, model the
bulk request itself as a first-class, uniquely identifiable batch resource.
A bulk-create action MAY return the identifier of the batch it created, in addition to the created resources' own identifiers:
{
"resources": { "...": "..." },
"metadata": {
"batch_id": "batch_5f3d9a1c"
}
}Subsequent bulk updates or replacements are then addressed to that batch, and
act on its constituent items the same way a single-resource PUT or PATCH
acts on one resource:
PUT|PATCH /v{major}/{namespace}/{resource}/actions/bulkUpdate/{batch_id}As with bulk create, every effort SHOULD be made to make a bulk update or replace atomic. Where atomicity is not practical, the response SHOULD follow the same partial-response shape as bulk create, described above.
HTTP statuses
If a collection is empty (ie. it has zero items), returning 404 Not Found is
not appropriate. It was the collection that was requested, not a specific item
in the collection. And the collection exists – it is just empty. So logically it
makes sense to return a 200 OK response with an empty items array.
Invalid query parameters SHOULD be signalled with a 404 Bad Request response.
Resources
Reading
Single resources are typically discovered through a collection, and are identified by a unique identifier. When reading single resources, a more detailed representation of the resource MAY be returned than the default, minimized representations included in collections.
A resource’s unique identifier SHOULD be unique to all resources of all types, not only resources of the same type or in the same collection. UUIDs are RECOMMENDED for this purpose, as each generated UUID is more-or-less guaranteed to be unique globally.
Identifiers for sensitive data SHOULD be non-sequential, and preferably non-numeric. In scenarios where this data might be used as a subordinate to other data, immutable string identifiers SHOULD be used for readability and debugging.
The following additional rules apply to the design of resource identifiers:
- A resource identifier’s lifecycle MUST be owned by the resource’s own domain model. Database-generated sequence numbers MUST NOT be used as resource identifiers, since doing so leaks an internal implementation detail (the choice of database and its auto-increment behavior) into the public contract, and typically also makes identifiers sequential and guessable. Where a UUID is not suitable, eg. for performance or storage reasons, an HMAC-based hashed identifier is an acceptable alternative.
- Where a sub-resource identifier is used (see
Sub-resources and sub-collections), it MUST be scoped for lookup purposes
within its parent resource only. For example, given
/users/1234/linked-accounts/ABCD, accountABCDMUST NOT be returned unless it is actually linked to user1234, even if an account with that identifier exists elsewhere in the system. - A URL path MUST NOT contain two resource identifiers in immediate succession,
eg.
/payments/12345/102030, since this is ambiguous as to whether the second segment is a sub-resource identifier or something else. Interpose a named sub-resource segment between them, per the templates above. - Resource identifiers SHOULD be restricted to ASCII characters. Where an identifier must be included in a URL and contains any character outside of the URI-unreserved set, it MUST be percent-encoded, as MUST any UTF-8 characters in query parameter values.
If the provided resource identifier is not found, even if the data is "soft
deleted" in the data source, the response status code should be 404 Not Found.
Otherwise, 200 OK MUST be returned when the resource is found.
GET /v{major}/{namespace}/{resource}/{resource_id}GET /v1/vault/customers/123e4567-e89b-12d3-a456-426614174000
Partial content
Where a resource may itself carry large or binary content, eg. one of the
files described in File uploads, below, the corresponding GET endpoint MAY
support ranged retrieval, so that a client can fetch the content in chunks
rather than downloading it in a single response.
Where supported, the response to an ordinary GET MUST include an
Accept-Ranges: bytes header, so that clients can discover the capability. A
client MAY then issue a HEAD request first to discover the resource’s total
size without transferring its content, followed by one or more ranged GET
requests using the Range header:
GET /v1/vault/documents/{document_id} HTTP/1.1
Range: bytes=0-2499The server responds with 206 Partial Content, a Content-Range header
identifying which part of the resource was returned, and a Content-Length
reflecting only the bytes in this response, not the resource’s total size:
HTTP/1.1 206 Partial Content Content-Length: 2500 Content-Range: bytes 0-2499/4580
Updating
There are two ways to update resources:
- Using
PUTto fully replace the resource. - Using
PATCHto partially update the resource.
In both cases, the shape of the input data SHOULD be consistent with the shape
of the resource representation returned by the API via the corresponding GET
requests. The only difference is that PATCH may submit fewer fields –
essentially a diff of what’s changed since the last GET.
For PUT requests, system-calculated values such as create_time and
update_time SHOULD be optional and SHOULD be ignored on deserialization by the
server. For PATCH requests, clients SHOULD be expected to omit these fields
from the request body, and the server SHOULD return 400 Bad Request if they
are included. For PATCH requests, the client is expected to submit only the
fields that have been updated by the client, and since the client cannot update
system-calculated values, trying to do so should be treated as a client error.
PUT|PATCH /v{major}/{namespace}/{resource}/{resource_id}The plain partial-representation form of PATCH described above is equivalent
to JSON Merge Patch, and where
implemented as such SHOULD use the application/merge-patch+json media type.
Because a null value in a JSON Merge Patch document means "delete this
field", merge patch is not suitable for a resource that can legitimately hold
an explicit null value in one of its own fields – there is no way to
distinguish "set this field to `null`" from "delete this field" in the patch
document itself.
Alternatively, standards such as
JSON Patch MAY be implemented for
PATCH requests, using the application/json-patch+json media type. Rather
than sending a partial representation of the resource, clients instead send a
list of operations to be made on particular members or fields of the
resource. JSON Patch does not share Merge Patch’s null ambiguity, and it
additionally allows operations to be sequenced, which matters where the order
changes MUST be applied in.
PATCH /widgets/abc123 HTTP/1.1
Host: api.example.com
Content-Length: ...
Content-Type: application/json-patch+json
[
{
"op": "replace",
"path": "/a/b/c",
"value": 42
},
{
"op": "remove",
"path": "/a/b/c"
},
{
"op": "move",
"from": "/a/b/c",
"path": "/a/b/d"
}
]The value of the "path" field is a
JSON Pointer that references the location
within the target document where the operation is to be performed. For example,
the path /a/b/c refers to the element "c" in the below JSON:
{
"a": {
"b": {
"c": "",
"d": ""
},
"e": ""
}
}The supported operations are "add", "remove", "replace", "move", "copy", and "test". To support partial updates to fixed-schema resources, APIs need to support only "add", "remove", and "replace" operations.
An API that supports PATCH MUST document which of these formats (plain
partial representation, JSON Merge Patch, or JSON Patch) it accepts, via the
request’s Content-Type header. A PATCH request whose Content-Type names
a format the endpoint does not support MUST receive
415 Unsupported Media Type, per Headers.
After a successful update operation, both PUT and PATCH operations SHOULD
normally respond with 204 No Content status, with no accompanying response
body. However, there may be use cases where it is preferable to instead return
200 OK with an updated resource in the response body. For example, this may be
required where clients need updates to system-calculated fields, or otherwise to
optimize client-server interactions. Alternatively, clients may opt-in to
receiving a 200 OK response with a response body by including the request
header Prefer:return=representation.
Any update request (whether PUT or PATCH) that fails input validation MUST
receive a 400 Bad Request response. If clients attempt to modify read-only
fields, or if the resource is in a non-updatable state, this is also a
400 Bad Request. If there are business rules or validation constraints, eg.
for data type, length, etc., that are not satisfied, a 400 Bad Request
response is appropriate. In addition, appropriate error codes and messages
SHOULD be encoded in the response body.
For situations that require interaction with upstream servers or external APIs
or processes, returning the 422 Unprocessable Entity status code may be more
appropriate than 400 Bad Request.
Deleting
In order to enable retries (eg. due to patchy connectivity), and for conformance
with HTTP standards, DELETE operations MUST be implemented to be idempotent.
This means that successful DELETE operations MUST always respond with
204 No Content, even if the resource is already deleted. Returning
404 Not Found is not appropriate for DELETE operations in this scenario, as
it suggests that the resource never existed at all. If necessary, clients can
use GET to verify the resource exists prior to DELETE.
DELETE /v{major}/{namespace}/{resource}/{resource_id}There may be use cases where a client expects resources to exist but they
unexpectedly disappear. This could be because a resource expired, or because of
some policy, such as a data retention operation that cleans-up stale data. In
these use cases, services MAY return a 410 Gone error code in response to a
request for a resource that no longer exists. Doing so provides the client with
extra information (it tells the client that the resource had already been
deleted).
For historical reasons, many web servers and HTTP client libraries do not expect
a message body to be included in HTTP messages sent using the DELETE method.
To support the widest possible range of clients, it remains good practice to
not require DELETE requests to be accompanied by a payload. This is an
OPTIONAL constraint, and is only REQUIRED if there are known to be clients that
will be unable to support DELETE requests with payloads.
Creating
There are two ways to create resources:
- Using
POSTto create a resource but have the server create an identifier for it. - Using
PUTto create a full resource, including a unique identifier that is generated client-side.
POST|PUT /v{major}/{namespace}/{resource}/{resource_id}PUT operations are idempotent by default, because the request payload has a
built-in unique identifier in the form of the resource ID, generated by the
client.
POST operations are NOT idempotent by default, and therefore there is risk
that duplicates of the same resource may be created if the client retries a
POST request. Where it is necessary to prevent this, clients MUST include a
unique identifier for the request message (eg. request_id). The server will
use the request ID to make sure it processes only the first instance that it
receives of each distinct request.
For PUT requests, system-calculated values – and other read-only fields – such
as create_time and update_time SHOULD be made optional and SHOULD be ignored
on deserialization by the server. But for POST requests, clients SHOULD be
expected to omit these fields from the request body, and therefore the server
SHOULD return 400 Bad Request when such fields are included in the request
content.
Otherwise, both operations SHOULD behave in the same way. Both POST and PUT
payloads MAY include only a subset of input fields (only the required fields,
for example), with the server filling in optional fields with default values.
The number of required fields SHOULD be minimized as much as possible. Implement as many default/fallback values as can reasonably be applied for each business case.
For both POST and PUT creation operations, successful outcomes MUST be
signalled by a 201 Created response, and a representation of the created
resource MUST be returned in the response body – including any server-generated
fields such as create_time.
Response messages SHOULD include a list of hypermedia links that represent all
the available operations that can be performed on the newly-created resource.
For example, if only GET and DELETE operations are available:
{
"resources": {
"vault/credit-cards": {
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"attributes": {
"type": "visa",
"number": "xxxxxxxxxxxx0331",
"expire_month": "11",
"expire_year": "2018",
"first_name": "Joe",
"last_name": "Shopper",
},
"links": [
{
"href": "https://api.example.com/v1/vault/credit-cards/123e4567-e89b-12d3-a456-426614174000",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.example.com/v1/vault/credit-cards/123e4567-e89b-12d3-a456-426614174000",
"rel": "delete",
"method": "DELETE"
}
]
}
]
}
}
}File uploads
Where a resource needs to carry binary content, eg. an image, a document, or an audio file, that content SHOULD NOT be embedded in a JSON payload as a Base64-encoded string. Base64 encoding inflates the size of the content by roughly a third, forces the entire file to be held in memory as a single JSON string on both client and server, and defeats HTTP-level features such as streaming and range requests. Two patterns are RECOMMENDED instead, depending on whether the file exists independently of other resource data.
Standalone file upload. Where the file is itself the primary subject of the
request, eg. uploading a profile photo, it SHOULD be submitted to a dedicated
URL using the multipart/form-data media type, per
RFC 2388. The request body SHOULD carry a
part for the binary file content, identified by a Content-Disposition header
with both name and filename parameters, and MAY carry a further part for
accompanying metadata.
POST /v1/{namespace}/{resource}/{resource_id}/actions/uploadDocument
Content-Type: multipart/form-data; boundary=ExampleBoundary
--ExampleBoundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json
{ "description": "Front of driving licence" }
--ExampleBoundary
Content-Disposition: form-data; name="file"; filename="licence-front.jpg"
Content-Type: image/jpeg
{binary data}
--ExampleBoundary--The response MUST include the uploaded file’s metadata, which clients then
reference in subsequent requests, rather than re-submitting the file’s content
each time. At minimum, this SHOULD include an id, create_time, size (in
bytes), url (from which the file’s content can subsequently be retrieved),
and type (its media type).
File upload as an attachment to structured data. Where a file needs to be
submitted together with structured JSON data in the same request, eg. uploading
a receipt alongside an expense claim’s other fields, the request SHOULD use
multipart/mixed or multipart/related, per
RFC 2387, with one part carrying the JSON
body and one or more further parts carrying the file content.
Sub-resources and sub-collections
Sub-resources and sub-collections SHOULD be used sparingly and only where they are essential to expressing an accurate representation of an API’s resource model.
GET /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}
GET /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
POST /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
PUT /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
PATCH /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
DELETE /v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}Where a resource of one type can exist independently of other resources of other types, these resources SHOULD be elevated to top-level resources in most use cases. But if one type of resource cannot exist without another, this is a candidate to be lowered to a sub-resource.
Sub-resources require multiple identifiers (composite keys, in database lexicon) to be uniquely identifiable. To identify a sub-resource, the parent resource’s identifier is required, as well as the sub-resource’s identifier. This is a potential source of complexity for client applications, as they need to manage multiple identifiers for essentially the same resource.
For this reason, sub-resources SHOULD be used sparingly. The need to encode hierarchies of resources can increase the complexity of both server-side and client-side code. So, even where there is a tight coupling between two types of resources, look to promoting dependent resources to top-level resources (with single identifiers) where practical.
Where sub-resources are necessary or beneficial, try to have no more than one level of sub-resources - that’s two levels of resources in total. Any more levels, and the complexity of client application code grows exponentially. Server code, too, needs to validate each level of resources, including how sub-resources relate to their parent resources, and this complexity also grows exponentially with each additional tier.
Sub-resources MUST have a named type.
/v{major}/{namespace}/{resource}/{resource_id}/{sub_resource_id} is not
acceptable because sub_resource_id has ambiguous meaning. Do this instead:
/v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}
Linking sub-resource identifiers to sub-resource types in the URL scheme also supports extensibility; other sub-resources can be more easily added in the future. This constraint also makes it easier to support different identifier naming conventions being used for different types of sub-resources, should that be necessary.
The string representation of an enumeration value MAY be used as a
sub-resource identifier, where the enumeration has a fixed, well-known set of
members. For example, a credit-cards/{credit_card_id}/limits/monthly
endpoint could use the enumeration value monthly directly as the
{sub_resource_id}, rather than requiring an opaque identifier to be looked
up first.
Sub-resources MAY be used as a solution to reducing the size of the parent resource, so segmenting a single large resource into multiple smaller resources. These types of sub-resources are known as singleton sub-resources and are identified by a static sub-resource name rather than a dynamically-generated identifier. Singleton sub-resources should be named using nouns in the singular form.
/v{major}/{namespace}/{resource}/{resource_id}/{sub_resource}/{sub_resource_name}GET /v1/customers/devices/123e4567-e89b-12d3-a456-426614174000/vendor-information
There will be a one-to-one relationship between a resource and each of its
singleton sub-resources. Singleton sub-resources are expected to always exist if
the parent resource exists, though they may have null values. (Do not return
404 Not Found if a singleton sub-resource does not exist; simply return null
for its value.)
Singleton sub-resources are not standalone resources, but are attached to their parent. Therefore, singleton sub-resources SHOULD be created and updated via operations performed on their parent resource, rather than having dedicated endpoints for each singleton sub-resource.
Singleton sub-resources SHOULD NOT duplicate resources from other collections, but SHOULD be unique to their parent resource.
Safeness and idempotency
The HTTP standards define the concepts of safeness and idempotency for HTTP methods.
A safe operation is one that does not modify the state of the resource – they
are read-only operations. The HTTP standards define the GET and HEAD
methods as safe methods, as these methods are intended not to request any kind
of operation except data retrieval.
An idempotent operation is one that has the same effect on the state of the requested resources, regardless of how many times the operation is performed. Clients can therefore retry operations, sending identical requests multiple times, without worrying about data corruption or other unexpected side effects of doing the retries.
There are many use cases for clients to send identical requests multiple times. Commonly, this is done in retry mechanisms in response to failed connection attempts.
Building in idempotency is an important aspect of the design of any HTTP API. It makes it easier for clients to interact with the API, and improves the fault tolerance of the server-side system.
The HTTP standards define the GET, HEAD, PUT, and DELETE methods as
being idempotent methods. HTTP APIs therefore MUST implement these operations to
be idempotent.
The HTTP standards do not specify the PATCH method as being neither safe nor
idempotent. However, it is strongly RECOMMENDED that PATCH operations be
implemented as idempotent ones.
POST operations are, by definition, neither safe nor idempotent. By default,
executing an identical POST operation multiple times will create multiple
discrete resources with different identifiers but duplicate data. There may be
legitimate use cases where this is the desirable behavior. For example, a "like"
operation on a social media post may not be required to be idempotent, as the
desired behavior of sending multiple instances of the same request may be to
toggle the user’s "like" state of the post.
However, for most use cases in most APIs it is expected that POST operations
will need to be implemented to be idempotent, to avoid unwanted duplicates of
data entities being created.
Naturally-idempotent endpoint design
Before reaching for the idempotency-key mechanism described below, consider whether the endpoint itself can be designed to be naturally idempotent, simply by choosing its verb and behavior carefully. A healthy majority of endpoints can be made idempotent this way, and doing so is preferable to idempotency keys: it needs no extra client-side bookkeeping, no server-side cache of past responses, and it is idempotent for every caller by construction, not only for callers that remembered to generate and resend a key.
The canonical shape is check-then-insert: a PUT endpoint, keyed by a
natural, client-known identifier (rather than a server-generated one), that
looks up whether a matching resource already exists and, within the same
transaction (see Request-scoped transactions in Actions), either creates
it or leaves it unchanged.
PUT /v1/vault/customers?email=jane@example.com HTTP/1.1
The first call finds no existing match, creates the resource, and returns
201 Created. Every subsequent call with the same parameters finds the
existing resource, makes no change, and returns 200 OK. The endpoint is
idempotent in the ordinary HTTP sense – repeating it has no further effect –
without any idempotency key being involved at all.
This naturally-idempotent posture extends beyond resource creation. Where an endpoint’s underlying work involves a non-idempotent network call to another service, eg. sending an email or provisioning infrastructure, that call SHOULD be moved out of the synchronous request path entirely and performed by a background job instead, per Asynchronous operations. The endpoint itself then only needs to be idempotent about enqueuing the job – a naturally-easier problem than making the network call itself idempotent.
Idempotency keys remain necessary for operations that cannot be shaped this
way: notably POST-created resources with server-generated identifiers,
which have no natural key to check-then-insert against, and operations with
irreversible external side effects, discussed next.
Genuinely non-idempotent operations
Some operations cannot be made naturally idempotent, and cannot be made safe merely by wrapping them in a database transaction, because part of their work is foreign – a mutation to state outside the API’s own database, such as charging a credit card through an external payment gateway, provisioning a server with a cloud provider, or any other synchronous call to an external system. Once such a call has been made, its effect cannot be rolled back the way a local database transaction can: the charge has been made, or the server has been provisioned, whether or not the rest of the request subsequently fails.
The idempotency-key cache-and-replay mechanism described below is insufficient on its own for this class of operation. Caching and replaying a stored response protects a client that retries after a response was successfully returned, but it does nothing for the case that actually causes retries in practice: the client disconnects, or the request times out, before a response is returned, while the foreign call may or may not have already gone through. A naive retry in that situation risks making the foreign call – eg. charging the card – a second time.
Operations with this shape MUST instead be built on a multi-stage transaction: the request’s work is broken into a sequence of discrete stages, each of which is durably recorded as it completes, so that a retried request can determine exactly how far the original attempt got and resume from there, rather than blindly re-executing every stage including the foreign one. See Idempotency keys with atomic phases, below, for how this standard’s idempotency-key mechanism is extended to implement this.
Idempotency keys
Idempotency keys MUST be used to implement idempotency in POST, PUT,
PATCH, and DELETE operations as required. An idempotency key is generated
client-side and it is a unique identifier for each discrete request. It is used
by the server to ensure that it processes only the first instance of each
discrete request it receives. Subsequent requests with the same idempotency key
are ignored, and the same response is returned as for the first request (the
server should assume that the client never received the first response).
Idempotency keys have other use cases too. They can double up as identifiers to
correlate requests with responses ("correlation IDs"), and they support the
tracing of cause-and-effect throughout distributed systems ("trace IDs"). For
this reason, it is RECOMMENDED that idempotency keys be implemented universally
across all operations in an API, including GET and HEAD operations.
In HTTP APIs, the header field X-Request-Id SHOULD be used as the idempotency
key. This is a widely-used header field, and it is used by many client libraries
and frameworks to generate unique identifiers for requests.
POST /v1/payments/payouts HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer {token}
X-Request-Id: 123e4567-e89b-12d3-a456-426655440000
{
// ...
}If a X-Request-Id header is not provided by the client, the server MAY
generate a unique identifier for the request. However, for most use cases it
will be more appropriate for the service to respond with a 400 Bad Request and
a link to the relevant documentation.
Whether an idempotency key is generated client-side or server-side, it MUST be
returned in response messages, also in the X-Request-Id header field.
Each idempotency key MUST be unique and MUST NOT be reused with other requests with different payloads. For simplicity of implementation, idempotency keys SHOULD be unique across all clients, too. For this reason, it is RECOMMENDED to use the UUID version 4 algorithm to generate idempotency keys. This probability of generating the same UUID twice is so low that it can be considered to be zero for all practical purposes.
If a client reused an idempotency key with a different request payload, the
server MUST reply with a 422 status code.
To implement idempotency, servers are required to cache response payloads
against their idempotency keys. The server MUST return the cached response
payload for each subsequent request with the same idempotency key, even if the
response status code is not 200 OK. This is to ensure that the client receives
the same response as it would have received if the request had not been retried.
Validity of idempotency keys SHOULD be time-based, allowing for servers to
optimize storage by periodically purging the cached response payloads associated
with expired idempotency keys (which are persisted for longer). The expiry time
MAY vary depending on each key’s use case, but a good default value is 24 hours.
After this time, the server SHOULD return a 400 Bad Request response for
requests with expired idempotency keys.
Idempotency-key locking
An idempotency key’s record SHOULD carry a locked_at timestamp, acquired
when a request first arrives with that key and released once the request
finishes. This lock serializes concurrent attempts to process the same key,
which the cache-and-replay behavior above does not by itself prevent – two
requests bearing the same key could otherwise both observe "no cached response
yet" and proceed to execute the underlying operation twice.
On receiving a request, the server MUST attempt to acquire the lock as part of looking up or creating the idempotency-key record, atomically:
- If no record exists for the key, one is created, locked, and the request proceeds.
- If a record exists, is unlocked (or its lock has exceeded a configured timeout, indicating a previous attempt is no longer actually in progress), and its parameters match the current request, the lock is acquired and the request proceeds.
- If a record exists and is currently locked by an in-progress attempt, the
server MUST respond with
409 Conflict. - If a record exists whose stored parameters do not match the current
request’s parameters, this is a reused key with a different payload; the
server MUST respond with
422 Unprocessable Entity, per the reuse rule above. - If a record exists and its status shows the request already finished, the server short-circuits directly to the cache-and-replay behavior above, returning the stored response without re-executing anything.
This lookup-and-lock sequence MUST itself run under SERIALIZABLE isolation
(see TS-43: Relational databases and SQL for the mechanism),
so that two transactions racing to lock the same key are guaranteed to leave
exactly one of them holding the lock and the other aborted, rather than both
believing they hold it.
If the request subsequently fails, the server SHOULD release the lock (set
locked_at back to empty) as part of handling the failure, so that a client
retry is not needlessly blocked behind a lock nothing will ever release.
Idempotency keys with atomic phases
The cache-and-replay mechanism above, together with locking, is sufficient for operations whose work is entirely local. For the genuinely non-idempotent operations described above – those involving a foreign state mutation – the idempotency-key record needs to carry more than a lock and a cached response: it needs to track how far the request got, so that a retry can resume rather than restart.
Recovery points and the request state machine
Extend the idempotency-key record with a recovery_point field. Model the
request’s server-side work as a sequence of named checkpoints, starting at
started and ending at finished, with one or more named checkpoints for
the operation’s specific stages in between, eg. charge_created,
payout_recorded. Each checkpoint name marks a point the request has
durably reached; the field is updated, as part of committing the work that
reaches it, to the name of the checkpoint just passed.
A request implemented this way is a simple state machine whose states are its
recovery points, forming a directed acyclic graph: execution moves forward
through the checkpoints toward finished and never cycles back. On the
original attempt, the request starts at started and works through each
checkpoint in order. On a retried attempt bearing the same idempotency key,
the server reads the stored recovery_point and resumes execution from
there, skipping any checkpoint the request already durably reached, rather
than re-running the whole request from the start.
Atomic phases
Each transition between recovery points is implemented as an atomic phase:
a set of local, ACID-transactional state mutations, executed together with
the update to recovery_point itself, that runs between two foreign state
mutations (or between started and the first foreign mutation, or between
the last foreign mutation and finished). Structure a multi-stage
transaction’s phases according to three rules:
- The idempotency-key upsert and lock acquisition described above is its own phase, always the first.
- Every foreign state mutation gets its own phase, isolated from all others, since it is the one part of the sequence a local transaction cannot make atomic.
- Every local state mutation that occurs between two foreign mutations (or between the key upsert and the first foreign mutation, or between the last foreign mutation and completion) is grouped into a single transaction, committed before the next foreign mutation is attempted.
Committing each local phase before the next foreign mutation begins is what
makes the sequence resumable: the local database durably records what has
already happened, so that if the process crashes or the foreign call itself
fails, a retry knows – from the committed recovery_point – exactly which
phase to resume from, rather than having to guess whether the last foreign
call actually took effect.
Foreign state mutations
The distinction between local and foreign state exists because the two offer fundamentally different guarantees. Local mutations, made within the API’s own ACID-compliant database, can always be rolled back cleanly if a transaction fails partway. Foreign mutations – calls to external services, including a payment gateway or a cloud provisioning API, but also, just as importantly, calls to the API’s own supporting infrastructure, eg. publishing a message to a queue such as Kafka – are irreversible: once made, the effect has happened, whether or not anything else in the request subsequently fails. Internal infrastructure calls are foreign, not local, and MUST be treated with the same atomic-phase discipline as a call to a third-party service, however tempting it is to assume they are "part of" the local transaction.
Indeterminate errors
Where a foreign state mutation fails, and the foreign service offers no idempotency mechanism of its own to make the mutation safely retryable, the server must decide how to record that failure. An error the foreign service reports explicitly (eg. a clear rejection) MAY be treated as a normal, retry-safe failure. But an indeterminate error – a connection reset, a timeout, or anything else where it cannot be established whether the foreign mutation actually took effect – MUST be treated conservatively, and recorded as permanently failed rather than retried, unless the foreign service provides an explicit "safe to retry" signal. Retrying an indeterminate failure risks performing the foreign mutation a second time with no way to detect that it has happened.
Completer and reaper
Two background processes support the idempotency-key mechanism in production:
- A completer periodically scans for idempotency-key records that are locked but have not progressed for longer than would be expected, meaning the originating client most likely disconnected before the request finished. The completer resumes and drives these requests to completion itself, from their last recovery point, so that abandoned requests do not languish half-done indefinitely.
- A reaper periodically deletes idempotency-key records older than a documented retention threshold – around 72 hours is RECOMMENDED as a starting point, long enough that a key survives across a bad deploy and an ensuing weekend before recovery, without retaining records indefinitely. The reaper is also an appropriate place to surface any request left in a permanently-failed state (per indeterminate-error handling, above) for human investigation, since these represent cases automated recovery could not resolve on its own.
Passive safety
The idempotency mechanisms described in this section exist in service of a broader goal: passive safety – a backend that ends up in a stable, consistent state regardless of what fails along the way, without requiring a human to intervene to put things right. Request-scoped transactions and naturally-idempotent design (above) and idempotency keys with atomic phases are the two complementary techniques this standard relies on to achieve it: the former for operations confined to the local database, the latter for operations that must also coordinate with the world outside it. A backend built this way treats failure as expected, not exceptional, and is designed so that ordinary retries – rather than manual recovery – are enough to resolve it.
Actions
In a RESTful-style of HTTP API, endpoints are resource-oriented. CRUD-like operations are performed against individual resources, and collections of resources, of various types. Resources are typically a conceptual mapping to a set of entities in a domain system.
But some operations may not neatly fit into the RESTful model. It is sometimes quite difficult to model business processes in a pure RESTful service. Classic examples include endpoints to "login", "logout", "reset password", "charge a credit card", "resend a notification", and to "configure permissions and roles".
In this technical standard, these standalone operations are referred to as "actions". Elsewhere, they may be referred to by other names such as "controllers", "procedures", "operations", or simply "endpoints".
Actions tend to be mapped directly to specific controller methods in the server-side code, and for this reason they are the HTTP API equivalent of RPC (remote procedure call) protocols.
A common use case for actions is to mutate the state of multiple resources in the same operation. These are known as composite actions. There will often be business operations that are not scoped to any one particular entity in the domain model. These are candidates for modelling as composite actions. Composite actions are a pattern for combining multiple atomic operations in a single transaction, abstracting away complex, multi-step processes behind a convenient facade for the client, so simplifying client-server interactions.
An example would be a "refund" action that would change the state of a payment, the customer’s account, and the merchant’s account, and the action would not be considered to be fulfilled until all of these changes are committed. Another example of a composite action would be the implementation of a search function that operates across multiple resource types.
Composite actions may be implemented for both performance optimization and usability reasons.
A composite action’s response SHOULD include hypermedia links (see Links in
Payloads) to every resource affected by the operation, not only the primary
one, so that the client can navigate to any of them without having to know in
advance which resources the action touches. For the refund example above, the
response would include links such as self (the refund itself),
parent_payment, and capture.
Another use case for actions is to implement transient operations. A transient
operation is one that does not mutate the state of any resources, or create new
ones. An example might be a "dry run" action that validates the input data for a
subsequent operation, such as a payment. A transient action SHOULD only be used
after resource-oriented alternatives have been considered and found unsuitable,
since it is about as RPC-like as an HTTP API design gets. A transient action
MUST return 200 OK, with a response body containing the calculated result;
because nothing is persisted, the same request MAY produce a different result
if repeated, eg. where the calculation depends on live, externally-sourced
data.
There are many other use cases for augmenting HTTP APIs, which are predominantly resource-oriented, with standalone RPC-like actions. You can think of actions as fulfilling a similar role to services in domain-driven design. In DDD, services are a pattern that encapsulate business logic that operates across multiple entities in a domain model. Similarly, actions trigger logic that doesn’t obviously belong to any one resource and/or any one CRUD operation.
There are risks and benefits to using actions in HTTP APIs. Action-oriented APIs can be harder to scale than resource-oriented ones. The number of URLs can grow much more quickly, producing increased configuration complexity for routing and externalization, among other things. There tends also to be fewer opportunities to promote code reuse in automated tests (because actions tend to have greater variability in their inputs and outputs than operations performed on resources).
However, for most HTTP APIs, not everything fits neatly into the RESTful architectural style. Some operations are simply better expressed as actions.
The preference should be to design as much of an HTTP API as possible around a resource-oriented model, and augment the API with actions where specific operations do not fit neatly into that model. We should not try to force everything into the resource model just for the sake of purity of the API design.
Request-scoped transactions
This standard requires atomicity explicitly for composite actions (above) and for bulk operations (see Bulk operations in Collections). But the same principle SHOULD be applied more generally, as a default posture for every idempotent request, not only the cases that obviously combine multiple operations: wherever practical, the whole of an idempotent request’s server-side work SHOULD execute within a single database transaction, so that its effects commit or abort together as one unit.
This 1:1 mapping between a request and a transaction gives the request lifecycle a natural transactional boundary. A request has a clear beginning, a clear end, and a single result; wrapping that lifecycle in a transaction means the boundary the client already experiences (it either gets a successful response or it doesn’t) matches the boundary the database enforces (either everything committed or nothing did). This protects against the failure modes that occur regularly at any real volume of traffic – a client disconnecting mid-request, a bug causing the handler to fail partway through its work, or a request timing out – any of which would otherwise leave the resources touched by the request in a partially-mutated, inconsistent state.
This principle applies most directly to requests whose work is entirely local – confined to the API’s own database. Where a request’s work also involves foreign state mutations – calls to external services, message queues, or other systems outside the local database’s transactional guarantees – a single wrapping transaction is not sufficient on its own; see Genuinely non-idempotent operations in Safeness and idempotency.
Note
The transaction-isolation mechanism this relies on (eg. SERIALIZABLE
isolation to protect a check-then-insert sequence from a concurrent
duplicate) is the scope of TS-43: Relational databases and
SQL, not this standard.
HTTP methods
Actions MUST be performed using HTTP’s POST method, except for actions that
retrieve read-only data such as logs or reports, in which case the GET method
MUST be used – to provide opportunities for client-side caching (POST
responses are not cacheable.)
URLs
The name of an action SHOULD suggest the type of CRUD operation to be performed, rather than this being baked into the semantics of the HTTP method. Because actions represent a processing function on the server side, it is usually more intuitive to express them using verbs such as "activate", "cancel", "validate", "accept", and "deny".
Action names should be like function names. Use lowerCamelCase with the first
segment being a verb. The rest of an action’s name should, typically, be in the
singular form: activateAccount, cancelSubscription, validateEmail,
acceptInvitation, denyRequest.
This naming convention helps to distinguish actions from resource-oriented endpoints, which are named using hyphen-delimited slugs.
Namespaces
Actions that operate on resources across multiple namespaces SHOULD be placed in
the root namespace of the API. For example, an action that sends a notification
to a user might be placed at /v1/sendNotification, rather than in either of
the "users" or "notifications" namespaces.
POST|GET /v{major}/{action}But it’s better to scope actions to namespaces wherever possible. Actions and resources MAY coexist in the same namespaces. All actions within a namespace MUST only operate on the resources (including sub-resources) in the same namespace. If this design constraint cannot be achieved, better to elevate the actions to the API’s global scope.
POST|GET /v{major}/{namespace}/{action}A good practice is to create a collection of actions within each namespace. Collections of actions SHOULD be named, simply, "actions". This helps to distinguish actions from resources in each namespace.
POST /v{major}/{namespace}/actions/{action}In addition, a GET /v{major}/{namespace}/actions endpoint MAY be provided to
list all available actions in a namespace – similarly to how a list of available
resources within a namespace can be retrieved.
Resource-scoped actions
There may be use cases for attaching actions to specific individual resources or collections, or even to sub-resources.
POST /v{major}/{namespace}/{resource}/{resource_id}/actions/{action}Resource-scoped actions may make sense to separate operations that are fundamentally business processes from operations that change the core state of the resources themselves.
A classic use case for resource-scoped actions is to avoid corrupting the entity
model of a subdomain with transient data like comments. For example, for
auditing purposes the business may require freeform comments to be attached to
subscription cancellations. Since the comments would not be part of the model of
a subscription resource, a resource-scoped action would be appropriate here.
Users would post their comments to a cancelSubscriptionComment action, run
subsequently to a DELETE /subscriptions/{id} request. This also works around a
technical constraint with DELETE requests: you can’t attach payloads to the
message body of DELETE requests.
Actions SHOULD be terminal resources within an HTTP API, which means they SHOULD NOT have sub-resources (including sub-actions) relative to them.
Reified actions
Where clients need to see the history of actions taken against a resource, eg. every time a subscription was cancelled and reinstated, a plain action endpoint is a poor fit: actions are RPC-like calls, not resources, so they have no natural collection to list or paginate through.
In these cases, consider reifying the action: instead of modelling it purely
as a verb (cancel), also model it as a noun – the record of the action having
been performed – exposed as a sub-resource collection (see
Sub-resources and sub-collections) using the plural form of the action’s name,
eg. cancel → cancellations.
POST /v{major}/{namespace}/{resource}/{resource_id}/{reified_action}
GET /v{major}/{namespace}/{resource}/{resource_id}/{reified_action}POST /v1/subscriptions/{subscription_id}/cancellations
GET /v1/subscriptions/{subscription_id}/cancellationsEach POST to the collection both performs the action and creates a durable
record of it, returned as an item in the collection; a subsequent GET
retrieves the full history. This pattern aligns naturally with event sourcing:
the reified collection is the event log for that kind of action on that
resource, and the current state of the resource can, in principle, be derived by
replaying it.
Reifying every action would undermine the simplicity that actions exist to
provide, so this pattern SHOULD be reserved for actions whose history is
genuinely useful to clients, eg. for audit, compliance, or support purposes. A
simple state transition that does not need a history of its own, eg. toggling a
resource’s status field, SHOULD continue to use a plain PUT or PATCH on
the resource rather than being modelled as an action at all. It is entirely
appropriate for a single resource to mix plain CRUD operations for its simple
state transitions with one or more reified action collections for the
transitions whose history matters – there is no need to force every state change
on a resource through the same mechanism.
Complex search actions
Simple collection filtering SHOULD use query parameters directly on the
collection endpoint, per the Filtering section of Collections. But some
search use cases demand a search syntax too complex to express as query
parameters – for example, combining many optional criteria, nested boolean
logic, or criteria whose values would run up against practical URL-length
limits. In these cases, a POST-based search action MAY be used instead,
accepting the search criteria as a JSON request body.
POST /v{major}/{namespace}/{resource}SearchPOST /v1/factory/widgetSearch
{
"created_before": "2026-05-13",
"status": "ACTIVE"
}A search action’s results SHOULD be paginated the same way as an ordinary
collection – see the Pagination section of Collections – with one
exception: because a search action is a POST and its results are not
addressable by a GET-able URL of their own, the page and per_page (or
page_token) parameters MUST be supplied as query parameters on the search
action’s URL, not in the request body, even though the search criteria
themselves are in the body. This is the one case in this standard where a
POST request combines a body with query parameters, and it exists
specifically so that hypermedia pagination links (next, previous, first,
last) can be generated as ordinary URLs that carry the paging state, without
requiring the client to resubmit the search criteria via a link.
POST /v1/factory/widgetSearch?page=2&per_page=25
{
"created_before": "2026-05-13",
"status": "ACTIVE"
}Status codes
The following response codes are appropriate for successful action operations:
200- The action was successfully executed, and the response body contains the result of the action, which may included updates to affected resources.201– The action successfully created one or more new resources. This will be appropriate for composite actions that create a root entity plus all its dependencies.204– Use this instead of200when there is no paylad in the response message. This will often be appropriate for actions that trigger out-of-band processes, such as sending notifications.
For errors, appropriate 4XX or 5XX error codes MAY be returned.
Asynchronous operations
In general, synchronous operations SHOULD be preferred over asynchronous ones, as they simplify implementations on both the server-side and client-side. But there are some use cases where asynchronous operations are necessary or beneficial.
Asynchronicity is particularly advantageous in long-running tasks, such as image processing and video transcoding. It is also useful in operations that require interactions with external systems, such as sending emails or SMS messages, where the response time of the external system is unpredictable and where the client requires only confirmation that the message was sent and does not require an immediate understanding of the outcome of that operation.
In implementing asynchronous operations, it is RECOMMENDED to conform to the following best practices.
Responses to resource creation, update, and deletion operations SHOULD return
the 202 Accepted status code. This indicates that the request has been
accepted for processing, but the processing has not yet been completed.
The response body MAY include hypermedia links to any created or updated
resources. There are two possible approaches to implementing this in the context
of asynchronous operations. The first option is to include the final URL of the
resource, from where clients can GET the latest representation of the resource
in the normal way. This can be a good option in scenarios where the resource’s
ID and path are already known. If a newly-created resource is not yet ready, or
if the resource has been deleted, the final URL SHOULD return the HTTP status
code 404 Not Found. Clients simply keep polling the provided endpoint until it
is confirmed that the resource has been mutated as expected.
{
"rel": "self",
"method": "GET",
"href": "/v1/namespace/resources/{resource_id}"
}A second option is to return a temporary URL where the status of the queued operation may be obtained via some kind of temporary identifier.
{
"rel": "self",
"method": "GET",
"href": "/v1/queue/requests/{request_id}"
}Where the operation resource supports more than GET – eg. the DELETE
cancellation described in Operations resource, below – the response SHOULD
include a link for each supported operation, not only self, so that clients
can discover what is possible without hard-coding assumptions about which
methods a given operation supports:
{
"links": [
{
"rel": "self",
"method": "GET",
"href": "/v1/queue/requests/{request_id}"
},
{
"rel": "cancel",
"method": "DELETE",
"href": "/v1/queue/requests/{request_id}"
}
]
}Where the second option is used and the operation creates a new resource, once
the operation completes, the status endpoint SHOULD respond with
303 See Other and a Location header pointing at the newly-created
resource’s URL, rather than continuing to return the operation’s own status
representation. This gives the client a single, explicit signal that
processing has finished and where the result now lives, without requiring it
to infer completion from the status body’s contents.
HTTP/1.1 303 See Other
Location: /v1/namespace/resources/{resource_id}Operations resource
Where an API implements a substantial amount of asynchronous processing, it is
RECOMMENDED to expose a dedicated /operations collection, rather than only
ad hoc per-request status URLs, so that operation status is modelled
consistently and clients have one place to discover in-flight and past work.
GET /v{major}/operations
GET /v{major}/operations/{operation_id}
DELETE /v{major}/operations/{operation_id}GET /v{major}/operations SHOULD support pagination, sorting, and filtering
per the Collections section. The default sort order SHOULD list operations
by state, in the order not_started, running, succeeded, failed, and by
creation time within each state.
An individual operation resource MUST support GET, and its representation
MUST include:
status: One ofnot_started,running,succeeded, orfailed.create_time: When the operation was created.update_time: When the operation’s status was last changed.percent_complete: An estimate of progress, where the underlying processing can support one. This field MAY be omitted where no meaningful estimate is available.
An operation resource MAY support DELETE, to allow a client to request
cancellation of an in-progress operation. Where implemented, DELETE MUST be
idempotent, per the Safeness and idempotency section – repeating a
cancellation request against an already-cancelled or already-completed
operation MUST NOT be treated as an error.
A server MAY retain a tombstone – a minimal record that the operation existed and how it concluded – for some period after an operation completes, rather than deleting the record immediately, so that a client that was disconnected during processing can still discover the outcome. This retention period MUST be documented per API, since the appropriate duration depends on how long a client might reasonably remain disconnected.
It is RECOMMENDED that all HTTP APIs that implement asynchronous processing also support a single webhook that clients may optionally implement to receive push notifications of any asynchronously-updated resources, or the results of any asynchronously-processed actions. This offers a third option for clients to keep their state synchronized with server changes – whether triggered by asynchronous operations or even by other clients. See TS-22: Webhooks for further guidelines on implementing this option.
It may be desirable to support both synchronous and asynchronous processing on
the same endpoints. One possible design pattern is to support synchronous
processing by default but allow clients to opt-in to asynchronous processing
using the Prefer=respond-async header.
Concurrency control
A common issue in network API design is how to manage concurrent operations. There is always the potential for multiple clients to attempt to modify the same resource at the same time. This can lead to data corruption or lost updates.
These are not always issues, but where they are, APIs will need to implement concurrency control mechanisms. The appropriate mechanism will depend on the specific use case. But common patterns typically involve the use of ETags.
ETags (Entity Tags) are used to implement a strategy known as optimistic concurrency control. They are used to prevent accidental overwrites – the "lost update" problem, in which the most recent update always wins – by allowing clients to check if a resource has already been modified before requesting further mutations to that resource.
ETags themselves are unique identifiers assigned by a web server to a specific version of a resource. When a resource changes, its ETag changes. ETags are returned in HTTP headers, allowing clients to detect changes to resources that originated from other clients.
When a client requests a resource, it receives an ETag header with a value
that represents the current version of the resource represented in the message
body. This may be any arbitrary value, but it is typically implemented as a hash
of the resource’s content.
ETag: "<etag_value>"
When the client subsequently requests an update to the resource, it includes in
the request the ETag of the last version of the resource that it has. This is
sent in the If-Match header.
The server then checks if the ETag in the request matches the current version of
the resource known to the server. If they match, the update proceeds. If they do
not match, it means another client has updated the resource in the meantime, and
the update fails with a 412 Precondition Failed status code.
An API SHOULD document its policy for a conditional update request that omits
If-Match entirely: either the update proceeds unconditionally (last write
wins), or it is rejected as a client error. Which is appropriate depends on
the resource; there is no single correct default.
Client retry pattern
A client performing a conditional update SHOULD implement a bounded retry
loop, rather than surfacing a single 412 directly to the end user:
GETthe resource, noting its currentETag.- Apply the intended change to the retrieved representation, locally.
- Attempt the update with
If-Matchset to the notedETag. - On
412 Precondition Failed, another client updated the resource in the interim; return to step 1 and retry, up to a small, documented maximum number of attempts.
This gives the client a chance to reconcile its change against a concurrent update automatically, rather than requiring the end user to retry the entire operation by hand.
Cache validation
ETags also support a second, distinct use: letting a client avoid re-fetching
a representation it already has, when it has not changed. Where a resource
supports this, the server MUST honor the If-None-Match request header: if
the ETag supplied by the client matches the resource’s current ETag, the
server MUST respond with 304 Not Modified and an empty body, rather than
re-sending the full representation.
Unlike If-Match, which is used to guard a mutating request against a lost
update, If-None-Match is used on a GET request to let the client decide
whether a refetch is necessary. See also Response caching in Headers.
Health check endpoints
Health check endpoints allow clients and infrastructure to verify the operational status and readiness of an HTTP API. These endpoints are particularly useful for load balancers, orchestration systems, and monitoring tools to determine if a service is healthy and capable of handling requests.
All HTTP APIs SHOULD implement a health check endpoint.
The following guidelines are based on [this IETF draft standard](https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-02).
Endpoint location
The health check endpoint SHOULD be located at a memorable, commonly-used URI that promotes self-discoverability by clients. It is RECOMMENDED to place the health check endpoint at the root level of the API, outside the versioned API path.
GET /health
Unless an APIs versions are hosted independently of each other, in which case they would each have different health statuses:
GET /v1/health GET /v2/health
Response format
Health check responses MUST use the JSON format with the media type
application/health+json.
The response MUST include a status field with one of three values:
pass: The API is operating normally and is ready to handle requests.warn: The API is operational but experiencing degraded performance or non-critical issues.fail: The API is not operational or unable to handle requests.
{
"status": "pass"
}The response MAY include additional optional fields such as:
version: The API version.releaseId: A unique identifier for the release or build.notes: An array of notes or advisories.serviceId: A unique identifier for the service.description: A human-readable description of the service.links: Hypermedia links related to the health check endpoint or service.
HTTP status codes
The HTTP response status code MUST correspond to the value of the status
field:
- For
passandwarnstatuses: HTTP response codes in the2xxor3xxrange MUST be used (typically200 OK). - For
failstatus: HTTP response codes in the4xxor5xxrange MUST be used (typically503 Service Unavailable).
Caching
Health check responses SHOULD be cacheable. The server SHOULD provide guidance
on appropriate caching using the Cache-Control header. A reasonable default
cache lifetime is 1 hour, but this MAY be adjusted based on the specific
requirements of the service.
GET /health HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/health+json
Cache-Control: max-age=3600
{
"status": "pass",
"version": "1.0.0",
"releaseId": "v1.2.3-abc123"
}Headers
HTTP header naming conventions
HTTP header field names are case-insensitive. This means that Content-Type,
content-type, and CONTENT-TYPE MUST be treated identically by HTTP clients
and servers to comply with RFC 7230 Section 3.2.
However, header values may be case-sensitive depending on their semantics. For
example, text/html and TEXT/HTML will usually be trated the same, but the
behavior of Bearer <token> versus bearer <token> may differ between
implementations.
Where you have control over the letter case of HTTP header fields, it is
RECOMMENDED to write them using Pascal Case (aka. Title Case) with words
delimited by hyphens: Content-Type, User-Agent, Accept-Encoding, etc. This
is the most widely used naming convention.
Avoid using underscores or alternative letter case conventions such as camelCase, even for your application’s own non-standard header fields.
Date header
Every response MUST include a Date header, giving the time at which the
server generated the response, formatted per RFC 5322 and expressed in GMT.
Date: Wed, 24 Aug 2016 18:41:30 GMT
Content negotiation
HTTP APIs use a small number of standard headers to negotiate the format of request and response bodies.
Accept: Clients SHOULD send this header to indicate which media types they are able to handle in the response. Servers SHOULD NOT assume this header is present, since some clients omit it. Where an API cannot produce a response in any of the media types requested, it MUST return406 Not Acceptable, rather than silently falling back to a different media type than the one requested.Accept-Charset: Where sent, this SHOULD includeutf-8. HTTP APIs following this standard only produce UTF-8 encoded content, so this header adds little in practice, but SHOULD be handled gracefully where present.Content-Type: Clients MUST include this header on any request that has a body, eg.POST,PUT, andPATCHrequests, and servers MUST include it on any response that has a body. Where the media type is text-based, eg.application/json, the header MUST include an explicitcharsetparameter, and that charset MUST beutf-8. Where a request’sContent-Typenames a media type the server does not support, the server MUST return415 Unsupported Media Type.
[source,http] ---- Content-Type: application/json; charset=utf-8 ----Content-Language: Responses SHOULD include this header to indicate the language of the response content, using a language tag. Where an API does not support content negotiation by language, it SHOULD still include this header with a fixed value representing the language it always responds in, eg.en-US.
Non-standard headers
Non-standard headers SHOULD be prefixed with X- to indicate that they are
custom headers, and to avoid potential conflicts with future standard headers.
Examples:
X-Request-Id: A unique identifier for the request, used for logging and tracing, and to implement idempotent operations.X-Correlation-Id: A unique identifier to correlate requests and responses through distributed systems. This may be useful in scenarios where a client does not supply anX-Request-Idheader, or where processes are initialized by the system (such as batch processes or scheduled jobs) rather than by a user.X-Client-Id: A unique identifier for the client application making the request. This may be useful where you want to track the behavior of a specific client application, rather than a specific user.
Header reliability
HTTP headers are convenient for carrying cross-cutting metadata, but they travel through more infrastructure than the response body does – proxies, gateways, CDNs, and client libraries can all inspect, add, remove, or modify headers in transit. For this reason:
- API consumers and API implementations SHOULD NOT assume that a particular header will always survive the journey from server to client intact. Business logic SHOULD NOT depend on a header being present or unmodified. Where a value is essential to correct behavior, it SHOULD be carried in the message body instead.
- Infrastructure components in the request path MAY reject a request outright based on a header, eg. rejecting an unauthenticated request before it reaches the API implementation, but SHOULD NOT otherwise silently alter a header’s semantic meaning.
Because of this unreliability, and because responses may be passed between layers of a client application that do not all have access to the original HTTP headers, hypermedia links MUST be conveyed in the response body rather than in headers. Specifically:
- The
Locationheader MUST NOT be used as a substitute for aselfhypermedia link in201 Createdresponses. A201response MUST include aselflink in the response body, even where aLocationheader is also present. - The
Linkheader MUST NOT be used to convey hypermedia controls. Use thelinksarray in the response body instead.
This does not apply to the genuine, protocol-level use of Location in 3xx
redirect responses, where the header is structurally required for HTTP
redirection to function and is handled transparently by HTTP client libraries.
CORS
HTTP APIs that are integrated into browser-based client applications MUST support Cross-Origin Resource Sharing (CORS), so that a browser will permit a page served from one origin to call the API on another.
An API SHOULD permit an allowed origin of and rely on the Authorization
header (a valid OAuth 2.0 Bearer token, per the *Authentication and
authorization section) to enforce access control, rather than supporting
user credentials with per-origin validation. An API with a genuine need to
restrict origins, eg. one that accepts cookie-based credentials, MAY instead
validate the Origin request header against an allow-list and echo it back,
but this is the exception, not the default.
A server MUST inspect the Origin request header and, where present, include
the appropriate CORS response headers (Access-Control-Allow-Origin and
related headers) on every response, not only on preflight responses.
A "simple" request – one using GET, HEAD, or POST with only CORS-safe
headers and content types – is not preflighted by the browser. Any other
request, eg. one using PUT, PATCH, or DELETE, or that sets a
non-standard header such as X-Api-Key, triggers a preflight: the browser
sends an OPTIONS request carrying Access-Control-Request-Method and
Access-Control-Request-Headers, and the server MUST respond with matching
Access-Control-Allow-Methods and Access-Control-Allow-Headers headers
before the browser will send the real request. Because a preflight adds a full
round-trip, performance-critical endpoints SHOULD prefer "simple" request
shapes where practical.
Prefer header
HTTP APIs MAY support the Prefer header. This standard HTTP header is
specified in RFC 7240. It is used by
clients to opt-in to specific behaviors when the server is processing the
client’s requests.
The Prefer header is useful for a number of use cases. Perhaps the most common
use case is to allow clients to opt-in to receiving a response body, encoding
up-to-date resources, for requests that would not otherwise receive one. For
example, instead of PUT and PATCH requests receiving a 204 No Content
response by default, clients can opt-in to receiving a 200 OK response with
the updated resource in the response body. This can be useful for clients that
need to capture system-generated fields, such as create_time and
update_time, or where clients benefit from receiving up-to-date
representations of resources that are particularly volatile.
By default, HTTP APIs should return full representations of requested and updated resources. But sometimes the client does not need the full representation and the client-server interaction can therefore be optimized by returning partial representations of resources. This can be particularly beneficial in collections in which individual resources are large objects in their complete representations. Clients may therefore choose to fetch minimal or summarized lists of resources, and then fetch the full representations of individual resources as-and-when needed.
The Prefer: return=minimal header MAY be used for this purpose. The definition
of a "minimal" representation is left to the discretion of the service, but it
SHOULD be documented as part of the API’s interface specification.
Besides controlling the shape of a response, the Prefer header MAY also be
used by clients to express a preference about the consistency and freshness of
the data returned, trading off strict accuracy for lower latency. This is
particularly relevant for read operations on data that is expensive to compute
or that is served from a cache or a read replica.
The following tokens MAY be supported for this purpose, in decreasing order of consistency guarantee:
Prefer: read-consistent: The client requires the response to be sourced from a durable, consistent data store, reflecting the result of all writes already acknowledged to any client. Where an API does not offer any other read consistency preference, this SHOULD be the default behavior, and clients SHOULD NOT need to set this token explicitly.Prefer: read-eventual-consistent: The client accepts a response that may be sourced from a cache or an eventually-consistent replica, and so may not reflect the most recent writes. If no such source is available, the server MAY fall back to a consistent, durable data store.Prefer: read-cache: The client prefers the response to be served from a cache, if available, prioritizing latency over freshness. On a cache miss, the server MAY fall back to an eventually-consistent or a fully consistent source.
An API that supports any of these tokens MUST document which of its endpoints
honor them, and MUST document the default behavior for endpoints where no
Prefer header is supplied.
Where a request includes a Prefer header, the response SHOULD include a
Preference-Applied header indicating which of the requested preferences were
actually honoured. This lets a client that requested, eg.
Prefer: return=minimal, read-cache, tell whether both preferences took
effect, only one did, or neither did – rather than having to infer this from
the response body’s shape.
Prefer: return=minimal Preference-Applied: return=minimal
Header propagation
Where a service makes its own downstream calls to other internal services in
order to fulfil a request, it MUST forward relevant custom headers, eg.
X-Request-Id and X-Correlation-Id, to those downstream calls. This allows a
single client-facing request to be traced consistently across all the internal
services that participate in fulfilling it.
Standard HTTP headers that carry cross-cutting concerns, eg. Authorization and
Accept-Language, SHOULD also be propagated downstream where the downstream
service needs the same context. Headers that are specific to the transport
between the client and the first-hop service, eg. Content-Length, SHOULD NOT
be blindly forwarded, since they may no longer be accurate for the downstream
request.
Response caching
HTTP API servers MUST provide guidance to clients on appropriate caching of response messages. Clients MAY cache responses based on the guidance issued by the server.
Client-side caching is typically guided using the Cache-Control header, but a
combination of other headers may be used:
Cache-Control: set appropriate directives (max-age, no-cache, private).ETag: implement entity tags for conditional requests.Last-Modified: include modification timestamps where applicable.Vary: specify headers that affect response content.
If you also are in control the client-size code, then you have more flexibility to control the downstream caching behavior.
Response caching can also be done upstream, too:
- CDN/Edge: Cache responses at edge locations for global distribution
- Application-level: Implement in-memory or distributed caching for computed results
- Database query: Cache expensive database operations
Consider cache invalidation techniques.
Payloads
The content, or payload, of HTTP request and response messages SHOULD be in the JSON format, for the majority of regular use cases. JSON is natively supported by most modern programming languages, it is human-readable as well as machine parsable, and has become the de facto standard for encoding data in HTTP APIs.
It is RECOMMENDED to always return some kind of content to the client, except
for 204 No Content statuses. Even if the content is just a message that
doesn’t add any more semantic meaning than is conveyed through the status code,
it can still be useful for the purpose of testing (eg. using tools like
Postman). A little bit of redundancy between payload content, headers, and
status codes is okay.
Naming conventions
Consistent naming – both within a single API and across an organization’s whole portfolio of APIs – reduces the learning curve for developers integrating with more than one of them, and allows client tooling to make reliable assumptions about field shapes. The following conventions SHOULD be applied to JSON field names.
- Field names SHOULD be lower_snake_case, consistent with the envelope fields
described later in this document (
create_time,total_items, and so on). Where a resource’sattributesare sourced from a domain model that uses a different convention, eg. lowerCamelCase, that convention MAY be preserved withinattributesfor consistency with the domain model, but SHOULD NOT be mixed with lower_snake_case within the same object. - Boolean fields SHOULD NOT be prefixed with
is_orhas_. The field’s name alone, combined with itsbooleantype and description, SHOULD make its meaning clear, eg.activerather thanis_active. - Fields whose value is an array SHOULD be named using a plural noun, eg.
authenticators,products, so that a reader can tell from the name alone, without consulting the schema, that more than one value may be present. - Enum values SHOULD be composed of upper-case alphanumeric characters and the
underscore character only, eg.
FIELD_TEN,NOT_EQUAL. - Hypermedia link relation names (the
relfield) MUST be lower-case.
Clients MUST NOT rely on the order in which fields appear within a JSON object in a request or response body. JSON object member order is not semantically significant, and servers, intermediaries, and serialization libraries are all free to reorder fields. Where an explicit, meaningful order is required, eg. the order of items in an array returned from a sorted collection (see the Sorting section of Collections), that ordering MUST be conveyed structurally, such as by array position, rather than by relying on object field order.
Primitive types
JSON’s own type system is looser than most programming languages assume, and several of its primitives have known cross-language interoperability problems. Where JSON Schema is used to describe request and response payloads (see TS-29: JSON Schema), the following constraints SHOULD be applied to each of the JSON primitive types.
Strings. A string field SHOULD always declare an explicit minLength and
maxLength. Without a maxLength, it is not possible to reliably size a
database column to store the value, nor to know in advance whether a future
change to the value’s length would be a breaking change. Without a minLength,
clients are typically able to submit an empty string in a field that should
never be empty. pattern SHOULD be used to further constrain a string’s format
where appropriate, eg. for structured identifiers, but fields SHOULD NOT be
over-constrained without a good technical reason to do so.
Numbers. JSON defines only one numeric type, an unbounded fixed-point value;
the distinction between number and integer exists only in JSON Schema, for
validation purposes. Many languages and runtimes, notably JavaScript, cannot
safely represent every value permitted by this type – for example, any integer
larger than 2^53 loses precision when parsed by a JavaScript JSON deserializer.
For this reason:
- The JSON Schema
numbertype SHOULD NOT be used. Depending on the receiving language, its values may be interpreted as either fixed-point or floating-point, which is a common source of rounding bugs. Decimal and monetary values SHOULD instead be represented as astring, constrained withpattern,minLength, andmaxLength. This applies to any value that may exceed 2^53 or otherwise exceed IEEE 754 double-precision range, not only monetary amounts, eg. a 64-bit database identifier exposed in a response. (An API MAY instead adopt a convention such as OData’sIEEE754Compatiblemode, in which large integers are serialized as JSON strings only when a client opts in via a request header, but this standard’s simpler default is to always use astringfor such values.) - The
integertype SHOULD be used only for values that fit within a 32-bit signed integer (-2147483648to2147483647), and an explicitminimumandmaximumSHOULD always be declared. If a value could plausibly grow beyond this range in the future, use astringinstead.
Arrays. An array field SHOULD declare a maxItems, both to protect against
unbounded response sizes and because several languages and runtimes place their
own hard limits on array length. Unless there is a specific reason to choose a
smaller limit, 32767 ((2^15) - 1) is a reasonable default upper bound.
maxItems SHOULD NOT be used to communicate the page size of a paginated
collection – that is a separate, independently-evolving concern (see the
Pagination section of Collections). minItems SHOULD also be declared, and
will typically be 0 or 1.
Enumerations. The JSON Schema enum keyword is difficult to extend safely:
adding a new value to an enum is a breaking change for any client that
validates responses against a previously-obtained copy of the schema, even
though the API’s own major version has not changed. For this reason:
enumSHOULD be used only where the complete set of valid values is fixed and genuinely will never need to grow.- Where new values might plausibly be added over time, use a
stringtype instead, constrained bypatternto the naming convention for enumerated values (see the section on naming conventions, below), and document the currently-known set of values in prose rather than enforcing it in the schema. Clients consuming such a field MUST be built to tolerate unrecognized values gracefully.
Null values. APIs SHOULD NOT produce or consume null values. A field that
has no value SHOULD simply be omitted from the JSON object, rather than being
present with a value of null. This distinction between "absent" and "defined
as null" is a frequent source of confusion in strongly-typed client languages,
where a deserializer often cannot tell the two cases apart. Omitting unset
fields entirely avoids the ambiguity.
Additional properties. JSON Schema’s additionalProperties: false keyword
locks an object down to precisely the set of properties declared in its
properties map. For schemas that describe extensible business data, eg. the
attributes of a ResourceItem (see below), this keyword SHOULD NOT be set to
false, because doing so turns the addition of any new field – ordinarily a
backwards-compatible, additive change – into a schema-breaking one for any
client that validates strictly against the schema. The envelope-level schemas
defined later in this document are a deliberate exception: they describe the
fixed structural "shape" of a message (the resources/metadata/links/
messages envelope, links, errors, and so on), which SHOULD change only
alongside a major version bump, so locking them down is intentional and
appropriate. The distinction to draw when authoring a schema is between
structure (lock it down) and business data (leave it open).
Schema composition
JSON Schema provides several keywords for composing schemas out of other schemas. These keywords are powerful, but not all of them are well supported by the documentation-generation, code-generation, and validation tooling typically used with HTTP APIs. The following guidance SHOULD be followed when authoring schemas.
Extending a type with allOf. The allOf keyword SHOULD be used where a
schema needs to extend a common type with additional, API-specific fields. For
example, a shipping_address field might reuse a shared address schema, while
adding a field that is specific to shipping addresses:
{
"shipping_address": {
"allOf": [
{ "$ref": "address.json" },
{
"properties": {
"type": { "type": "string", "pattern": "^[0-9A-Z_]+$" }
},
"required": ["type"]
}
]
}
}Avoiding anyOf and oneOf. These keywords SHOULD NOT be used to describe
request or response payloads. A field whose shape can vary between several
possible schemas, depending on some other field’s value, is difficult for client
tooling to work with: code generators typically cannot produce a single concrete
type for it, and clients written in statically-typed languages are forced to
write custom deserialization logic that standard libraries do not provide out of
the box.
A flatter structure, with one optional field per variant, is easier for clients to consume, at the cost of a small amount of redundancy in the schema. For example, prefer:
{
"activity_type": { "type": "string", "pattern": "^[0-9A-Z_]+$" },
"payment": { "$ref": "payment.json" },
"money_request": { "$ref": "money_request.json" }
}– where only the field named by activity_type is expected to be populated –
over a single extensions field typed as oneOf a payment schema or a
money_request schema.
Marking fields as immutable with readOnly. Where a resource has fields that
a client MUST NOT modify via PUT or PATCH, eg. server-generated identifiers
or timestamps, these SHOULD be marked with "readOnly": true in the schema,
rather than relying only on prose documentation. This allows tooling to flag
accidental attempts by a client to submit a value for such a field.
{
"properties": {
"id": {
"type": "string",
"description": "Identifier of the resource.",
"readOnly": true
}
}
}Payload schema
The structure of JSON payloads SHOULD be consistent across all endpoints in an API. Consistency makes it easier for clients to understand the API, and to write reusable code for interacting with it.
As for what that structure should look like, there is no universal standard. There have been some attempts to standardize JSON API structures, notably the Open Data (OData) protocol (https://www.odata.org/), which is the closest thing we have to an industry standard for application-level messaging in JSON. But OData is an overly complex system for most use cases, and for this reason it is not widely adopted. Lighter weight, community-driven standards include JSON API (https://jsonapi.org/) and JSON RPC (https://www.jsonrpc.org/).
An appropriate schema SHOULD be designed to meet the specific needs of each API’s particular use cases.
The following is a RECOMMENDED starting point for schema design. This describes a generic schema for the payloads of HTTP response messages. A subset of this schema MAY also be adopted for HTTP request message payloads. The schema design is heavily influenced by the JSON API standard, but is not compatible with it.
Top-level properties
This schema defines four top-level properties:
resourcesmetadatalinksmessages
The values of the first two properties are objects. The values of the third and fourth properties are arrays.
{
"resources": {},
"metadata": {},
"links": [],
"messages": []
}Only the resources property is REQUIRED for response payloads, where the body
of the HTTP message is not empty. However, the value of this property MAY be an
empty object. Therefore, the minimum payload REQUIRED for response messages is:
{
"resources": {}
}Resources
The "resources" field encapsulates the main resource representations within an HTTP message.
The value of this field MUST be a ResourcesContainer object (defined below),
or an empty object if there are no relevant resource representations to supply
to the client.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"patternProperties": {
"[a-z][a-z-/]*$": {
"$ref": "#/$defs/ResourceTypeContainer"
}
},
"additionalProperties": false,
"$defs": {
"ResourceTypeContainer": {
"type": "object",
"properties": {
"metadata": {
"$ref": "#/$defs/MetadataContainer"
},
"items": {
"$ref": "#/$defs/ResourceCollection"
},
"links": {
"$ref": "#/$defs/LinksCollection"
}
},
"required": ["items"],
"additionalProperties": false
},
"ResourceCollection": {
"type": "array",
"items": {
"$ref": "#/$defs/ResourceItem"
}
},
"ResourceItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"attributes": {
"type": "object"
},
"metadata": {
"$ref": "#/$defs/MetadataContainer"
},
"related": {
"$ref": "#/$defs/RelatedCollection"
},
"links": {
"$ref": "#/$defs/LinksCollection"
}
},
"required": ["id"],
"additionalProperties": false
},
"MetadataContainer": {
"type": "object"
},
"LinksCollection": {
"type": "array",
"items": {
"$ref": "#/$defs/LinkItem"
}
},
"LinkItem": {
"type": "object",
"properties": {
"rel": {
"type": "string"
},
"method": {
"type": "string",
"default": "GET"
},
"href": {
"type": "string",
"format": "uri"
}
},
"required": ["rel", "href"],
"additionalProperties": false
},
"RelatedCollection": {
"type": "array",
"items": {
"$ref": "#/$defs/RelatedItem"
}
},
"RelatedItem": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"id": {
"type": "string"
},
"additionalProperties": false
}
}
}
}ResourcesContainer schemaAll the keys in a ResourcesContainer object map to the name of a resource
type. Each key is a string representing the full name of a resource type,
including its namespace and parent resource (if the resource is a sub-type).
Resource keys SHOULD match exactly the URL paths of their corresponding resource
types in resource-oriented endpoints.
{
"resources": {
"{namespace}/{resource}": {},
"{namespace}/{resource}/{resource_id}/{sub_resource}": {},
}
}The value of each key in a ResourcesContainer object is another object that
encapsulates one or more resources, plus associated metadata and links, of the
referenced resource type. This object is of the type ResourceTypeContainer,
which is defined in the JSON Schema above. This object is REQUIRED to have at
least one property named "items". Other OPTIONAL properties are "metadata" and
"links".
{
"resources": {
"{namespace}/{resource}": {
"metadata": {},
"items": [],
"links": []
}
}
}The "items" key references a ResourcesCollection array. Each object in a
ResourcesCollection is a ResourceItem, which is a representation of exactly
one resource. Every ResourceItem instance MUST have a property named "id",
whose value is a unique identifier for the resource. Other attributes of a
resource MAY be listed in a hashmap referenced via an "attributes" property.
Other OPTIONAL properties of ResourceItem objects are "metadata", "related",
and "links".
{
"resources": {
"{namespace}/{resource}": {
"metadata": {},
"items": [
{
"id": "{uuid}",
"metadata": {},
"attributes": {
"{field}": "{value}",
"{field}": "{value}",
"{field}": "{value}"
},
"related": [],
"links": [
{
"rel": "self",
"method": "GET",
"href": "https://api.example.com/v1/{namespace}/{resource}/{uuid}"
}
]
}
],
"links": []
}
}
}The "attributes" field of the ResourceItem object is important. This is an
object whose data is derived from the business domain of the application.
Normally, this will be a representation of a domain object or other entity.
Conceptually, individual attributes map to fields in a domain object or columns
in a relational database table.
For this reason, the naming convention for ResourceItem "attributes" fields –
shown using the {field} placeholder in the above code example – MAY differ
from the naming convention for other properties of the response schema. For
example, resource attributes might adopt a "lowerCamelCase" naming convention
for their field names, differentiating them from the "lower_snake_case"
convention for other fields in the schema.
It is RECOMMENDED that ResourceItem attributes be composed from a consistent
set of common types, defined separately using JSON Schema. See
TS-29: JSON Schema for further guidance on best
practices for defining libraries of common types using JSON Schema.
Metadata
All instances of the "metadata" key in the schema reference either an empty
object or a MetadataContainer object. A MetadataContainer object
encapsulates metadata about the response, a resource collection, or an
individual resource. Metadata is not part of any representations of resources,
but it MAY provide additional information about resources.
A MetadataContainer object is a hashmap of key-value pairs. The keys MUST be a
string. The values can be the native JSON types string, number, or boolean.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"patternProperties": {
"[a-z][a-z_.]*$": {
"type": ["string", "number", "boolean"]
}
},
"additionalProperties": false
}MetadataContainer schemaThe "metadata" field at the root level of the schema MAY be used to provide more granularity about the status of a response, where HTTP status codes do not provide sufficient detail on their own. For example, a "status" metadata field could be included to provide a more specific status code, perhaps one originating from the business domain.
The "metadata" field of a ResourceTypeContainer object MAY be used to provide
information about the collection of resources attached to the object’s "items"
property, such as pagination details. Metadata fields such as total_items and
total_pages would be appropriate here.
Metadata may also be attached to individual ResourceItem objects.
Metadata SHOULD NOT duplicate information that is already provided in the parts
of HTTP response messages, such as their headers. For example, the
Content-Type header SHOULD be used to indicate the media type of the response
body, and the Content-Length header SHOULD be used to indicate the length of
the response body.
Related resources
ResourceItem instances have an optional property named "related". If included,
the value is a RelatedCollection of `RelatedItem`s.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"items": {
"$ref": "#/$defs/RelatedItem"
},
"$defs": {
"RelatedItem": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"id": {
"type": "string"
},
"additionalProperties": false
}
}
}
}RelatedCollection schemaEach RelatedItem instance is used to create a relationship between entities of
different types of resources, all of which MUST exist within the same
ResourcesContainer instance.
{
"resources": {
"{namespace}/{resource}": {
"items": [
{
"id": "{uuid}",
"attributes": {
"{field}": "{value}",
"{field}": "{value}",
"{field}": "{value}"
},
"related": [
{
"type": "{namespace}/resource}",
"id": "{uuid}"
}
]
}
]
},
"{namespace}/{resource}": {
"items": [
{
"id": "{uuid}",
"attributes": {
"{field}": "{value}",
"{field}": "{value}",
"{field}": "{value}"
},
"related": [
{
"type": "{namespace}/resource}",
"id": "{uuid}"
}
]
}
]
}
}
}In the following example, the "related" field is used to create a relational link between a payment card and prior orders paid with the same card.
{
"resources": {
"vault/payment-cards": {
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"attributes": {
"type": "visa",
"number": "xxxxxxxxxxxx0331",
"expires": {
"month": "11",
"year": "2028",
},
"name": {
"first": "Joe",
"last": "Shopper"
}
},
"related": [
{
"type": "history/orders",
"id": "792eb20b-159a-48d3-9d62-b3b28308a432",
}
]
}
]
},
"history/orders": {
"items": [
{
"id": "792eb20b-159a-48d3-9d62-b3b28308a432",
"attributes": {
"status": "shipped",
"total": 3999,
"currency": "USD"
}
}
]
}
}
}Requesting related resources
By default, the "related" field is populated at the server’s discretion. An API
MAY additionally support a client-driven expand query parameter, giving
clients control over which related resources are embedded, to avoid the
round-trips that would otherwise be needed to fetch them individually.
GET /v1/vault/payment-cards/{id}?expand=history/ordersWhere supported, the expand query parameter’s value SHOULD be a
comma-separated list of the type values (in the same
{namespace}/{resource} form used by the "related" field’s own type
property) that the client wants embedded. An API MAY restrict which relations
are expandable, and MUST document the supported set for each endpoint. An
expand value naming an unsupported relation SHOULD be ignored rather than
causing the request to fail, consistent with this standard’s tolerant-reader
posture towards client input.
Links
This HTTP message schema supports the embedding of hypermedia controls, aka. links.
Links may be associated with the response message itself, or a collection of resources, or individual resources within a collection.
{
"resources": {
"{namespace}/{resource}": {
"items": [
{
"id": "{uuid}",
"links": [
// Links related to this resource
]
}
],
"links": [
// Links related to this collection of resources
]
}
},
"links": [
// Links related to all collections of resources
]
}Links related to a specific resource may encode instructions for clients to update and delete the resource, for example. Links related to a collection of resources may encode instructions for clients to add new resources to the collection, or to query or filter the collection. Links at the root of the schema may encode other instructions for clients that are not scoped to any particular collection or resource.
Embedding hypermedia controls in HTTP response messages has numerous benefits, including easier discoverability of resources and actions, improved extensibility of APIs, and reduced coupling between clients and servers.
There are numerous standards and conventions for defining structured hypermedia links in JSON payloads, including HAL (Hypertext Application Language), JSON API, and Siren. The following convention takes some design cues from each of these, but is primarily inspired by PayPal’s convention.
PayPal’s API conventions for hypermedia links align with the principles of HATEOAS (Hypermedia as the Engine of Application State). In PayPal’s API, hypermedia links are used to make the API self-descriptive, by embedding all the information that clients need to interact with the available resources. This is the proper use of hypermedia links in API design.
Each hypermedia link is composed of an object with three properties:
rel: Indicates the relationship of the link to the current resource. For example, the word "self" is a self-reference to the current resource, while the word "next" is used to navigate to the next page of results in a paginated collection.href: Specifies the URL of the resource. This MUST be an absolute URI, and SHOULD be resolvable as a URI Template per RFC 6570 where it carries variables. Clients typically retain a link’shreffor later use, so an API MUST NOT return a relative path here.method: Defines the HTTP method – eg.GET,POST– that can be used with the link. This property MAY be omitted, in which case it defaults toGET.
For example, a typical PayPal API response might include links like this:
{
"links": [
{
"rel": "self",
"href": "https://api.paypal.com/v1/payments/payment/PAY-123456789",
"method": "GET"
},
{
"rel": "approval_url",
"href": "https://www.paypal.com/checkoutnow?token=EC-123456789",
"method": "REDIRECT"
},
{
"rel": "execute",
"href": "https://api.paypal.com/v1/payments/payment/PAY-123456789/execute",
"method": "POST"
}
]
}This structure provides clear guidance on how to interact with the API, including links for retrieving, approving, or executing a payment. Commonly, resource-related links will embed controls to perform CRUD-like operations on the resources. Example:
{
"metadata": {
"total_items": 1,
"total_pages": 1
},
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"attributes": {
"type": "visa",
"number": "xxxxxxxxxxxx0331",
"expires": {
"month": "11",
"year": "2018",
},
"name": {
"first": "Joe",
"last": "Shopper"
}
},
"metadata": {
"create_time": "2014-01-13T07:23:15Z",
"update_time": "2014-01-13T07:23:15Z",
},
"links": [
{
"rel": "self",
"method": "GET"
"href": "https://api.example.com/v1/vault/credit-cards/123e4567-e89b-12d3-a456-426614174000"
},
{
"rel": "delete",
"method": "DELETE",
"href": "https://api.example.com/v1/vault/credit-cards/123e4567-e89b-12d3-a456-426614174000"
},
{
"rel": "patch",
"method": "PATCH",
"href": "https://api.example.com/v1/vault/credit-cards/123e4567-e89b-12d3-a456-426614174000"
}
]
}
],
"links": [
// Pagintion links.
]
}Hypermedia links with rel attributes for "next", "previous", "first", and
"last" pages SHOULD be included in paginated collections, to make it easier for
clients to navigate through collections. The page and per_page query
parameters, inputted by the client, MUST be maintained for each link, to ensure
consistent client behavior.
Relationship | Description |
|---|---|
| Refers to the current page of the collection. |
| Refers to the first page of the collection. (This link type MUST NOT be returned when page tokens are used for navigation instead.) |
| Refers to the last page of the collection. Returning this link is OPTIONAL. (It also MUST NOT be returned when page tokens are used for navigation instead.) |
| Refers to the next page of the collection. This link MAY be omitted if it is known that the current page is the last in the available range (but this is not always known, because available resources may dynamically change between requests). |
| Refers to the previous page of the collection. This link MUST NOT be provided where the current page is the first. |
{
"metadata": {
"total_items": 1,
"total_pages": 1
},
"items": [
// ...
],
"links": [
{
"rel": "self",
"method": "GET",
"href": "https://api.example.com/v1/vault/credit-cards?page=3&per_page=10&sort_by=create_time&sort_order=asc"
},
{
"rel": "prev",
"method": "GET",
"href": "https://api.example.com/v1/vault/credit-cards/?page=2&per_page=10&sort_by=create_time&sort_order=asc
},
{
"rel": "next",
"method": "GET",
"href": "https://api.example.com/v1/vault/credit-cards/?page=4&per_page=10&sort_by=create_time&sort_order=asc"
},
{
"rel": "first",
"method": "GET",
"href": "https://api.example.com/v1/vault/credit-cards/?per_page=10&sort_by=create_time&sort_order=asc"
},
{
"rel": "last",
"method": "GET",
"href": "https://api.example.com/v1/vault/credit-cards/?page=12&per_page=10&sort_by=create_time&sort_order=asc"
}
]
}Beyond the resource-CRUD and pagination relations already described, the following link relation types are also commonly useful, and SHOULD be used where their meaning applies, in preference to inventing an equivalent API-specific name:
Relationship | Description |
|---|---|
| Refers to an endpoint that can be used to create a new resource in a collection. |
| Refers to a |
| Refers to a |
| Refers to the collection that a resource belongs to. |
| Refers to the current, latest version of a resource that has multiple versions. |
| Refers to an endpoint that can be used to search the linked resource or collection. |
| Refers to the parent resource of the current resource, in a hierarchy of resources. |
For an action’s link (see Actions), the action’s own name SHOULD be used as
the rel value, eg. activate, cancel, refund.
Hypermedia use cases
Embedding hypermedia links serves several concrete purposes beyond general discoverability. The following use cases are worth designing for explicitly.
A single entry point. Where an API leans into HATEOAS, it SHOULD provide a single, well-known starting point from which every other resource and operation can eventually be discovered by following links, rather than requiring clients to have every URL pattern hard-coded in advance. Depending on the shape of the API, this entry point typically takes one of the following forms:
- A natural top-level collection or object serves as the entry point, eg.
GET /v1/{namespace}/{resource}, from which links to individual resources and their available operations are reached. - For a complex, multi-step business process, the first step in the process
serves as the entry point, and each response returns links to the possible
next steps, based on the current state of the process. For example, a credit
application might begin at an
applyForCreditaction, whose response links to asignAgreementaction, whose response in turn links to anapproveordeclineoutcome. Where a step fails because the client’s data was incomplete or incorrect, the response MAY return only a link back to the step that needs correcting, eg. aPATCHlink to resubmit the missing or invalid data, rather than a link to the next state in the happy path. - For a namespace that exposes mostly independent, action-oriented endpoints
rather than resources (see Actions), the
GET /v{major}/{namespace}/actionsendpoint described in Actions serves as the entry point, linking to each available action.
Clients SHOULD treat every href value as an opaque identifier, rather than
attempting to construct or otherwise compose URIs themselves from constituent
parts. The responsibility for URI construction belongs to the server; a client
that parses or builds URLs by convention forfeits the ability of new versions
of the API to change unrelated parts of the URL structure without breaking
that client.
Service-controlled flow. Because the set of links returned with a resource MAY
vary based on that resource’s current state, links can be used to communicate
which operations are presently valid, without the client needing to embed that
business logic itself. For example, a cancel link SHOULD only be present on an
order while it is in a cancellable state, eg. pending, and SHOULD be absent
once the order reaches a terminal state, eg. completed. This allows new state
transitions to be introduced on the server side over time without requiring a
corresponding client-side change to recognize them.
Error resolution links. An error response (see Error handling) MAY include
links that help the client resolve the error, eg. a 422 Unprocessable Entity
response for an inactive account might include an activate link pointing the
client at the operation that would resolve the problem.
Messages
The "messages" field is an array of MessageItem objects that convey
informational messages to the client. Unlike error responses, informational
messages are included in successful (2xx) responses and do not indicate that
anything went wrong. They MAY be used to communicate warnings, deprecation
notices, or other supplementary information that is not part of the resource
representation itself.
Each MessageItem object has the following properties:
type: The category of the message (e.g.warning,info).code: A machine-readable code that identifies the specific message.title: A short, human-readable summary of the message.description: A longer, human-readable description providing additional detail.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The category of the message."
},
"code": {
"type": "string",
"description": "A machine-readable code that identifies the message."
},
"title": {
"type": "string",
"description": "A short, human-readable summary."
},
"description": {
"type": "string",
"description": "A longer, human-readable description."
}
},
"additionalProperties": false
}MessageItem schemaA 2xx response MUST NOT contain error codes or error messages in the
messages array. Errors MUST be reported using the error response schema
described in the next section.
Error handling
HTTP status codes and human-readable reason phrases alone are not sufficient to
convey detailed information about errors in a machine-readable manner. To enable
non-human consumers of HTTP APIs to understand and resolve errors
programmatically, all error responses — HTTP status codes in the 4xx and 5xx
ranges — MUST include a JSON error representation in the response body that
conforms to the error schema defined in this section.
Error responses use a separate schema from the normal response payload schema
described earlier in this document. An error response does not include the
resources, metadata, or links top-level properties. Instead, it uses the
error-specific schema defined below.
Status reporting rules
The following rules govern the use of status codes and error responses.
- Success MUST be reported with a status code in the
2xxrange. A2xxstatus code MUST only be returned if the complete code execution path — including any framework code as well as business logic code — is successful. - Failures MUST be reported in the
4xxor5xxrange. - A
2xxresponse MUST NOT contain error codes or error messages in the response body. - A
4xxor5xxresponse MUST return an error response body conforming to the error schema defined below. - For client errors (
4xx), the error response SHOULD provide enough information for the client to determine what caused the error and how to fix it. - For server errors (
5xx), the error response SHOULD limit the amount of information to avoid exposing internal service implementation details. Service developers SHOULD use logging and tracking utilities to capture additional diagnostic information. In particular, an error response MUST NOT include a stack trace or any other information that could assist an attacker in understanding the server’s internals; see also Security. 5xxstatus codes SHOULD NOT be used for validation or logic errors. These are client-side errors and MUST be reported with4xxstatus codes.- A service that calls an upstream or downstream service MUST NOT blindly
propagate that dependency’s error response to its own caller. The service
SHOULD instead re-map the error to a response appropriate to its own
contract – for example, an upstream
400 Bad Requestcaused by the service’s own malformed call to that upstream SHOULD be reported to the service’s own caller as a500 Internal Server Error, not forwarded as a400, since the fault lies with this service, not with its caller. Blindly forwarding an upstream error also risks exposing that upstream’s implementation details to a caller who has no relationship with it.
Error response schema
An error response MUST include the following fields:
name: A human-readable, unique name for the error type (e.g.VALIDATION_ERROR,OBJECT_NOT_FOUND_ERROR).details: An array that contains individual instances of the error with specifics. This field is REQUIRED for client-side errors (4xx).field: A JSON Pointer (RFC 6901) reference to the field in the request body that caused the error. For errors in path parameters or query parameters, this is the name of the parameter.value: The value of the field that caused the error.issue: A human-readable description of the reason for the error.location: The location of the field in error —body,path, orquery. If omitted, the default value isbody.
debug_id: A unique error identifier generated on the server side, logged for correlation and diagnostic purposes.message: A human-readable message describing the error. This MUST describe the problem, not suggest how to fix it.links: HATEOAS links specific to the error scenario. These links MAY provide more information about the error and how to resolve it.
The following field is OPTIONAL and deprecated:
information_link: A URI for expanded developer information related to this error. Uselinksinstead.
Note
This error schema is not an implementation of the IETF’s
RFC 7807 Problem Details format
(application/problem+json, obsoleted by RFC 9457). RFC 7807’s type,
title, status, detail, and instance members serve a similar purpose to
this schema’s name, message, and debug_id, but this standard’s schema is
retained for its richer per-field details array and its links and error
catalog machinery, described below.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A human-readable, unique name for the error."
},
"details": {
"type": "array",
"items": {
"$ref": "#/$defs/ErrorDetail"
}
},
"debug_id": {
"type": "string",
"description": "A unique error identifier for correlation."
},
"message": {
"type": "string",
"description": "A human-readable message describing the error."
},
"links": {
"type": "array",
"items": {
"$ref": "#/$defs/ErrorLink"
}
},
"information_link": {
"type": "string",
"description": "(Deprecated) URI for expanded developer information."
}
},
"required": ["name", "debug_id", "message"],
"additionalProperties": false,
"$defs": {
"ErrorDetail": {
"type": "object",
"properties": {
"field": {
"type": "string",
"description": "JSON Pointer to the field in error, or parameter name."
},
"value": {
"type": "string"
},
"issue": {
"type": "string",
"description": "Reason for the error."
},
"location": {
"type": "string",
"enum": ["body", "path", "query"],
"default": "body"
}
},
"additionalProperties": false
},
"ErrorLink": {
"type": "object",
"properties": {
"rel": {
"type": "string"
},
"method": {
"type": "string"
},
"href": {
"type": "string"
}
},
"additionalProperties": false
}
}
}JSON Pointer usage
The field property in error details SHOULD use JSON Pointer syntax as defined
in RFC 6901 to identify the specific field
in the request body that caused the error. For example,
/credit_card/expire_month points to the expire_month field within the
credit_card object.
For errors in path parameters or query parameters, the field property SHOULD
contain the parameter name, and the location property MUST be set to path or
query respectively.
An API that has already published a version identifying field by some other
means (eg. a dotted path, or the bare field name) MAY continue using that
existing scheme unchanged, rather than being forced to adopt JSON Pointer
retroactively. But migrating an already-LIVE version to JSON Pointer is a
breaking change for any client that parses field programmatically, and MUST
be released as a new major version, per Versioning, with migration support
for existing consumers.
Input validation errors
In validating requests, different types of input errors should be addressed in the following order of precedence:
Issue | HTTP status code |
|---|---|
Not well-formed JSON. |
|
Contains validation errors that the client can change (e.g. a required field is missing, or a value is invalid). |
|
Cannot be executed due to factors outside of the request body. The request was well-formed but was unable to be followed due to semantic errors. |
|
Error samples
This section provides sample error responses in various scenarios.
Validation error — single field
The following sample shows a validation error in one field. Because this is a
client error, a 400 Bad Request HTTP status code should be returned.
{
"name": "VALIDATION_ERROR",
"details": [
{
"field": "/credit_card/expire_month",
"issue": "Required field is missing",
"location": "body"
}
],
"debug_id": "123456789",
"message": "Invalid data provided"
}Validation error — multiple fields
The following sample shows validation errors of the same type in two fields.
Note that details is an array listing all instances of the error.
{
"name": "VALIDATION_ERROR",
"details": [
{
"field": "/credit_card/expire_month",
"issue": "Required field is missing",
"location": "body"
},
{
"field": "/credit_card/currency",
"value": "XYZ",
"issue": "Currency code is invalid",
"location": "body"
}
],
"debug_id": "123456789",
"message": "Invalid data provided"
}Multiple heterogeneous errors (bulk operations)
For heterogeneous types of client-side errors, an errors array is returned.
Each error instance is represented as an item in this array. Because these are
client validation errors, a 400 Bad Request HTTP status code should be
returned.
{
"errors": [
{
"name": "OBJECT_NOT_FOUND_ERROR",
"debug_id": "38cdd677a83a4",
"message": "Bundle is not found.",
"details": [
{
"field": "/bundles/0/bundle_id",
"value": "33333",
"issue": "BUNDLE_NOT_FOUND",
"location": "body"
}
]
},
{
"name": "MULTIPLE_CORE_BUNDLES",
"debug_id": "52cde38284sd3",
"message": "Multiple CORE bundles.",
"details": [
{
"field": "/bundles/5/bundle_id",
"value": "88888",
"issue": "MULTIPLE_CORE_BUNDLES",
"location": "body"
}
]
}
]
}Semantic validation error
In cases where client input is well-formed and valid, but the request action
cannot be performed due to semantic errors (e.g. insufficient account balance),
an HTTP status code 422 Unprocessable Entity should be returned.
{
"name": "BALANCE_ERROR",
"debug_id": "123456789",
"message": "The account balance is too low. Add balance to your account to proceed."
}Error declaration in API specifications
Error responses SHOULD be declared in API specifications (e.g. OpenAPI) so that documentation generation tools and client/server code generation tools can recognize them. Each operation SHOULD declare the error responses it can return, referencing the error schema.
"responses": {
"200": {
"description": "Address successfully found and returned.",
"schema": {
"$ref": "address.json"
}
},
"404": {
"description": "The requested address does not exist.",
"schema": {
"$ref": "error.json"
}
},
"default": {
"description": "Unexpected error response.",
"schema": {
"$ref": "error.json"
}
}
}Error samples in documentation
API documentation SHOULD include samples showing error scenarios, not only samples showing successful execution. It is equally important — perhaps more so — to show API consumers how an API propagates errors in a machine-readable form, so that they can build applications that handle errors gracefully and meaningfully.
Error catalog
An error catalog is a structured collection of error specifications (metadata) for an API namespace. Cataloging errors provides several benefits:
- Externalization: Error message strings are externalized from the API implementation, making them easy to modify without code changes or redeployment.
- Localization: Externalized error strings can be localized by documentation or internationalization teams without service developer involvement.
- Synchronization: API documentation and runtime error responses remain in sync, increasing consumer confidence and reducing support costs.
An error catalog is a JSON file containing a collection of error specifications. Each error specification includes the error name, message, issue details, and related links. The catalog supports multiple locales — there should be one default catalog (e.g. in English) and corresponding locale-specific catalogs for each additional supported locale.
Each error specification SHOULD include:
name: A human-readable, unique name for the error. This MUST match thenamefield in the error response.message: A human-readable message describing the error. This MUST match themessagefield in the error response. This value MAY be localized.log_level: The log level associated with this error. This MUST NOT be exposed in error responses or external documentation.http_status_codes: The HTTP status codes applicable to this error.legacy_code: An older error code, used if and only if a previously published version of the error catalog already used this code and it must continue to be supported for backward compatibility. This SHOULD NOT be used for new errors.suggested_application_actions: Practical actions that the developer of an application consuming the API could take to resolve the error.suggested_user_actions: Practical actions that an end user could take to resolve the error. These MAY be localized.links: HATEOAS links specific to the error scenario.issues: Issue details associated with this error. Each issue corresponds to an item in the error responsedetailsarray.
Each issue within an error specification SHOULD include:
id: A catalog-unique identifier for the issue.issue: The reason for the error. This MUST match theissuefield in the error responsedetailsarray. This value MAY be localized and MAY contain parameterized strings for runtime substitution.
{
"namespace": "payments",
"language": "en-US",
"errors": [
{
"error_spec": {
"name": "VALIDATION_ERROR",
"message": "Invalid request - see details",
"log_level": "ERROR",
"http_status_codes": [400],
"issues": [
{
"id": "InvalidCreditCardType",
"issue": "Value must be visa, mastercard, amex, or discover"
}
]
}
}
]
}paymentsCanonical error names
An organization maintaining multiple HTTP APIs SHOULD adopt a shared,
canonical vocabulary of top-level error name values for conditions that recur
across APIs, so that a client integrating with more than one of the
organization’s APIs can handle common failure modes generically. The following
names are RECOMMENDED as a starting point:
INVALID_ARGUMENT: The request is malformed or fails validation.FAILED_PRECONDITION: The request is valid, but the current state of the server does not permit the operation.OUT_OF_RANGE: A parameter is outside the range of valid values, distinct fromINVALID_ARGUMENTin that the valid range may itself be dynamic.UNAUTHENTICATED: The request lacks valid authentication credentials.PERMISSION_DENIED: The caller is authenticated but lacks permission for the operation.NOT_FOUND: The requested resource does not exist.ALREADY_EXISTS: The resource the client attempted to create already exists.ABORTED: The operation was aborted, typically due to a concurrency conflict (see Concurrency control).UNAVAILABLE: The service is currently unavailable; the request is retryable.
An API-specific error catalog MAY define further name values for conditions
specific to its own domain, alongside these canonical ones. Once published,
adding a new top-level canonical name to the shared vocabulary is a breaking
change (see Versioning); a new, narrower error condition SHOULD instead be
expressed as a details entry under an existing canonical name.
JSON schema
The payload structure described above can be validated against the following JSON schema.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"resources": {
"$ref": "#/$defs/ResourcesContainer"
},
"metadata": {
"$ref": "#/$defs/MetadataContainer"
},
"links": {
"$ref": "#/$defs/LinksCollection"
},
"messages": {
"$ref": "#/$defs/MessagesCollection"
}
},
"required": [],
"additionalProperties": false,
"$defs": {
"ResourcesContainer": {
"type": "object",
"patternProperties": {
"[a-z][a-z-/]*$": {
"$ref": "#/$defs/ResourceTypeContainer"
}
},
"additionalProperties": false
},
"ResourceTypeContainer": {
"type": "object",
"properties": {
"metadata": {
"$ref": "#/$defs/MetadataContainer"
},
"items": {
"$ref": "#/$defs/ResourceCollection"
},
"links": {
"$ref": "#/$defs/LinksCollection"
}
},
"required": ["items"],
"additionalProperties": false
},
"ResourceCollection": {
"type": "array",
"items": {
"$ref": "#/$defs/ResourceItem"
}
},
"ResourceItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"attributes": {
"type": "object"
},
"metadata": {
"$ref": "#/$defs/MetadataContainer"
},
"related": {
"$ref": "#/$defs/RelatedCollection"
},
"links": {
"$ref": "#/$defs/LinksCollection"
}
},
"required": ["id"],
"additionalProperties": false
},
"MetadataContainer": {
"type": "object",
"patternProperties": {
"[a-z][a-z_.]*$": {
"type": ["string", "number", "boolean"]
}
},
"additionalProperties": false
},
"LinksCollection": {
"type": "array",
"items": {
"$ref": "#/$defs/LinkItem"
}
},
"LinkItem": {
"type": "object",
"properties": {
"rel": {
"type": "string"
},
"method": {
"type": "string",
"default": "GET"
},
"href": {
"type": "string",
"format": "uri"
}
},
"required": ["rel", "href"],
"additionalProperties": false
},
"RelatedCollection": {
"type": "array",
"items": {
"$ref": "#/$defs/RelatedItem"
}
},
"RelatedItem": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"id": {
"type": "string"
},
"additionalProperties": false
}
},
"MessagesCollection": {
"type": "array",
"items": {
"$ref": "#/$defs/MessageItem"
}
},
"MessageItem": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"code": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
}
}Tip
Use the online JSON Schema Validator to validate your JSON payloads against the schema above.
Versioning and managing breaking changes
HTTP APIs SHOULD have a major version number that is incremented whenever there are breaking changes in the API. The major version number SHOULD be encoded in the first path segment of the URL.
/v{major}/v1
Use the expanding contract pattern to maintain backwards compatibility with clients, while incrementally evolving the API’s design and capabilities. This is also known as the additive approach to building programmatic interfaces.
In practice, this means:
- Breaking changes MUST NOT be made to APIs that are already in use.
- Developers MUST NOT add new required parameters to existing APIs.
- Developers MUST NOT remove existing required parameters.
- Developers MUST NOT change the meaning of existing parameters.
- APIs MUST be designed to be extensible.
The rules above capture the intent of backwards compatibility, but "no breaking
changes" is easy to state and surprisingly easy to violate by accident. The
following, more exhaustive list of rules SHOULD be used as a checklist when
reviewing a proposed change to an API that is already LIVE.
- A resource URL MAY gain new, OPTIONAL query parameters, but MUST NOT gain a new REQUIRED one.
- A request that omits a newly-added, optional query parameter MUST continue to behave exactly as it did before that parameter was introduced.
- The semantics of an existing parameter, or of an existing resource representation, MUST NOT change.
- A previously-valid parameter value MUST continue to be accepted; it MUST NOT begin to be rejected as invalid.
- The set of HTTP methods supported on a URL MUST NOT shrink, though it MAY grow to support a new method.
- The set of HTTP status codes an endpoint can return for a given scenario MUST NOT change for requests that were already valid.
- The name and type of an existing request or response header MUST NOT change, though new, OPTIONAL headers MAY be added.
- An existing property of a JSON response object MUST continue to be returned, with the same name and the same JSON type.
- Where a property’s value is an array, the type of the array’s contents MUST NOT change.
- Where a property’s value is an object, these same compatibility rules apply recursively to that object.
- New properties MAY be added to a representation at any time, provided they are OPTIONAL and do not change the meaning of any existing property.
- A property that is documented as always being present in the response MUST continue to always be present.
- For an
enum-typed field, no previously-supported value may be removed, and the meaning of any previously-supported value MUST NOT change. - For a hypermedia link (see the Payloads section), the
relandhrefvalues for an existing link relation MUST remain stable in meaning, even if the specific URL they point to changes.
Clients MUST ignore unknown fields in a JSON response, rather than treating their presence as an error (the Tolerant Reader pattern). This tolerance is what makes the additive rules above possible: a server can add a new response field within a minor version specifically because clients are required not to choke on it.
The equivalent tolerance does NOT extend to requests. A server SHOULD reject a
request body containing an unrecognized field with 400 Bad Request, rather
than silently ignoring it. Silently ignoring an unknown request field hides
client-side mistakes, such as a misspelled field name, that would otherwise go
undetected; and once a server begins ignoring a field, adding real support for
that field name later becomes a breaking change for any client that was
relying on the old, silently-ignored behavior.
A good example of a scalable API design is one that avoids array of scalar data types (strings, integers, etc.). Consider the following example:
{
"countries": [
"Brazil",
"France"
]
}This data structure is impossible to extend without introducing breaking changes. Always prefer arrays of objects, eg.:
{
"countries": [
{ "name": "Brazil" },
{ "name": "France" }
]
}Breaking changes include any changes to the request or response message formats, changes to the semantics of the API, or changes to the behavior of the API.
Where breaking changes are unavoidable, the breaking changes MUST be implemented in a new major version of the API. The old version of the API MUST be maintained for a reasonable period of time to allow clients to migrate to the new version.
APIs MUST have a documented lifecycle policy, which describes the support and maintenance of each major version of the API.
API lifecycle
Every major version of an API MUST be understood to be in one of the following states at any given time. Documenting which state a version is in helps both API owners and API consumers plan around its availability.
State | Description |
|---|---|
| Development of this version has been scheduled, but it is not yet available to any consumer. |
| This version is operational and available to selected consumers, for the purposes of testing and validating the new version before it is generally available. Its contract MAY still change in response to feedback. |
| This version is operational, fully supported, and available to new consumers. Its contract is stable, per the backwards-compatibility rules in this document. |
| This version remains operational and fully supported for existing consumers, including bug fixes, but is no longer available to new consumers. Consumers SHOULD migrate to a newer version. See Deprecation, below. |
| This version is no longer available to any consumer, at any time. All server-side infrastructure supporting it SHOULD be decommissioned. |
At any given time, for a given piece of API functionality, there SHOULD be only
one version in the live state. This gives consumers a single, unambiguous
answer to the question of which version they should integrate with.
A major version SHOULD NOT move to deprecated until a replacement version is
live and provides a documented migration path for the functionality being
carried forward. Once deprecated, a major version MUST remain available for a
reasonable, published minimum period of time before it moves to retired, to
give consumers adequate notice to migrate. Where a version in the live or
deprecated state has no remaining consumers, it MAY move to retired
immediately.
Minor versions are subject to a simpler rule, because by definition they carry
no breaking changes relative to earlier minor versions of the same major version
(see the Expanding contract guidance, above). A minor version SHOULD move
directly to retired as soon as a newer minor version of the same major version
becomes live, without passing through deprecated – since, by the
backwards-compatibility rules that govern minor versions, no consumer should be
able to observe a difference by being moved onto the newer one.
Because introducing a new major version imposes a real migration cost on every existing consumer, the decision to do so SHOULD NOT be taken lightly. Before starting work on a new major version, it is RECOMMENDED to document why the change cannot be made within the current major version’s expanding-contract model, what value the new version delivers that justifies the migration cost, and how existing consumers will be supported through the transition. The following structured checklist is RECOMMENDED for this justification:
Business case.
- What customer value does the new version deliver that is not possible within the existing version’s expanding-contract model?
- What is the cost-benefit comparison between maintaining the deprecated version and shipping the new one?
- What alternatives to a new major version were considered, and why were they rejected? (This item MAY be omitted where the change is required to address a critical security issue.)
API design.
- How does the new version’s domain model compare to the version it replaces?
- What operations and use cases does the new version implement?
- What service level objectives (performance, availability) does the new version commit to, relative to the version it replaces?
Migration strategy.
- How many existing consumers are impacted, across internal, external, and partner integrations?
- What is the communication and support plan for informing existing consumers of the new version, its value, and the migration path?
Deprecation
When defining an API, developers make many decisions that have long-lasting implications. The objective is to make a long-lived, durable, and reusable API. In practice, however, not every decision will stand the test of time. New requirements emerge, understanding of the problem domain evolves, and decisions that seemed sound at the time may later limit the ability to extend the API elegantly.
Creating a new major version of an API allows developers to leave past decisions behind and start fresh. Unfortunately, this also means that all clients need to migrate to new endpoints for the new version to deliver customer value. This is hard. Many clients will not move without good incentives, and there is significant overhead in managing customer migration. Moreover, an API product may have multiple endpoints, but the breaking changes may only affect one of them — forcing all clients to migrate all endpoints just to "fix" one small part is often unjustifiable from an ROI standpoint.
Deprecation provides a middle ground — a practical path forward when minor changes are needed, without requiring a new major version. It is an extension to the versioning policy described above and is consistent with the expanding contract pattern.
Terms
The following terms are used throughout this section:
- API Element. Any individually addressable part of an API that can be deprecated. Examples include an endpoint, an HTTP method on an endpoint, a query parameter, a path parameter, a property within a JSON object schema, an entire JSON object schema, an enum value, or a custom HTTP header.
- Old API. The existing minor or major version of an API, or an existing different API that a new API supersedes.
- New API. A new minor or major version of an API, or a new different API that supersedes the old API.
Requirements
The following requirements govern the deprecation of API elements:
- An API developer SHOULD be able to deprecate an API element in a minor version of an API.
- An API specification (eg. OpenAPI) MUST highlight one or more deprecated elements of the API so that API consumers are aware.
- An API server MUST inform client applications regarding deprecated elements present in the request and/or response at runtime, so that tools can recognize them, log warnings, and highlight the usage of deprecated elements as needed.
- Deprecated API elements MUST remain supported for the life of the major version or until clients are no longer using them. The means to determine this are left to the discretion of the API owner, since it is their clients who will ultimately be impacted.
API specification: the x-deprecated annotation
An optional annotation named x-deprecated is used to mark an API element as
deprecated in the API specification (eg. swagger.json or an OpenAPI document).
The x-deprecated annotation SHOULD be used inline precisely where the API
element is defined. It is expected that API documentation generation tools would
recognize this annotation and highlight the corresponding API element as
deprecated. The annotation MAY be completely ignored by tools that generate
implementation bindings (eg. POJOs) — it is not a requirement that any
implementation language-specific construct (such as Java’s @Deprecated) would
be generated for the x-deprecated annotation.
API documentation SHOULD highlight deprecated API elements annotated by
x-deprecated distinctly and at the appropriate granularity.
Common schema elements
The following common JSON schema types are used across the specific deprecation schemas described below:
{
"x-deprecatedValue": {
"type": "string",
"description": "Value of the element that is deprecated. Use to deprecate a particular value in a parameter or schema property as applicable."
},
"x-deprecatedSee": {
"type": "string",
"description": "URI (indirect or absolute) or name of the replacement parameter, resource, method, or other API element, as applicable."
},
"x-apiVersion": {
"type": "string",
"pattern": "^[1-9][0-9]*[.][0-9]+$",
"minLength": 3,
"maxLength": 8,
"description": "The release or version number at which this element became deprecated, in the format '{major}.{minor}' (no leading 'v')."
}
}Deprecated resource
The following schema MUST be used to deprecate resource objects in the API definition. Examples of resource objects in an OpenAPI document include operations and paths.
{
"x-deprecatedResource": {
"type": "object",
"title": "Schema for a deprecated resource",
"description": "Schema for deprecating a resource API element. A resource API element could be an operation or paths.",
"properties": {
"see": {
"$ref": "#/definitions/x-deprecatedSee"
},
"since_version": {
"$ref": "#/definitions/x-apiVersion"
}
}
}
}"/commercial-entities": {
"x-deprecated": {
"see": "financial-entities",
"since_version": "1.4"
}
}"/commercial-entities/{merchant_id}/agreements": {
"put": {
"summary": "Updates the Commercial Entity Agreements Details for a Merchant.",
"operationId": "commercial-entity.agreement.update",
"x-deprecated": {
"see": "patch",
"since_version": "1.4"
}
}
}Deprecated parameter
Query parameters and custom HTTP headers can be deprecated. The following schema
MUST be used for the x-deprecated annotation when applied to a parameter.
{
"x-deprecatedParameter": {
"type": "object",
"title": "Schema for a deprecated parameter",
"description": "Schema for deprecating an API element inline. The API element could be a custom HTTP header or a query param.",
"properties": {
"value": {
"$ref": "#/definitions/x-deprecatedValue"
},
"see": {
"$ref": "#/definitions/x-deprecatedSee"
},
"since_version": {
"$ref": "#/definitions/x-apiVersion"
}
}
}
}"parameters": [
{
"name": "record_date",
"in": "query",
"description": "The date to use for the query; defaulted to yesterday.",
"required": false,
"type": "string",
"format": "date",
"x-deprecated": {
"since_version": "1.5",
"see": "transaction_date"
}
},
{
"name": "transaction_date",
"in": "query",
"description": "The date to use for the query; defaulted to yesterday.",
"required": false,
"type": "string",
"format": "date"
}
]"parameters": [
{
"name": "CLIENT_INFO",
"in": "header",
"description": "Optional header for passing API caller tracking information.",
"x-deprecated": {
"since_version": "1.5"
}
}
]"parameters": [
{
"name": "fields",
"in": "query",
"description": "Fields to return in response. Possible values are x, y, z.",
"required": false,
"type": "string",
"x-deprecated": {
"since_version": "1.5",
"value": "y"
}
}
]Deprecated schema property or schema
To deprecate a JSON object schema itself or one or more properties within a JSON
object schema, use the deprecatedSchema schema for the x-deprecated
annotation.
{
"x-deprecatedSchema": {
"type": "array",
"description": "Schema for a collection of deprecated items in a schema.",
"items": {
"$ref": "#/definitions/x-deprecatedSchemaProperty"
}
},
"x-deprecatedSchemaProperty": {
"type": "object",
"title": "Schema for a deprecated schema property or schema itself",
"description": "Schema for deprecating an API element within a JSON object schema. The API element could be an individual property or an entire schema.",
"required": ["api_element"],
"properties": {
"api_element": {
"type": "string",
"description": "JSON Pointer to the API element that is deprecated. If the API element is the schema itself, the JSON pointer MUST point to the root of that schema. If the API element is a property of the schema, the JSON pointer MUST point to that property."
},
"value": {
"$ref": "#/definitions/x-deprecatedValue"
},
"see": {
"$ref": "#/definitions/x-deprecatedSee"
},
"since_version": {
"$ref": "#/definitions/x-apiVersion"
}
}
}
}This annotation SHOULD be used in the API definition where a schema is
referenced (e.g. next to a $ref). Rather than extending JSON Schema with
custom keywords, the x-deprecated annotation is placed alongside the
reference.
Note
OpenAPI 3.0 introduced a deprecated flag that can be applied at the
operation, parameter, and schema field levels. The x-deprecated annotation MAY
be used alongside the deprecated flag to provide additional useful information
for the deprecated API element.
"responses": {
"200": {
"description": "The Commercial Entity.",
"schema": {
"$ref": "./model/commercial_entity.json",
"x-deprecated": [
{
"api_element": "./model/commercial_entity.json#/address",
"see": "./model/commercial_entity.json#/global_address",
"since_version": "1.4"
}
]
}
}
}"responses": {
"200": {
"description": "The Commercial Entity.",
"schema": {
"$ref": "./model/commercial_entity.json",
"x-deprecated": [
{
"api_element": "./model/commercial_entity.json#/state",
"value": "FAILED",
"since_version": "1.4"
}
]
}
}
}Runtime: the deprecation response header
The API server MUST inform client applications of deprecated API elements present in the request and/or response at runtime. This is achieved using a custom HTTP response header.
The service MUST respond with a custom HTTP header (e.g. Foo-Deprecated) in
the following cases:
- The caller has used one or more deprecated elements in the request.
- There is one or more deprecated elements in the response.
To avoid bloating responses with static information related to deprecation that does not change from response to response on the same endpoint, the header value SHOULD be an empty JSON object:
Foo-Deprecated: {}Note
Applications consuming this header MUST NOT take any action based on the value of the header at this time. Instead, these applications SHOULD take action based only on the existence of the header in the response.
In the future, the header value MAY be enhanced to provide enough information for tools to scan responses for deprecation and take appropriate actions such as notifying application developers or administrators.
Documentation and interface definition
HTTP APIs MUST be thoroughly documented.
An HTTP API’s interface SHOULD be described using a machine-readable interface definition language (IDL), such as OpenAPI, rather than relying on prose documentation alone. A machine-readable contract enables generated client SDKs, generated server-side request validation, and automated compatibility checking against the versioning rules in Versioning and managing breaking changes.
Design-first versus code-first
There are two common approaches to producing an API’s interface definition, and an organization SHOULD pick one deliberately rather than let it emerge by accident.
Design-first. The interface definition is authored before implementation begins, and both the producer and consumer teams review and agree it as a contract. This gives early design feedback, establishes clear expectations for both sides before code is written, and lets producer and consumer teams work in parallel – the consumer can build against a mock generated from the contract while the producer implements it. It works best where the interface is relatively stable once agreed; frequent requirement changes during development increase the overhead of keeping the contract and the discussion around it up to date.
Code-first. The interface definition is generated from the implementation (eg. from code annotations), after or alongside writing it. This allows rapid iteration, since there is no separate contract to keep synchronized while requirements are still settling, and it is often more familiar to developers already comfortable with the implementation language and framework. Its central risk is drift: a generated definition can silently fall out of sync with the code’s actual behavior across refactors, so automated checks comparing the generated definition against actual API behavior are RECOMMENDED wherever this approach is used.
Whichever approach is used, the resulting interface definition SHOULD be committed to version control alongside the API’s implementation, so that its history can be reviewed the same way as code changes.
Common types
Certain kinds of data – addresses, money, dates, country and currency codes – recur across almost every API in an organization’s portfolio. Where each API independently invents its own representation for these concepts, clients end up writing bespoke parsing and validation logic for each API they integrate with, even where the underlying concept is identical.
It is RECOMMENDED that a shared library of common JSON Schema types be maintained for an organization’s APIs to reuse, covering at least the types described in this section. See TS-29: JSON Schema for guidance on structuring and publishing such a library.
Money
Monetary amounts MUST be represented as an object containing, at minimum, a currency code and a value, rather than as a bare number.
{
"currency_code": "USD",
"value": "19.99"
}currency_codeandvalueMUST both be present. A monetary amount without a currency is not meaningful.valueMUST be represented as astring, not a number, per the general JSON number-precision rule in the Payloads section’s Primitive types. This is also true where a value is expressed in a currency’s minor unit (eg. cents) as an integer, rather than as a decimal major-unit amount. A 64-bit floating-point representation cannot reliably preserve every value a service may need to store — JavaScript’s numeric type, for example, only represents integers exactly up to 2^53, beyond which values silently round. An integer minor-unit amount also carries an implicit scale (how many minor units make a major unit) that varies by currency, forcing every client to know and correctly apply the right scale for each currency it handles. A decimalstringavoids both problems. It is exact regardless of magnitude, and its scale is self-describing rather than implied. Some currencies, eg. Japanese yen, have no minor unit. For these, the fractional part ofvalueSHOULD be0.- A monetary amount SHOULD NOT be negative. Whether a particular amount represents a debit or a credit is a property of the transaction or account context it appears in, not of the amount itself.
Percentages and rates
Percentages, interest rates, and similar proportional values SHOULD be
represented as a fixed-point decimal string, expressed as a percentage rather
than a fraction. For example, an interest rate of 19.99% MUST be serialized as
the string "19.99", not "0.1999". The field’s description SHOULD state
explicitly that the value is a percentage.
Formatting a percentage for display – including locale-specific decisions such as which character is used as the decimal separator – is the responsibility of the client. Services MUST NOT vary the representation of a value based on end-user display concerns.
Country, currency, and language codes
Consistent use of established international standards, rather than organization-specific codes, avoids ambiguity and allows values to be validated against well-known reference lists.
- Country codes MUST use the two-letter ISO 3166-1 alpha-2 standard.
- Currency codes MUST use the three-letter ISO 4217 standard.
- Language codes MUST use an IETF BCP 47 language tag.
- Where a locale needs to be expressed as a single value, it SHOULD be composed
from a language code and a country code (eg.
en-US), optionally extended with an IANA timezone identifier where the time zone also needs to be conveyed.
Addresses
A common address type SHOULD be shared across all APIs that need to represent
a postal address, decomposed into fields such as address lines, locality,
administrative area (state or province), postal code, and country code (using
the country code type, above).
Address formats vary significantly between countries, so a common address type
SHOULD avoid assuming any single country’s conventions, eg. by using generic
field names like admin_area rather than country-specific ones like state.
Where the admin_area value needs to be validated or compared programmatically
rather than only displayed, it SHOULD be represented using the subdivision code
from ISO 3166-2 (eg. US-CA for
California), rather than a free-text name.
Dates, times, and time zones
- All date-time values MUST conform to the
date-timeformat defined by RFC 3339. Where only a date, or only a time, is meaningful, use the correspondingfull-dateorfull-timesubset of the same standard, rather than a bespoke format. - Responses MUST express date-time values in UTC. Services SHOULD accept date-time values with a non-UTC offset in requests, normalizing them to UTC on receipt, for compatibility with clients that cannot normalize times themselves.
- A UTC offset alone MUST NOT be used to derive a time zone. The same offset
corresponds to different time zones depending on location and time of year
(eg. daylight saving). If the business logic genuinely needs to know the time
zone of an event, capture it explicitly in a separate field, using an
IANA time zone database identifier (eg.
Europe/London), not a fixed offset. - Some date or date-time values are inherently "floating", ie. not associated with any particular time zone, eg. a date of birth, or a card expiry date. These values SHOULD be represented without any time zone or offset information at all, since attaching one would misrepresent the value as referring to a specific instant, when it does not.
- Some floating values carry only a year and month, eg. a card’s expiry date.
These SHOULD be represented as a
year_monthstring inYYYY-MMformat (the same ordering as RFC 3339’sfull-date, with the day omitted), eg."2028-11", rather than as a fullfull-datewith a fabricated day-of-month value.
Durations and intervals
- A span of time not anchored to a specific start or end, eg. "renew every 30
days", SHOULD be represented as an ISO 8601 duration string, in the form
P[n]Y[n]M[n]DT[n]H[n]M[n]S. For example,P3Y6M4DT12H30M5Srepresents three years, six months, four days, twelve hours, thirty minutes, and five seconds. Only the components that are non-zero need be present. - A span of time anchored to specific points, eg. a booking’s occupancy
period, SHOULD be represented as an ISO 8601 interval string, in one of
three forms: start and end (
2007-03-01T13:00:00Z/2008-05-11T15:30:00Z), start and duration (2007-03-01T13:00:00Z/P1Y2M10DT2H30M), or duration and end (P1Y2M10DT2H30M/2008-05-11T15:30:00Z). - A recurring schedule, eg. "every week for the next five occurrences", SHOULD
be represented as an ISO 8601 repeating interval, formed by prefixing an
interval expression with
R[n]/, where[n]is the repetition count, eg.R5/2008-03-01T13:00:00Z/P1Y2M10DT2H30M.
Alternative HTTP API styles
This standard defines its own conventions for hypermedia, filtering, sorting, pagination, and RPC-style actions, described throughout the preceding sections. Several established alternatives exist for parts of this design space. TS-21 does not adopt any of them wholesale — its own conventions take cues from some of them, as noted where relevant — but a reader evaluating whether TS-21’s design fits a given API SHOULD be aware of the alternatives and what each one specifically standardizes.
Hypermedia media types
The following are complete, standalone specifications for hypermedia-driven
JSON payloads, each defining its own document structure for embedding links
and related resources. TS-21’s own resources/metadata/links/related
envelope (see Resources and Related resources in Payloads) takes design
cues from several of these but does not implement any of them; adopting one of
these specifications directly, rather than TS-21’s envelope, is a valid
alternative design choice for an API that wants to interoperate with tooling
built around a named standard.
- HAL (Hypertext Application
Language) — a lightweight convention adding
_linksand_embeddedmembers to any JSON (or XML) representation, plus CURIEs for compact, namespaced link relations. - JSON:API — a more prescriptive
specification covering compound documents (
included), sparse fieldsets (fields[TYPE]), and a defined document structure for resources, relationships, and errors. - Siren — models a representation as
entities,actions, andfields, giving the richest treatment of embedding available actions (with their expected input) directly in the representation, closest in spirit to TS-21’s ownlinksmechanism. - Ion — a hypermedia type built on Amazon’s Ion data format, with value/collection objects, embedded forms, and a registered type system.
- OData
JSON Format — part of the wider OData specification, which also defines
its own query language (
$filter,$orderBy,$top/$skip) and change- tracking model ($delta), both broader in scope than TS-21’s lighter filtering, sorting, and pagination conventions. - JSON-LD — a W3C standard for expressing
Linked Data in JSON, with
@context,@list/@set, and node/value objects; oriented around Semantic Web interoperability rather than HTTP-API hypermedia specifically.
RPC-style alternatives
- JSON-RPC 2.0 — a transport-agnostic
remote-procedure-call protocol built around a
jsonrpc/method/params/idenvelope, with support for notifications (no response expected) and batched calls. Unlike this standard’s Actions (see Actions), which are layered on top of an otherwise resource-oriented, RESTful HTTP design, JSON-RPC dispenses with the RESTful style entirely: every call is aPOSTto a single endpoint, and the operation being invoked is named inside the body rather than encoded in the URL or HTTP method. An API that is overwhelmingly RPC-shaped, with few or no resource-oriented endpoints, MAY be a better fit for JSON-RPC than for TS-21’s hybrid model.
References
- Hypertext Transfer Protocol (HTTP/1.1) - RFC 7231
- PATCH method - RFC 5789
- Additional HTTP Status Codes - RFC 6585
- Uniform Resource Identifier (URI): Generic Syntax - RFC 3986
- URI Template - RFC 6570
- Web Linking - RFC 5988
- IANA Link Relations
- RESTful Web Services Cookbook by Allamaraju, S.
- Introducing JSON
- JSON Data Interchange Format as described in - RFC 7159
- OpenAPI Specification v2.0
- Semantic Versioning 2.0.0
- Returning Values from Forms: multipart/form-data - RFC 2388
- The MIME Multipart/Related Content-type - RFC 2387
- Jason Harmon’s API standards, based on PayPal’s API design guidelines
- API Design by SmartBear