# Workspaces & Projects

> API reference for Warmerly workspaces and projects: the tenant/billing hierarchy that mailboxes, campaigns, and inbox data attach to via X-Project-Id.

Source: https://warmerly.com/docs/workspaces

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](https://warmerly.com/docs/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](https://warmerly.com/docs/workspaces#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

```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
```

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

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

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

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

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

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

## Members

### Change a member's role

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

Requires `owner` or `admin`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `role` | `"owner"` \\| `"admin"` \\| `"member"` | Yes | New 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 }
```

## 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`.

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

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

## 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
```

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

```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](https://warmerly.com/docs/how-warmup-works) for why the two must not be mixed).

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

## Errors

In addition to the standard [error envelope](https://warmerly.com/docs#conventions), 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.

## Each part of this reference has its own page (6)

- [Roles](https://warmerly.com/docs/workspaces/roles)
- [Workspaces](https://warmerly.com/docs/workspaces/workspaces)
- [Members](https://warmerly.com/docs/workspaces/members)
- [Invites](https://warmerly.com/docs/workspaces/invites)
- [Projects](https://warmerly.com/docs/workspaces/projects)
- [Workspace housekeeping](https://warmerly.com/docs/workspaces/workspace-housekeeping)

## More in API reference

- [Webhooks](https://warmerly.com/docs/webhooks): API reference for Warmerly webhook subscriptions: get HTTP-signed notifications for mailbox health events…
- [API Keys](https://warmerly.com/docs/keys): API reference for managing Warmerly API keys programmatically: create, list, and revoke keys used to…
- [Usage & Limits](https://warmerly.com/docs/usage): Read every monthly allowance and every plan limit for a Warmerly workspace over HTTP: GET /usage/quotas for…
