Skip to content

Reference

Roles

Roles are defined once per environment and assigned per membership. Permissions travel inside the session, so checking one is a local array lookup rather than a network call — which is the whole point: an authorization check that costs a round trip is an authorization check somebody will eventually skip.

The role object

A role is a named bundle of permission slugs. Your code should check permissions, not roles: a check written against invoices:write survives the day a customer asks for a Finance role that is Admin without member management, and a check written against role === admin does not.

FieldTypeDescription
idstringUnique identifier, prefixed role_.
objectstringAlways "role".
namestringHuman-readable name. Shown to customers in the Admin Portal.
slugstringStable machine identifier, lower-case with hyphens. This is what appears in a session; never rename it once code depends on it.
descriptionstring | nullOne line explaining the role to whoever assigns it.
typestringEnvironmentRole for a role you define, or OrganizationRole for one a customer defined for their own tenant.
permissionsarrayPermission slugs granted by the role. Keep them resource:action shaped so the list stays readable at fifty entries.
created_attimestampRFC 3339.
updated_attimestampRFC 3339.

Slugs are an interface

Once a slug appears in a session it appears in your code, in your customers' scripts and in their runbooks. Renaming one is a breaking change even though nothing in this API stops you. Change the name field instead — it exists for exactly this reason.

Roles and memberships

A membership joins one user to one organization and carries exactly one role. That constraint is what keeps authorization answerable: given a user id and an organization id there is one role, one permission set, and no union to compute at request time.

FieldTypeDescription
idstringPrefixed om_. The join between one user and one organization.
user_idstringThe person.
organization_idstringThe tenant.
roleobjectThe assigned role, returned as a slug. Exactly one role per membership.
statusstringactive, inactive or pending. A pending membership is an invitation not yet accepted.

Endpoints

Paths are relative to https://api.paycux.com. See the reference overview for authentication.

MethodPathWhat it does
GET/rolesList the roles defined in this environment.
POST/rolesCreate a role with a slug and a set of permissions.
GET/roles/:idRetrieve a single role with its full permission list.
PUT/roles/:idChange a role's name, description or permission set.
DELETE/roles/:idDelete a role that is no longer assigned to anybody.
PUT/memberships/:idChange the role held by one person in one organization.

List roles

Roles are environment-scoped, which means the same set applies to every organization unless a customer has defined their own. Read this list to render a role picker rather than hard-coding the options in your interface.

GET /roles
1curl https://api.paycux.com/roles \
2 -H "Authorization: Bearer sk_example_123456789"
response
1HTTP 200
2{
3 "object": "list",
4 "data": [
5 {
6 "object": "role",
7 "id": "role_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
8 "name": "Admin",
9 "slug": "admin",
10 "description": "Full access, including billing and member management",
11 "type": "EnvironmentRole",
12 "permissions": ["members:manage", "invoices:read", "invoices:write", "settings:write"],
13 "created_at": "2026-03-02T08:00:00.000Z",
14 "updated_at": "2026-03-02T08:00:00.000Z"
15 },
16 {
17 "object": "role",
18 "id": "role_01HQ8ZK3M4N5P6R7S8T9V0W1X3",
19 "name": "Member",
20 "slug": "member",
21 "description": "Everyday access with no administrative rights",
22 "type": "EnvironmentRole",
23 "permissions": ["invoices:read"],
24 "created_at": "2026-03-02T08:00:00.000Z",
25 "updated_at": "2026-03-02T08:00:00.000Z"
26 }
27 ],
28 "list_metadata": { "before": null, "after": null }
29}

Create a role

Start with three roles and add the fourth when a customer asks for it by name. A permission list is easy to extend; a role taxonomy invented in advance is hard to retire, because slugs leak into code, into scripts, and into the customer's own documentation.

POST /roles
1curl -X POST https://api.paycux.com/roles \
2 -H "Authorization: Bearer sk_example_123456789" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "name": "Billing admin",
6 "slug": "billing-admin",
7 "description": "Can manage invoices and payment methods, nothing else",
8 "permissions": ["invoices:read", "invoices:write", "payment-methods:write"]
9 }'
response
1HTTP 201
2{
3 "object": "role",
4 "id": "role_01HQ8ZK3M4N5P6R7S8T9V0W1X4",
5 "name": "Billing admin",
6 "slug": "billing-admin",
7 "description": "Can manage invoices and payment methods, nothing else",
8 "type": "EnvironmentRole",
9 "permissions": ["invoices:read", "invoices:write", "payment-methods:write"],
10 "created_at": "2026-05-02T11:41:09.006Z",
11 "updated_at": "2026-05-02T11:41:09.006Z"
12}

