Internal API standards
Audience: Bitwarden engineers and AI agents building or consuming a service-to-service API.
Scope. Internal, service-to-service APIs. Bitwarden's existing public API is out of scope and is not changing; nothing here applies to it.
This page is the living standard, adopted in ADR-0035. Its rules evolve by pull request without superseding that decision.
RFC 2119 keywords (MUST, MUST NOT, SHOULD,
SHOULD NOT, MAY) are used deliberately. A MUST or MUST NOT is not negotiable at team level;
a team that needs an exception brings the case to the architecture group.
General
In general, RESTful APIs are resource-oriented and do one of 4 things:
- Create a resource.
- Read a resource.
- Update a resource.
- Delete a resource.
This is the familiar CRUD paradigm and, while there will always be exceptions, developers SHOULD
strive to think in these terms for every API created. Both for simplicity and consistency.
There is, however, a 5th type of API that doesn't cleanly fit the CRUD paradigm:
- Act upon a resource.
Standards for each type of API are documented below.
JSON:API
Internal Bitwarden APIs are based on top of JSON:API unless otherwise noted in this document. Where our standards are silent, JSON:API standards are assumed.
Well-defined APIs
A well-defined API spells out exactly how it should be called and what the caller can expect in
return - for both the happy path and the not-so-happy path. Developers SHOULD strive to think in
terms of resources and be on guard against API proliferation that can result from over-tailoring
APIs to the unique needs of this caller or that.
In general, it is better to have one API that can be called two different ways (e.g. query parameters) than two APIs that can only be called one way.
Authentication and authorization
How a caller proves its identity, how a service authorizes an operation, and how the current organization travels with a request are the subject of a forthcoming standard.
OpenAPI
APIs MUST be documented in OpenAPI format.
- APIs
MUSTprovide a description written with the API consumer as the audience in mind, free of implementation details. - APIs
SHOULDprovide realistic example JSON for both the request and response. - APIs
SHOULDdocument which attributes are filterable and which are sortable. - Fields that must always be present
MUSTbe marked asrequired. - Fields whose value may be null
MUSTbe marked asnullable. - Fields whose value is set by the server
MUSTbe marked asreadOnly. If a request includes a read-only field, the APIMUSTignore it.
Operation identifiers
Every operation MUST carry an explicit, stable operationId, and it MUST include the version
(e.g. getGroupV1, listGroupsV1, replaceGroupV1, getPolicyV2). These IDs are necessary for
stable, generated clients:
- Without the version,
/api/v1/groups/{id}and/api/v2/groups/{id}collide into one method name. - Without an explicit identifier, generators invent one from the route which can be brittle.
API paths
API paths MUST use lowercase "kebab-case" and conform as follows:
- The first element of the API path
MUSTbe a namespace. By default, the namespaceSHOULDbeapi. - The second element of the API path
MUSTbe the version number starting withv1. - The third element of the API path
MUSTspecify the resource or resources it targets. For resource-oriented APIs, this elementSHOULDbe the "resource plural" (e.g.users). - For resource-oriented APIs, the fourth element of the API path
SHOULDbe the ID of the resource it targets.
Why versioning in the path? It is visible in logs, traces, routing rules and curl commands; it needs no content negotiation to read; and it lets two versions coexist behind one host. Header and media-type versioning are both defensible but harder to operate.
Examples
/api/v1/users
/api/v1/users/123
/api/v1/users/123/addresses
Paths SHOULD be "hackable". If GET /api/v1/users/123/addresses/456 returns the details about
address 456 of user 123, then every parent path SHOULD resolve:
GET /api/v1/users/123/addressesshould return all addresses of user 123.GET /api/v1/users/123should return details about user 123.GET /api/v1/usersshould return all users.
Organization IDs SHOULD NOT appear in the path because "the current organization" is part of the
context of almost every request and, thus, need not be
duplicated in the URL path.
Breaking changes and versioning
APIs MUST NOT make breaking changes. A breaking change is defined as follows:
- Removing (or relocating) a field.
- Changing the datatype of a field.
- Making an optional field required.
- Adding additional constraints to a field.
The lone exception to this rule is when the constraints that need to be added are fixing a bug. In this case, even though it is technically a "breaking change", API versioning is not required: the constraint may be added to the existing version of the API.
If changes need to be made that would be breaking changes, a new version of the API must be created and the old one deprecated.
Adding a value to a constrained field deserves a second look. It is additive, so it is not a breaking change by the definition above, and a caller that treats the field as an open string is unaffected. But a generated client that deserializes the field into a closed enumeration will fail on a value it has never seen — and it will fail at the client, on a change that looked safe from the service. Consider whether the callers of that field are tolerant of unknown values before adding one.
Unrecognized fields, query parameters, and headers
Sometimes a new, optional field, query parameter, or header is added to an API. Consumers upgrade to new clients and start passing the new fields/parameters/headers and everything is great until a security issue is discovered. The service is rolled back to the previous version but consumers are still using newer versions of the client.
To keep from having to also revert all of the consumers, APIs MUST ignore unrecognized fields,
parameters, and headers.
The exception is a parameter that shapes the result. Ignoring an unrecognized field means doing less
than the caller asked; ignoring one of these means returning something other than what was asked
for. APIs MUST reject the request with 422 when given:
- a
filter[{name}],sort, orfieldsparameter naming a field they do not support, or - paging parameters when the API does not support paging, or for a paging style it does not implement.
JSON
- Dates
MUSTbe formatted in ISO 8601 format (e.g.2026-01-01T00:00:00Z) andMUSTinclude a time zone indicator andSHOULDstandardize on UTC. - Field values that are constrained to a fixed set of values should enumerate the valid values in
all caps (e.g.
RED,GREEN,BLUE) both in the OpenAPI spec and in example JSON to help distinguish these fields from free-text string fields. At runtime, however, APIsMUSTignore case when validating these values. - APIs
SHOULDstrip leading and trailing whitespace from all strings before processing them. - APIs
MUSTtreat an empty string the same as if the field is not present. - APIs
MUSTtreat a field that isn't present the same asnull. - APIs
SHOULD NOTincludenullfields in responses.
Naming conventions
- Field names
MUSTbe in camel case (e.g.firstName,lastName). - Fields holding a date or date/time
MUSTend inAt(e.g.createdAt,expiresAt). - Boolean fields
MUST NOTbe prefixed withis(e.g.active, notisActive). - Fields whose value is the identifier of another resource
SHOULD NOTbe suffixed withId. A string-valuedassignedTois self-evidently the identifier of the user it is assigned to;assignedToIdadds nothing.
Content negotiation
- Services
MUSTrespect theAcceptmedia type requested by the caller. If the caller asks for XML and the server cannot return XML, the serviceMUSTreturn406 Not Acceptable. - Because our APIs are largely based on JSON:API, services
MUSTaccept a request whoseContent-Typeisapplication/vnd.api+json, andMUSThonor anAcceptofapplication/vnd.api+json, even though the API itself only advertisesapplication/json. - Services
MUSTreturn415 Unsupported Media Typeif the server can't process the specifiedContent-Type.
Standard responses
APIs SHOULD NOT document standard responses because these responses apply to every API and
documenting them over and over again just creates noise. The following responses are possible for
every API:
| Response | Description |
|---|---|
400 | A malformed request (i.e. invalid JSON). |
401 | The caller isn't recognized (i.e. not authenticated). |
403 | The caller isn't authorized to invoke the API (i.e. forbidden). |
404 | The resource specified via the URL path does not exist (or the caller isn't allowed to know it exists). |
405 | The HTTP method is not allowed. |
406 | The API doesn't return the specified media type. |
409 | The attempted update conflicts with some other update (i.e. was changed since the caller last read it). |
415 | The API doesn't accept the specified media type. |
422 | An error the client can fix. |
429 | The caller has made too many requests. |
500 | An unexpected error the client cannot fix. |
501 | The API has just been stubbed out and has not been implemented, yet. |
503 | The request cannot be completed because a dependency is unavailable or timed out. |
400 vs. 422
- Return
400for requests that are malformed and cannot even be parsed (e.g. invalid JSON). - Return
422for invalid requests. See Request validation.
403 vs. 404
- Return
403for attempts to read, write, or act upon resources the user is allowed to know exist. - Return
404for attempts to read, write, or act upon resources the user should not know exist.
404 vs. 422
- If the resource specified by the URL is not found, or the caller should not know it exists,
APIs
MUSTreturn404. - If the resource specified by the URL is found, but a resource referenced within the payload
does not exist (or the caller should not know it exists), APIs
MUSTreturn422.
500 vs. 503
- Return
500for unexpected errors that occur within the service that the client cannot fix. By definition, a500is a bug. - Return
503if some dependency of the service is down or unreachable or times out.
Resource fields
Every resource MUST include the following fields:
| Field | Description | Type |
|---|---|---|
attributes | Details about the resource. | object |
id | The unique ID of the resource. Even if the ID is numeric, it MUST be a string. | string |
type | The singular name of the resource (e.g. user). | string |
The only exception is when a resource is being created via POST against an API that will generate
the ID. In this case, the id field MUST be omitted.
Standard fields
When relevant, resources MUST use the following field names:
| Field | Description | Type |
|---|---|---|
createdAt | The date/time the resource was created, in ISO 8601 format. | string |
createdBy | The ID of the user that created the resource. | string |
deletedAt | The date/time the resource was deleted, in ISO 8601 format. See Soft deletes. | string |
deletedBy | The ID of the user that deleted the resource. | string |
updatedAt | The date/time the resource was last updated, in ISO 8601 format. | string |
updatedBy | The ID of the user that last updated the resource. | string |
version | The current version of the resource. See Optimistic concurrency. | string |
Verbs
APIs MUST use the standard verb semantics:
| Verb | Meaning |
|---|---|
DELETE | Delete a resource. |
GET | Fetch a resource. Safe, idempotent, and never changes state. |
PATCH | Partially update a resource. |
POST | Create a resource. |
PUT | Update a resource with "completely replace" semantics. APIs that support create-or-update MUST do so via PUT. |
The only exceptions are actions, advanced queries,
bulk updates, and bulk deletes, which use POST without creating
a resource.
Creating resources
- HTTP verb
MUSTbePOST. - API path
SHOULDbe like/api/v1/{resource plural}.
Example
POST /api/v1/users
Content-Type: application/json
{
"data": {
"attributes": {
"firstName": "Bob",
"lastName": "Smith"
},
"type": "user"
}
}
Success responses
Upon success, APIs SHOULD return 201 but MAY return 202 or 204.
| Status | Description | Response |
|---|---|---|
201 Created | Resource was created. | The latest representation of the resource. |
202 Accepted | Resource is scheduled to be created. | The job that was scheduled to create the resource. |
204 No Content | Resource was created. | Nothing. |
Read many
- HTTP verb
MUSTbeGET. - API path
SHOULDbe like/api/v1/{resource plural}.
Example
GET /api/v1/users
Accept: application/json
{
"data": [
{
"attributes": {
"firstName": "Bob",
"lastName": "Smith"
},
"id": "62bed180-1f78-45d4-8a56-c996936a2947",
"type": "user"
},
{
"attributes": {
"firstName": "Alice",
"lastName": "Jones"
},
"id": "cacba8c1-29fa-4018-8950-acd400ec76b7",
"type": "user"
}
]
}
Success responses
Upon success, APIs MUST return 200.
| Status | Description | Response |
|---|---|---|
200 OK | Request was successful. | The resources requested. |
See also:
Read one
- HTTP verb
MUSTbeGET. - API path
SHOULDbe like/api/v1/{resource plural}/{id}.
Example
GET /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947
Accept: application/json
{
"data": {
"attributes": {
"firstName": "Bob",
"lastName": "Smith"
},
"id": "62bed180-1f78-45d4-8a56-c996936a2947",
"type": "user"
}
}
Success responses
Upon success, APIs MUST return 200.
| Status | Description | Response |
|---|---|---|
200 OK | Request was successful. | The resource requested. |
Updating resources
- HTTP verb
MUSTbePUT. - API path
SHOULDbe like/api/v1/{resource plural}/{id}.
Example
PUT /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947
Accept: application/json
Content-Type: application/json
{
"data": {
"attributes": {
"firstName": "Bob",
"lastName": "Smith"
},
"id": "62bed180-1f78-45d4-8a56-c996936a2947",
"type": "user"
}
}
Success responses
Upon success, APIs SHOULD return 200 but MAY return 202 or 204.
| Status | Description | Response |
|---|---|---|
200 OK | The resource was updated. | The latest representation of the resource. |
202 Accepted | The resource is scheduled to be updated. | The job that was scheduled to update the resource. |
204 No Content | The resource was updated. | Nothing. |
See also: Partial updates
Partial updates
Services SHOULD NOT support partial updates — see the FAQ for why.
A service that does support them MUST use PATCH and MUST ignore any field that isn't
specified.
Example
PATCH /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947
Accept: application/json
Content-Type: application/json
{
"data": {
"attributes": {
"firstName": "Bobby"
},
"id": "62bed180-1f78-45d4-8a56-c996936a2947",
"type": "user"
}
}
Success responses
Upon success, APIs SHOULD return 200 but MAY return 202 or 204.
| Status | Description | Response |
|---|---|---|
200 OK | The resource was updated. | The latest representation of the resource. |
202 Accepted | The resource is scheduled to be updated. | The job that was scheduled to update the resource. |
204 No Content | The resource was updated. | Nothing. |
Optimistic concurrency
- APIs that update resources
SHOULDimplement optimistic concurrency to detect concurrent modifications. Those that doMUSTdo so using aversionfield markedrequiredand notreadOnly. - The
versionfieldSHOULDbe a simple integer that is incremented after every successful update but, regardless of what value is used, the fieldMUSTbe formatted as a string. - If the value provided does not match the stored version, the API
MUSTrespond409 Conflict. - Upon successfully updating the resource, the API
MUSTupdate the version identifier.
Deleting resources
- HTTP verb
MUSTbeDELETE. - API path
SHOULDbe like/api/v1/{resource plural}/{id}.
Example
DELETE /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947
Success responses
Upon success, APIs SHOULD return 204 but MAY return 202.
| Status | Description | Response |
|---|---|---|
202 Accepted | The resource is scheduled to be deleted. | The job that was scheduled to delete the resource. |
204 No Content | The resource was deleted. | Nothing. |
Soft deletes
Services MAY support "soft deletes" for any number of reasons. Services that do, SHOULD follow
the following guidelines:
- Delete APIs perform a soft delete by default and read-many APIs exclude soft-deletes by default.
- If callers can request that a delete be "hard", callers pass
permanent=truequery parameter. - If callers can request that soft-deletes be included in responses, callers pass
includeDeleted=truequery parameter. - Soft-deleted resources
SHOULDcarry adeletedAtfield.
Examples
DELETE /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947?permanent=true
GET /api/v1/users?includeDeleted=true
Acting upon resources
APIs that don't fit cleanly into the CRUD paradigm can typically be modeled as actions.
- HTTP verb
MUSTbePOST. - API path
SHOULDbe like/api/v1/{resource plural}/{id}/actions/{action}.
POST /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947/actions/send-email
Accept: application/json
Content-Type: application/json
{
"subject": "Hello",
"body": "World!"
}
Unlike the CRUD APIs, actions impose no restrictions on the shape of the JSON posted.
Success responses
Upon success, APIs SHOULD return 204 but MAY return 200 or 202.
| Status | Description | Response |
|---|---|---|
200 OK | The action was successful. | The latest representation of the resource. |
202 Accepted | The action is scheduled. | The job that was scheduled to execute the action. |
204 No Content | The action was successful. | Nothing. |
Filtering
- Read-many APIs that allow callers to specify search criteria
MUSTdo so via one or morefilter[{name}]query parameters where thenameSHOULDbe the same as one of the resource attributes. - APIs
MAYsupport wildcard matching. Those that doMUSTsupport*for "starts with", "ends with", and "contains". - APIs that support filtering by an attribute of an attribute
MAYuse dot-notation (e.g.address.city).
Multi-value parameters
APIs that support specifying multiple values for a query parameter MUST do so via a
comma-delimited list. This allows APIs that started with single-value query parameters to evolve to
supporting multiple-values without changing the external-facing contract. When multiple values are
provided, "or" query semantics MUST be applied.
Ranges
Query parameters that allow the caller to specify a range of values should do so using two values,
separated by commas, and using [ and ] to represent inclusive begin and end, and ( and )
to represent exclusive begin and end. An asterisk * may be used to represent no limit.
Ranges are also how "less than", "greater than", "less than or equal to", and "greater than or equal to" are implemented.
Examples
| Example | Description |
|---|---|
filter[lastName]=Smith | Users whose last name is 'Smith'. |
filter[lastName]=S* | Users whose last name begins with 'S'. |
filter[lastName]=*th | Users whose last name ends with 'th'. |
filter[lastName]=*mi* | Users whose last name contains 'mi'. |
filter[lastName]=Smith,Jones | Users whose last name is 'Smith' or 'Jones'. |
filter[failedLoginCount]=[3,*) | Users who have 3 or more failed login attempts. |
filter[birthDate]=(*,2000-01-01T00:00:00Z) | Users born before the year 2000. |
filter[birthDate]=[2000-01-01T00:00:00Z,2001-01-01T00:00:00Z) | Users born in the year 2000. |
See also Advanced Queries.
Paging
APIs SHOULD support paging and those that do MUST support either offset/limit style paging
or cursor style paging.
Offset/limit style paging
APIs that implement offset/limit style paging do so via the following query parameters:
| Parameter | Description |
|---|---|
page[number] | The page number to return, one-based. |
page[size] | The number of resources to return. |
Paging metadata MUST be populated within the meta field of responses as follows:
| Field | Description |
|---|---|
pageCount | The total number of pages. |
pageNumber | The current page number. |
pageSize | The current page size. |
totalCount | The total number of resources matching the request. |
Example
GET /api/v1/users?page[number]=1&page[size]=2
Accept: application/json
{
"data": [
{
"attributes": {
"firstName": "Bob",
"lastName": "Smith"
},
"id": "62bed180-1f78-45d4-8a56-c996936a2947",
"type": "user"
},
{
"attributes": {
"firstName": "Alice",
"lastName": "Jones"
},
"id": "cacba8c1-29fa-4018-8950-acd400ec76b7",
"type": "user"
}
],
"meta": {
"pageCount": 5,
"pageNumber": 1,
"pageSize": 2,
"totalCount": 9
}
}
Cursor-style paging
APIs that implement cursor-style paging do so via the following query parameters:
| Parameter | Description |
|---|---|
page[size] | The number of resources to return. |
page[after] | An opaque cursor. Returns the resources that follow that position. |
A cursor MUST be treated as opaque. Callers MUST NOT construct, parse, or modify one, and a
service MAY change its encoding at any time without a new API version.
Paging metadata MUST be populated within the meta field of responses as follows:
| Field | Description |
|---|---|
pageSize | The current page size. |
hasMore | Whether more resources follow this page. |
cursor | The cursor to pass as page[after] to retrieve the next page. |
Example
GET /api/v1/users?page[size]=2&page[after]=dXNlcnM6NjJiZWQxODA
Accept: application/json
{
"data": [
{
"attributes": {
"firstName": "Bob",
"lastName": "Smith"
},
"id": "62bed180-1f78-45d4-8a56-c996936a2947",
"type": "user"
},
{
"attributes": {
"firstName": "Alice",
"lastName": "Jones"
},
"id": "cacba8c1-29fa-4018-8950-acd400ec76b7",
"type": "user"
}
],
"meta": {
"cursor": "dXNlcnM6Y2FjYmE4YzEtMjlmYQ",
"hasMore": true,
"pageSize": 2
}
}
The final page MUST return hasMore as false and MUST omit cursor.
Sorting
APIs that support sorting MUST do so using a single query parameter named sort whose value is a
comma-delimited list of field names to sort by. Any field preceded by a hyphen MUST be interpreted
as descending.
Examples
| Example | Description |
|---|---|
sort=lastName | Sort by the user's last name ascending. |
sort=-lastName | Sort by the user's last name descending. |
sort=lastName,firstName | Sort by the user's last name and then by first name. |
Sparse fieldsets
APIs MAY allow callers to specify which fields to return. Those that do MUST accept a fields
query parameter whose value is a comma-delimited list of the fields to include. id and type are
always returned.
Example
GET /api/v1/users?fields=firstName,lastName
Advanced queries
The filter query parameters cover simple predicates, but they cannot express arbitrary AND and
OR combinations, and a long expression will exceed the practical limit on URL length. APIs that
need richer queries MUST expose a search operation that carries the expression in the request
body.
- HTTP verb
MUSTbePOST. - API path
MUSTbe like/api/v1/{resource plural}:search.
The expression is written in JSON Logic and placed in a top-level filter
field. JSON Logic is adopted for its notation only. Its full operator set includes control flow,
iteration, and arithmetic, none of which belong in a query, so only the following operators are
supported:
| Operator | Meaning |
|---|---|
var | Reference a field of the resource. |
==, != | Equal, not equal. |
>, >=, <, <= | Numeric and date comparison. |
in | Membership in a list, or a substring match. |
and, or | Boolean composition, each taking two or more terms. |
! | Negation. |
A request that uses any other operator, or references a field that does not exist, MUST be
rejected with 422.
Example
Find active or invited users whose KDF iterations are below the current minimum:
POST /api/v1/users:search?page[size]=50&sort=-createdAt
Accept: application/json
Content-Type: application/json
{
"filter": {
"and": [
{ "<": [{ "var": "kdfIterations" }, 600000] },
{ "in": [{ "var": "status" }, ["ACTIVE", "INVITED"]] }
]
}
}
The response MUST be identical in shape to the equivalent read-many request.
Success responses
Upon success, APIs MUST return 200.
| Status | Description | Response |
|---|---|---|
200 OK | The search was successful. | The matching resources, paged and sorted. |
Bulk updates
Bulk updates apply the same change to every resource matching a filter.
- HTTP verb
MUSTbePOST. - API path
MUSTbe like/api/v1/{resource plural}:bulk-update. filterMUSTbe present and use the advanced query expression language.- An
updatefield contains the fields being changed.
Example
POST /api/v1/users:bulk-update
Accept: application/json
Content-Type: application/json
{
"filter": {
"in": [{ "var": "status" }, ["INVITED", "PENDING"]]
},
"update": {
"status": "DISABLED"
}
}
Success responses
Upon success, APIs SHOULD return 202 but MAY return 200.
| Status | Description | Response |
|---|---|---|
200 OK | The resources were updated. | meta.affectedCount, the number of resources updated. |
202 Accepted | The resources are scheduled to be updated. | The job that was scheduled to update the resources. |
Bulk deletes
Bulk deletes delete every resource matching a filter.
- HTTP verb
MUSTbePOST. - API path
MUSTbe like/api/v1/{resource plural}:bulk-delete. filterMUSTbe present and use the advanced query expression language.
Example
POST /api/v1/users:bulk-delete
Accept: application/json
Content-Type: application/json
{
"filter": {
"and": [
{ "==": [{ "var": "status" }, "INVITED"] },
{ "<": [{ "var": "invitedAt" }, "2026-06-01T00:00:00Z"] }
]
}
}
Success responses
Upon success, APIs SHOULD return 202 but MAY return 200.
| Status | Description | Response |
|---|---|---|
200 OK | The resources were deleted. | meta.affectedCount, the number of resources deleted. |
202 Accepted | The resources are scheduled to be deleted. | The job that was scheduled to delete the resources. |
Request validation
Requests MUST be exhaustively validated before attempting to process the request and SHOULD
return all errors at once (not stop after the first error is encountered). Exhaustive validation
includes the following:
- Every required field, parameter, and header is present, not set to null, and not an empty string.
- The data type of every field, parameter, and header is the correct data type. In the case of parameters and headers, since those always originate as strings, it also means "can be converted to the declared type".
- Field names specified by
sortorfieldsparameters are valid field names. - Values provided for constrained fields are among the enumerated values.
- Values provided for fields that restrict the minimum value, the maximum value, and/or the maximum length are within those constraints.
- Identifiers that refer to other resources, whether managed by the service or some other service, are valid.
With some enhancements, developers should get all of the above "for free" from the
Bitwarden.Server.Sdkwhich will guarantee that no request reaches developer code that doesn't conform to the declared request model.
Errors
Errors MUST be returned as an array of error objects within a top-level errors field. Each error
populates the following fields, except that at most one source member applies to any one error:
| Field | Description | Type |
|---|---|---|
code | A stable, machine-readable error code. | string |
detail | A human-readable explanation specific to this occurrence. May be localized. | string |
id | A unique identifier for this particular occurrence of the problem. | string |
source | A reference to the primary source of the error. | object |
source.header | The name of the request header that caused the error. | string |
source.parameter | The query parameter that caused the error. | string |
source.pointer | A JSON Pointer to the field in error (e.g. /data/attributes/title). | string |
status | The HTTP status code applicable to this problem. | string |
title | A short, human-readable summary of the problem that doesn't change from occurrence to occurrence. | string |
Example
{
"errors": [
{
"id": "9f3c1e2a-7d40-4c8b-9b17-2f5a1c6e83d1",
"status": "422",
"code": "resource-not-found",
"title": "Referenced resource does not exist",
"detail": "'e3d2eb3e-755c-41cd-86f0-0e0649043ef6' is not a group in this organization.",
"source": {
"pointer": "/data/attributes/groups/0"
}
}
]
}
A
500responseMUST NOTinclude any detail about the failure.titleanddetailMUSTbe generic, andsourceMUSTbe omitted. Exception messages, stack traces, type names, connection strings, and dependency identities are all disclosure risks and belong in traces and logs, which are not reachable by the caller. Theidfield is how a caller and an operator correlate a report with the logged detail.
Jobs
APIs that accept work to be completed asynchronously return 202 along with a job — a resource
representing the accepted work and its progress. A formal standard for jobs is forthcoming.
Deprecation
Deprecation announces that an API should no longer be called, tells callers what they should be calling, instead, and, ideally, how much time they have to migrate.
- Every deprecated API
MUSTbe markeddeprecated: truein the service's OpenAPI spec and thedescriptionfieldMUSTname the replacement. - Every response from a deprecated API
MUSTinclude theDeprecationandSunsetheaders andSHOULDinclude aLinkheader naming the replacement.
Learn more about the Deprecation and Sunset headers.
Example
Deprecation: @1688169599
Sunset: Sun, 30 Jun 2024 23:59:59 GMT
Link: </api/v2/groups>; rel="successor-version"
| Header | Description |
|---|---|
Deprecation | When the operation became deprecated, as an HTTP structured field date — @ followed by seconds since the Unix epoch. |
Sunset | When it will stop working, as an HTTP date. It MUST be a real date, and it MUST NOT pass without either removal or a published extension. |
Link | Points at the replacement. |
Divergences from JSON:API
- Request and responses use the
application/jsonmedia type, notapplication/vnd.api+json. - We do not support compound documents or
the related
includequery parameter. - Paged responses are not required to include
links. - For sparse fieldsets, callers specify the fields to include via a
fieldsquery parameter. We do not support thefields[{type}]form.
Frequently asked questions
Why JSON:API?
We believe an established standard, with thoughtful answers to every API question, will be more robust than any standard we might invent ourselves. It is widely adopted among some of the largest SaaS vendors in the industry including Datadog.
We adopt the standard selectively, however, to get most of the benefits of JSON:API without the burden of full conformance.
Why is there an envelope? Can't the API just return the object?
- The envelope gives pagination, counts, and other response metadata somewhere to live that is not mixed in with the resource itself.
- It is a serialization concern, not a programming model concern. Handlers take and return plain
model classes; how those serialize is the framework's business. No application code should ever
see
dataorattributes.
Why are internal APIs versioned? Public APIs aren't.
We feel strongly that internal APIs should be formally versioned. Without formal versioning, every change must be additive, which means the shape can never change and every new field is optional. Contracts constrained like that get weaker over time, and what we want to express eventually cannot be expressed, because we have committed ourselves to "additive changes only". This rules out adopting the existing public API conventions, which are expressly unversioned.
Why do I have to specify the ID in the path and the JSON body?
- So that the JSON is comprehensive and self-describing.
- Establishing just one rule to "always include it" is easier to remember than multiple rules for when it is required and when it is not required.
Why do I have to specify the type? You can infer that from the URL path.
- So that the JSON is comprehensive and self-describing.
- Establishing just one rule to "always include it" is easier to remember than multiple rules for when it is required and when it is not required.
Why should empty strings be treated like
null?
A system where "" and null mean two different things requires every caller, every service, and
every database column to agree on the distinction. This can be a challenge to preserve as data gets
serialized to and from various representations across various languages and, in our experience, this
is one class of bug that can be entirely eliminated by just treating them the same.
Why shouldn't we support partial updates?
PUTwith completely-replace semantics is simpler to implement and far more common.- UI teams tend to prefer read-modify-write semantics which works well with
PUT. - Services that support
PATCHfrequently have to also supportPUTwhich creates two endpoints to do the same thing. - It is hard to tell the difference between "null this field out" vs. "do not update this field".
To be clear, teams can support partial updates. We just think you will be better served by just sticking to simple updates.
How do I support just adding or removing an element from a collection?
You MAY implement the JSON Patch specification, which
was designed for exactly this. However, we think an action works just as
well and is simpler:
POST /api/v1/groups/62bed180-1f78-45d4-8a56-c996936a2947/actions/add-collection
Why are we standardizing on JSON Logic instead of OData?
- OData's
$filteris a string expression language, so both building and parsing it require string handling where JSON is already right there. - Composing a filter by concatenating strings means quoting and escaping, which is how injection bugs happen. Composing a JSON object does not.
- OData brings a great deal more than filtering — metadata documents,
$expand, batch, its own conventions — and we only want the filtering.