//Org Invitations

Org Invitations

Org admins can invite people to join their organization by issuing single-use, tokenized invitation links. Tokens are stored hashed on the server — the plaintext is shown to the inviter exactly once, on creation, and the inviter must capture it then (or share the bundled signup_url).

This is how a multi-person company onboards onto ButtrBase. The first member creates the org as part of their own signup (org_choice: { type: "create", ... }); subsequent members are added via invitations issued from inside the org.

---

1. Create an invitation (admin only)

Endpoint: POST /api/organizations/{org_uuid}/invitations

Caller must be authenticated and be a member of the org with role admin or owner.

curl -X POST https://api.buttrbase.com/api/organizations//invitations \
  -H "Authorization: Bearer " \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "role": "member",
    "expires_in_hours": 168
  }'

All three body fields are optional. Defaults:

  • email: null (the invitation is an "open link" — anyone with the token can use it; useful for #onboarding channels)
  • role: "member"
  • expires_in_hours: 168 (7 days). Server clamps to [1, 720].

Response (200):

{
  "data": {
    "id": 42,
    "org_uuid": "...",
    "email": "alice@example.com",
    "role": "member",
    "expires_at": "2026-06-10T15:00:00Z",
    "token": "Bd9...",
    "signup_url": "https://buttrbase.com/signup?invite=Bd9..."
  }
}

The token field is shown once. Server only stores SHA-256(token). Save the signup_url and share it with the invitee — that link drops them straight into the accept-invite path of the signup flow with the field prefilled.

---

2. Preview an invitation (public)

Before submitting, a signup page typically calls this to show "you're invited to Acme Inc as member":

Endpoint: GET /api/auth/invitations/{token}

{
  "data": {
    "org_uuid": "...",
    "org_name": "Acme Inc",
    "email": "alice@example.com",
    "role": "member",
    "expires_at": "2026-06-10T15:00:00Z",
    "valid": true,
    "invalid_reason": null
  }
}

If valid: false, invalid_reason is one of expired | accepted | revoked | not_found.

---

3. Accept an invitation

There are two accept paths depending on whether the user already has an account:

3a. Brand-new user

They consume the invitation as part of registration via org_choice:

{
  "org_choice": { "type": "accept_invite", "invitation_token": "Bd9..." }
}

posted to POST /api/auth/finalize-registration alongside the usual signup_token + email + password.

3b. Already-signed-in user joining an additional org

Endpoint: POST /api/auth/invitations/{token}/accept

(Authenticated.) Idempotent — accepting twice from the same user returns the same response without duplicating membership.

---

4. List + revoke

Endpoint: GET /api/organizations/{org_uuid}/invitations (admin only)

Returns all invitations for the org with their current status.

Endpoint: DELETE /api/organizations/{org_uuid}/invitations/{invitation_id} (admin only)

Marks the invitation revoked. Already-accepted invitations stay accepted; only pending invitations are revocable.