Most OCPI integration mistakes are not protocol misunderstandings. The spec is clear enough; the data model is sensible; the messages are well-documented. If you need a refresher on what the protocol does and where it sits between roaming partners, see what OCPI is. The pitfalls that actually sink integrations are operational — handling partner variation, designing for the failure modes production presents, and getting the small details right at scale.
The pattern repeats: a first integration works fine against one partner, then breaks against the second because a quiet assumption (single-page results, trusted timestamps, exact-case identifiers) never got generalized. Below are the mistakes that show up most often, each with the concrete fix. If you’re starting an OCPI integration in 2026, use this as a checklist to test your design against before your second partner exposes the gaps for you.
1. Treating the Credentials handshake as one-time
The mistake: implementing the Credentials handshake as a one-time setup ritual, hard-coded into a deploy script.
Why it’s a problem: tokens need to rotate. Partners change their endpoints. New partners get onboarded. A one-shot script means every change becomes engineering work.
What to do instead: build a proper partner-management interface (UI or admin tool) that supports adding partners, rotating tokens, suspending and re-enabling partners, and viewing the current state of each partnership. Treat partners as first-class entities in your domain model. The token lifecycle here is the same one covered in OCPI security and authentication (coming soon) — build the rotation path before you need it.
2. Not handling case sensitivity for identifiers
The mistake: storing party IDs and country codes with mixed case, or comparing them case-sensitively when matching.
Why it’s a problem: OCPI specifies uppercase for both, but real-world data is messy. Some partners send lowercase, some uppercase, some mixed. Your code that expects exact-case match will silently drop messages.
What to do instead: normalize on ingest. Uppercase everything. Validate format strictly. When comparing, compare the normalized form. Add tests that exercise mixed-case inputs.
3. Ignoring pagination
The mistake: calling GET /locations once, taking the first page, and assuming you got everything.
Why it’s a problem: many partners (especially hubs) paginate large result sets. Their first page might be 100 locations; they have 10,000 total. You silently miss 99% of their data and don’t know it.
What to do instead: always follow the Link header for pagination. Implement cursor-based iteration as a core utility. Log the total number of pages and items fetched so you can spot anomalies. Some partners return non-standard link header formats — handle them defensively.
4. Trusting timestamps blindly
The mistake: using the timestamps in incoming messages as ground truth for billing, ordering, or reconciliation.
Why it’s a problem: partner clocks drift. Some partners stamp messages at the charger; others at their backend; others at their CSMS-to-OCPI gateway. The same session can have meaningfully different timestamps depending on which timestamp you look at.
What to do instead: record both the message timestamp AND your receipt timestamp. Use receipt timestamps for operational ordering (“which arrived first?”). Use message timestamps with skepticism. For billing, define a single authoritative timestamp source per session (usually the CPO’s session_start) and document it.
5. Polling when push would work (or vice versa)
The mistake: polling a partner’s /sessions endpoint every minute “to make sure you have the latest data.”
Why it’s a problem: OCPI is push-first for most modules. Sessions and CDRs come to you via the partner PUTing/POSTing to your endpoints. Polling on top of that doubles your load, may rate-limit you, and indicates a misunderstanding of the protocol.
What to do instead: implement the push receivers correctly. Use polling only for modules that are inherently pull (Locations are often pull-once-then-cache, for example). If you’re not receiving expected pushes, investigate why the push isn’t happening rather than papering over it with polls. If the push-versus-pull split across modules still feels fuzzy, OCPI pull vs push patterns (coming soon) walks through which direction each module uses and why.
6. Idempotency failures
The mistake: treating every received message as a new event, even if the same session_id or cdr_id has been seen before.
Why it’s a problem: partners retry. Networks have hiccups. The same Session update or CDR may arrive twice. Without idempotency, you might double-bill, double-count, or create inconsistent state.
What to do instead: deduplicate by (party_id, session_id) for sessions, by (party_id, cdr_id) for CDRs. Make your ingestion handler explicitly idempotent. Test with replays.
A safe ingestion path looks like this:
flowchart TD
A[Message arrives] --> B{Seen this<br/>dedup key?}
B -->|Yes| C[Return 200<br/>no-op]
B -->|No| D[Validate and<br/>normalize]
D -->|Invalid| E[Return OCPI<br/>StatusCode error]
D -->|Valid| F[Upsert with<br/>unique constraint]
F --> G[Return 200<br/>success]
style C fill:#e8f5e9,stroke:#43a047
style E fill:#ffebee,stroke:#e53935
style G fill:#e8f5e9,stroke:#43a047
7. Inadequate error responses
The mistake: returning HTTP 500 with no body when something goes wrong on your side.
Why it’s a problem: the partner has no idea what happened. They can’t debug. They open a support ticket with you. You spend 3x the time you would have spent including a meaningful error message.
What to do instead: return OCPI’s StatusCode format with a meaningful status_message. Include enough detail for the partner to act (“invalid country_code: must be ISO 3166 alpha-2 uppercase”) without leaking internal state. Test your error paths in your CI.
8. Trusting the partner’s claimed timezone
The mistake: accepting datetime values in partner-local timezone.
Why it’s a problem: OCPI mandates UTC ISO 8601 datetimes. Some partners send local times anyway. Some partners send mixed (UTC for some fields, local for others). Your code silently mishandles them.
What to do instead: validate UTC strictly on ingest. Reject (with a clear error) any datetime that isn’t UTC. When generating, always emit UTC with a Z suffix. Tests should explicitly cover the bad cases.
9. Underestimating Locations data volume
The mistake: storing Locations data in a single big table with no indexing strategy, assuming it’s static.
Why it’s a problem: a hub may give you tens of thousands of Locations. Each Location can have multiple EVSEs, multiple Connectors, hours of operation, images, terms. The data is large. Querying it without indexes is slow. Updating it (when a partner changes a tariff or adds a connector) requires careful diff handling. The Locations module data model — how the Location/EVSE/Connector hierarchy nests and how partial PATCH updates flow — is worth internalizing before you design the schema that has to hold it.
What to do instead: design a schema that supports efficient lookup by location ID, by geo-bounding-box, by connector type, by CPO. Use partial updates (EVSE- and Connector-level PATCH deltas) rather than full snapshots when possible. Have a strategy for “this location is no longer published” (soft delete, archive).
10. Not having a mock partner
The mistake: testing only against real partners.
Why it’s a problem: real partners aren’t always available. Real partner outages or quirks can mask bugs in your code. You can’t run integration tests against real partners in CI.
What to do instead: build a mock partner — a small server that speaks OCPI well enough to round-trip every message your real code uses. Run it in CI. Test against it. When a real partner has unusual behavior, model that behavior in the mock.
11. Trusting HTTP 200 instead of the OCPI status_code
The mistake: treating an HTTP 200 OK from a partner as confirmation that your request succeeded.
Why it’s a problem: OCPI wraps every response in an envelope carrying its own status_code, and the two layers are independent. A partner can return HTTP 200 while the envelope says 2001 (invalid or missing parameters) — your call was received but rejected. If you only check the transport status, you record a failed PUT as a success, and the two sides quietly drift out of sync until a reconciliation surfaces the gap.
What to do instead: always read the envelope status_code, not just the HTTP status. Anything in the 1xxx range is success (1000 is the generic success code). 2xxx is a client error you caused (fix the request), 3xxx is a server error on the partner side (retry or alert), and 4xxx is a hub-level error such as an unknown receiver. Log the status_message alongside the code — that free-text field is where partners explain what went wrong.
Bonus: the underrated investments
A few things that don’t make the “pitfall” list but consistently distinguish good OCPI integrations from mediocre ones.
Per-partner observability. Build dashboards (Grafana, Datadog, whatever) that show, per partner: request volume, error rate, latency, last-successful-CDR-time. When something is wrong, you see it per-partner immediately. When everything is fine, you have evidence of that. Whether your partners are large networks like ChargePoint, EVgo, Electrify America, or Flo in North America, or IONITY, Allego, or a national hub in Europe, the discipline is the same: one dashboard row per party, and an alert when a row goes quiet.
Idempotency at the database layer. Use unique constraints. Use upsert patterns. Don’t depend on application code alone to enforce uniqueness; let the database catch the duplicates you missed.
Schema validation in CI. Use the community-maintained OCPI JSON Schema (where available) to validate every outgoing payload in your test suite. Catches malformed data before it ever hits a partner.
Friendly retry logic. When a partner is unreachable, queue and retry with exponential backoff. Don’t retry forever (eventually give up and alert), but don’t fail-fast on a transient network blip either.
Documentation per partner. A simple internal doc per partner: who they are, their party_id/country_code, who to contact for support, known quirks. Saves enormous time when something breaks at 2am.
Clear ownership. Who in your org owns the OCPI integration end-to-end? Engineering? Operations? Both? Make sure there’s a single team accountable for “is OCPI working” and that they have the tools (dashboards, logs, alerts) to know.
A typical project timeline
If you’re starting from zero, a realistic timeline:
- Weeks 1-2: spec reading, design, basic data model.
- Weeks 3-5: Credentials handshake, basic Locations + Sessions implementation, mock partner.
- Weeks 6-8: Tariffs, CDRs, Tokens, Commands. Auth.
- Weeks 9-10: First real-partner testing. Bugs.
- Weeks 11-13: Hardening, error handling, observability.
- Weeks 14-16: Second partner. More bugs (different ones).
- Beyond: roll out to additional partners, refine, iterate.
The first partner takes weeks because you’re building both the integration AND learning. The second partner exposes the parts of your implementation that were too partner-specific. The third partner usually goes smoothly if you absorbed the lessons from one and two.
Where most teams under-invest
If I had to pick the single most under-invested area in OCPI implementations, it would be observability. Teams build the protocol-correct code and then ship it with minimal monitoring. The result: in production, something silently breaks, nobody notices for days or weeks, and by the time they do the damage (lost CDRs, billing errors, frustrated partners) is significant.
Spend the engineering time on dashboards, alerts, and audit logs. It pays back many times over.
Key takeaways
OCPI integration is harder than the spec suggests but easier than it feels in week 3. The mistakes that matter cluster in a few places:
- Most failures are operational — partner variation and failure-mode handling — not protocol misunderstanding.
- The highest-leverage habits are idempotent ingestion, strict UTC validation, reading the envelope
status_code(not just HTTP 200), following pagination, and per-partner observability. - Treat partners as first-class entities in your domain model, and build a mock partner and a token-rotation path before you need them.
- Budget for the second-partner-exposes-different-bugs reality; it is the norm, not bad luck.
Get those right and OCPI integration is durable. Skip them and it becomes a long tail of fire-fighting. Which OCPI version you target shapes several of these details too, so pin the right baseline before you start rather than discovering mid-build that a field behaves differently than you assumed.