> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.rray.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.rray.app/_mcp/server.

# Workspaces

A **workspace** is a sending tenant inside your account. It has its own message history,
its own API keys, and the sending-IP pool its mail goes out through. Every message,
delivery and event belongs
to exactly one workspace, and a read never crosses that boundary: an id from another
workspace is a `404`, not a `403`.

## The default workspace

Every account starts with one workspace named `default`. If you never create another,
you never have to think about workspaces again — a workspace key carries its own and the
API routes everything there.

## When to create more

Create a workspace when you want a **separate history and separate keys**:

* **Per environment** — `production` and `staging`, so a test run never shows up in the
  numbers you report on.
* **Per product** — one per application, so each team reads only its own traffic.
* **Per tenant** — if you resell sending, one workspace per customer of yours keeps
  volumes and limits separate.

Do not create one per campaign or per message type. Filtering
[`GET /v1/messages`](/api) by `status`, `recipient` and `created_at` is the cheaper way
to slice traffic you want kept together.

```bash
curl -X POST https://api.rray.app/v1/workspaces \
  -H "Authorization: Bearer $RRAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "production"}'
```

```json
{
  "id": "ws_01KYA3AWSZGCB9YMNKM5AQBA8G",
  "object": "workspace",
  "name": "production",
  "status": "active",
  "egress_pool": "default",
  "monthly_delivery_limit": null,
  "customer_id": "cus_01KYA3AWSYX8ZWK2CJ4N0JYQ7D",
  "created_at": "2026-10-07T07:48:02.607020Z"
}
```

Names are 1 to 100 characters and unique within your account; a repeat is
`409 workspace.name_taken`. List what you have with
[`GET /v1/workspaces`](/api) — it is [cursor-paginated](/docs/concepts/pagination) like
any other list.

The returned `id` is the value you send as `RRay-Workspace-Id`. `monthly_delivery_limit`
is `null` until you cap the workspace in the dashboard, at which point that workspace is
bounded by its own ceiling as well as the account's.

## Naming the workspace on a request

A **workspace key** already names its workspace, so it sends no header at all. A
**platform key** acts for the whole account and therefore names the workspace on every
operation that acts in one.

| Operation                              | Workspace comes from                                                                  |
| -------------------------------------- | ------------------------------------------------------------------------------------- |
| Send, list, read a message or its body | The workspace key, or `RRay-Workspace-Id` with a platform key                         |
| `GET /v1/stats`                        | The workspace key, or `RRay-Workspace-Id` / `RRay-Workspace-Ids` with a platform key  |
| Create or list workspaces              | The account — no header                                                               |
| `GET /v1/usage`, `GET /v1/billing`     | The account — no header. A workspace key sees only its own workspace in the breakdown |

Sending `RRay-Workspace-Id` with a workspace key is `400
validation.workspace_header_invalid`, even when the value matches. See
[Credentials](/docs/concepts/authentication) for the full header table.

## Reading across workspaces

`GET /v1/stats` is the one endpoint that spans several workspaces. A platform key may
send `RRay-Workspace-Ids` there instead of the singular header: 1 to 50 comma-separated
ids.

```bash
curl "https://api.rray.app/v1/stats?group_by=day&last=7" \
  -H "Authorization: Bearer $RRAY_PLATFORM_KEY" \
  -H "RRay-Workspace-Ids: ws_01KYA3AWSZGCB9YMNKM5AQBA8G,ws_01KYA3AX0QV6N8XQ8E8G3RZ7MC"
```

Three rules hold:

1. **Never both headers.** The singular and the plural together is always
   `400 validation.workspace_header_conflict`.
2. **The plural header is `GET /v1/stats` only.** Anywhere else it is
   `400 validation.workspace_header_invalid` — a single-workspace operation will not
   silently pick one id out of a list.
3. **One bad id fails the whole request.** A foreign or unknown id returns
   `404 workspace.not_found` instead of being dropped, so a typo can never read as "this
   workspace had no traffic".

Ids must be unique within the list, and the list is capped at 50.

## Limits per workspace

A workspace can carry its own `monthly_delivery_limit` on top of the account allowance.
When it does, a workspace key sending from it sees `workspace.monthly` in the
`RRay-Quota-*` headers instead of the account totals, and a send beyond the ceiling is
`403 billing.workspace_limit_exceeded`. The account's own ceilings still apply underneath
— see [Limits & facts](/docs/get-started/limits).