Transport
The OCPI envelope, status-code rules, headers, credentials tokens, pagination, routing and PATCH — with no HTTP stack.
On this page
Everything between the JSON and the HTTP: the envelope, status codes, headers, credentials tokens, pagination, routing, endpoint URLs, PATCH and per-peer quirks.
This layer has no HTTP client and no async runtime. It is the shared vocabulary that the client, server and hub are written in, and it is deliberately usable on its own if you want to keep your own HTTP stack.
The envelope
Every OCPI response body is the same shape:
pub struct OcpiResponse<T> {
pub data: Option<T>,
pub status_code: StatusCode,
pub status_message: Option<String>,
pub timestamp: DateTime,
}Status codes are almost never HTTP status codes
This is the rule that OCPI implementations get wrong most often:
Only five situations get an HTTP error status. Everything that reached the OCPI layer is
200 OKwith a four-digit code in the body.
A 2001 Invalid or missing parameters is a 200. A 2003 Unknown Location is a 200. An
authentication failure is a real 401, and an unroutable request is a real 404, and that is nearly
the whole list. OcpiError::http_status() encodes the complete rule, so the mapping happens once
and correctly rather than at every handler.
StatusClass groups the codes the way the specification does: 1xxx success, 2xxx client error,
3xxx server error, 4xxx hub error.
Credentials tokens
CredentialsToken is not a String:
- comparison is constant-time (
subtle), so a token check is not a timing oracle DebugandDisplayare redacted, so it cannot leak into a log line by accident- the value is zeroised on drop
TokenRoledistinguishesA,BandC, andmay_access()enforces thatCREDENTIALS_TOKEN_Ais scoped to thecredentialsandversionsmodules only
Headers
RequestIds carries X-Request-ID and X-Correlation-ID. The rule a hub must not break has its
own constructor:
let onward = incoming.forwarded(); // new X-Request-ID, same X-Correlation-ID
RoutingHeaders carries the four OCPI-{to,from}-{country-code,party-id} headers, and
applies_to() knows that they belong on functional modules and not on credentials, versions or
hubclientinfo.
Pagination
PageQuery builds date_from/date_to/offset/limit; PageMeta reads X-Total-Count,
X-Limit and the Link: rel="next" header; Page<T> is the two together.
crawl_adjustment() handles the case every crawler hits: the server capped your limit to
something smaller than you asked for, and your offset arithmetic is now wrong. It tells you what to
do about it instead of leaving you to find out from a partner's duplicate-object complaint.
PATCH
OCPI PATCH is RFC 7396 JSON Merge Patch, with one extra rule:
A patch must carry
last_updated.
Patch::apply enforces it — a patch without it is the specification's own example of a 2001,
and it never reaches your handler.
Endpoint URLs
The URLs of the endpoints in this document are descriptive only. The exact URL can be found by fetching the endpoint information from the API info endpoint.
So SenderEndpoint and ReceiverEndpoint take the discovered base URL and append only the
parts the specification does define — the client-owned object path
({base}/{country_code}/{party_id}/{object_id}), the nested Location path, the command name.
Nothing here invents a base path.
Quirks
Interop knowledge that is otherwise tribal, turned into documented, testable flags on a Quirks
struct: accept_unencoded_token, send_unencoded_token, omit_routing_headers,
case_insensitive_module_ids, peer_max_page_limit, lenient_content_type.
Six, and every one of them changes behaviour — cargo run -p xtask -- dead-config fails the build
otherwise. Accommodations that are unconditional, such as tolerating a trailing slash on a
discovered URL, are documented as unconditional rather than dressed up as a setting you could
turn off. See Interop notes.