Accounts
Accounts
An account is the tenancy boundary: it owns workspaces, members, invitations, and billing. Every route below requires the authenticated user to be a member of the account, except POST /accounts (creating a brand new account, which has no members yet).
GET /accounts/me
Get the current authenticated user's account profile.
Authentication: JWT Bearer token
Request
No path or query parameters. The account is resolved from the authenticated session (req.user.chainerId).
Response
Response Example
{
"data": {
"id": "acc-1234-5678",
"slug": "alices-team",
"legalName": "Alice's Team, Inc.",
"displayName": "Alice's Team",
"status": "active",
"website": "https://alicesteam.example.com",
"industry": "Software",
"billingEmail": "billing@alicesteam.example.com",
"defaultTimezone": "America/New_York",
"avatarUrl": null,
"createdAt": "2026-01-15T08:00:00.000Z",
"updatedAt": "2026-03-10T14:30:00.000Z"
},
"meta": null,
"error": null
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Account ID |
slug | string | URL-safe account identifier, unique |
legalName | string | Legal/organisation name |
displayName | string | null | Display name shown in UI, falls back to legalName when unset |
status | string | null | active, suspended, closed, deletion_approved, or hard_deleted |
website | string | null | Organisation website URL |
industry | string | null | Free-text industry label |
billingEmail | string | null | Email address used for billing notices |
defaultTimezone | string | null | IANA timezone used for scheduling defaults |
avatarUrl | string | null | Account avatar/logo URL |
createdAt | string | null | ISO 8601 timestamp of account creation |
updatedAt | string | null | ISO 8601 timestamp of last update |
Code Examples
curl "https://api.chainabit.com/api/v1/accounts/me" \
-H "Authorization: Bearer $TOKEN"const response = await fetch(`${BASE_URL}/accounts/me`, {
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
const data = await response.json();import requests
response = requests.get(
f"{BASE_URL}/accounts/me",
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = response.json()POST /accounts
Create a new organisation account owned by the authenticated user. Unlike every other route in this section, this one has no membership requirement -- there is no account yet to be a member of. The response includes the id of a workspace created for you by default, so a client always has somewhere to land immediately after creation.
Authentication: JWT Bearer token
Request
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
slug | string | Yes | 3-64 chars, ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$ | URL-safe account identifier, unique |
legalName | string | Yes | 2-180 chars | Legal/organisation name |
displayName | string | No | Max 180 chars | Display name shown in UI |
website | string | No | Valid URL | Organisation website |
industry | string | No | Max 120 chars | Free-text industry label |
billingEmail | string | No | Valid email | Email address used for billing notices |
defaultTimezone | string | No | Max 120 chars | IANA timezone used for scheduling defaults |
avatarUrl | string | No | Valid URL | Account avatar/logo URL |
There is deliberately no status field: a newly created account always starts active.
Response
Response Example
{
"data": {
"id": "acc-1234-5678",
"slug": "alices-team",
"legalName": "Alice's Team, Inc.",
"displayName": "Alice's Team",
"status": "active",
"website": null,
"industry": null,
"billingEmail": null,
"defaultTimezone": null,
"avatarUrl": null,
"accountType": "organization",
"defaultWorkspaceId": "wksp-0001",
"ownerChainerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"createdAt": "2026-03-17T11:00:00.000Z",
"updatedAt": "2026-03-17T11:00:00.000Z"
},
"meta": null,
"error": null
}Response Fields
All fields from the account object above, plus:
| Field | Type | Description |
|---|---|---|
accountType | string | Type of account created: always organization for this route |
defaultWorkspaceId | string | ID of the workspace created by default for this account |
ownerChainerId | string | User ID of the account owner (the authenticated caller) |
Code Examples
curl -X POST "https://api.chainabit.com/api/v1/accounts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "alices-team",
"legalName": "Alice'"'"'s Team, Inc."
}'const response = await fetch(`${BASE_URL}/accounts`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
slug: "alices-team",
legalName: "Alice's Team, Inc.",
}),
});
const data = await response.json();import requests
response = requests.post(
f"{BASE_URL}/accounts",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"slug": "alices-team", "legalName": "Alice's Team, Inc."},
)
data = response.json()GET /accounts/:id
Get an account by ID. The authenticated user must be a member of the account.
Authentication: JWT Bearer token (must be an account member)
Request
Use the account id from GET /accounts/me -- don't copy the example value below.
The account returned is always the one the caller is actually a member of, never an arbitrary account looked up by the id you pass. Passing an id you are not a member of returns a 403 rather than someone else's account data.
Response
Response Example
{
"data": {
"id": "acc-1234-5678",
"name": "Alice's Team",
"slug": "alices-team",
"role": "owner",
"createdAt": "2026-01-15T08:00:00.000Z",
"updatedAt": "2026-03-10T14:30:00.000Z"
},
"meta": null,
"error": null
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Account ID |
name | string | Account display name |
slug | string | URL-safe account identifier |
role | string | Authenticated user's role in this account (owner, admin, member) |
createdAt | string | ISO 8601 timestamp of account creation |
updatedAt | string | ISO 8601 timestamp of last update |
Code Examples
curl "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID" \
-H "Authorization: Bearer $TOKEN"const response = await fetch(`${BASE_URL}/accounts/${ACCOUNT_ID}`, {
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
const data = await response.json();import requests
response = requests.get(
f"{BASE_URL}/accounts/{ACCOUNT_ID}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = response.json()PATCH /accounts/:id
Update an account's name. Requires owner or admin role.
Authentication: JWT Bearer token + Owner/Admin role required
Request
All fields are optional; send only what changes.
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
slug | string | No | 3-64 chars, ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$ | URL-safe account identifier |
legalName | string | No | 2-180 chars | Legal/organisation name |
displayName | string | No | Max 180 chars | Display name shown in UI |
status | string | No | "active" or "suspended" only | Account status. Terminal states (closed, deletion_approved, hard_deleted) cannot be set here -- they belong to the account deletion flow |
website | string | No | Valid URL | Organisation website |
industry | string | No | Max 120 chars | Free-text industry label |
billingEmail | string | No | Valid email | Email address used for billing notices |
defaultTimezone | string | No | Max 120 chars | IANA timezone used for scheduling defaults |
avatarUrl | string | No | Valid URL | Account avatar/logo URL |
Response
Response Example
{
"data": {
"id": "acc-1234-5678",
"slug": "alices-team",
"legalName": "Alice and Team, Inc.",
"displayName": "Alice and Team",
"status": "active",
"website": null,
"industry": null,
"billingEmail": null,
"defaultTimezone": null,
"avatarUrl": null,
"createdAt": "2026-01-15T08:00:00.000Z",
"updatedAt": "2026-03-17T11:00:00.000Z"
},
"meta": null,
"error": null
}Response Fields
Same shape as the account object returned by GET /accounts/me.
Code Examples
curl -X PATCH "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Alice and Team"
}'const response = await fetch(`${BASE_URL}/accounts/${ACCOUNT_ID}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
displayName: "Alice and Team",
}),
});
const data = await response.json();import requests
response = requests.patch(
f"{BASE_URL}/accounts/{ACCOUNT_ID}",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"displayName": "Alice and Team"},
)
data = response.json()DELETE /accounts/:id
Suspend an account (soft delete). The account's status becomes suspended; no data is erased. Once the account is suspended, its members, including owners, lose access to it, and its API keys are refused. Requires owner role.
This route does not permanently delete an account. See Current limitations.
Authentication: JWT Bearer token + Owner role required
Request
No path or query parameters beyond the account id.
Response
Response Example
{
"data": {
"id": "acc-1234-5678",
"slug": "alices-team",
"legalName": "Alice's Team, Inc.",
"displayName": "Alice's Team",
"status": "suspended",
"website": null,
"industry": null,
"billingEmail": null,
"defaultTimezone": null,
"avatarUrl": null,
"createdAt": "2026-01-15T08:00:00.000Z",
"updatedAt": "2026-03-17T11:00:00.000Z"
},
"meta": null,
"error": null
}Response Fields
The account object above, with status set to suspended.
Code Examples
curl -X DELETE "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID" \
-H "Authorization: Bearer $TOKEN"const response = await fetch(`${BASE_URL}/accounts/${ACCOUNT_ID}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
const data = await response.json();import requests
response = requests.delete(
f"{BASE_URL}/accounts/{ACCOUNT_ID}",
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = response.json()Account Members
An account member is a chainer who already has a Chainabit user account and has been attached directly to this account. To bring in someone by email address who may not have a Chainabit account yet, use Account Invitations instead -- accepting an invitation is what creates the membership row these endpoints operate on.
Assignable roles are admin, analyst, billing, viewer, member. owner cannot be assigned through these endpoints -- ownership transfer requires a dedicated flow.
GET /accounts/:accountId/members
List all members of an account. Any member can view the roster, not just owner/admin.
Authentication: JWT Bearer token (must be an account member)
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Page size, up to 500 |
offset | number | No | Number of results to skip |
Response
Response Example
{
"data": [
{
"accountId": "acc-1234-5678",
"chainerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"role": "owner",
"status": "active",
"email": "alice@example.com",
"fullName": "Alice Johnson",
"username": "alice_j",
"avatarUrl": null,
"invitedBy": null,
"joinedAt": "2026-01-15T08:00:00.000Z"
},
{
"accountId": "acc-1234-5678",
"chainerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"role": "member",
"status": "active",
"email": "bob@example.com",
"fullName": "Bob Smith",
"username": "bob_dev",
"avatarUrl": null,
"invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"joinedAt": "2026-02-20T10:00:00.000Z"
}
],
"meta": {
"total": 2
},
"error": null
}Response Fields
| Field | Type | Description |
|---|---|---|
accountId | string | Account ID |
chainerId | string | Member's user ID |
role | string | Member's role (owner, admin, analyst, billing, viewer, member) |
status | string | invited (seat held, has not started working yet) or active |
email | string | null | Member's sign-in email address |
fullName | string | null | Member's full name |
username | string | null | Member's username |
avatarUrl | string | null | Member's avatar URL |
invitedBy | string | null | User ID of whoever added this member, null for the account owner |
joinedAt | string | null | ISO 8601 timestamp of when the member joined |
Code Examples
curl "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/members" \
-H "Authorization: Bearer $TOKEN"const response = await fetch(`${BASE_URL}/accounts/${ACCOUNT_ID}/members`, {
headers: {
Authorization: `Bearer ${TOKEN}`,
},
});
const data = await response.json();import requests
response = requests.get(
f"{BASE_URL}/accounts/{ACCOUNT_ID}/members",
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = response.json()POST /accounts/:accountId/members
Attach an existing chainer to an account by their chainerId. Requires owner or admin role. This is not an email invite -- the caller must already know the target's chainerId (e.g. from a prior workspace or a shared organisation). To bring in someone by email address, use POST /accounts/:accountId/invitations instead.
Authentication: JWT Bearer token + Owner/Admin role required
Request
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
chainerId | string | Yes | UUID | User ID of the chainer to add |
role | string | No | One of the assignable roles above, defaults to member | Role to assign |
Response
Response Example
{
"data": {
"accountId": "acc-1234-5678",
"chainerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"role": "member",
"status": "active",
"email": "carol@example.com",
"fullName": "Carol Nguyen",
"username": "carol_n",
"avatarUrl": null,
"invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"joinedAt": "2026-03-17T11:00:00.000Z"
},
"meta": null,
"error": null
}Response Fields
Same shape as the member object above.
Code Examples
curl -X POST "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/members" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chainerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"role": "member"
}'const response = await fetch(`${BASE_URL}/accounts/${ACCOUNT_ID}/members`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
chainerId: "c3d4e5f6-a7b8-9012-cdef-123456789012",
role: "member",
}),
});
const data = await response.json();import requests
response = requests.post(
f"{BASE_URL}/accounts/{ACCOUNT_ID}/members",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"chainerId": "c3d4e5f6-a7b8-9012-cdef-123456789012", "role": "member"},
)
data = response.json()PATCH /accounts/:accountId/members/:chainerId
Update a member's role in an account. Requires owner or admin role.
Authentication: JWT Bearer token + Owner/Admin role required
Request
chainerId is the member's user ID, returned as chainerId in the GET /accounts/:accountId/members response -- not the literal value shown below.
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
role | string | Yes | One of the assignable roles above | New role for the member |
Response
Response Example
{
"data": {
"accountId": "acc-1234-5678",
"chainerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"role": "admin",
"status": "active",
"email": "bob@example.com",
"fullName": "Bob Smith",
"username": "bob_dev",
"avatarUrl": null,
"invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"joinedAt": "2026-02-20T10:00:00.000Z"
},
"meta": null,
"error": null
}Response Fields
Same shape as the member object above.
Code Examples
curl -X PATCH "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/members/$CHAINER_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"role": "admin"
}'const response = await fetch(
`${BASE_URL}/accounts/${ACCOUNT_ID}/members/${CHAINER_ID}`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ role: "admin" }),
}
);
const data = await response.json();import requests
response = requests.patch(
f"{BASE_URL}/accounts/{ACCOUNT_ID}/members/{CHAINER_ID}",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"role": "admin"},
)
data = response.json()DELETE /accounts/:accountId/members/:chainerId
Remove a member from an account. Requires owner or admin role. Also revokes the enterprise API keys the member created in this account and removes their memberships of this account's workspaces.
Removal affects only this account. The person is not signed out: their session keeps working for their personal account and for any other account they belong to. Their access to this account ends from their next request; a request that still selects this account or one of its workspaces (with the x-account-id or x-workspace-id header) is refused with 403, not 401.
Authentication: JWT Bearer token + Owner/Admin role required
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
releaseSeat | boolean | No | Release the vacated seat back to the subscription. Prorated by the payment provider, so it is never inferred -- omit it (or send anything other than the literal true) to leave billing untouched. Defaults to false. |
Response
Response Example
{
"data": {
"accountId": "acc-1234-5678",
"chainerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"role": "member",
"status": "active",
"email": "bob@example.com",
"fullName": "Bob Smith",
"username": "bob_dev",
"avatarUrl": null,
"invitedBy": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"joinedAt": "2026-02-20T10:00:00.000Z",
"revokedApiKeys": [
{ "id": "key-01", "name": "CI deploy key", "keyPrefix": "cb_live_", "lastFour": "a1b2" }
],
"removedWorkspaceMemberships": 3,
"revokedSessions": 0,
"seatRelease": { "status": "not_requested" }
},
"meta": null,
"error": null
}Response Fields
Member fields as in GET /accounts/:accountId/members, plus:
| Field | Type | Description |
|---|---|---|
revokedApiKeys | array | Enterprise API keys owned by this member that were revoked as part of removal (id, name, keyPrefix, lastFour) |
removedWorkspaceMemberships | number | Count of workspace memberships removed along with this account membership |
revokedSessions | number | Always 0. Removal does not end the person's sessions. Kept for compatibility. |
seatRelease.status | string | not_requested (no releaseSeat sent), requested (billing provider accepted a lower seat quantity -- actual billing change confirmed asynchronously by webhook), or a reason the release did not happen |
Code Examples
curl -X DELETE "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/members/$CHAINER_ID?releaseSeat=true" \
-H "Authorization: Bearer $TOKEN"const response = await fetch(
`${BASE_URL}/accounts/${ACCOUNT_ID}/members/${CHAINER_ID}?releaseSeat=true`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${TOKEN}`,
},
}
);
const data = await response.json();import requests
response = requests.delete(
f"{BASE_URL}/accounts/{ACCOUNT_ID}/members/{CHAINER_ID}",
headers={"Authorization": f"Bearer {TOKEN}"},
params={"releaseSeat": "true"},
)
data = response.json()Account Audit Logs
GET /accounts/:id/logs
List audit events for an account. Requires owner or admin role.
Authentication: JWT Bearer token + Owner/Admin role required
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Number of results to return (default: 20) |
cursor | string | No | Pagination cursor from a previous response |
Response
Response Example
{
"data": [
{
"id": "log-0001",
"action": "member.invited",
"actorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"actorUsername": "alice_j",
"targetId": "carol@example.com",
"metadata": {
"role": "member"
},
"createdAt": "2026-03-17T11:00:00.000Z"
},
{
"id": "log-0002",
"action": "account.updated",
"actorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"actorUsername": "alice_j",
"targetId": "acc-1234-5678",
"metadata": {
"field": "name",
"oldValue": "Alice's Team",
"newValue": "Alice and Team"
},
"createdAt": "2026-03-17T11:00:00.000Z"
}
],
"meta": {
"total": 2,
"hasMore": false,
"cursor": null
},
"error": null
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Audit log entry ID |
action | string | Event type (e.g. "member.invited", "account.updated") |
actorId | string | User ID of the person who performed the action |
actorUsername | string | Username of the person who performed the action |
targetId | string | ID or identifier of the resource affected |
metadata | object | Additional context about the event; shape varies by action type |
createdAt | string | ISO 8601 timestamp of when the event occurred |
Code Examples
curl "https://api.chainabit.com/api/v1/accounts/$ACCOUNT_ID/logs?limit=20" \
-H "Authorization: Bearer $TOKEN"const response = await fetch(
`${BASE_URL}/accounts/${ACCOUNT_ID}/logs?limit=20`,
{
headers: {
Authorization: `Bearer ${TOKEN}`,
},
}
);
const data = await response.json();import requests
response = requests.get(
f"{BASE_URL}/accounts/{ACCOUNT_ID}/logs",
headers={"Authorization": f"Bearer {TOKEN}"},
params={"limit": 20},
)
data = response.json()Current limitations
- Permanent deletion is available only for personal accounts. An account deletion request permanently erases a personal account and is accepted only from that account's holder. A deletion request for an Organization account (every account created with
POST /accounts) is refused with403and error codeORGANIZATION_DELETION_UNSUPPORTED. A request from anyone other than the holder of a personal account is refused with403andDELETION_REQUESTER_NOT_HOLDER. Suspending an account withDELETE /accounts/:iddoes not erase it. - An approved deletion locks the account. A deletion request can be cancelled until it is approved. Once it is approved, the account is locked, its holder is signed out and cannot sign in, the request can no longer be cancelled through the API, and the account is permanently erased after a 30-day quarantine.