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

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

GET

Read

Yes

Yes

Used to retrieve the requested resource or collection of resources. GET operations MUST NOT modify the state of the requested resources, and there MUST NOT be any other side effects.

HEAD

Read

Yes

Yes

This behaves the same as GET, except the body of response messages is empty. HTTP response messages consist only of headers.

POST

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. POST operations are not natively idempotent, but idempotency can be achieved by clients including a unique idempotency key, eg. request_id.

PUT

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.

PATCH

Update

No

Yes

Used for partial updates to a resource. The request payload SHOULD contain only the fields that are being updated. PATCH requests SHOULD be designed to be idempotent, although the HTTP specification does not require this.

DELETE

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 DELETE requests.

QUERY

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 GET query parameters. Like GET, QUERY operations MUST NOT modify the state of the requested resources, and there MUST NOT be any other side effects. Responses to QUERY requests MAY be cached, but the cache key MUST incorporate the request body.

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 its Accept header. 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 its Content-Type header, is not supported by the server. This is distinct from 400 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 a Retry-After header 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

POST

PUT

PATCH

DELETE

  • GET: Retrieves a resource. 200 is the normal success response. An empty collection is still 200, but a missing single resource is 404.
  • POST: Typically creates a resource (201), but is also used for actions, where 200 is more usual. 202 applies where the operation is processed asynchronously.
  • PUT: Typically updates or creates a resource with a client-supplied identifier. 204 is the normal success response, since there is usually no need to echo the request back to the client. 200 is appropriate only where the response needs to carry server-generated fields the client could not have supplied itself.
  • PATCH: Behaves like PUT, but 200 with a response body SHOULD be avoided, since PATCH is intended for frequent, partial updates, and echoing the full resource on every call wastes bandwidth unnecessarily.
  • DELETE: 204 is 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}
Template
/v1
Example

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}
Template
/v1/vault
Example

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}
Templates

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}
Templates

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:

  • account
  • users
  • billing
  • charge-points
  • charge-points/{charge_point_id}/sessions
  • credit-cards
  • credit-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}
Template
GET /v1/vault/credit-cards
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",
      }
    }
  ]
}
Example response

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_time or {property_name}_after: An ISO-8601 date and time string that represents the start of a temporal range. start_time may 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_time or {property_name}_before: An ISO-8601 date and time string indicating the end of a temporal range. end_time may 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=closed

Where 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, account ABCD MUST NOT be returned unless it is actually linked to user 1234, 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}
Template
GET /v1/vault/customers/123e4567-e89b-12d3-a456-426614174000
Example

Updating

There are two ways to update resources:

  • Using PUT to fully replace the resource.
  • Using PATCH to 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}
Template

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}
Template

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 POST to create a resource but have the server create an identifier for it.
  • Using PUT to create a full resource, including a unique identifier that is generated client-side.
POST|PUT /v{major}/{namespace}/{resource}/{resource_id}
Template

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"
            }
          ]
        }
      ]
    }
  }
}
Example response

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}
Templates

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}
Template
GET /v1/customers/devices/123e4567-e89b-12d3-a456-426614174000/vendor-information
Example

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}
Template

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}
Template

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}
Template

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}
Template

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. cancelcancellations.

POST /v{major}/{namespace}/{resource}/{resource_id}/{reified_action}
GET /v{major}/{namespace}/{resource}/{resource_id}/{reified_action}
Template
POST /v1/subscriptions/{subscription_id}/cancellations
GET /v1/subscriptions/{subscription_id}/cancellations
Example