Retrieve a role

Useful on an admin screen that explains what a role grants before somebody assigns it. The permission list is authoritative here; a session carries the same set, but only as of the moment it was issued.

GET /roles/:id
1curl https://api.paycux.com/roles/role_01HQ8ZK3M4N5P6R7S8T9V0W1X4 \
2 -H "Authorization: Bearer sk_example_123456789"
response
1HTTP 200
2{
3 "object": "role",
4 "id": "role_01HQ8ZK3M4N5P6R7S8T9V0W1X4",
5 "name": "Billing admin",
6 "slug": "billing-admin",
7 "description": "Can manage invoices and payment methods, nothing else",
8 "type": "EnvironmentRole",
9 "permissions": ["invoices:read", "invoices:write", "payment-methods:write"],
10 "created_at": "2026-05-02T11:41:09.006Z",
11 "updated_at": "2026-05-02T11:41:09.006Z"
12}

Update a role

The permissions array replaces the previous one rather than merging into it. Existing sessions keep the permissions they were issued with until they refresh, so a permission you remove here is not gone from every browser the instant this call returns.

PUT /roles/:id
1curl -X PUT https://api.paycux.com/roles/role_01HQ8ZK3M4N5P6R7S8T9V0W1X4 \
2 -H "Authorization: Bearer sk_example_123456789" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "permissions": ["invoices:read", "invoices:write"]
6 }'
response
1HTTP 200
2{
3 "object": "role",
4 "id": "role_01HQ8ZK3M4N5P6R7S8T9V0W1X4",
5 "name": "Billing admin",
6 "slug": "billing-admin",
7 "permissions": ["invoices:read", "invoices:write"],
8 "updated_at": "2026-06-21T09:12:38.441Z"
9}

Delete a role

A role still held by a membership cannot be deleted; the call returns 409 with the number of memberships in the way. Reassign those people first, which is deliberate friction — deleting a role out from under a live session is a good way to lock a customer's admin out of their own account.

DELETE /roles/:id
1curl -X DELETE https://api.paycux.com/roles/role_01HQ8ZK3M4N5P6R7S8T9V0W1X4 \
2 -H "Authorization: Bearer sk_example_123456789"
response
1HTTP 409
2{
3 "code": "role_in_use",
4 "message": "This role is assigned to 12 memberships and cannot be deleted.",
5 "request_id": "req_01HQ8ZK3M4N5P6R7S8T9V0W1X2"
6}

Assign a role to a membership

Roles are assigned per membership, not per user, so this is the call that grants somebody admin rights in one tenant while leaving them an ordinary member in another. The change takes effect for that person at their next token refresh.

PUT /memberships/:id
1curl -X PUT https://api.paycux.com/memberships/om_01HQ8ZK3M4N5P6R7S8T9V0W1X2 \
2 -H "Authorization: Bearer sk_example_123456789" \
3 -H "Content-Type: application/json" \
4 -d '{ "role_slug": "billing-admin" }'
response
1HTTP 200
2{
3 "object": "organization_membership",
4 "id": "om_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
5 "user_id": "user_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
6 "organization_id": "org_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
7 "role": { "slug": "billing-admin" },
8 "status": "active",
9 "updated_at": "2026-06-21T09:15:02.118Z"
10}

Errors

A duplicate slug returns 422. Deleting a role that is still assigned returns 409 with the membership count. Assigning a slug that does not exist in the environment returns 422 rather than creating it, so a typo fails loudly instead of granting nothing.

Pagination

GET /roles is cursor paginated, though most environments have fewer than a dozen roles and never see a second page. Membership lists, which do grow, are paginated on the organizations resource.

authorize.ts
1// Yetki kontrolu oturumdan okunur — ag cagrisi yok.
2function can(session, permission) {
3 return session.permissions.includes(permission);
4}
5
6if (!can(session, 'invoices:write')) {
7 return new Response('Forbidden', { status: 403 });
8}