Auth API

building

Workspaces inside your app — create them, set roles, invite people, and fill in org_id.

Organizations

Workspaces inside your app — a company, a team, a tenant of yours. Not to be confused with your own xplatform organization; these live in your project's database and we never see them.

POST/v1/auth/organization/create

Publishable key and a session cookie.

Request
1curl -X POST "$BASE/v1/auth/organization/create" \2  -H "authorization: Bearer $PK" \3  -H "content-type: application/json" \4  -b "session cookie" \5  -d '{"name":"Acme Engineering","slug":"acme-eng"}'

Whoever creates one is its owner, and it becomes their active organization.

The roles

organizationmembersinvitations
ownerupdate, deleteadd, update, removesend, cancel
adminupdateadd, update, removesend, cancel
memberread onlyread onlyread only

A member attempting any of it gets a 403, which we checked rather than assumed.

It fills in org_id

The JWT has carried org_id and role since the first day this service existed, and until organizations shipped both were always null. That was deliberate — a consumer written against those claims last month keeps working today without touching their code, which is the thing that bites teams who add the claim afterwards.

json
{ "sub": "Nrzmwy…", "org_id": "2kbhx7…", "role": "owner", "sid": "…" }

org_id is the active organization on the session, because one person can belong to several and switch without their identity changing. Set it with POST /v1/auth/organization/set-active.

role is read from the membership at the moment the token is minted, not copied onto the session at sign-in — so demoting an admin takes effect on their next token rather than their next sign-in.

POST/v1/auth/organization/invite-member
Request
1curl -X POST "$BASE/v1/auth/organization/invite-member" \2  -H "authorization: Bearer $PK" \3  -H "content-type: application/json" \4  -b "session cookie" \5  -d '{"email":"grace@example.com","role":"member"}'

The page it lands on is yours. We deliver the mail; your app renders the accept screen and calls accept-invitation. Point it somewhere with the invitationUrl setting; the invitation id is appended:

invitationUrl = https://app.acme.com/join   →   https://app.acme.com/join/<id>

Unset, it guesses at /accept-invitation on your first registered origin, so invitations work before you have configured anything.

An invitation whose email fails still returns 200. The row is written before the mail is attempted, so a bounced or refused message does not roll it back. GET /v1/auth/organization/list-invitations gives you the id, and the link is your URL plus that id — so a re-send is always possible without creating a second invitation.

Errors

StatusCodeMeans
403—A member attempting an owner or admin action
400ORGANIZATION_ALREADY_EXISTSThat slug is taken within your project
404—No such organization, or the caller is not in it