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, chosen with X-Workspace-Id (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
GET /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,
"products": ["outreach"]
}
]
}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. products lists the products enabled for the workspace.
#Create a workspace
POST /workspaces| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1-80 characters. |
products | array of strings | No | Products the new workspace starts with, at least one. Outreach alone when omitted. |
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.
Creating a workspace answers 201. If you already have a pending workspace, that one is returned (renamed to name) instead of creating a duplicate, with status 200. Your first workspace is always active, as are workspaces created by staff or comped accounts.
#Get a workspace
GET /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-...",
"senderOrgName": null,
"senderPostalAddress": null,
"deletionScheduledFor": null,
"purgeAt": null
},
"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 plain member role (only owner and admin can see pending invites). senderOrgName and senderPostalAddress are the sender identity used in campaign email footers. A workspace you do not belong to, or that does not exist, returns 404 not_found.
#Rename a workspace
PATCH /workspaces/{id}Requires owner or admin.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | 1-80 characters. |
senderOrgName | string | No | Up to 120 characters. An empty or blank string clears it. |
senderPostalAddress | string | No | Up to 400 characters. An empty or blank string clears it. |
Every field is optional, and only the fields you send are changed.
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",
"senderOrgName": null,
"senderPostalAddress": null
}
}#Delete a workspace
DELETE /workspaces/{id}Requires owner. Optionally send a JSON body with reason and note (both strings), which are recorded in the audit log. 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 subscription that is active or trialing | The subscription is set to cancel at the end of the current period; the workspace works until effectiveAt, then enters recovery. |
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 your earliest other active workspace, and is null for scheduled (nothing stopped being usable yet) or when you have no other active workspace. purgeAt is null for removed and scheduled.
#Restore a workspace
POST /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. A workspace that is not being deleted returns not_deleted and nothing changes.
{ "outcome": "free" }outcome is deletion_cancelled, agency_pool, subscription_kept, free or not_deleted.
#Members
#Change a member's role
PATCH /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). - An invalid or missing
rolereturns400 bad_request, and a user who is not a member returns404 not_found.
#Remove a member
DELETE /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). A user who is not a member returns 404 not_found.
An owner can never be removed by another member (403 forbidden).
{ "ok": true }#Invites
#List pending invites
GET /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
POST /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. The email is lowercased. 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 email sending is configured, an invite email is sent automatically; the invite is still created if sending fails.
Your own email address must be verified, otherwise this returns 403 with error.code email_not_verified. Invites are also rate limited: 20 per hour and 50 per day per person, 30 per hour and 100 per day per workspace, and 3 per day to any one recipient address. Going over returns 429 too_many_requests with a Retry-After header.
#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 rather than a workspace ID in the URL: the one named in X-Workspace-Id if you belong to it, otherwise your own workspace. GET /projects also accepts a workspaceId query parameter to list another workspace you belong to (a workspace you are not a member of returns 403 forbidden).
#List projects
GET /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
POST /projects| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Project name, at least 1 character. |
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",
"userId": "3c2b1a09-...",
"description": null,
"color": null,
"icon": null,
"defaultTimezone": null,
"createdAt": "2026-07-01T09:00:00.000Z"
}
}userId is the user who created the project. 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
GET /projects/{id}Returns the project. Requires membership in the project's workspace (403 forbidden otherwise, 404 not_found if the project does not exist).
{
"project": {
"id": "9d4f2a11-...",
"name": "Q3 Outbound",
"slug": "q3-outbound",
"workspaceId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"userId": "3c2b1a09-...",
"description": null,
"color": null,
"icon": null,
"defaultTimezone": null,
"createdAt": "2026-07-01T09:00:00.000Z"
}
}#Project stats
GET /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 }
}Requires membership in the project's workspace. accounts.total counts every mailbox attached to the project, so it can be higher than accountCount from GET /projects, which counts only mailboxes you connected yourself. replyRate is a percentage with one decimal place, 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 with the image in a field named file (PNG, JPEG, WebP or SVG, up to 2 MB) and returns { "logoUrl": "…" }; DELETE removes it and returns { "ok": true }. Both need owner or admin. DELETE /workspaces/{id}/invites/{inviteId} (owner or admin) revokes an invite and returns { "ok": true }. activate works for any member and uses no body.
#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 doesn't exist or you're not a member of it (workspace endpoints return404, not403, to non-members). For a project, a missing project returns404and a project in a workspace you don't belong to returns403 forbidden.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.