IAM API

Manage members and invitations of an organization workspace. Uses an iam key bound to the workspace. IAM applies only to organization workspaces — calls with a key bound to a personal workspace return 400 not_an_org_workspace.

Base   https://dev.bodek.us/v1/iam
Auth   Authorization: Bearer bf_live_…

Reading the member list needs only iam:read. Every other endpoint also requires the key's owner to be a workspace admin or the owner (403 not_admin otherwise); changing a member's role is owner-only. The key acts with its creator's current role, and stops working if the creator leaves the workspace or is disabled. Responses use the same data envelope, error format and X-RateLimit-* headers as the Files API.

List members

GET /v1/iam/members

Scope iam:read. Returns active members and pending invitations.

curl https://dev.bodek.us/v1/iam/members \
  -H "Authorization: Bearer bf_live_XXXX"
{
  "data": [
    { "user_id": 3,  "email": "owner@example.com", "name": null, "role": "owner",  "status": "active",  "joined_at": "2026-01-15 09:30:00" },
    { "user_id": 51, "email": "new@example.com",   "name": null, "role": "member", "status": "pending", "joined_at": null }
  ]
}

For a pending invitation there is no user yet, so user_id holds the invitation's id — the same value as invite_id in GET /v1/iam/invites.

Invite a member

POST /v1/iam/members

Scope iam:write, admin or owner. Emails an invitation link valid for 14 days; the person joins when they accept it. Inviting an address that already has a pending invitation re-sends it with a fresh link.

Because invitations send email, they are rate limited: at most 3 per recipient per workspace per day, 20 per inviting user per hour and 100 per workspace per day (429 invite_rate_limited, with Retry-After), and a workspace can have at most 200 pending invitations (429 too_many_pending_invites).

curl -X POST https://dev.bodek.us/v1/iam/members \
  -H "Authorization: Bearer bf_live_XXXX" -H "Content-Type: application/json" \
  -d '{ "email": "teammate@example.com", "role": "member" }'
{ "data": { "email": "teammate@example.com", "role": "member", "status": "invited", "invited_at": "2026-09-23T10:00:00+00:00" } }

Returns 201. role may be member (default) or admin; the person joins with that role. Only the workspace owner can invite as admin (403 owner_required). Errors: 400 invalid_email, 400 invalid_role, 400 invite_failed (e.g. already an active member), 403 owner_required, 429 invite_rate_limited, 429 too_many_pending_invites.

Change a role

PATCH /v1/iam/members/{user_id}

Scope iam:write; owner only (403 owner_required for admins and members, matching the web app). Body { "role": "admin" } or { "role": "member" }. {user_id} is the member's user id (as in List members). The owner's role can't be changed, and only active members can be promoted. Fires member.role_changed.

curl -X PATCH https://dev.bodek.us/v1/iam/members/42 \
  -H "Authorization: Bearer bf_live_XXXX" -H "Content-Type: application/json" \
  -d '{ "role": "admin" }'
{ "data": { "user_id": 42, "role": "admin" } }

Errors: 400 invalid_role, 400 role_change_failed, 403 owner_required, 404 member_not_found.

Remove a member

DELETE /v1/iam/members/{user_id}

Scope iam:write, admin or owner. Returns 204 No Content and fires member.removed. The owner can't be removed, and neither can the user who owns the calling key (400 cannot_remove_self). Other errors: 400 remove_failed, 404 member_not_found. The removed member's API keys for this workspace stop working immediately.

Pending invitations

List pending invitations

GET /v1/iam/invites

Scope iam:read, admin or owner. Email invitations that haven't been accepted yet, newest first.

curl https://dev.bodek.us/v1/iam/invites \
  -H "Authorization: Bearer bf_live_XXXX"
{
  "data": [
    {
      "invite_id": 51,
      "email": "new@example.com",
      "role": "member",
      "status": "pending",
      "invited_at": "2026-09-20 14:02:11",
      "expires_at": "2026-10-04 14:02:11",
      "expired": false
    }
  ]
}

An expired invitation stays listed until it's cancelled or re-sent (re-send with POST /v1/iam/members).

Cancel an invitation

DELETE /v1/iam/invites/{invite_id}

Scope iam:write, admin or owner. The emailed link stops working. Returns 204. Errors: 404 invite_not_found (unknown id, or already accepted).

Invite codes

Invite codes are the shareable …/join/{code} links from the web app. Each code works once: the first signed-in person to use it joins the workspace as a member. All three endpoints require an admin or owner.

List active codes

GET /v1/iam/invite-codes

Scope iam:read. Unused codes, newest first.

{
  "data": [
    { "code": "K7QW3MPX", "link": "https://files.bodek.us/join/K7QW3MPX", "single_use": true, "created_at": "2026-09-23 10:00:00" }
  ]
}

Create a code

POST /v1/iam/invite-codes

Scope iam:write. No body. Returns 201 with the new code. A workspace can have at most 50 unused codes (429 too_many_invite_codes).

curl -X POST https://dev.bodek.us/v1/iam/invite-codes \
  -H "Authorization: Bearer bf_live_XXXX"

Revoke a code

DELETE /v1/iam/invite-codes/{code}

Scope iam:write. Returns 204. Errors: 404 invite_code_not_found (unknown or already used).

Using a files key

A files key that also carries iam:read / iam:write (or *) can reach the same endpoints under its own workspace:

/v1/files/workspaces/{id}/members[/{user_id}]
/v1/files/workspaces/{id}/invites[/{invite_id}]
/v1/files/workspaces/{id}/invite-codes[/{code}]

{id} must be the workspace the key is bound to; any other id returns 403 cross_workspace. See Files API → Members & invites.

Scopes

ScopeGrants
iam:readList members; list pending invitations and invite codes (admin/owner).
iam:writeInvite, change roles, remove members; cancel invitations; create and revoke invite codes.

Errors

StatusCodeMeaning
400not_an_org_workspaceThe key is bound to a personal workspace.
401missing_authorization, invalid_token, …See authentication errors.
403wrong_serviceNot an iam key (use the files-key routes instead).
403insufficient_scopeMissing iam:read / iam:write.
403not_adminThe key's owner isn't an admin or the owner.
403owner_requiredRole changes and admin invitations are owner-only.
404workspace_not_found, member_not_found, invite_not_found, invite_code_not_foundNothing with that id.
405method_not_allowedWrong HTTP method for the path.
401token_orphaned, account_disabledThe key's creator left the workspace or was disabled.
429rate_limited, invite_rate_limited, too_many_pending_invites, too_many_invite_codesKey rate limit or invitation limits exceeded.