Roles and permissions
This page is about buyer-side permissions: what a person at a customer company may do in a storefront. It is not about what a Cockpit operator may see (that is a view's permissions array — see Permissions and scope) and not about what your app is allowed to reach (that is manifest.permissions).
The model in three sentences
A role is held by a contact and applies inside that contact's organization — there is no global customer role. Permissions are derived from the role at read time and never stored per contact, so a role change takes effect immediately and cannot leave a stale grant behind. The role-to-permission mapping is per tenant and configurable.
Read the catalogue
curl https://api.revenexx.com/v1/customers/roles \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
One read answers every role a contact of this tenant can hold, the permissions each grants, and the built-in permission vocabulary those grants are drawn from.
Built-in roles, least to most privileged:
| Key | Title |
|---|---|
viewer | Viewer |
requester | Requester |
buyer | Buyer |
approver | Approver |
admin | Administrator |
The response's source says which answer you got: tenant means the configured mapping answered, defaults means this tenant has no roles yet (or custom_roles_enabled locks the ledger) and the built-ins answered.
POST /v1/customers/roles/defaults seeds the built-in roles.
Change what a role grants
curl -X PUT "https://api.revenexx.com/v1/customers/roles/buyer/permissions" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"permissions":["orders.read","orders.create","contacts.read","addresses.manage"]}'
The whole new set in one call. That is the shape a role editor actually produces, and the one that cannot leave a half-applied grant behind if a second call fails. Duplicates and blanks are ignored; an empty array revokes everything.
It seeds the built-in roles first when the tenant has none, so editing works without calling /defaults.
Permission keys are free text on purpose: they belong to whichever app declared them, and a grant for an app that is not installed simply has nothing to act on. That is what makes the ledger extensible — your own app declares its grants in provides_permissions and a tenant can hand them to a role without a release of the customers app.
Read a contact's effective grants
curl "https://api.revenexx.com/v1/customers/contacts/{contact_id}/permissions" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Computed from contacts.role on every call — nothing is persisted, so this always reflects the role the contact holds right now.
| Field | Meaning |
|---|---|
permissions | What this role may do. |
role | The role held, and the only input to permissions. |
organization_id | The organization the role applies inside. Null for a standalone buyer. |
active | false while the contact is blocked or its registration is pending/rejected — it holds the role but must not act on it. |
order_approval_limit | Amount ceiling in the market's currency. Only meaningful together with orders.approve. |
Check active before you check permissions. A blocked contact still has a role.
The same block comes back inline on POST /v1/customers/auth/login and /auth/me, so a storefront back end does not need a second call to decide what to render.
Principal resolution
POST /v1/customers/principal/resolve is the capability the gateway calls to turn a caller's principal assertion into the permission set it forwards to every other app.
curl -X POST https://api.revenexx.com/v1/customers/principal/resolve \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"contact_id":"…"}'
This is the hot path of every attributed storefront request: one contact read plus the tenant's role map. You normally do not call it — the gateway does, and your app receives the resolved permissions. It is documented because it is what makes buyer identity available to every commerce app rather than only to the customers app.
A blocked or pending contact always resolves with active: false. What its permissions then say is the tenant's blocked_contact_behavior setting: keep (the default — the role's grants), catalog_only, or deny_all.
Two amounts that are not grants
contacts.order_approval_limit and organizations.credit_limit are amounts. They constrain what a grant may do; they never confer one. A person with orders.approve and a null limit may sign off anything; a person without it and a limit of ten million may sign off nothing.
Where to go next
- Contacts — where
rolelives. - Orders — what
orders.*grants act on. - App permissions — what your app is allowed to reach, which is a different ledger.
- Permissions and scope — gating an operator view.