Members and invitations
Developer·4 minutes to read
Verified against docs/superpowers/specs/2026-09-15-p4-a-typed-http-api-openapi-webhooks.md §4.3, §4.4, §5.1, §5.2, §6; re-verified against the code at close-out.
What it does
The members of the organisation your key belongs to, and the invitations that are still open: list them, change a role, remove a person, invite one, revoke an invitation (F-ID-5 — every run carries an actor_user_id, and a bot never has an identity of its own).
Authentication
GET /members, GET /members/{userId} and GET /invitations need a key of scope read or wider. Every write on this page needs admin. Each write is passed to the repository as a human actor — the admin the key is bound to — so the same role rules that refuse a change in the app refuse it here. See Authentication and keys.
Request
GET /members
<!-- generated:begin GET /members --> <!-- generated:end GET /members -->GET /members/{userId}
<!-- generated:begin GET /members/{userId} --> <!-- generated:end GET /members/{userId} -->PATCH /members/{userId}
<!-- generated:begin PATCH /members/{userId} --> <!-- generated:end PATCH /members/{userId} -->DELETE /members/{userId}
<!-- generated:begin DELETE /members/{userId} --> <!-- generated:end DELETE /members/{userId} -->GET /invitations
<!-- generated:begin GET /invitations --> <!-- generated:end GET /invitations -->POST /invitations
<!-- generated:begin POST /invitations --> <!-- generated:end POST /invitations -->DELETE /invitations/{id}
<!-- generated:begin DELETE /invitations/{id} --> <!-- generated:end DELETE /invitations/{id} -->A role is owner, admin or member on a change; an invitation's role is admin or member and defaults to member. Two refusals are worth knowing before you script against this page: the last owner cannot be demoted, and a person cannot remove themself. Both answer forbidden with the product's own fixed sentence.
An invitation created here moves a seat exactly as one created in the app does, and the token is not returned. The invitee receives it by mail, on the same path the app's invite uses, so a script never holds another person's credential.
Two further refusals come straight from the repository and are worth designing around. A personal organisation has no members to manage, so every verb on this page refuses there; and an invitation is refused once the organisation is at its ceiling of open invitations (50 by default) or at its ceiling of live members plus open invitations (200 by default). An operator can raise either number for one organisation.
The access-request flow — a colleague asking for access from the sign-in screen, and the admin queue that answers it (F-ID-8) — has no route on this surface. It stays in the app.
curl -sS "$BOTSEON_API_ORIGIN/api/v1/members?limit=50" \
-H "authorization: Bearer $BOTSEON_API_KEY" \
-H "accept: application/json"
const res = await fetch(`${process.env.BOTSEON_API_ORIGIN}/api/v1/members?limit=50`, {
headers: {
authorization: `Bearer ${process.env.BOTSEON_API_KEY}`,
accept: 'application/json',
},
});
if (!res.ok) throw new Error(`members: ${res.status}`);
const page = (await res.json()) as {
data: Array<{ userId: string; email: string; displayName: string | null; role: string }>;
nextCursor: string | null;
};
for (const member of page.data) console.log(member.role, member.email);
Response
{
"data": [
{
"userId": "8c1d4e5f-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
"email": "ida@example.test",
"displayName": "Ida",
"role": "admin",
"createdAt": "2026-03-04T09:12:00.000Z"
}
],
"nextCursor": null
}
An invitation row carries its id, the e-mail, the role, when it was created, when it expires, and when it was accepted or revoked — and no token.
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | unauthenticated | The credential is missing, refused, or below admin on a write. | Use an admin key for the writes. |
| 403 | forbidden | The last owner would be demoted, a person is removing themself, or the organisation is a personal one and has no members to manage. | Promote another owner first, ask a second admin, or use a team organisation. |
| 400 | invalid_request | The role is not one of the accepted values, the e-mail is not an address, or the POST carries no Idempotency-Key. | Read details. |
| 404 | not_found | No such member or invitation in this organisation. | List first. |
| 402 | payment_required | The organisation needs a trial or a payment before a seat moves. | Settle the account, then retry. |
| 429 | quota_exceeded | The organisation is at its member ceiling, or at its ceiling of open invitations. | Revoke an open invitation, or remove a member, then retry. |
| 429 | rate_limited | A bucket is full. | Wait for Retry-After. |
Rate limits
Every call on this page is counted against three one-minute buckets: 300 requests per key, 1 000 per organisation, and — for the platform-key routes — 60 per platform key. A full bucket answers rate_limited with Retry-After: 60; wait that many seconds and retry. Failed authentication is counted separately, against the caller's address, so a working key's allowance is never spent by somebody else's bad credential. See Envelopes, errors, idempotency and paging.
Last verified against build c0f77aa.