Permissions and policies

Give each person and API key exactly the reach they need with permission policies, at the organization or in one project, and read a 403 or 404 correctly.

8 min readEdit this page
On this page

Everything a caller may do is a named permission, such as asset.charger.read. A policy is a set of permissions, and a binding gives a policy to a member or an API key, either across the whole organization or in one project, and to a person who charges, at a project, a location or a single charger. Each endpoint's page in the API reference names the permission it needs.

The permissions

Every permission is named service.resource.verb. The catalogue is the same for everyone who is signed in:

GET/v1/permissionsbash
curl "$API/permissions" -H "Authorization: Bearer $TOKEN"

Each entry has a name, a plane and a plain-words description. The management plane is for running an organization, and the driver plane is for charging. Two more fields help you decide what to give out: lowestBundle is the lowest provided policy that includes the permission (user, admin or owner, and absent on the driver plane), and privileged is true when holding it makes an account need two kinds of sign-in factor and recovery codes. The full list is on List permissions.

json
{
  "name": "identity.apikey.manage",
  "plane": "management",
  "description": "List, create, revoke API keys",
  "lowestBundle": "admin",
  "privileged": true
}

The policies Flowion provides

Flowion provides four policies you give out, and you give them to people:

PolicyHolds
OwnerEverything for running an organization, including deleting it and handing ownership to someone else. One per organization, and it can't be given: it moves only by transferring ownership
AdminEverything except what is reserved for the owner: members, API keys and charger settings. Given at the organization, so it reaches every project, now and later
UserDay-to-day work: chargers, sessions, commands. Given at one project, so it reaches that project and nothing else
ChargingThe permission to start a charge, and nothing else. Given to a person who charges, at a project, a location or a single charger: see Let people charge

The first three are on the management plane and Charging is on the driver plane. No management policy allows starting a charge, so being the Owner of an organization does not make a card charge.

