Auth API
buildingWorkspaces 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.
Publishable key and a session cookie.
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
| organization | members | invitations | |
|---|---|---|---|
owner | update, delete | add, update, remove | send, cancel |
admin | update | add, update, remove | send, cancel |
member | read only | read only | read 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.
{ "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.
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
| Status | Code | Means |
|---|---|---|
403 | — | A member attempting an owner or admin action |
400 | ORGANIZATION_ALREADY_EXISTS | That slug is taken within your project |
404 | — | No such organization, or the caller is not in it |