Workspaces & Projects
Every account belongs to one or more workspaces — the top-level tenant that owns
billing, members, and branding. Each workspace contains one or more projects, and
mailboxes, campaigns, and inbox data all attach to a project (selected via the
X-Project-Id header on other endpoints — see Authentication).
Workspace and project management endpoints below identify the workspace/project by ID
in the URL path, so they generally do not require X-Project-Id. The one
exception is listing/creating projects, which operates on your active workspace
(see Projects below).
Roles
Workspace members have one of three roles:
| Role | Can do |
|---|---|
owner | Everything, including deleting the workspace and managing billing. Every workspace has exactly one owner. |
admin | Rename the workspace, manage members/invites and branding. Cannot delete the workspace, manage billing, or modify/remove the owner. |
member | Read the workspace and use its projects. Cannot manage members, rename, or delete. |
Granting the owner role to another member transfers ownership — the caller must
already be the owner, and their own role is downgraded to admin.
Workspaces
List your workspaces
/workspacesReturns every workspace you're a member of.
{
"workspaces": [
{
"id": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"name": "Acme Inc",
"role": "owner",
"status": "active",
"logoUrl": "https://app.warmerly.com/api/images/...",
"isOwner": true,
"planName": "Agency",
"billedThrough": null,
"clientCount": 2,
"deletionScheduledFor": null,
"purgeAt": null
}
]
}status is "active", "pending" (an extra workspace waiting for its own plan) or
"deleted" (in its 30-day recovery window). billedThrough is set on an Agency client
workspace and names the Agency workspace whose plan it shares; planName is then null.
clientCount is how many client workspaces share an Agency workspace's plan.
Create a workspace
/workspaces| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–80 characters. |
curl https://app.warmerly.com/api/v1/workspaces \
-X POST \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Inc" }'{
"workspace": {
"id": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"name": "Acme Inc",
"role": "owner",
"status": "active",
"logoUrl": null
},
"billedThrough": { "id": "3c59dc04-8f37-4b8b-9c0e-7d3a1f1e2b4d", "name": "My Agency" }
}The caller becomes the workspace's owner. How it is paid for depends on your plan:
- On Agency, it is a client workspace:
statusis"active"straight away, it shares your Agency plan's allowances, andbilledThroughnames the Agency workspace. - Otherwise,
statusis"pending"andbilledThroughisnulluntil a paid plan is chosen for it in the app. Free is not available for extra workspaces.
If you already have a pending workspace, that one is returned (renamed to name) instead
of creating a duplicate.
Get a workspace
/workspaces/{id}Returns the workspace, its members, and (if you're an owner/admin) its pending
invites.
{
"workspace": {
"id": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"name": "Acme Inc",
"logoUrl": null,
"role": "owner",
"ownerUserId": "3c2b1a09-..."
},
"members": [
{
"userId": "3c2b1a09-...",
"email": "you@acme.com",
"name": "Jordan",
"image": null,
"role": "owner",
"createdAt": "2026-01-10T12:00:00.000Z"
}
],
"invites": [
{
"id": "b2f9...",
"email": "new.hire@acme.com",
"role": "member",
"status": "pending",
"createdAt": "2026-06-01T09:00:00.000Z",
"expiresAt": "2026-06-08T09:00:00.000Z"
}
]
}invites is empty for members without members:manage permission (i.e. plain
member role).
Rename a workspace
/workspaces/{id}Requires owner or admin.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–80 characters. |
curl https://app.warmerly.com/api/v1/workspaces/8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a \
-X PATCH \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Incorporated" }'{ "workspace": { "id": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a", "name": "Acme Incorporated" } }Delete a workspace
/workspaces/{id}Requires owner. What happens depends on the workspace, and phase says which:
phase | When | What happens |
|---|---|---|
removed | A pending workspace that was never paid for | Deleted immediately. |
scheduled | It has its own paid subscription | The subscription stops renewing; the workspace works until effectiveAt, then enters recovery. No refund. |
retention | Anything else (Free, or an Agency client) | Locked now and kept until purgeAt (30 days), then permanently deleted with everything under it. |
{
"ok": true,
"phase": "retention",
"effectiveAt": "2026-09-23T10:00:00.000Z",
"purgeAt": "2026-10-23T10:00:00.000Z",
"nextWorkspaceId": "3c59dc04-8f37-4b8b-9c0e-7d3a1f1e2b4d"
}nextWorkspaceId is the workspace you were switched to, when the deleted one stopped
being usable.
Restore a workspace
/workspaces/{id}/restoreRequires owner. Undoes a deletion without a card: a scheduled deletion is called off
and the subscription carries on, and a workspace in recovery comes back into its Agency
plan, keeps its own subscription if it still has one, or otherwise returns on the Free plan.
{ "outcome": "free" }outcome is deletion_cancelled, agency_pool, subscription_kept, free or
not_deleted.
Members
Change a member's role
/workspaces/{id}/members/{userId}Requires owner or admin.
| Field | Type | Required | Description |
|---|---|---|---|
role | "owner" | "admin" | "member" | Yes | New role for the member. |
curl https://app.warmerly.com/api/v1/workspaces/8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a/members/3c2b1a09-... \
-X PATCH \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "role": "admin" }'{ "ok": true }Notes:
- Only an
ownermay setrole: "owner"— doing so transfers ownership; the caller's own role becomesadmin. - An
admincannot change anowner's role (403 forbidden).
Remove a member
/workspaces/{id}/members/{userId}Requires owner or admin to remove another member. A member may remove
themselves (leave the workspace) regardless of role, except the owner — the
owner must transfer ownership or delete the workspace first (400 bad_request).
An owner can never be removed by another member (403 forbidden).
{ "ok": true }Invites
List pending invites
/workspaces/{id}/invitesRequires owner or admin.
{
"invites": [
{
"id": "b2f9...",
"email": "new.hire@acme.com",
"role": "member",
"status": "pending",
"createdAt": "2026-06-01T09:00:00.000Z",
"expiresAt": "2026-06-08T09:00:00.000Z"
}
]
}Invite a member
/workspaces/{id}/invitesRequires owner or admin.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Invitee's email address. |
role | "admin" | "member" | Yes | Role granted on acceptance. You cannot invite someone directly as owner. |
curl https://app.warmerly.com/api/v1/workspaces/8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a/invites \
-X POST \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "email": "new.hire@acme.com", "role": "member" }'{
"invite": {
"id": "b2f9...",
"email": "new.hire@acme.com",
"role": "member",
"status": "pending",
"createdAt": "2026-06-01T09:00:00.000Z",
"expiresAt": "2026-06-08T09:00:00.000Z"
}
}201 Created on success. If the email already belongs to a member, returns
409 conflict. A new invite replaces any existing pending invite for the same
email. Invites expire after 7 days. If transactional email is configured, an
invite email is sent automatically.
Projects
A project hangs off a workspace and is what campaigns, mailboxes, and inbox data
attach to via X-Project-Id. Each workspace has exactly one project. Unlike the
workspace/member endpoints above, listing and creating projects operates on your
active workspace (the workspace currently selected in the dashboard/session)
rather than a workspace ID in the URL or header.
List projects
/projects{
"projects": [
{
"id": "2b6e1c3d-...",
"name": "Default",
"slug": "default",
"description": null,
"color": null,
"icon": null,
"createdAt": "2026-01-10T12:00:00.000Z",
"campaignCount": 3,
"accountCount": 5
}
]
}If your active workspace has no projects yet, a default one is created automatically before the list is returned, so this endpoint never returns an empty array.
Create a project
/projects| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Project name. |
slug | string | Yes | Lowercase letters, numbers, and hyphens only (^[a-z0-9-]+$). A random suffix is appended automatically if the slug is already taken. |
description | string | No | Up to 2000 characters. |
color | string | No | Up to 32 characters. |
icon | string | No | Up to 64 characters. |
defaultTimezone | string | No | Up to 64 characters. |
curl https://app.warmerly.com/api/v1/projects \
-X POST \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Q3 Outbound", "slug": "q3-outbound" }'{
"project": {
"id": "9d4f2a11-...",
"name": "Q3 Outbound",
"slug": "q3-outbound",
"workspaceId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"description": null,
"color": null,
"icon": null,
"defaultTimezone": null,
"createdAt": "2026-07-01T09:00:00.000Z"
}
}201 Created on success. Returns 409 conflict if the active workspace already has a
project — a workspace can only ever have one.
Get a project
/projects/{id}Returns the project. Requires membership in the project's workspace.
{
"project": {
"id": "9d4f2a11-...",
"name": "Q3 Outbound",
"slug": "q3-outbound",
"workspaceId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"description": null,
"color": null,
"icon": null,
"defaultTimezone": null,
"createdAt": "2026-07-01T09:00:00.000Z"
}
}Project stats
/projects/{id}/statsA rollup for one project — what the dashboard's project switcher shows.
{
"project": { "id": "…", "name": "SaaS Outreach", "slug": "saas-outreach-05adc35c", "workspaceId": "…", "userId": "…" },
"campaigns": { "total": 3, "active": 2 },
"accounts": { "total": 12 },
"leads": { "total": 145 },
"emails": { "sent": 300, "replied": 17, "replyRate": 5.7 }
}replyRate is a percentage of real campaign sends — not a warmup figure (see
How warmup works for why the two must not be mixed).
Workspace housekeeping
POST /workspaces/{id}/activate
POST /workspaces/{id}/logo
DELETE /workspaces/{id}/logo
DELETE /workspaces/{id}/invites/{inviteId}activate switches which workspace is "current" for session-based (dashboard) use and
returns { "ok": true }. API-key callers rarely need it: pass X-Workspace-Id on the
request instead, which scopes that one call without changing any stored state.
logo takes a multipart/form-data upload and returns { "logoUrl": "…" }; DELETE
removes it. DELETE /workspaces/{id}/invites/{inviteId} revokes a pending invite that has
not been accepted.
Errors
In addition to the standard error envelope, these endpoints return:
403 forbidden— you're a member of the workspace/project but lack the required role for the action (e.g. amembertrying to invite someone, or anadmintrying to remove anowner).404 not_found— the workspace or project doesn't exist, or you're not a member of it (workspace/project membership checks return404, not403, to avoid leaking existence to non-members).409 conflict— inviting an email that's already a member.400 bad_request— invalid body (e.g. bad slug format), or an owner trying to leave their own workspace without transferring ownership first.
Summarize with AI

