Errors, pagination and versioning
Handle every error with one code path, page through any list with a cursor, and know which API changes can and cannot break your integration.
On this page
Three conventions hold for every endpoint. Learn them once and you can predict how an endpoint you have never called will behave.
Handle errors
Every error the API returns, whatever its status, has the same body:
erroris a stable code, usually camelCase. The refusals to change who holds what are dotted, such aspermission_policy.not_grantableorbinding.holder_outside_home_project. Switch on it.messageis for people and may be reworded at any time. Never parse it.requestIdidentifies this request in Flowion's logs.
New codes can appear within /v1, so give your switch a fallback that treats an unknown code as a generic error of that HTTP status. Each endpoint's page in the API reference lists the codes it returns today.
A 500 always reads internalError with a fixed message. The cause is logged on Flowion's side, under the request id.
A 503 is one of two codes, and both carry a Retry-After header with the seconds to wait. serviceUnavailable means something Flowion needs did not answer just now; vendorUnavailable means the charger maker's own cloud (Easee or Zaptec) did not. Wait, then send the request again, with the same Idempotency-Key if it had one. Which part failed is in Flowion's logs under the requestId, so quote that to support. A 504 (timedOut) is different: the request may have been carried out, so read before you repeat a write that had no key.
Find out why a request was refused
Every request is checked against the permission policies the caller holds. A refusal comes in one of four forms, and each tells you what to look at next:
| Response | Means | What to do |
|---|---|---|
403 forbidden | The caller is a member, but no policy they hold allows this, or a deny does | Ask for the policy that carries the permission, or remove the deny |
404 notFound | The caller has no reach: not a member of the organization, outside the project, or no longer active (a removed member, a revoked or expired key) | Check the id, then the caller's access. A 404 never says which |
403 assistantNotEnabled | An assistant reached an organization that hasn't turned the assistant connection on | Someone who can manage the organization's capabilities turns it on |
500 internalError | Flowion could not make the decision safely, for example a policy that cannot be read | Retry, then quote the request id. The request is refused, never let through |
The message of a 403 is never the reason to switch on. To learn the reason for certain, ask: Explain a decision takes the member, the permission and where, and returns the decision the API made, or would make, with the reason spelled out. It needs identity.policy.explain.
reason | Means |
|---|---|
allowed | A statement allows it, and nothing denies it. decidedBy names the statement |
denied | A statement denies it, and a deny beats any allow. decidedBy names it |
no-allow-found | No policy the member holds allows it. This also answers a permission that doesn't exist: an unknown name is never allowed |
principal-not-active | The member or key is no longer active, so no policy is read at all |
policy-unreadable | A policy they hold could not be read, so the answer is a refusal. Without this a deny that could not be read would quietly stop denying. warnings says which |
not-a-member | The person is not a member of the organization. Explain answers 404 for such a person, as for one that doesn't exist, so you will rarely read it here |
assistant-not-enabled | You asked about the member's assistant, and the organization hasn't turned it on |
See Write your first policy for an explain call from start to finish.
Policy and binding errors
Writing a policy, giving one to someone or taking one away can be refused for one of these reasons. Each is a stable error code, and the message names the statement when a document is at fault:
| Code | Status | Meaning |
|---|---|---|
permission_policy.invalid_document | 422 | The document is not valid: no statements, more than 50, a bad time window, an empty condition |
permission_policy.unknown_permission | 422 | A statement names a permission that is not in the catalogue |
permission_policy.deny_on_owner_floor | 422 when writing a policy, 409 when giving one | A deny names a permission only a transfer of ownership can move |
permission_policy.limit_exceeded | 422 when writing a policy, 409 when giving one | The organization already holds as many policies, or as many bindings, as it may |
permission_policy.name_taken | 409 | Another policy of the organization, or a provided one, has this name |
permission_policy.in_use | 409 | A policy that is given to a member, a key or a person who charges, or carried by a pending invitation, can't be deleted. Take it away first |
permission_policy.managed_immutable | 409 | A policy Flowion provides can't be changed or deleted |
permission_policy.stale | 409 | An update named an expectedVersion that is no longer the policy's version. Nothing was changed. Read the policy again and retry |
permission_policy.would_remove_last_editor | 409 | The change would leave nobody able to change who holds what |
permission_policy.management_permission_for_non_member | 409 | A permission for running an organization can only be held by a member or an API key, never by a person who charges or the public group |
permission_policy.not_grantable | 403 | The policy allows something the caller doesn't hold here. You can only give what you hold |
permission_policy.deny_not_allowed | 403 | The policy denies, and the caller doesn't hold identity.policy.deny |
binding.driver_policy_requires_access_permission | 403 | The policy decides who may start a charge, and the caller holds the permission to change bindings but not the access permission for this place: authorization.access_policy.manage at a project or a location, authorization.access_policy.manage_charger at a charger |
binding.holder_outside_home_project | 422 | A person who charges can be given a policy only in their own project and mode |
Giving a policy someone already holds here, or the Owner's own policy, answers 409 conflict. Everything else a policy request can be refused for is a 403 forbidden or a 404 notFound, as above.
Quote the request id
Every response, success or error, carries an x-request-id header. Quote it when you contact support, and Flowion can find the exact request.
To tie requests to your own logs, send your own x-request-id: up to 128 letters, digits, -, _, . and :. Flowion keeps it and echoes it back. Anything else is replaced with a generated id rather than refused. Browsers can read the header from another origin.
Page through lists
Every list pages with a cursor. Ask for a page with limit, then pass each page's nextCursor back as cursor until it comes back null:
200Response
limitdefaults to 20 and goes up to 100. A larger value is lowered to the maximum, not refused. A few endpoints have their own default and maximum; their reference page says so.cursoris opaque: do not build, parse or store one. It works only with the filters andlimitit was issued with. Change either and start from the first page. A cursor used anywhere else answers400 invalidCursor.nextCursorisnullon the last page. A short page does not mean the end, so checknextCursor.
Lists have no page numbers and no totals: page and perPage answer 400 invalidRequest, like any parameter an endpoint does not take. When you need a count, use an endpoint built for it. For chargers, GET /v1/projects/{projectId}/fleet-health returns total, online, offline, faulted and neverConnected in one call.
A few short lists, such as a charger's outlets and connectors, return everything at once with data only. The reference shows which lists take cursor.
Rely on /v1
Every route lives under /v1. Within it, changes only add:
Safe, and made without notice: a new response field, a new optional request field, a new endpoint, a new error code, a new value in a response enum, a new variant of a response union.
Breaking, and only in a new version: removing or renaming a field, changing a field's type, or accepting less than before.
So read responses tolerantly: ignore fields you do not know, handle codes you do not recognise, and give every switch on a response enum or union a fallback. A value your code has not seen is not an error in the API.
Expect six months' notice
Something that is going away is deprecated first, and stays for at least six months before it is removed. During that time it works exactly as it did. Three signals tell you, so you can pick up whichever your integration already reads:
In the reference. The endpoint's page carries a notice with the removal date and a link to its replacement. A deprecated field is marked the same way.
In the API document. The operation or field is marked
deprecated, withx-deprecated-since,x-sunset(the earliest removal date) and, when there is one,x-replaced-by.On every response. A deprecated endpoint answers with a
Deprecationheader, aSunsetheader carrying the removal date, and, when it has a replacement, aLinkheader withrel="successor-version". Errors carry them too, so a client that only ever sees a failure still learns of it.
A change that cannot be made additively arrives as /v2, served beside /v1 for the whole notice period, so you move when you are ready and not on the day it ships. Removing a deprecated endpoint or field before the six months are up is refused by Flowion's own release checks.
Pin an SDK version
The SDKs are versioned as one family. Every SDK shares the same MAJOR.MINOR and each has a PATCH of its own, so every 0.3.x describes the same API, and a patch fixes one language without moving the others.
They start at
0.x. Until Flowion declares 1.0, a change that is not backwards compatible raisesMINOR, so pin to aMINORand upgrade it deliberately.From 1.0 on,
MINORonly adds andMAJORis the one that can break you.Which number moves is computed from what changed in the API. It is never chosen by hand, so a release that breaks you cannot ship as a patch.
Next: Sending commands.