Users
Invites and org member management — all admin-gated. See Users and quotas for the CLI-level walkthrough and the blast-radius warning on removal.
POST /v1/invites — create an invite
Auth: admin.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | The invitee's email. |
role |
string | yes | "admin" or "user". |
ttl_seconds |
integer | no | Seconds until the invite expires. 0 defaults to 72 hours (259200). Must be between 900 (15m) and 2,592,000 (720h) if set — anything outside that range is rejected. |
$ curl -s https://api.stagdb.com/v1/invites -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","role":"user","ttl_seconds":604800}'
Response 201 Created
{"invite_token": "9c2f7e51d4a8b3660f1e8d2c5a9b4370c6e1f8a25d3b7c90e4f6a1d82b5c3e70"}
invite_token is a bare hex string, shown once and never retrievable
again — the server stores only its hash. This is the raw value that goes
into POST /v1/invites/accept's token field. The adj1.… self-contained
invite code you see in the CLI's output is a client-side wrapper the CLI
builds around this raw token (base64url-packing the server URL, org slug,
and this token) — the API itself never produces or consumes the adj1.…
form.
Inviting the same email again is not an error — each call creates an
additional pending invite (any of them can be accepted, and each is
individually revocable via DELETE /v1/invites/{id} below).
| Status | When |
|---|---|
| 400 | email empty, role not admin/user, or ttl_seconds outside 15m–720h — wire body is the generic {"error": "invalid input"} in all three cases. |
| 403 | Caller is not an admin. |
GET /v1/invites — list invites
Auth: admin.
$ curl -s https://api.stagdb.com/v1/invites -H "Authorization: Bearer $TOKEN"
Response 200 OK
{
"invites": [
{
"id": "1f2e3d4c-5b6a-4978-8c9d-0e1f2a3b4c5d",
"email": "[email protected]",
"role": "user",
"expires_at": "2026-07-26T21:03:12Z",
"accepted_at": null,
"created_at": "2026-07-19T21:03:12Z"
}
]
}
Includes both pending and already-accepted invites; accepted_at is null
until accepted.
| Status | When |
|---|---|
| 403 | Caller is not an admin. |
DELETE /v1/invites/{id} — revoke an invite
Auth: admin.
$ curl -s https://api.stagdb.com/v1/invites/1f2e3d4c-5b6a-4978-8c9d-0e1f2a3b4c5d -X DELETE \
-H "Authorization: Bearer $TOKEN" -o /dev/null -w '%{http_code}\n'
204
Invalidates a pending invite. Has no effect on an invite that's already been accepted (the user already exists — use disable/remove instead).
Response: 204 No Content.
| Status | When |
|---|---|
| 400 | {id} is not a valid UUID. |
| 403 | Caller is not an admin. |
| 404 | No such invite in this org. |
GET /v1/users — list members
Auth: admin.
$ curl -s https://api.stagdb.com/v1/users -H "Authorization: Bearer $TOKEN"
Response 200 OK
{
"users": [
{
"id": "7a1e9c3d-5b2f-4c8a-9d0e-2f3a4b5c6d7e",
"email": "[email protected]",
"display_name": "Alex Park",
"role": "admin",
"status": "active",
"created_at": "2026-06-02T14:03:11Z"
},
{
"id": "3e6f0a2b-7c8d-4e1f-9a2b-3c4d5e6f7a8b",
"email": "[email protected]",
"display_name": "Dana Lin",
"role": "user",
"status": "active",
"created_at": "2026-06-05T10:22:41Z"
}
]
}
| Status | When |
|---|---|
| 403 | Caller is not an admin. |
PATCH /v1/users/{id} — disable or enable a member
Auth: admin.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
status |
string | yes | "disabled" or "active". |
$ curl -s https://api.stagdb.com/v1/users/3e6f0a2b-7c8d-4e1f-9a2b-3c4d5e6f7a8b -X PATCH \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"status":"disabled"}'
Response 200 OK
{
"id": "3e6f0a2b-7c8d-4e1f-9a2b-3c4d5e6f7a8b",
"email": "[email protected]",
"role": "user",
"status": "disabled",
"created_at": "2026-06-05T10:22:41Z"
}
Disabling a member blocks future logins and revokes their existing sessions —
their running instances are untouched. This is the reversible hold; see
DELETE /v1/users/{id} below for the destructive alternative.
You cannot change your own status — an admin disabling themselves could
lock the org out via its last admin, so the request is rejected (a 400; on
the wire the body is the generic {"error": "invalid input"}). Have another
admin do it.
| Status | When |
|---|---|
| 400 | {id} not a valid UUID; status not active/disabled; or {id} is your own user id (self-status change rejected — generic "invalid input" body). |
| 403 | Caller is not an admin. |
| 404 | No such user in this org. |
DELETE /v1/users/{id} — remove a member
Auth: admin.
$ curl -s https://api.stagdb.com/v1/users/3e6f0a2b-7c8d-4e1f-9a2b-3c4d5e6f7a8b -X DELETE \
-H "Authorization: Bearer $TOKEN" -o /dev/null -w '%{http_code}\n'
202
This destroys every running instance the member owns. Removal revokes the member's access immediately and reaps (destroys) their live instances asynchronously in the data plane — there is no undo. It's safe to call twice: already-removed members and already-absent instances are tolerated, not re-errored.
Response: 202 Accepted, empty body — the removal and instance teardown
proceed asynchronously.
You cannot remove yourself — the request is rejected with a 400 (generic
{"error": "invalid input"} body on the wire), same rationale as the
self-status guard above.
| Status | When |
|---|---|
| 400 | {id} not a valid UUID, or {id} is your own user id (self-removal rejected — generic "invalid input" body). |
| 403 | Caller is not an admin. |
| 404 | No such user in this org. |