A fourth, Statements, is given beside one of those to the people who bill residents: it holds the three settlement.statement permissions (read, close and carry a month's resident statements), which Owner holds and Admin does not.

List them, with the permissions each one allows and the ids you send as policyId:

GET/v1/organizations/$ORG/permission-policiesbash
curl "$API/organizations/$ORG/permission-policies" \
  -H "Authorization: Bearer $TOKEN"

Each policy also carries holders: how many members, API keys, pending invitations and others hold it, and the total. To see where each one holds it, read List who holds a permission policy.

Every member also holds what every member holds: reading the organization, its members, its projects and its capabilities. Someone with no policy of their own can see that much, and reach no project.

Give a member one project

Give the User policy at a project, and nothing else, to hand a client's staff their own location:

POST/v1/organizations/$ORG/members/$MEMBER/bindingsbash
curl -X POST \
  "$API/organizations/$ORG/members/$MEMBER/bindings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"policyId": "'"$USER_POLICY"'", "projectId": "'"$PROJECT"'"}'

Leave out projectId to give a policy across the organization. To take it away, DELETE the binding by its id: DELETE .../members/{memberId}/bindings/{bindingId}.

Giving and removing policies takes the identity.binding.manage permission where the binding sits. You can't give a policy that grants something you don't hold yourself, and a change that would leave nobody able to change bindings is refused with permission_policy.would_remove_last_editor.

You can also name what someone holds when you add or invite them, with policyBindings on Add a member and Invite a member.

Let people charge

Who may start a charge at a charger is a policy too. Charging allows session.transaction.start, the one permission on the driver plane, in live and in sandbox, and its id is the same in every organization: 00000133-0000-4000-8000-000000000006. Give it to a person who charges at the place they should charge: the whole project, one location or a single charger. $PERSON is the person's id from List people.

POST/v1/projects/$PROJECT/bindingsbash
curl -X POST "$API/projects/$PROJECT/bindings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"principalId": "'"$PERSON"'", "policyId": "00000133-0000-4000-8000-000000000006"}'
200Response
{
  "id": "6c1f0d3e-8a52-4b97-b0e4-2d7a9f35c168",
  "createdAt": "2026-10-05T09:12:44Z",
  "node": { "kind": "project", "id": "4e2b7a90-1c5d-4f3a-9b86-0a7d3e5c2f14" },
  "policy": { "id": "00000133-0000-4000-8000-000000000006", "name": "Charging" },
  "principalId": "a3d85e1b-7f02-4c69-8e1a-5b0c9d2f4a73"
}

The same body, posted to /locations/$LOCATION/bindings or /charging-stations/$CHARGER/bindings, gives the person access there and nowhere else. DELETE the binding by its id to take it away. Every card the person holds then charges, including cards they get later.

  • The door is the permission, not who you are. Giving Charging at a project or a location needs authorization.access_policy.manage, and at a charger authorization.access_policy.manage_charger. A member and an API key that hold the same permission get the same answer. Holding only identity.binding.manage is refused with binding.driver_policy_requires_access_permission.

  • You can give it to whom you like in the project. Giving a driver permission is not limited to what you hold yourself, because nobody holds session.transaction.start except the people who charge. Giving a policy that also allows something for running an organization still is.

  • A person charges only where they live. A person who charges can be given a policy only in their own project and mode, or the request is refused with binding.holder_outside_home_project.

  • Listing the bindings of people who charge is GET /projects/$PROJECT/bindings?principalKind=holder, and the same under a location or a charger. Without principalKind=holder they are left out. A project's list needs identity.principal.read; a location's or a charger's needs authorization.access_policy.read.

See who may charge

To see who may start a charge at a place, and where each of them got it from, ask Who can charge:

GET/v1/charging-stations/$CHARGER/who-can-chargebash
curl "$API/charging-stations/$CHARGER/who-can-charge" \
  -H "Authorization: Bearer $TOKEN"
200Response
{
  "data": [
    {
      "principalId": "a3d85e1b-7f02-4c69-8e1a-5b0c9d2f4a73",
      "kind": "holder",
      "displayName": "Anna Andersson",
      "levels": ["project"],
      "onList": true
    },
    {
      "principalId": "c7e2410a-9d38-4f15-a6b0-3e8d1f59b2c4",
      "kind": "holder",
      "levels": ["charger"],
      "onList": false,
      "notOnListReason": "windowed"
    }
  ],
  "nextCursor": null
}

Each entry says what kind of principal it is (holder, publicGroup, member or apiKey) and the levels the allow is bound at, broadest first: organization, project, location or charger. It carries a name for a person when Flowion has one, and never an email address, a phone number or a card. At a project or a location there is no list, so onList and notOnListReason are left out there.

At a charger, onList says whether the person's card is on the charger's own list, which is what lets it accept the card with no network. A card is left off the list when its access is not true at every moment, and notOnListReason says why: windowed (the policy only allows a time window), deny-may-apply (a deny could apply here), no-unconditional-allow (no allow reaches this charger) or principal-not-active (the person is suspended). Such a person still charges while the charger can reach Flowion, because Flowion decides each time.

The public group

Every organization has one public group, and it appears in who-can-charge as publicGroup. It applies to all external users, meaning every card Flowion does not recognise, and to known people who have no grant of their own. A deny on the public group, or no allow there, blocks anonymous charging entirely, and it also refuses the known people who hold no grant of their own. A person with a grant of their own is judged on that first.

What a change reaches, and when

  • Online, at once. Flowion decides afresh each time a card is presented and keeps no copy of who may charge, so a binding you remove refuses the very next charge.

  • A charge already running. Flowion finds the charges the policy no longer allows and asks the charger to stop each one. A time window that closes never stops a charge.

  • A charger that is offline keeps the list it last received until it reconnects. A person you remove can still charge on it until then. Nothing expires an entry on its own, and a window that closes never takes anyone off a list: a person allowed only in a window is never put on one.

Write a policy of your own

When none of the three fits, write one: a bookkeeper who holds billing and nothing else, or a shift that may send commands to chargers only in the morning. A policy of your own is a list of statements. Each allows or denies permissions you name exactly, and can be limited to a mode, to days and hours in a time zone you choose, or to named projects, locations or chargers. A deny beats any allow.

  • Writing and changing policies takes identity.policy.manage, which the Owner holds. A policy that denies also takes identity.policy.deny.

  • A policy can only allow what its author holds, and you can only give what you hold, wherever you give it: attaching a policy, adding or inviting a member, or making an API key.

  • Ask Explain a decision why someone can or can't do something, and Simulate what a change would do before you make it. Both take identity.policy.explain.

Write your first policy takes you through it with working requests.

When you change a policy that people already hold, send the version you read as expectedVersion: if someone saved first, nothing is written and you get permission_policy.stale. To preview a draft before saving it, send it to Simulate as replace.

In the console

The console has a page for all of this. Open the organization's settings, then Access. It replaces the separate Members and API keys pages, and the old addresses lead there.

  • Members and keys shows who holds which policy, across the organization or in one project, and why: held directly, or inherited from the organization. Change on a row opens the grant panel, which previews what the grant adds and takes away before you press Grant.

  • Policies lists the provided policies and your own, with how many hold each (holders), and opens the editor. A change to an existing policy goes through a review of the lines that change and what each holder gains or loses. It compares your draft with the current version; there is no history of earlier versions.

  • Troubleshoot asks why a member or key can or can't do something. A refused action offers Why was this refused? and a member's row offers Why can't they…, both opening it with the question filled in.

What you see follows what you hold: reading policies needs identity.policy.read, writing them identity.policy.manage, and the troubleshooter and the previews identity.policy.explain. The console shows the same decisions the API gives, and never reads a role. Write your first policy walks through the editor.

See what you hold

Any member can read what they hold, and what they may do:

bash
# your own bindings in an organization
curl "$API/organizations/$ORG/members/me/bindings" \
  -H "Authorization: Bearer $TOKEN"

# what you may do in a project, right now
curl "$API/projects/$PROJECT/permissions/me" \
  -H "Authorization: Bearer $TOKEN"

viewerPermissions on the organization lists what you may do across it. Use these to decide which controls to show. They are for your interface, not for security: the server checks every request again.

API keys

A key holds exactly one binding, and works the same way:

  • A key holding Admin at the organization reaches every project and can do what the Admin policy allows. It never does what only the owner may, because a key can never hold Owner.

  • A key holding User at a project reaches that one project, and nothing at the organization level.

See API keys and sign-in to make one.

Read a 403 and a 404

StatusMeansWhat to do
403You can see this, but you don't hold the permission it needsAsk someone who can give you the policy, or use a key that holds it
404It does not exist, or it is outside your reachCheck the id, then check you have access to its organization and project

A 404 is deliberate when you lack access: outside your reach, a resource looks exactly like one that does not exist. If a 404 arrives for something you know is there, your credential most likely does not reach its project: a member without a binding there, or a key made for a different project.

Both carry the standard error body, with error set to forbidden or notFound. Refusals to change a binding carry their own stable codes, which start with permission_policy.. See Errors, pagination and versioning.

Access is checked against what each member and key holds, on every request. If that check cannot be made, the request is refused rather than let through.

Next: Errors, pagination and versioning.

In the API reference