Roles and permissions

How buyer-side grants work on revenexx — the per-tenant role catalogue, replacing a role's permission set, reading a contact's effective grants, and the principal resolution the gateway calls.

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

Request
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:

KeyTitle
viewerViewer
requesterRequester
buyerBuyer
approverApprover
adminAdministrator

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

Request
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

Request
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.

FieldMeaning
permissionsWhat this role may do.
roleThe role held, and the only input to permissions.
organization_idThe organization the role applies inside. Null for a standalone buyer.
activefalse while the contact is blocked or its registration is pending/rejected — it holds the role but must not act on it.
order_approval_limitAmount 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.

Request
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

Was this page helpful?