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
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
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
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
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
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
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
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
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
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
| Scope | Grants |
|---|---|
iam:read | List members; list pending invitations and invite codes (admin/owner). |
iam:write | Invite, change roles, remove members; cancel invitations; create and revoke invite codes. |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | not_an_org_workspace | The key is bound to a personal workspace. |
| 401 | missing_authorization, invalid_token, … | See authentication errors. |
| 403 | wrong_service | Not an iam key (use the files-key routes instead). |
| 403 | insufficient_scope | Missing iam:read / iam:write. |
| 403 | not_admin | The key's owner isn't an admin or the owner. |
| 403 | owner_required | Role changes and admin invitations are owner-only. |
| 404 | workspace_not_found, member_not_found, invite_not_found, invite_code_not_found | Nothing with that id. |
| 405 | method_not_allowed | Wrong HTTP method for the path. |
| 401 | token_orphaned, account_disabled | The key's creator left the workspace or was disabled. |
| 429 | rate_limited, invite_rate_limited, too_many_pending_invites, too_many_invite_codes | Key rate limit or invitation limits exceeded. |