Request for comment
Welcome to Enturs API guidelines, designed to ensure a consistent and robust approach to developing RESTful APIs. REST (Representational State Transfer) is an architectural style that uses standard HTTP methods to create scalable, lightweight, and maintainable services.
- To establish common guidelines and best practices for API design
- To simplify integration between different systems and applications
- To contribute to a secure, efficient and user-friendly implementation of APIs
This guide is for developers, architects, and technical designers who work on the design, implementation, and maintenance of APIs. By following these guidelines, we ensure APIs are easy to understand, consistent in structure, and simple to integrate with other systems.
For details on how to contribute to these guidelines, see the contributing guide.
Throughout this document, these requirement levels are used:
- MUST: This is an absolute requirement
- SHOULD: There may be valid reasons to ignore this requirement, but implications must be understood and carefully weighed
- MAY: This is optional
Throughout this document, rules are marked with the following indicators:
- 👀 Consistency - Make sure the API is easy to understand and predictable
- ✅ HTTP Methods - API operations MUST use standard HTTP methods (GET, POST, PUT, PATCH, DELETE)
- ✅ Data Format - API endpoints SHOULD support the JSON data format, unless there is a good reason not to
- ✅ APIs MUST be documented using OpenAPI 3.x
- ✅ Documentation - All functionality SHOULD be documented with examples and descriptions
- ✅ Encryption: All communication MUST be over HTTPS
- ✅ You SHOULD not use localhost (or 127.0.0.1) host names in
info.servers. - ✅
info.titleMUST be non-empty and MUST not contain the word 'api'.
- 👀 We encourage to follow a Contract-First workflow:
- Create the API specification before implementing the API
- The specification is the primary reference for both development and documentation
- Update the specification throughout development to reflect changes
- 👀 Lint your API spec
- 👀 Separate API specifications per target audience/visibility (public, partner, internal). An API spec SHOULD only contain endpoints for one target audience. This audience is used in the Developer Portal to organize APIs.
- 👀 Differentiate APIs based on the target audience:
- Internal APIs - May have extended functionality and less stringent requirements, but MUST still be documented and tested
- External APIs - MUST be carefully documented with a focus on stability, security, and consistency
Use securitySchemes inside components to define security schemes. Then, refer to schemes using security either at the root level of the spec,to apply security to all operations, or under individual operations which are secured, which replaces the root config.
See:
- https://spec.openapis.org/oas/v3.1.2.html#security-scheme-object
- https://spec.openapis.org/oas/v3.1.2.html#security-requirement-object
- 👀 These endpoints are secured using JWT tokens. This MUST be documented using a
jwtscheme.
Example:
{
"security": [{ "jwt": [] }],
"components": {
"securitySchemes": {
"jwt": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
}
}
}More information on Authentication
- ☑️ Endpoints that require permissions MUST be documented with the
x-entur-permissionsextension.
Example:
{
...,
"paths": {
"/items": {
"get": {
"operationId": "getItems",
...,
"x-entur-permissions": {
"value": "items:les"
}
},
"post": {
"operationId": "createItem",
...,
"x-entur-permissions": {
"value": "items:opprett"
}
}
}
}
}where items:les means that you need the access les on the operation items.
You can also specify that an endpoint requires multiple permissions, for example:
{
"x-entur-permissions": {
"value": {
"all": [
"organisations:les",
{
"any": [
"items:opprett",
"items-global:opprett"
]
}
]
}
}
}This means that the endpoint requires organisations:les, as well as either items:opprett or items-global:opprett.
If you need to explain the required permissions in more detail, you can declare a description field at the root.
{
"x-entur-permissions": {
"description": "To call this endpoint you need access to read organisations, as well as creating items for your organisation.",
"value": {
"all": [
"organisations:les",
{
"any": [
"items:opprett",
"items-global:opprett"
]
}
]
}
}
}All OpenAPI specifications published to Enturs developer portal must declare a block x-entur-metadata in the info section of the specification.
| Field name | Type | Description |
|---|---|---|
| id | string |
REQUIRED. Unique id for this specification. Read more. |
| audience | string |
REQUIRED. Who this specification is targeted to. Must be one of "open", "partner", "internal" |
| owner | string |
REQUIRED. The Entur team responsible for this specification. Read more. |
| parentId | string |
Id of the parent specification, used when merging. Read more. |
Example:
{
"info": {
"x-entur-metadata": {
"id": "items",
"owner": "team-api",
"audience": "partner"
}
}
}
- ✅ OpenAPI specifications SHOULD use the
idproperty in thex-entur-metadataextension in theinfosection.
Choosing an id
The id is used to uniquely identify a specification - each id results in an entry in the Developer Portal API catalogue, and an entry in the linting results.
Because of this, the id should not change over time. The current api title (in kebab-case) could be a good id - but, of course, you should not update the id if the title changes in the future.
The id should not have a -id suffix (or prefix).
- ✅ If the id is present, it MUST be in lower kebab-case and contain only dashes, digits and letters (a-z).
- ✅ OpenAPI specifications SHOULD use the
ownerproperty in thex-entur-metadataextension in theinfosection.
The owner field declares which team is responsible for maintaining a specification. It should follow the same format as the owner field in the platform orchestrator.
Sometimes it is useful to develop some API endpoints separately, but still document them externally as a single specification. To achieve this, you can use the x-entur-metadata field parentId. This field may contain a reference the x-entur-metadata.id field in another published specification.
Nesting is not supported. parentId must refer to a specification that does not itself declare a parentId.
If there are problems with the merging, for example conflicts between the schemas, none of the specifications will be exposed.
There are some limitations when merging specifications:
servers.urlin all specs must be a subpath of the declaredservers.urlin the "parent specification". The merged specification will use theserversdefinition from the parent specification, and the subpath of the other specifications will be added to the operation paths. Ifserversare not compatible, none of the specifications will be published.- Root level
securitymust be identical in all specs to be merged. - If there are multiple non-equal schemas with the same name, they will be namespaced with the
x-entur-metadata.idfield. So if multiple specs declare a schemaItemthat differ from each other, there will be one schemaItem_alphaand one schemaItem_beta.
Example
For example, say you have three microservices, `alpha`, `beta` and `gamma`:Microservice alpha publishes the specification:
{
"info": {
"x-entur-metadata": {
"id": "alpha"
}
},
...
}Microservice beta publishes the specification:
{
"info": {
"x-entur-metadata": {
"id": "beta",
"parentId": "alpha"
}
},
...
}Microservice gamma publishes the specification:
{
"info": {
"x-entur-metadata": {
"id": "gamma",
"parentId": "alpha"
}
},
...
}Here, only 1 specification will be shown on the Developer Portal, which is a combination of alpha, beta and gamma. Since alpha is the parent specification, the combined specification will use alpha as the base, so fields like info.title will be picked from there.
- 👀 You SHOULD use plural Nouns - For example:
/customers,/benefitsand/offers- Exception: Singleton resources that represent a unique entity MAY use singular form, such as
/user/profile,/me
- Exception: Singleton resources that represent a unique entity MAY use singular form, such as
- 👀 Hierarchical Structure - For related resources you SHOULD use hierarchical URL structure. For example:
/orders/{orderId}/fees - 👀 No Actions in URL - Actions (such as create, delete) SHOULD be handled via the HTTP method, not in the URL itself
- ✅ URL in kebab-case - URL for APIs MUST be in kebab-case. For example
/realtime-deviations/v1/subscription - ✅ Field Names in camelCase - For request and response body field names and query parameters, camelCase MUST be used
- 👀 Consistent Terminology - Similar concepts across different APIs SHOULD use consistent naming. For example, don't mix
/usersand/peopleor/proposalsand/offerswhen referring to the same concept across different APIs. - ✅ Server URL MUST be in lowercase
- ✅ Paths SHOULD NOT contain "api" in the name
- 👀 URL Based Versioning - You MUST include the version number in the URL
- 👀 Best Practices on Version Info - You SHOULD place the version number immediately after the domain, and before the resource path itself
- Example:
https://api.entur.io/sales/v1/orders?distributionChannelId=1
- Example:
API spec versioning is done automatically when publishing the spec.
The version is set based on the CalVer versioning convention, and uses the format YYYY.MM.MICRO. When the spec is
published, the current year and month is used, and MICRO is set to "00". If a version already exists for the given year and month, "01" is used (and so on).
If the published spec is equal to the current spec, no new version is created. Because versioning is done automatically, the value in info.version is ignored,
but must be set in order for linting to pass, therefore a placeholder value like 1.0.0 may be used.
- 👀 You MUST not remove or modify existing fields or endpoints
- 👀 You MUST introduce new versions for changes that break previous contracts
- 👀 You MUST clearly document which features are deprecated and provide guidance for migration
👀 Tags should be used as a logical grouping that reflects functional domains, use cases or data types, not internal architecture.
- Bad example:
internal,partner,production,route-service-v2,misc,other - Good example, when using tags for functional domains:
Journey Planning,Realtime Departures - Good example, when using tags for use-cases :
Search,Booking - Good example, when using tags for data types :
Routes,Departures
- ✅ Request body is only allowed for PUT, POST and PATCH
| Code | Description | GET | POST | PUT | PATCH | DELETE |
|---|---|---|---|---|---|---|
| 200 | OK | ✅ | ✅ | ✅ | ✅ | ✅ |
| 201 | Created | ✅ | ✅ | |||
| 202 | Accepted | ✅ | ||||
| 204 | No Content | ✅ | ✅ | ✅ | ✅ | |
| 302 | Found | ✅ | ||||
| 303 | See Other | ✅ | ||||
| 400 | Bad Request | ✅ | ✅ | ✅ | ✅ | ✅ |
| 401 | Unauthorized | ✅ | ✅ | ✅ | ✅ | ✅ |
| 403 | Forbidden | ✅ | ✅ | ✅ | ✅ | ✅ |
| 404 | Not Found | ✅ | ✅ | ✅ | ✅ | ✅ |
| 409 | Conflict | ✅ | ✅ | ✅ | ✅ | |
| 500 | Internal Server Error | ✅ | ✅ | ✅ | ✅ | ✅ |
| 503 | Service Unavailable | ✅ | ✅ | ✅ | ✅ | ✅ |
When an error occurs, the IETF standard for Problem Details for HTTP APIs (RFC 9457) SHOULD be followed, with few additional guidelines:
- ✅ Error responses MUST either use media type
application/problem+json, ORapplication/problem+xml(if Accept header isapplication/xml) - ✅ The fields
titleandstatusMUST be included - 👀 The
detailfield SHOULD be included when it provides additional useful information - 👀 The
typefield MAY be included. If included, it MUST be an absolute URI - 👀 The
instanceMAY be included. If included, it MUST be an absolute URI
Example:
HTTP/1.1 403 Forbidden Content-Type: application/problem+json Content-Language: en
{
"type": "https://example.com/request-endpoint",
"title": "Access forbidden",
"status": 403,
"detail": "You do not have permission to access this resource.",
"instance": "https://example.com/something/something",
"balance": 30, //Custom field
"recommended-action": "Something." //Custom field
}-
👀 You MAY allow clients to specify a preferred language via the
Accept-Languageheader or a query parameter. -
👀 You MAY return the
Content-Languageheader to inform clients of language used in response. -
☑️
Accept-LanguageandContent-Languagevalues MUST be valid IETF BCP 47 language tags. BCP 47 uses ISO 639-1 codes when available, and ISO 639-3 codes if no two-letter code exist. E.g. "en", not "eng". Also, as specified by BCP 47, clients may send multiple tags with quality values (e.g. nb,en;q=0.9). -
☑️ Macrolanguage tags (e.g., "no") MUST NOT be used - instead use the specific language variant (e.g., "nb" or "nn").
-
👀 British English spelling as defined in the Oxford English Dictionary SHOULD be used for consistency
Example:
GET /api/v1/info Accept-Language: nb
GET /api/v1/info?lang=nb
- 👀 You SHOULD use the ISO 8601 standard for all date and timestamps
Example:
"lastUpdated": "2025-02-13T15:30:00Z"
- 👀 Prices should be specified as floating-point numbers, represented as strings
- 👀 Request: Max 18 digits total; max 5 decimals (follows ISO 20022)
- 👀 Response: Always serialized with the standard number of decimals. For NOK, this means 2, since øre is the smallest unit.
Example:
{
"amount": "99.00"
"currency": "NOK"
}
- 👀 You MUST encode all text in UTF-8
- 👀 Set the Content-Type header to for example
application/json; charset=utf-8- Tip: test the api with international characters (e.g. æ, ø, å)
- ☑️ HTTP headers MUST use Hyphenated-Pascal-Case format (e.g.,
Content-Type,Accept-Language) - 👀 Custom HTTP headers SHOULD use the prefix
Entur-, with some exceptions.
- ✅ All endpoints SHOULD allow consumers to identify themselves using the request header
ET-Client-Name. The header is added automatically to all endpoints in a specification before publication to the developer portal.
Used to declare which "point-of-sale" a request is coming from.
Used to declare which distribution channel (sales channel) a request is coming from.
A "de-facto" standard for correlating a request throughout a microservice architecture. At Entur, this header is handled by our logging library cloud-logging. It is automatically added to all endpoints in a specification before publication to the developer portal.
- 👀 You MAY allow filtering, sorting, and pagination to retrieve specific data
- 👀 If you implement pagination, you MUST use either query parameters "page" (zero based page to get) and "size" (number of items per page), OR query parameters offset (zero based) and limit (number of items)
- 👀 If you implement sorting, you SHOULD use query parameter "sort". Sorting can be done on multiple levels, and sort order (desc / asc) is also specified, like so:
sort=<field1>,<asc|desc>&sort=<field2>,<asc|desc> - TODO: Requirements for response format for pagination and sorting
The requirements above are based on the Spring way of doing things: https://docs.spring.io/spring-data/rest/reference/paging-and-sorting.html
Example:
GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=something,desc&page=0&size=20
- 👀 You MAY let clients choose which fields to include to reduce data transfer
Example:
GET /api/v1/bus-stops?fields=id,name,location
- 👀 When multiple operations need to be handled in a single call, the API SHOULD support batch operations
Example:
POST /api/v1/batch
{
"operations": [
{ "method": "GET", "path": "/v1/resource/1" },
{ "method": "DELETE", "path": "/v1/resource/2" }
]
}- 👀 You MAY use HTTP headers such as Cache-Control, ETag, and Last-Modified
Example:
HTTP/1.1 200 OK
Cache-Control: max-age=3600
ETag: "abc123"
Last-Modified: Fri, 13 Feb 2025 15:30:00 UTCThe client can then cache the result for 3600 seconds, and after that it can issue a conditional GET using the ETag and Last-Modified headers
Example:
GET /your-resource HTTP/1.1
If-None-Match: "abc123"
If-Modified-Since: Fri, 13 Feb 2025 15:30:00 UTCIf both If-None-Match and If-Modified-Since are sent, the server MUST ignore If-Modified-Since (RFC7232 §3.3). If the resource has not changed, the server can respond with 304 Not Modified.
Example:
HTTP/1.1 304 Not Modified- 👀 You SHOULD support JSON, unless you have a good reason not to.
- ✅ Use the HTTP Accept header to specify desired response format
Example:
GET /api/v1/data
Accept: application/json
- 👀 You SHOULD validate all incoming data and return detailed error messages with appropriate HTTP status codes
- 👀 Error messages SHOULD be specific enough to guide the client toward fixing the issue
- 👀 Validation errors SHOULD return 400 Bad Request status code
Example:
HTTP/1.1 400 Bad Request Content-Type: application/problem+json Content-Language: en
{
"type": "https://api.entur.io/v1/orders",
"title": "Invalid input",
"status": 400,
"detail": "Field must be valid email.",
"errors": [
{
"field": "email",
"message": "Field must be valid email.",
"code": "INVALID_FORMAT"
}
]
}- 👀 You MAY include links to related resources in responses to enable easy API navigation
Example:
{
"id": 123,
"name": "Sentralstasjonen",
"links": [
{ "rel": "self", "href": "/api/v1/bus-stops/123" },
{ "rel": "schedules", "href": "/api/v1/bus-stops/123/schedules" }
]
}There may be situations where a pure REST architecture is not the best solution. In such cases, consider alternative design patterns. The choice should be justified based on performance requirements, complexity, and consistency with other systems, as well as overall guidelines in the architect's intent.
- GraphQL: For flexible queries where the client can specify exactly what data is needed
- Event-driven architecture: For asynchronous or event-based scenarios
- WebSocket: For real-time bidirectional communication
- gRPC: For high-performance, strongly-typed services
Must existing APIs conform the guidelines?
- Non-breaking changes (like adding example values) SHOULD be updated to be compliant with the guidelines.
- Breaking changes MAY be added in a new version of the API.
- New APIs MUST follow the guidelines.