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": "[email protected]",
"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": "organisation",
"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 (e.g. organisation) |
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
Soft delete an account. This action is reversible within the retention period. Requires owner role.
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",
"deletedAt": "2026-03-17T11:00:00.000Z"
},
"meta": null,
"error": null
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Account ID |
deletedAt | string | ISO 8601 timestamp of when the account was soft-deleted |
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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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 any enterprise API keys the member owns, removes their workspace memberships, and revokes their active sessions.
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": "[email protected]",
"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": 1,
"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 | Count of active sessions revoked |
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": "[email protected]",
"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()