Reservation rules and limits
A Reservation holds a specific Connector on a specific ChargeBox for a window of time, tied to one of your EV cards. Understanding what the system enforces — and what it doesn’t — helps you read 4xx responses and the suggestion chips that appear when your first choice is taken.
The model
Section titled “The model”A reservation is the tuple (chargeBox, connector, card, startAt, endAt). Every reservation is owned by exactly one card; the card determines who can see it and which Lago subscription the eventual session bills against.
erDiagram USER ||--o{ CARD : "owns" CARD ||--o{ RESERVATION : "anchors" CHARGEBOX ||--|{ CONNECTOR : "exposes" CONNECTOR ||--o{ RESERVATION : "is held by" RESERVATION { timestamp startAt timestamp endAt string status }Two rules govern the lifecycle of every reservation request:
- Scope. You can only see and create reservations against cards you own. The customer API filters all reads by
scope.ocppTagPksand, on writes, callsassertOwnership("card", …)before doing anything else. If you reference a card that isn’t yours, the response is404 Card not found— by design, ownership failures and missing rows look identical. - Capability. Creating a reservation requires the
reserveCapability. Drivers whose accounts are inactive get403 Account inactivewith the capability name attached, regardless of whether the requested window is free.
Validation order
Section titled “Validation order”The POST handler applies checks in a fixed order. Knowing the order tells you which problem the API is complaining about first:
flowchart TD A[Request] --> B{Authenticated?} B -->|no| B1[401] B -->|yes| C{Impersonating?} C -->|yes| C1[403 read-only] C -->|no| D{Body shape valid?} D -->|no| D1[400] D -->|yes| E{endAt > startAt?} E -->|no| E1[400] E -->|yes| F{Has 'reserve' capability?} F -->|no| F1[403 Account inactive] F -->|yes| G{Owns the card?} G -->|no| G1[404 Card not found] G -->|yes| H{Window free?} H -->|no| H1[409 + suggestions] H -->|yes| I[201 Created]There is no minimum or maximum duration enforced at this layer beyond endAt > startAt. Practical limits come from the Tariff and from how long the ChargeBox will actually hold an OCPP reservation — both of which can shorten what you booked, but neither will prevent the booking itself.
Conflicts and suggestions
Section titled “Conflicts and suggestions”When the requested window overlaps any existing reservation on the same (chargeBox, connector), the API responds with 409 and a structured body:
{ "error": "Time window conflicts with existing reservation(s)", "conflicts": [ /* the blocking reservations */ ], "suggestions": [ /* up to 2 alternative windows */ ]}suggestions are computed by suggestAlternatives. They have two properties worth knowing:
- Same duration. Each suggestion is exactly as long as the window you asked for.
- After the conflicts. Suggestions are free windows that start after the conflicting reservations end. The algorithm doesn’t look backwards.
At most two suggestions are returned. The web UI renders them as one-tap chips so accepting an alternative is a single click.
Listing rules
Section titled “Listing rules”GET /api/customer/reservations follows the same scoping rule and accepts three optional filters:
status=— a comma-separated subset of the canonical statuses. An unknown status returns400with the allowed list.upcoming=true— restricts to reservations whoseendAtis in the future and sorts ascending bystartAt. Otherwise the list is sorted descending (newest first).limit/skip— paginated;limitis clamped to[1, 500]and defaults to50.
If your scope contains no cards, the endpoint short-circuits to an empty list rather than running a query — so a brand-new account with no provisioned cards is indistinguishable from one with cards but no reservations.
Why it works this way
Section titled “Why it works this way”Cards are the unit of ownership. Routing scope through idTag primary keys (rather than user IDs) means the same checks work for shared cards, multi-user fleets, and future cases where one human holds cards across multiple tenants. The customer API never asks “is this your reservation?” — it asks “is this your card?” and the reservation follows.
Ownership errors masquerade as 404s. Returning 404 Card not found for both missing and unauthorized cards prevents the API from being used as an enumeration oracle.
Impersonation is read-only. An operator using Impersonation can browse a customer’s reservations to help them troubleshoot, but cannot create one on their behalf. Mutations under an impersonated session return 403. Operators who need to book on behalf of a driver use admin tools, which carry their own audit log trail.
Suggestions only look forward. The intent is “you missed this slot; here’s the next one and the one after.” Backward search would surface windows that are technically free but probably useless (the user already chose a time for a reason).
What this means for you
Section titled “What this means for you”As a driver:
- A
404on a card you swear you own usually means the card is provisioned to a different account, not that it’s been deleted. - A
403 Account inactiveis independent of the time you picked — fixing the window won’t help; resolve the account state first. - A
409is not a dead end. Checksuggestionsbefore retrying with a fresh window of your own. - You can’t create reservations on behalf of someone else, even if you have their card’s primary key. The card must be in your scope.