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
- 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.
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.
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.
- 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.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.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.
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.
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}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.
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.
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.
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
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}Alternatively, standards such as
JSON Patch MAY be implemented for
PATCH requests. 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.
PATCH /widgets/abc123 HTTP/1.1
Host: api.example.com
Content-Length: ...
Content-Type: application/json-patch
[
{
"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.
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.
POST /v1/{namespace}/{resource}/{resource_id}/actions/uploadDocument
Content-Type: multipart/form-data; boundary=...The response MUST include an identifier or URL for the uploaded file (see Creating), which clients then reference in subsequent requests, rather than re-submitting the file’s content each time.
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.
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.
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.
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.
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.
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.
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.
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}"
}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.
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.
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.
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.
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.
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. - 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"
},
"href": {
"type": "string"
}
},
"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"
}
}
]
}
}
}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.method: Defines the HTTP method – eg.GET,POST– that can be used with the link.
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. - 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.
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. 5xxstatus codes SHOULD NOT be used for validation or logic errors. These are client-side errors and MUST be reported with4xxstatus codes.
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.
{
"$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.
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.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.
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"
},
"href": {
"type": "string"
}
},
"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.
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.
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.
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. 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.
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.
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