Less boo. More booked. Halloween offer: 30 days free on any paid plan. Claim 30 days freeEvery new account gets one free mailbox for warmup and replies. No domain to buy. See how it works

Skip to content

Workspaces & Projects

API reference10 min read7 sections

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).

API reference

#Roles

Workspace members have one of three roles:

RoleCan do
ownerEverything, including deleting the workspace and managing billing. Every workspace has exactly one owner.
adminRename the workspace, manage members/invites and branding. Cannot delete the workspace, manage billing, or modify/remove the owner.
memberRead 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.

API reference

#Workspaces

#List your workspaces

text
GET /workspaces

Returns every workspace you're a member of.

json
{
  "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

text
POST /workspaces
FieldTypeRequiredDescription
namestringYes1-80 characters.
productsarray of stringsNoProducts the new workspace starts with, at least one. Outreach alone when omitted.
bash
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" }'
json
{
  "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: status is "active" straight away, it shares your Agency plan's allowances, and billedThrough names the Agency workspace.
  • Otherwise, status is "pending" and billedThrough is null until 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

text
GET /workspaces/{id}

Returns the workspace, its members, and (if you're an owner/admin) its pending invites.

json
{
  "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

text
PATCH /workspaces/{id}

Requires owner or admin.

FieldTypeRequiredDescription
namestringNo1-80 characters.
senderOrgNamestringNoUp to 120 characters. An empty or blank string clears it.
senderPostalAddressstringNoUp to 400 characters. An empty or blank string clears it.

Every field is optional, and only the fields you send are changed.

bash
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" }'
json
{
  "workspace": {
    "id": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
    "name": "Acme Incorporated",
    "senderOrgName": null,
    "senderPostalAddress": null
  }
}

#Delete a workspace

text
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:

phaseWhenWhat happens
removedA pending workspace that was never paid forDeleted immediately.
scheduledIt has its own subscription that is active or trialingThe subscription is set to cancel at the end of the current period; the workspace works until effectiveAt, then enters recovery.
retentionAnything else (Free, or an Agency client)Locked now and kept until purgeAt (30 days), then permanently deleted with everything under it.
json
{
  "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

text
POST /workspaces/{id}/restore

Requires 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.

json
{ "outcome": "free" }

outcome is deletion_cancelled, agency_pool, subscription_kept, free or not_deleted.

API reference

#Members

#Change a member's role

text
PATCH /workspaces/{id}/members/{userId}

Requires owner or admin.

FieldTypeRequiredDescription
role"owner" \| "admin" \| "member"YesNew role for the member.
bash
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" }'
json
{ "ok": true }

Notes:

  • Only an owner may set role: "owner", doing so transfers ownership; the caller's own role becomes admin.
  • An admin cannot change an owner's role (403 forbidden).
  • An invalid or missing role returns 400 bad_request, and a user who is not a member returns 404 not_found.

#Remove a member

text
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).

json
{ "ok": true }
API reference

#Invites

#List pending invites

text
GET /workspaces/{id}/invites

Requires owner or admin.

json
{
  "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

text
POST /workspaces/{id}/invites

Requires owner or admin.

FieldTypeRequiredDescription
emailstringYesInvitee's email address.
role"admin" \| "member"YesRole granted on acceptance. You cannot invite someone directly as owner.
bash
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" }'
json
{
  "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.

API reference

#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

text
GET /projects
json
{
  "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

text
POST /projects
FieldTypeRequiredDescription
namestringYesProject name, at least 1 character.
slugstringYesLowercase letters, numbers, and hyphens only (^[a-z0-9-]+$). A random suffix is appended automatically if the slug is already taken.
descriptionstringNoUp to 2000 characters.
colorstringNoUp to 32 characters.
iconstringNoUp to 64 characters.
defaultTimezonestringNoUp to 64 characters.
bash
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" }'
json
{
  "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

text
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).

json
{
  "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

text
GET /projects/{id}/stats

A rollup for one project: what the dashboard's project switcher shows.

json
{
  "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).

API reference

#Workspace housekeeping

text
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.

API reference

#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. a member trying to invite someone, or an admin trying to remove an owner).
  • 404 not_found: the workspace doesn't exist or you're not a member of it (workspace endpoints return 404, not 403, to non-members). For a project, a missing project returns 404 and a project in a workspace you don't belong to returns 403 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.
Was this page helpful?