Each 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 of 200 when 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
Template

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
Template

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"
}
Minimal example

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 pass and warn statuses: HTTP response codes in the 2xx or 3xx range MUST be used (typically 200 OK).
  • For fail status: HTTP response codes in the 4xx or 5xx range MUST be used (typically 503 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"
}
Example

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 return 406 Not Acceptable, rather than silently falling back to a different media type than the one requested.
  • Accept-Charset: Where sent, this SHOULD include utf-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, and PATCH requests, 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 explicit charset parameter, and that charset MUST be utf-8. Where a request’s Content-Type names a media type the server does not support, the server MUST return 415 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 an X-Request-Id header, 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 Location header MUST NOT be used as a substitute for a self hypermedia link in 201 Created responses. A 201 response MUST include a self link in the response body, even where a Location header is also present.
  • The Link header MUST NOT be used to convey hypermedia controls. Use the links array 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’s attributes are sourced from a domain model that uses a different convention, eg. lowerCamelCase, that convention MAY be preserved within attributes for 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_ or has_. The field’s name alone, combined with its boolean type and description, SHOULD make its meaning clear, eg. active rather than is_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 rel field) 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 number type 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 a string, constrained with pattern, minLength, and maxLength.
  • The integer type SHOULD be used only for values that fit within a 32-bit signed integer (-2147483648 to 2147483647), and an explicit minimum and maximum SHOULD always be declared. If a value could plausibly grow beyond this range in the future, use a string instead.

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:

  • enum SHOULD 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 string type instead, constrained by pattern to 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:

  • resources
  • metadata
  • links
  • messages

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 schema

All 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 schema

The "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.

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 schema

Each 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"
          }
        }
      ]
    }
  }
}

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.

Page navigation

Relationship

Description

self

Refers to the current page of the collection.

first

Refers to the first page of the collection. (This link type MUST NOT be returned when page tokens are used for navigation instead.)

last

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.)

next

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).

prev

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"
    }
  ]
}
Example

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

create

Refers to an endpoint that can be used to create a new resource in a collection.

edit

Refers to a PATCH operation for partially updating the linked resource.

replace

Refers to a PUT operation for fully replacing the linked resource.

collection

Refers to the collection that a resource belongs to.

latest-version

Refers to the current, latest version of a resource that has multiple versions.

search

Refers to an endpoint that can be used to search the linked resource or collection.

up

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 applyForCredit action, whose response links to a signAgreement action, whose response in turn links to an approve or decline outcome.
  • For a namespace that exposes mostly independent, action-oriented endpoints rather than resources (see Actions), the GET /v{major}/{namespace}/actions endpoint 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 schema

A 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 2xx range. A 2xx status 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 4xx or 5xx range.
  • A 2xx response MUST NOT contain error codes or error messages in the response body.
  • A 4xx or 5xx response 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.
  • 5xx status codes SHOULD NOT be used for validation or logic errors. These are client-side errors and MUST be reported with 4xx status 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, or query. If omitted, the default value is body.
  • 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. Use links instead.
{
  "$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
    }
  }
}
Error response schema

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:

Input validation error classification

Issue

HTTP status code

Not well-formed JSON.

400 Bad Request

Contains validation errors that the client can change (e.g. a required field is missing, or a value is invalid).

400 Bad Request

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.

422 Unprocessable Entity

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 the name field in the error response.
  • message: A human-readable message describing the error. This MUST match the message field 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 response details array.

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 the issue field in the error response details array. 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
    }
  }
}
Full JSON Schema

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}
Template
/v1
Example

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.

URL and request stability
  • 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.
Payload stability
  • 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 rel and href values 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

planned

Development of this version has been scheduled, but it is not yet available to any consumer.

beta

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.

live

This version is operational, fully supported, and available to new consumers. Its contract is stable, per the backwards-compatibility rules in this document.

deprecated

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.

retired

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:

  1. An API developer SHOULD be able to deprecate an API element in a minor version of an API.
  2. An API specification (eg. OpenAPI) MUST highlight one or more deprecated elements of the API so that API consumers are aware.
  3. 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.
  4. 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"
  }
}
Example: Deprecated resource
"/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"
    }
  }
}
Example: Deprecated method
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"
  }
]
Example: Deprecated query parameter
"parameters": [
  {
    "name": "CLIENT_INFO",
    "in": "header",
    "description": "Optional header for passing API caller tracking information.",
    "x-deprecated": {
      "since_version": "1.5"
    }
  }
]
Example: Deprecated custom HTTP header
"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"
    }
  }
]
Example: Deprecated query parameter value
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"
        }
      ]
    }
  }
}
Example: Deprecated property in response
"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"
        }
      ]
    }
  }
}
Example: Deprecated enum value in response

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:

  1. The caller has used one or more deprecated elements in the request.
  2. 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_code and value MUST both be present. A monetary amount without a currency is not meaningful.
  • value MUST be represented as a string, 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 decimal string avoids 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 of value SHOULD be 0.
  • 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-time format defined by RFC 3339. Where only a date, or only a time, is meaningful, use the corresponding full-date or full-time subset 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