These are the guidelines we use at Monite to design and develop our APIs. We aim to apply the same set of rules both to our public and internal APIs to make it easier for us to achieve consistency and make high-quality APIs from the get go. However, we can sometimes apply certain rules differently to our internal APIs, if our technology or security considerations require us to do so.
- Summary
- Requirement Level Keywords
- Quick Reference
- Section 1: General
- Section 2: Language
- Section 3: Security
- Section 4: Data Types and Formats
- Section 5: URIs
- Section 6: REST & Resources
- Section 7: JSON Payload
- Section 8: HTTP Requests
- Section 9: HTTP Responses
- Section 10: HTTP Headers
- Section 11: Webhooks
- Section 12: Hypermedia
- Section 13: Performance
- Section 14: Pagination
- Section 15: Compatibility & Versioning
- Section 16: Deprecation
- REST, but not always HATEOAS
- Security is super important
- American English
- Mostly snake_case
- API First, based on OpenAPI
| Element | Convention | Example |
|---|---|---|
| URIs | lowercase, snake_case | /v1/customer_orders |
| Field names | snake_case | customer_name |
| HTTP headers | kebab-case | x-monite-entity-id |
| Resource IDs | opaque strings | e675f59e-ddd1-4835-8bc2-edd76c54fad4 |
| Method | Purpose | Status Codes |
|---|---|---|
| GET | Retrieve resources | 200, 400, 401, 403, 404, 405, 422, 500 |
| POST | Create resources or actions | 200, 201, 202, 400, 401, 403, 404, 405, 409, 422, 500 |
| PATCH | Partial updates | 200, 400, 401, 403, 404, 405, 409, 422, 500 |
| PUT | Replace resources | 200, 400, 401, 403, 404, 405, 422, 500 |
| DELETE | Delete resources | 204, 400, 401, 403, 404, 405, 422, 500 |
| Type | OpenAPI Format | Example |
|---|---|---|
| Money | object with amount (int64) and currency |
{"amount": 1000, "currency": "EUR"} |
| Address | object with standard fields |
{"line1": "123 Main St", "city": "Berlin"} |
| Date/Time | string with date-time format |
"2022-07-17T08:26:40.252Z" |
| UUID | string with uuid format |
"279fc665-d04d-4dba-bcad-17c865489dfa" |
The requirement level keywords "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", "MAY", and "OPTIONAL" used in this document should be interpreted as described in RFC 2119.
- MUST and MUST NOT mean that it is a critical rule that we must always follow.
- SHOULD and SHOULD NOT mean that there are exceptions to the rule, but we should be very careful when applying those exceptions. Sometimes we start with these keywords and eventually change them to MUST and MUST NOT.
When working on a new product on our platform, or expanding an existing product, we always start with an API. In practice, this means the following:
- We build an API before building the corresponding UI.
- We design future APIs extensively (API-Design-First) by including all relevant stakeholders in the discussion and getting as much feedback as possible, before starting implementing the API.
- We strive to make every API externalizable from the very beginning, to make it easier to publish it in the future, if needed.
Why
The recent experience from successful tech companies prove that following the API-First principle can significantly improve the quality of API design decisions, hence saving development time and reducing integration friction during the API lifecycle.
Q&A
Q: Is API-First contradicting modern agile development practices and introducing waterfall processes?
A: No. API-First, when implemented properly, implies a lot of iterations, evolution of API prototypes, and building common understanding of the API design through collaboration and early feedback from multiple parties.
See also
Our public APIs must follow the REST architectural style. This means implementing REST principles (constraints): Client–server architecture, Statelessness, Cacheability, Layered system, Code on demand, and Uniform interface.
Note: Although REST is extremely widespread, there is no official standard that defines exactly every part of the REST architectural style. We want to follow the most common and most logical best practices that already exist around REST, while also staying pragmatic in our choices. This style guide is our attempt to formalize how we see REST and how we implement it with our APIs.
Why
REST is the most popular architectural style at the moment (2022 State of the API | Postman). This means that this architectural style is very well-known by the majority of developers in the world; there are already a lot of tools, frameworks, libraries and best practices around REST – and therefore it provides the flattest learning curve and best developer experience for most of the developers who will be integrating with Monite.
Q&A
Q: What about other styles (SOAP, GraphQL, gRPC)?
A: We will never support SOAP, but will consider introducing more modern styles/frameworks in the future, if this is needed for our API consumers and there is a clear benefit for it.
Q: What about Level 3 REST and HATEOAS?
A: At the moment, we are not planning to support HATEOAS in all parts of our API. However, we aim to introduce HATEOAS where it makes sense.
In general, our APIs must expose only what is really necessary for our API clients. This is an important best practice, which allows us to keep the minimal API surface – to document, maintain, monitor, and protect – depending on the real use cases of our API clients.
This practice is usually referred to as the YAGNI principle and is often extended by Postel's law (Robustness principle):
Be conservative in what you send, be liberal in what you accept.
In practice, this means that our APIs should not expose resources, parameters, actions, headers, data unless it's really clear why and how they will be used.
Note: However, we cannot expect that our API clients will follow the same principle. So, we must build our API in a way that is tolerant to accepting something that is not part of the API contract.
Similar to the abstraction principle in OOP, API abstraction allows API providers to achieve several goals:
- The reason for API consumers to use our APIs lies in the fact that they want to rely on our expertise in a specific field and use our services instead of building themselves. Therefore, our APIs must be easy to understand and integrate for API clients who don't know and should not know all the internal details of how the API platforms works under the hood.
- Exposing internal details can give extra information to potential attackers, since this is not only increasing the API attack surface, but also gives invaluable details on how the system is built and works internally.
- Decoupling internal implementation from public APIs is crucial for us to have full control on this implementation and easily evolve it, if necessary, without breaking the public API contract. On the contrary, if API internals are exposed to API clients who start using these APIs for some reason, it would be much more difficult to migrate these clients from the API parts that were not intended for the public use.
Spectral rule: monite-general-exposing-internals
We use the U.S. English (or American English) for all the parts of our APIs (like API URIs, field names, parameter names, header names, etc.).
To decide whether a certain term belongs to the U.S. English or not, we consult with Wikipedia and most renowned English dictionaries.
Why
The U.S. version of English is now being commonly used in modern software products and programming frameworks. On the contrary, British English definitely has a much smaller scope in Tech. As for using languages other than English, we don't want to use them in our API names because this might cause a lot of confusion and other problems with API adoption.
Spectral rules:
We should strive to create our API that can be consumed by a wide variety of audiences, with no or very limited expertise in our API's business domain.
For this, we should try to use the terms that are easier to follow, easier to understand, and more common in different parts of the world.
| ❌ Not recommended | 👍 Recommended |
|---|---|
card.pan |
card.number |
Spectral rule: monite-language-avoid-jargon
The language used in our APIs must reflect the modern understanding of inclusive, gender-neutral and bias-free communication. We should constantly educate ourselves on the topics of diversity, equity and inclusion, and make sure our APIs represent these values.
For more information on this, read Bias-free communications and Avoid unnecessarily gendered language.
| ❌ Not recommended | 👍 Recommended | Comment |
|---|---|---|
blackList, whiteList |
blockList, allowList |
|
master/slave |
primary/replica (in case of identical instances) or primary/secondary (for other use cases) |
|
master |
master -> main (for example, see GitHub) |
|
person.gender = {male, female} |
shopper.gender = {male, female, unknown, unspecified} |
Provide more options, and also critically assess if collecting gender information is even needed in this case. |
Spectral rule: monite-language-non-inclusive
When naming fields or other elements of an API, avoid using unnecessary filler words like "code", "details", or "info". Usually the same name works well without additional filler words, which are redundant in most of the cases.
| ❌ Not recommended | 👍 Recommended |
|---|---|
company_info |
company |
address_details |
address |
country_code |
country |
Spectral rule: monite-language-filler-words
Read first:
HTTP is not secure and its scope must be very limited. For our APIs we must always use encrypted connections, and unencrypted API calls must be rejected.
Spectral rule: monite-security-https-only
All API endpoints must be protected behind authentication to avoid broken authentication issues.
The only exception to this requirement is the OAuth 2.0 service, which exposes endpoints like /auth/token and /auth/revoke that by design might be accessible without any authentication.
Use standard authentication instead (e.g., JWT, OAuth).
Spectral rule: monite-security-no-http-basic
If you have any sensitive data in a URL, there is a high chance that this data might be intercepted/recorded/modified by a malicious actor. URLs can be exposed in many ways, like browsers, emails, UI and so on. Even if they are not displayed in a web browser and used only for backend-to-backend interaction in an encrypted HTTPs connection, such URLs can still appear in server logs and other places.
For sensitive data like credentials, passwords, security tokens, API keys and similar:
- use the standard Authorization header.
For sensitive data like PCI or PII:
- use request/response body.
Spectral rule: monite-security-no-secrets-in-path-or-query-parameters
To achieve high consistency between different parts of our API and improve its interoperability, we want to limit the variety of data types we use for API elements (request and response fields, parameters and HTTP headers) and use one of the allowed data types.
We achieve this by mostly using data types and formats commonly adopted by other industry specifications, such as JSON Schema, OpenAPI, and various ISO and IETF standards.
| Type | OpenAPI type | OpenAPI format | Description | Example |
|---|---|---|---|---|
| Boolean | boolean |
One of the two Boolean values (true or false). | true | |
| Object | object |
A complex object consisting of one or several fields. | ||
| Array | array |
An array containing values of the same type. | ||
| Integer | integer |
int32 |
A 4-byte signed integer in the range -2,147,483,648 to 2,147,483,647 (inclusive). | 7721071004 |
| Long integer | integer |
int64 |
A 8-byte signed integer in the range -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 (inclusive). | 772107100456824 |
| Float number | number |
float |
A single precision decimal number (binary32 in IEEE 754-2008/ISO 60559:2011). | 3.1415927 |
| Double | number |
double |
A double precision decimal number (binary64 in IEEE 754-2008/ISO 60559:2011). | 3.141592653589793 |
| Decimal | string |
decimal |
An arbitrarily precise signed decimal number. | "3.141592653589793238462643383279" |
| String | string |
An arbitrary string of characters. | "Monite rocks!" | |
| Date & time | string |
date-time |
A timestamp following RFC 3339 (a subset of ISO 8601). | "2022-07-17T08:26:40.252Z" |
| Date | string |
date |
A date following RFC 3339 (a subset of ISO 8601). | "2022-07-17" |
| Time | string |
time |
Time value following RFC 3339 (a subset of ISO 8601). | "08:26:40.252Z" |
string |
email |
An email address following RFC 5322. | "someone@example.com" | |
| URI | string |
uri |
A web URI following RFC 3986. | "https://www.example.com" |
| UUID | string |
uuid |
A Universally Unique Identifier following RFC 4122. | "279fc665-d04d-4dba-bcad-17c865489dfa" |
| Base64 string | string |
base64 |
A string that contains Base64-encoded data following RFC 4648 Section 4. | "VGVzdA==" |
| Binary | string |
binary |
Arbitrary binary data, such as the contents of an image file. Typically used for file uploads and downloads. | |
| Regular expression | string |
regex |
A regular expression following ECMA 262. | "^[a-z0-9]+$" |
Note: If you want to use a data type that is not part of the table above, make a suggestion to this API Style Guide.
Spectral rules:
- monite-data-incorrect-integer-format
- monite-data-incorrect-number-format
- monite-data-incorrect-string-format
For some data types (related to localization and regionality), it's common to use enumerations with limited sets of predefined string values, based on corresponding ISO standards.
To easily find all API elements of such data types and treat these elements in the same way, we should use string as their type and corresponding format values from the table below.
| Type | OpenAPI type | OpenAPI format | Description | Example |
|---|---|---|---|---|
| Language | string |
lang |
A two-letter language code following ISO 639-1. | "en" |
| Country | string |
country |
A two-letter country code following ISO 3166-1 alpha-2. | "DE" |
| Currency | string |
currency |
A three-letter currency code following ISO 4217. | "EUR" |
👍 Recommended
country:
type: string
format: country
enum:
- AF
- AX
- AL
- DZ
- AS
- ...
description: Country of a customer.For all data types that support providing their format, we should specify this format in schema models. This will allow API consumers to better understand what values can be represented by our API elements and therefore build better validation logic on their side.
Once specified in schema models, this format should be propagated to OpenAPI files, technical documentation, server-side libraries, and other artifacts that improve developer experience of API consumers.
❌ Not recommended
website:
type: string
description: Customer website.👍 Recommended
website:
type: string
description: Customer website.
format: uriSpectral rules:
For unification purposes, we should use the same schema for all objects representing a postal address. This object should contain the following fields, and their presence should be either required or not depending on the context where this address is used in the API.
| Field Name | OpenAPI type | OpenAPI format | Description |
|---|---|---|---|
postal_code |
string |
Also referred to as a "ZIP code". | |
country |
string |
country |
Specified by a country code. |
state |
string |
Also referred to as a "province" or "county". | |
city |
string |
||
line1 |
string |
Combines a street address, house number, apartment number and any other suffixes of the address | |
line2 |
string |
Usually optional and being used only if the address is very long and doesn't fit into line1. |
For unification purposes, we should use the same schema for all objects representing money values. This object should contain the following fields, both are always required:
| Field Name | OpenAPI type | OpenAPI format | Description |
|---|---|---|---|
amount |
integer |
int64 |
Represented in "minor units" |
currency |
string |
currency |
"Minor units" depend on the currency value. |
Note: Minor units are specified according to ISO 4217 and can be found in this table.
Note: We strongly recommend against using string, float or double values for representing amounts, because of arising problems around precision and serialization/deserialization from JSON. Storing amount values as long integers is a common best practice, adopted by API payment providers like Adyen and Stripe.
See also
- [JSON can safely represent integers only in the [- 2^53+1, 2^53-1] range](https://datatracker.ietf.org/doc/html/rfc7159#section-6)
- [OpenAPI incompatible with I-JSON](OAI/OpenAPI-Specification#1517)
- [Google API: Type and format summary](https://developers.google.com/discovery/v1/type-format)
The forward-slash (/) character is used in the path portion of the URI to indicate a hierarchical relationship between resources, for example:
https://api.example.com/v1/invoiceshttps://api.example.com/v1/invoices/{id}https://api.example.com/v1/invoices/{id}/partshttps://api.example.com/v1/invoices/{id}/parts/{id}
Spectral rule: monite-uri-no-backslash
As the last character within a URI's path, a forward slash (/) adds no extra value and might cause confusion. So, it's better to drop it completely from the URI.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| https://api.example.com/v1/resources/ | https://api.example.com/v1/resources |
Spectral rule: OAS: path-keys-no-trailing-slash
Empty path segments could cause a lot of ambiguity, so we must not have them in a path.
Spectral rule: monite-uri-no-empty-path-segments
Always prefer lowercase letters in URI paths, for simplicity and consistency.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| HTTPS://API.EXAMPLE.COM/v1/resources | https://api.example.com/v1/resources |
| https://api.example.com/V1/Resources | https://api.example.com/v1/resources |
Spectral rule: monite-uri-no-uppercase
We want the "api" suffix to be part of the host name (e.g. https://api.sandbox.monite.com). This means that using "api" in a base path is redundant, and we MUST NOT do this.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| https://api.example.com/v1/api/resources | https://api.example.com/v1/resources |
| https://api.example.com/v1/payments-api/orders | https://api.example.com/v1/payments/orders |
Spectral rule: monite-uri-no-api-suffix
File extensions look bad and do not add any advantage. Removing them decreases the length of URIs as well. No reason to keep them.
Apart from the above reason, if you want to highlight the media type of API using file extension, then you should rely on the media type, as communicated through the Content-Type header, to determine how to process the body's content.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| https://api.example.com/v1/me/document.xml | https://api.example.com/v1/me/document |
Spectral rule: monite-uri-no-file-extensions
We restrict path segments and query parameter names to ASCII snake_case strings matching regex ^[a-z][a-z\_0-9]*$. The first character must be a lowercase letter and subsequent characters can be letters, underscores (_), and numbers.
We prefer snake_case over kebab-case because we use snake_case for resource and field names, and resource names can be exposed in segment paths, query parameters and request/response payloads. If we decide to use kebab-case for resources in a URI and keep snake_case in payloads, this will cause a lot of inconsistencies.
Spectral rules:
When designing a REST API, always start with identifying resources – the main notions (objects) around which an API client performs various actions. These actions can be either CRUD (typically represented with POST/GET/PATCH/DELETE HTTP methods), or some other (e.g. resulting in changing a resource's state).
The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g., a person), and so on. In other words, any concept that might be the target of an author's hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time.
A resource can be either a part of a resource collection (with other resources of the same type in the same collection) or a singleton resource (exactly one instance of the resource always exists within any given parent).
For example, customers is a collection of resources accessible via the /customers URI, where each individual resource can be accessed by its id via /customers/{id}.
A common example of a singleton resource can be a config object that always exists for a given project and is accessible via /projects/{id}/config.
Note: Singleton resources must not have an ID field, because there is always only one singleton resource for any parent resource.
Note: Singleton resources must not define the CREATE or DELETE standard methods. The singleton is implicitly created or deleted when its parent is created or deleted.
Note: Singleton resources should define the GET and PATCH methods.
Note: Singleton resource names are always singular.
A resource may contain sub-resources (either a collection or singleton).
For example, a customer resource can have an accounts collection of sub-resources, which is accessible via /customers/{customer_id}/accounts.
This way, a single account sub-resource can be accessed via /customers/{customer_id}/accounts/{account_id}.
To get access to a collection of resources or a singleton resource, an API client must navigate using path segments of API URIs.
For example, this is how one can retrieve a collection of invoice resources:
https://api.example.com/v1/invoices
This is how one can retrieve a collection of subresources:
https://api.example.com/v1/resources/{id}/subresources
REST URIs should refer to a resource that is a thing (noun) instead of referring to an action (verb). Actions are also possible, but only around a specific resource.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| https://api.example.com/v1/navigate | https://api.example.com/v1/directions |
| https://api.example.com/v1/similar | https://api.example.com/v1/similarities |
In some cases, we can use verbs in a URI to represent actions performed on a resource. This is mostly for actions that cannot be represented with standard HTTP methods (POST, GET, PATCH, PUT, DELETE) and, for example, result in an asynchronous change of a resource state.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| POST https://api.example.com/v1/archiveUser | POST https://api.example.com/v1/users/{id}/archive |
Do not use URIs to indicate a CRUD (Create, Read, Update, Delete) function. URIs should only be used to uniquely identify the resources and not any action upon them.
Use the corresponding HTTP methods instead.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| POST https://api.example.com/v1/createUser | POST https://api.example.com/v1/users |
| POST https://api.example.com/v1/getUser | GET https://api.example.com/v1/users/{id} |
| POST https://api.example.com/v1/updateUser | PATCH https://api.example.com/v1/users/{id} |
| POST https://api.example.com/v1/replaceUser | PUT https://api.example.com/v1/users/{id} |
| POST https://api.example.com/v1/deleterUser | DELETE https://api.example.com/v1/users/{id} |
Spectral rule: monite-rest-no-crud-in-uri-names
When naming a collection of resources, use the plural version of a noun. An exception is a singleton resource, which is always unique and only one in the entire API context.
| 👍 Recommended | Explanation |
|---|---|
| https://api.example.com/v1/invoices | There might be multiple invoices to be processed by this API. |
| https://api.example.com/v1/invoices/{id} | A single invoice can be retrieved from a collection by its ID. |
| https://api.example.com/v1/users | There might be multiple users to be processed by this API. |
| https://api.example.com/v1/me | Here, me is a singleton resource pointing to the API user. |
| https://api.example.com/v1/company | If there is only one company that can be accessed by an API user, it is also a singleton resource, because an API user cannot access any other company. |
| https://api.example.com/v1/company/settings | Although settings is plural, it's a singleton resource because there can be only one set of settings for a company (unless there is an API design that allows for multiple sets of different settings to be provided for a company. |
We don't want to have too many nested levels for API URLs, because it leads to unnecessary complexity in understanding the API, as well as might result in too long URLs not fitting the browser limitations.
Spectral rule: monite-rest-limited-resource-levels
For unification and consistency, every collection resource should have these fields:
id: a unique ID that allows API consumers to refer to this resource instance.created_at: a date-time value indicating when this resource instance was created.updated_at: a date-time value indicating when this resource instance was modified last time.
Note: These fields MUST NOT be exposed in singleton resources. The id field is not necessary because there is always only one instance for a singleton resource per its parent resource; while created_at and updated_at are always the same as its parent resource.
Since IDs should be used in API URIs (to refer to a specific resource instance), they must be URL-friendly.
A resource ID value should uniquely identify a specific resource instance within the scope of the entire API platform. No resources should have the same value as their ID, as it can cause a lot of confusion.
To make sure all resource IDs meet our requirements, we must always generate them ourselves and assign them to each specific resource instance upon resource creation.
Because all resource IDs must be generated by us, we don't allow API consumers to generate such IDs (even if they use exactly the same ID format as we do).
However, we understand that our API consumers might also need to store some IDs assigned to resource instances by their platform. In this case, we allow API consumers to store their own IDs in a partner_internal_id field.
API consumers must never build any business logic based on the ID format and must always treat resource IDs as random strings. To fulfill this requirement, resource IDs should look like opaque strings, even if there is some logic and format behind the ID generation algorithm.
Once we settle on the format of resource IDs, we should try to do our best to make sure these IDs always have the same length. The reason is that API consumers set a fixed column length in their code and databases to process and store such IDs, and changing the length (especially increasing the length) can have a drastic impact on their integration with our API platform.
If changing the length of resource IDs is inevitable, treat it as a breaking change and prepare your API consumers in advance, with additional communication and fuzzy testing.
Using sequential numbers for resource IDs is considered an awful development practice. The main reasons are:
- In this case, resource IDs are easily guessable. This makes it much easier for a malicious user to attempt to access resources that they shouldn't have to.
- The last generated resource ID gives any API client information of how many resources of a certain type exist on an API platform. This might expose some critical business information (like the total number of API clients, the total number of payment transactions, etc.), which in a normal situation should never be available for people outside an organization owning this API platform.
- Quite often these IDs directly correspond to the auto-incremented database indexes from a data table storing information about these resources. This gives even more information to a potential attacker in case they can get access to the internal systems.
One of the easiest ways to get an opaque string that is guaranteed to be unique and hence can be used as resource IDs is to generate UUID values.
An alternative (and more powerful) option is to generate so-called Snowflake IDs that follow some generation format, which is unknown to API consumers. Since API consumers don't know the exact format of such IDs, they still treat them as opaque and unique. However, this makes it possible for API producers to de-construct such IDs and decode some values from it. For example, in the case of distributed systems, such IDs can be used for smart routing and data storage decisions.
To make it faster to determine which resource type a specific resource ID is referring to, some API producers add predefined prefixes to each resource ID value. These prefixes can be 2, 3, or 4 characters long and uniquely correspond to a specific resource type on an API platform.
For example, all resource IDs for a payment resource can follow either the PA_* or PAY_* or PYMT_* format.
Using such IDs makes it much quicker to troubleshoot different cases and identify a situation when some IDs are being used in the wrong context.
Each resource ID uniquely identifies a specific resource instance. This means that once a resource ID has been generated and assigned to a specific resource instance, it becomes an inherent part of that resource.
Resource IDs MUST follow the "resource_id" format when being referred in payloads of other resources
For example, if an API has the following product resource:
{
"id" : "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"name" : "Tomato"
}The invoice resource should link to it by the product_id field:
{
"line_items" : [
{
"product_id" : "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"number" : 1200
}
]
}Every request and response payload must be a valid JSON object, representing structured resource data. This allows API consumers to easily parse, construct, and validate such payloads; and this allows us to safely expand such payloads with new keys in the future, if needed.
The JSON format is a well-known and established industry standard, defined in RFC 7159. We prefer to additionally apply restrictions of the RFC 7493 "Internet JSON" standard, which means in particular:
- a JSON payload cannot contain duplicate keys on the same level, each key must be unique.
- a JSON payload must use UTF-8 encoding and consist of valid Unicode strings.
❌ Not recommended
[
100,
120,
176
]❌ Not recommended
<prices>
<price>100</price>
<price>120</price>
<price>176</price>
</prices>👍 Recommended
{
"prices" : [
100,
120,
176
]
}Spectral rule: monite-json-root-json-objects
This enables better grouping and easier extensibility in the future.
❌ Not recommended
{
"account_payout_delay_days" 2,
"account_payout_interval" = "daily"
}👍 Recommended
{
"account" : {
...
"payout_schedule" : {
"delay_days" : 2,
"interval" : "daily"
}
}
}We restrict field names to ASCII snake_case strings matching regex ^[a-z][a-z\_0-9]*$. The first character must be a lowercase letter, and subsequent characters can be letters, underscores (_), and numbers.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| sales-order-id | sales_order_id |
| salesOrderId | sales_order_id |
| sales-order-ID | sales_order_id |
Spectral rule: monite-json-field-names-snake-case
The names of arrays must be pluralized to indicate that they contain multiple values.
❌ Not recommended
{
"price" : [
100,
120,
176
]
}👍 Recommended
{
"prices" : [
100,
120,
176
]
}To avoid confusion, empty arrays must be still represented as arrays, not as nulls.
❌ Not recommended
{
"prices" : null
}👍 Recommended
{
"prices" : []
}Using certain name conventions for most popular data types makes it easier for API consumers to understand what to expect from a field just by looking at its name.
For date-time properties we want to use the "_at" suffix, preceded by a verb in a present or past tense, which is quite common for many modern APIs.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| created | created_at |
| modification_date | updated_at |
| start_date | starts_at |
| expire_at | expires_at or expired_at |
| will_expire_at | expires_at |
In our REST APIs, an operation can use the following HTTP methods:
POST– to create new resources or perform an action on a resource.GET– to return a resource or collection of resources.PATCH– to (partially) update a resource.PUT– to replace a resource.DELETE– to delete a resource. Note: always evaluate if this method is needed for real use cases or not; and when it's really needed, consider using the "soft delete" technique.
For more specific guidance on how to use these HTTP methods, refer to the corresponding rule in this section.
When necessary, it is allowed to use other HTTP methods (for example, the OPTIONS method for pre-flight requests to support CORS).
Note: A successful POST request must always return the created resource in a response with HTTP status code 201 Created.
Example of a POST request:
curl -X POST https://api.example.com/v1/products \
-H 'Content-Type: application/json' \
-d '{
"name": "Potato",
"price": {
"currency": "EUR",
"value": 1000
}
}'Successful response (201 Created):
{
"name": "Potato",
"price": {
"currency": "EUR",
"value": 1000
},
"id": "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"created_at": "2022-05-02T15:13:29.901787+00:00",
"updated_at": "2022-05-02T15:13:29.901787+00:00"
}When using the POST call to create a resource, we should create only one resource at a time. To create subresources, a separate POST call should be made.
This is done so to avoid a situation when one of the resources is created successfully, while another is not. In this case, it's not clear if the response should be Success or Error. Most likely, the whole request should be treated as an atomic operation and hence should return the error code.
To avoid this ambiguity, we expect separate API calls for creating a resource and its subresources.
For example:
curl -X POST https://api.example.com/v1/resources \
-H 'Content-Type: application/json' \
-d '{
"name": "My resource"
}'201 Created response:
{
"id" : "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"name" : "My resource"
}Another request:
curl -X POST https://api.example.com/v1/resources/e675f59e-ddd1-4835-8bc2-edd76c54fad4/subresources \
-H 'Content-Type: application/json' \
-d '{
"name": "My sub-resource"
}'Another 201 Created response:
{
"id" : "d654f59e-dad1-4835-8bc2-edd76c54faf2",
"name" : "My sub-resource",
"parent_id" : "e675f59e-ddd1-4835-8bc2-edd76c54fad4"
}When an action to be performed with a resource doesn't belong to the variety of CRUD operations (and hence cannot be represented with standard HTTP methods), it is allowed to use a POST call to initiate this action. In such cases, an action is represented with a verb appended to a resource URI.
Example of a POST request performing resource verification with a /verify action:
curl -X POST https://api.example.com/v1/resources/e675f59e-ddd1-4835-8bc2-edd76c54fad4/verify \
-H 'Content-Type: application/json' \
-d '{
"verification_tier": "medium"
}'Successful 202 Accepted response:
{
"verification_status" : "scheduled"
}Note: A request body is not allowed for GET calls.
Note: All resources must be wrapped into a data array, for better extensibility of a response body (for example, to add pagination-related properties).
Note: When there might be a lot of resources returned by a GET call (more than 20), always consider adding pagination to return only chunks of the resource collection.
Example of a GET request:
curl https://api.example.com/v1/productsSuccessful 200 OK response:
{
"data": [
{
"name": "Potato",
"price": {
"currency": "EUR",
"value": 1000
},
"id": "3278430a-512e-4eca-967b-3dc59743d0bc",
"created_at": "2022-05-02T15:13:26.517562+00:00",
"updated_at": "2022-05-02T15:13:26.517572+00:00"
},
{
"name": "Tomato",
"price": {
"currency": "USD",
"value": 2000
},
"id": "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"created_at": "2022-05-02T15:13:29.901787+00:00",
"updated_at": "2022-05-02T15:13:29.901796+00:00"
}
]
}When the GET call should return no resources, a successful 200 OK response must still have a data array, empty in this case:
{
"data": []
}Spectral rules:
Note: A request body is not allowed for GET calls.
Note: A resource must be returned on the root level of a response and not be wrapped into any other objects.
Example of a GET request:
curl https://api.example.com/v1/products/3278430a-512e-4eca-967b-3dc59743d0bcSuccessful 200 OK response:
{
"name":"Potato",
"price":{
"currency":"EUR",
"value":1000
},
"id":"3278430a-512e-4eca-967b-3dc59743d0bc",
"created_at":"2022-05-02T15:13:26.517562+00:00",
"updated_at":"2022-05-02T15:13:26.517572+00:00"
}Spectral rules:
To filter a collection of returned resources against one or several criteria, use query parameters.
Example of a GET request:
curl https://api.example.com/v1/products?name=TomatoSuccessful 200 OK response:
{
"data": [
{
"name": "Tomato",
"price": {
"currency": "USD",
"value": 2000
},
"id": "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"created_at": "2022-05-02T15:13:29.901787+00:00",
"updated_at": "2022-05-02T15:13:29.901796+00:00"
}
]
}When it's necessary to filter by nested fields, use the dot notation for query parameter names:
curl https://api.example.com/v1/products?price.currency=EURSuccessful 200 OK response:
{
"data": [
{
"name": "Potato",
"price": {
"currency": "EUR",
"value": 1000
},
"id": "3278430a-512e-4eca-967b-3dc59743d0bc",
"created_at": "2022-05-02T15:13:26.517562+00:00",
"updated_at": "2022-05-02T15:13:26.517572+00:00"
}
]
}In some cases you might need to pass sensitive data along with your GET request. Since GET calls don't have a request body, make sure you never pass this data in a request URI.
Instead, consider passing this data in HTTP headers, which would be much more secure. When it's not possible for some reason, then you can change your GET call to POST and pass sensitive data in a request body.
❌ Not recommended
curl https://api.example.com/v1/resources?sensitive_data=sensitive_value👍 Recommended
curl -X POST https://api.example.com/v1/resources \
-H 'Content-Type: application/json' \
-d '{
"sensitive_data": "sensitive_value"
}'To update a resource, an API client should send only the fields that need to be changed. All the other fields should stay intact.
Note: A successful PATCH request must always return the updated resource in a response.
Note: The entire PATCH operation is atomic. This means that if some fields cannot be set to the specified values, the entire PATCH request should be rejected with a validation error.
Example of a PATCH request:
curl -X PATCH https://api.example.com/v1/products/e675f59e-ddd1-4835-8bc2-edd76c54fad4 \
-H 'Content-Type: application/json' \
-d '{
"name": "New potato"
}'Successful response (200 OK) with changed name and updated_at fields:
{
"name": "New potato",
"description": "This is a potato",
"price": {
"currency": "EUR",
"value": 1000
},
"id": "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"created_at": "2022-05-02T15:13:29.901787+00:00",
"updated_at": "2022-07-02T15:13:29.901787+00:00"
}Also, for nullable fields it should be possible to set them back to null. For example:
curl -X PATCH https://api.example.com/v1/products/e675f59e-ddd1-4835-8bc2-edd76c54fad4 \
-H 'Content-Type: application/json' \
-d '{
"description": null
}'Successful response (200 OK) with changed description and updated_at fields:
{
"name": "New potato",
"description": null,
"price": {
"currency": "EUR",
"value": 1000
},
"id": "e675f59e-ddd1-4835-8bc2-edd76c54fad4",
"created_at": "2022-05-02T15:13:29.901787+00:00",
"updated_at": "2022-07-02T15:13:29.901787+00:00"
}In general, we should prefer using PATCH for changing a resource. However, in some cases it might be more convenient to use PUT to update the entire resource in one API call (for example, when uploading a config file from a file storage).
Note: A successful PUT request must always return the updated resource in a response.
Example of a PUT request:
curl -X PUT https://api.example.com/v1/company/config \
-H 'Content-Type: application/json' \
-d '{
"option1": "value1",
"option2": "value2",
"option3": "value3",
"option4": "value4"
}'Successful 200 OK response:
{
"option1": "value1",
"option2": "value2",
"option3": "value3",
"option4": "value4"
}When deleting a resource, always consider using "soft delete" to mark a resource as deleted in our database but not actually deleting it. However, even in the case of soft delete, an API client should never see the difference with a regular delete operation and should treat a deleted resource as gone.
Note: A request body is not allowed for DELETE calls.
Example of a DELETE request:
curl -X DELETE https://api.example.com/v1/products/3278430a-512e-4eca-967b-3dc59743d0bcA successful DELETE response must return a 204 No Content HTTP status code and provide no response body.
A failed DELETE response must return a 404 Not Found HTTP status code, no matter if the specified resource ID is actually not found or if it is found but marked as deleted.
Note: After deleting a resource (even in case of a "soft delete"), this resource instance must never be accessible again. This means, for example, that the GET /v1/resources/{id} call to this resource must always return 404 Not Found after resource deletion.
Spectral rules:
Mass deletion of resources can have a drastic impact on the data safety of API integrations, intentionally or unintentionally. We find these risks to be too high and decided to avoid introducing a DELETE operation for resource collections.
❌ Not recommended
curl -X DELETE https://api.example.com/v1/resourcesOur APIs must use only HTTP status codes that are defined by RFC 9110.
Creating custom status codes is not allowed.
When using standard HTTP status codes, we must return them to identify the use cases according to the RFC 9110 standard.
For example, in case of a resource not found, we must return 404 (Not Found) and not something different.
This way we can ensure that our API behavior is predictable and consistent for API clients.
To minimize the amount of HTTP status codes that our clients need to process, we should stick to a limited subset of codes that make sense to use for our API.
Currently, this list is the following:
- '200' (OK)
- '201' (Created)
- '202' (Accepted)
- '204' (No Content)
- '400' (Bad Request)
- '401' (Unauthorized)
- '403' (Forbidden)
- '404' (Not Found)
- '405' (Method Not Allowed)
- '406' (Not Acceptable)
- '409' (Conflict)
- '422' (Unprocessable Content)
- '500' (Internal Server Error)
For GET responses, only the following codes are allowed:
| Code | Comment | GET /resources |
GET /resources/{id} |
|---|---|---|---|
| 200 | To return a resource or a list of resources | ✅ | ✅ |
| 400 | To indicate an error with parsing a request | ✅ | ✅ |
| 401 | To respond to unauthorized requests | ✅ | ✅ |
| 403 | To indicate that accessing a resource is forbidden | ✅ | ✅ |
| 404 | To indicate that an individual resource is not found | ✅ | |
| 405 | To indicate that the requested method is not allowed | ✅ | ✅ |
| 422 | To indicate that submitted values cannot be processed | ✅ | ✅ |
| 500 | To inform about an internal error on a platform side | ✅ | ✅ |
For POST responses, only the following codes are allowed:
| Code | Comment | POST /resources |
POST /resources/{id}/action |
|---|---|---|---|
| 200 | To indicate that an action was successfully performed | ✅ | |
| 201 | To return a created resource | ✅ | |
| 202 | To indicate that the request was accepted and will be performed asynchronously | ✅ | ✅ |
| 400 | To indicate an error with parsing a request | ✅ | ✅ |
| 401 | To respond to unauthorized requests | ✅ | ✅ |
| 403 | To indicate that accessing a resource is forbidden | ✅ | ✅ |
| 404 | To indicate that an individual resource is not found | ✅ | |
| 405 | To indicate that the requested method is not allowed | ✅ | ✅ |
| 409 | To indicate that the requested action is conflicting with the current state of the resource | ✅ | |
| 422 | To indicate that submitted values cannot be processed | ✅ | ✅ |
| 500 | To inform about an internal error on a platform side | ✅ | ✅ |
For PATCH responses, only the following codes are allowed:
| Code | Comment |
|---|---|
| 200 | To return a resource |
| 400 | To indicate an error with parsing a request |
| 401 | To respond to unauthorized requests |
| 403 | To indicate that accessing a resource is forbidden |
| 404 | To indicate that an individual resource is not found |
| 405 | To indicate that the requested method is not allowed |
| 409 | To indicate that the requested update is conflicting with the current state of the resource |
| 422 | To indicate that submitted values cannot be processed |
| 500 | To inform about an internal error on a platform side |
For PUT responses, only the following codes are allowed:
| Code | Comment |
|---|---|
| 200 | To return a resource |
| 400 | To indicate an error with parsing a request |
| 401 | To respond to unauthorized requests |
| 403 | To indicate that accessing a resource is forbidden |
| 404 | To indicate that an individual resource is not found |
| 405 | To indicate that the requested method is not allowed |
| 422 | To indicate that submitted values cannot be processed |
| 500 | To inform about an internal error on a platform side |
For DELETE responses, only the following codes are allowed:
| Code | Comment |
|---|---|
| 204 | To indicate that resource deletion was successful |
| 400 | To indicate an error with parsing a request |
| 401 | To respond to unauthorized requests |
| 403 | To indicate that accessing a resource is forbidden |
| 404 | To indicate that an individual resource is not found |
| 405 | To indicate that the requested method is not allowed |
| 422 | To indicate that submitted values cannot be processed |
| 500 | To inform about an internal error on a platform side |
We restrict HTTP header names to ASCII kebab-case strings.
| ❌ Not recommended | 👍 Recommended |
|---|---|
| X_Monite_Entity_ID | x-monite-entity-id |
Spectral rule: monite-headers-kebab-case
Webhooks payloads can contain sensitive information, which must never be available to any party between a webhook sender and a webhook receiver.
For this reason, we must send webhooks to our API clients only via an encrypted TLS connection.
We should send webhooks only to endpoints that are secured with any of the modern authentication methods.
Links to other resources must always use full absolute URIs.
This makes it easier for API clients to resolve the URIs and retrieve relevant resources.