AuralisDocs
OperationsInvitations

Invite someone to the workspace at a named role

Invite one address at one role, or resend if one is already pending.

/v1/workspace/invitationsAdmin
Operation id
issue_invitation
Access
An editor or a viewer is refused.
POST/v1/workspace/invitations

Invite one address at one role, or resend if one is already pending.

Sends the invitation email and returns the record. The email carries the only copy of the link: the token is not in this response, is not in the list, and cannot be fetched later. If it does not arrive, resend.

role is required and has no default, because the role is named in the email the invitee reads. You cannot invite anyone above your own role.

Inviting an address that already has a pending invitation is a resend, not a conflict. It rotates the token, takes the role and issuer from this request, and resets the expiry. resent in the response says which of the two happened. delivery reports what became of the message: a failed state with last_error means the record exists and the mail did not go, so the fix is to resend rather than to invite again.

The response is the same shape whether or not the address already has an account here. That is deliberate and nothing in it can be read to find out.

Issuing is rate limited per workspace, and a resend is charged as an issue.

assert_may_grant is the existing escalation rule and is reused verbatim (FR-88): nobody may invite above their own role, so an invitation can never be a route around the role control. That is also the first of the two guards FR-121's promote-on-acceptance ships with, and the reason a promotion at acceptance cannot exceed what its issuer held when they chose it.

One statement, upserted onto the pending slot. 0073's partial unique index over (tenant_id, email) WHERE accepted_at IS NULL AND revoked_at IS NULL is inferred here as the conflict target, so re-inviting a pending address is a resend (FR-90) atomically rather than through a SELECT-then-INSERT two admins can both pass.

A resend takes the new request's role and issuer rather than keeping the old ones. Both are deliberate: the admin has just stated a role and assert_may_grant has just bound it, so keeping the old one would grant something nobody asked for; and FR-101 re-reads the issuer's authority live at acceptance, so the issuer recorded has to be the person who most recently chose this invitation, not whoever happened to be first.

Nothing here reads users. See the module docstring: FR-89 is a property of the query that is not written.

Tenancy: the request is scoped to one workspace, and the credential presented is what selects it. A caller with no membership in that workspace is refused with 404, which is indistinguishable from a workspace that does not exist.

Authorisation: admin or owner. An editor or viewer receives 403 insufficient_role.

Access: Authorization: Bearer carrying either a realm access token or an API key. A key must hold the invitations:write scope, and the workspace role of the person who owns it must meet the floor above.

Authorization

AuthorizationBearer <token>

A realm-issued access token, presented as Authorization: Bearer <token>. It acts as the person who signed in, and every operation resolves their workspace role live from the membership record.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

curl -X POST "https://example.com/v1/workspace/invitations" \  -H "Content-Type: application/json" \  -d '{    "email": "sam.okafor@northwind.example",    "role": "editor"  }'
{  "id": "3a9c7e12-58bd-4f06-9a44-2c7e1b0d8f31",  "email": "sam.okafor@northwind.example",  "role": "editor",  "state": "pending",  "expires_at": "2026-08-21T10:04:00+00:00",  "created_at": "2026-08-14T10:04:00+00:00",  "invited_by": {    "id": "c0f4a7d3-1b62-4e58-8d90-6f3a2c5e7b18",    "display_name": "Dana Osei",    "email": "dana.osei@northwind.example"  },  "delivery": {    "state": "sent",    "attempts": 1,    "message_id": "0100019180a4c1f2-9f0a4a2e-1e4c-4d31-9a77-5b8c2d1e0f44-000000",    "last_sent_at": "2026-08-14T10:04:01+00:00"  },  "resent": false}