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
GET /workspaces
Returns 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/..."
}
]
}
status is "active" or "pending" (new workspaces beyond your first may require
approval before becoming active).
Create a workspace
POST /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
}
}
The caller becomes the workspace's owner. If you already have a pending workspace
awaiting approval, that same workspace is returned instead of creating a duplicate.
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-..."
},
"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
PATCH /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
DELETE /workspaces/{id}
Requires owner. Permanently deletes the workspace and everything under it (members,
projects, campaigns, accounts).
{ "ok": true }
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).
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).
An owner can never be removed by another member (403 forbidden).
{ "ok": true }
Invites
List pending invites
GET /workspaces/{id}/invites
Requires 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}/invites
Requires 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
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. |
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
GET /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"
}
}
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.
