Every OCPI response — success or failure — arrives wrapped in the same JSON envelope, and inside that envelope is a four-digit status_code that tells you what actually happened. The trap is that this code is completely independent of the HTTP status. A partner can return 200 OK at the transport layer while the envelope says 2001, meaning your request was received and rejected. If you only check the HTTP status, you log a failed call as a success and the two sides drift apart until a reconciliation surfaces the gap.

This is a reference and a debugging guide. Below is what each status_code range means, the specific codes you hit most often, and a repeatable method for going from “a partner returned an error” to “here is the field that was wrong.” If you want to see the envelope and these codes live rather than just read about them, try the interactive OCPI simulator — it lets you send well-formed and malformed messages and watch the exact status_code and status_message come back.
The response envelope
Every OCPI response body has the same shape, regardless of module or whether it succeeded:
{
"data": { },
"status_code": 1000,
"status_message": "Success",
"timestamp": "2026-07-12T09:41:00Z"
}
Four fields matter for debugging:
status_code— the OCPI-level result. This is the field that tells you what happened, not the HTTP status.status_message— an optional but strongly recommended human-readable explanation. On errors, this is where the real information lives.timestamp— when the responding system generated the message. Useful for ordering and for spotting clock drift between parties.data— present on success, usually absent or empty on error.
The status_code is always a four-digit integer, and its first digit tells you which class of outcome you are dealing with. Everything else is a lookup.
The four ranges
OCPI groups status codes into four ranges by leading digit. Internalize the ranges first; the specific codes are secondary.
- 1xxx — Success. The request was processed.
1000is the generic success code and the one you see almost all the time. If you get a1xxx, the operation worked; readdata. - 2xxx — Client errors. You caused it. The request reached the partner but was malformed, incomplete, or referenced something unknown. Retrying the same request will fail the same way — you have to fix the request.
- 3xxx — Server errors. The receiving party failed to process an otherwise valid request. This is their side breaking. Retrying with backoff or alerting the partner is the right response, not editing your payload.
- 4xxx — Hub errors. A roaming hub sitting between you and the intended receiver raised the error. The receiver may be unknown to the hub, disconnected, or slow to respond. The fix lives in routing and connectivity, not in your message content.
That single mental model — my fault (2xxx), their fault (3xxx), the middle (4xxx) — resolves most of the “who do I go talk to” question before you even read the specific code.
The specific codes worth memorizing
You do not need to memorize the whole table, but a handful come up constantly.
Success
1000— Generic success. Nearly every good response.
Client errors (2xxx) — fix your request
2000— Generic client error. The catch-all when nothing more specific fits. Lean onstatus_messagehere.2001— Invalid or missing parameters. The single most common error you will debug. A required field is absent, the wrong type, or malformed (a non-UTC timestamp, a lowercasecountry_code, a bad enum value).2002— Not enough information. The request was structurally valid but lacked the data needed to act — for example, a real-time authorization request that does not carry enough to make a decision.2003— Unknown Location. The referenced Location (or EVSE) is not known to the receiver.2004— Unknown Token. The referenced Token is not known — common in authorization and Tokens-module flows.
Server errors (3xxx) — the partner’s side broke
3000— Generic server error. Something failed on their end.3001— Unable to use the client’s API. The receiver tried to call back to your API and could not — often an auth, endpoint, or reachability problem on the return path.3002— Unsupported version. The version you are speaking is not supported by the receiver.3003— No matching endpoints, or expected endpoints are missing. Surfaces during the Versions and Credentials exchange when the two endpoint lists do not line up.
Hub errors (4xxx) — routing between parties
4000— Generic hub error.4001— Unknown receiver. Thetoparty the hub was asked to route to is not known to the hub.4002— Timeout on a forwarded request. The hub forwarded your message but the receiver did not answer in time.4003— Connection problem. The receiving party is not currently connected to the hub.
How to debug an OCPI error, step by step
When a partner returns an error, resist the urge to immediately change your payload. Work the layers in order.
1. Read the envelope, not the HTTP status. Confirm whether the failure is at the transport layer (a real HTTP 4xx/5xx, meaning the call may not have been processed at all) or an OCPI-level rejection carried inside a 200. These are different problems. A common source of confusion is an HTTP 401/403, which is an authentication failure at the transport layer — covered in OCPI security and authentication (coming soon) — versus an OCPI 2xxx inside a 200, which means you got past auth and the payload itself was rejected.
2. Classify by the leading digit. 2xxx means stop and inspect your request. 3xxx means the partner’s system failed — capture the evidence and open a ticket with them rather than mutating your message. 4xxx means talk to the hub about routing or connectivity.
3. Read status_message carefully. This free-text field is where a decent implementation tells you the actual cause: invalid country_code: must be ISO 3166-1 alpha-2 uppercase, or missing required field: connector_id. If the message is empty or useless, that itself is a finding — note which partner ships bare error codes so you know to lean on your own request logs when debugging with them.
4. Reproduce in isolation. Take the exact rejected payload and replay it against a controlled target. This is where the OCPI simulator earns its keep: send the same message, tweak one field at a time, and watch the status_code change from 2001 back to 1000. Reproducing against a mock removes partner availability and partner-side noise from the equation, so you are testing your payload and nothing else.
5. Confirm the fix and add a regression test. Once you know the offending field, add a test that sends the previously-broken shape and asserts the correct code. Error paths deserve tests as much as happy paths — a point covered in more depth in the OCPI integration pitfalls (coming soon) checklist.
The error responses you send matter too
Debugging is bidirectional. When you are the receiver and you reject a request, the error you return determines how fast the other party can fix their side. Two habits separate good implementations:
Return the right code, not a blanket 3000. If the request is missing a parameter, that is a 2001 (client error), not a server error. Miscategorizing your own errors sends partners chasing the wrong side. A malformed field is their bug to fix; returning 3000 implies it is yours.
Always populate status_message with something actionable. invalid or missing parameters is barely better than nothing. missing required field: party_id lets the partner fix it without opening a ticket. Include enough for them to act, without leaking internal stack traces, database errors, or infrastructure detail. The difference between a good and a bad error message is often the difference between a five-minute fix and a three-day support thread.
One caution: distinguish a real error from rate limiting. A burst of 3000s or transport-level 429s under load may not be a bug in your payload at all but a throttle kicking in — the mechanics of which are covered in OCPI rate limiting and abuse protection (coming soon). Backing off is the fix there, not re-crafting the message.
Where this fits
If you are new to how these messages flow between roaming parties in the first place, start with what OCPI is for the overall picture of CPOs, eMSPs, and hubs — whether that is a North American network like ChargePoint, EVgo, or Electrify America, or a European one like IONITY or Allego. The error model is the same across all of them: one envelope, one status_code, four ranges.
The fastest way to build intuition is to make the errors happen on purpose. Open the OCPI simulator, send a clean request to see 1000, then break it deliberately — drop a required field, lowercase a country_code, send a local-time datetime — and watch the exact code and message the receiver returns. Once you have triggered 2001, 2003, and a version mismatch yourself, reading them in production logs becomes routine instead of a puzzle.
Key takeaways
- OCPI carries its own
status_codein the response envelope, independent of the HTTP status. HTTP200withstatus_code2001means received and rejected — always read the envelope. - The leading digit classifies the failure:
1xxxsuccess,2xxxyour request,3xxxthe partner’s server,4xxxthe hub. That alone tells you who to go fix. 2001(invalid or missing parameters) is the error you will debug most; the specific offending field lives instatus_message.- When you send errors, return the correct range and a specific, actionable
status_message— it directly determines how fast the other side can fix things. - Reproduce rejected payloads in isolation. The interactive simulator lets you trigger and resolve each code one field at a time.