Reference · Clients API
Clients API
The businesses a workspace works for. Members read; owners and admins write. Money — billing terms, cost to serve, expected fees — is returned to owners and admins only, and is null for everyone else. See Clients for the model.
Clients
GET/v1/workspaces/{workspaceId}/clientsList clients with this month’s roll-ups. Filter with status; includeArchived to see archived ones.
POST/v1/workspaces/{workspaceId}/clientsAdd a client. Takes a requestId: a retry returns the first answer. Owners and admins.
GET/v1/clients/{clientId}One client, its projects and roll-ups. Returns an ETag.
PATCH/v1/clients/{clientId}Change name, slug, status, billing or notes. Needs the etag (body or If-Match).
DELETE/v1/clients/{clientId}Archive. A client with history is never deleted.
GET/v1/clients/{clientId}/eventsIts status history, newest first.
create, with a safe retryjson
POST /v1/workspaces/{workspaceId}/clients
{
"name": "Northside Dental",
"status": "active",
"billing": { "model": "retainer", "amountCents": 300000, "period": "quarter", "renewsOn": "2027-04-01" },
"requestId": "c7e1a9b2-new-client"
}
201 Created · ETag: "1"
{
"id": "6b1f…", "workspaceId": "220c…",
"name": "Northside Dental", "slug": "northside-dental", "status": "active",
"billing": { "version": 1, "model": "retainer", "amountCents": 300000, "currency": "usd",
"period": "quarter", "scope": [], "renewsOn": "2027-04-01" },
"renewsOn": "2027-04-01",
"etag": "\"1\"",
"projects": [],
"rollup": { "periodStart": "2026-10-01", "costUsd": 0.0, "waitingOnYou": 0, "made": 0, "expectedFeeCents": 100000 }
}edit, with the etagjson
PATCH /v1/clients/{clientId}
If-Match: "1"
{ "status": "paused", "statusNote": "Paused over the holidays" }
200 OK · ETag: "2"
// someone changed it first
409 Conflict
{ "detail": { "code": "etag_mismatch", "current": { "…": "the client as it is now" } } }Contacts
GET/v1/clients/{clientId}/contactsThe people at a client.
POST/v1/clients/{clientId}/contactsAdd a contact. Emails are unique per client, ignoring case (409 on a duplicate).
PATCH/v1/clients/{clientId}/contacts/{contactId}Change a contact, including whether they approve work.
DELETE/v1/clients/{clientId}/contacts/{contactId}Remove a contact outright: personal data, not history.
Projects
A project belongs to at most one client, in the same workspace. Only owners and admins can change it; a client from another workspace is refused with 422 client_not_found.
linking a projecthttp
PATCH /v1/workspaces/{workspaceId}/projects/{projectId}
{ "clientId": "6b1f…" } // null moves it back to Your brands
GET /v1/workspaces/{workspaceId}/projects?clientId=none // Your brandsErrors
| Status | Code | When |
|---|---|---|
| 403 | admin_required | A member tried to change something. |
| 409 | client_conflict | A live client already has that slug or Stripe customer. |
| 409 | contact_exists | A contact with that email is already on this client. |
| 409 | etag_mismatch | The client changed since you read it; the body has the current one. |
| 409 | request_id_reused | The same requestId with a different body. |
| 422 | client_not_found | Linking a project to a client outside its workspace. |
| 428 | etag_required | An edit without an etag. |
Statuses
lead, active, paused, past. Leads and past clients stay out of totals.