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

# Workspaces & write keys

A **workspace** is an isolated event stream with its own write key and its own
destinations. Events, people, attribution and reporting never cross a workspace
boundary — which is what makes one workspace per brand or per site the natural
way to organize things.

Use a **separate workspace per environment** too. Staging and production should
never share one.

> **Warning**
>
> Do not rely on the `test_mode` flag to separate environments. It does **not**
> stop delivery — a `test_mode` event still dispatches to whatever destinations
> the workspace has connected, so pointing staging at a production workspace
> uploads test conversions to your live ad accounts.
>
> `test_mode` excludes an event from your billable usage and lets you filter it
> out of the events list. That's all it does. It is not an environment boundary;
> a separate workspace is.

## Write keys

Every ingestion request authenticates with a **write key**, sent as the
`X-Write-Key` header:

```bash
curl https://api.pivolio.com/track \
  -H "X-Write-Key: wk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "01JBQ8Z9YV3K7M2N4P6R8T0W1X",
    "event_name": "page_view",
    "event_time": 1770000000.0,
    "anonymous_id": "anon_7d5b6f2c-1a3e-4c88-9f10-2b7e4d9a6c31"
  }'
```

Write keys are **ingest-only**. They can send events and nothing else — they
can't read your data, change your destinations, or touch your account. That's
why it's safe to ship one in client-side JavaScript where anyone can read it.

Everything else — workspaces, destinations, reporting, members — is a
**management** endpoint and authenticates with the dashboard session cookie
instead. See [Authentication](/documentation/guides/authentication).

> **Note**
>
> The write key identifies the workspace an event belongs to. There is no
> workspace ID in the event body; the key *is* the routing.

## Rotating a key

You rotate a write key when it may have been misused — a leaked server-side
key, a departing contractor, a scraped repository.

Rotation is not a soft cutover. The old key stops being accepted, and **every
client still sending it stops ingesting**. Before you rotate, know where the key
is deployed:

#### Inventory the callers

SDK `writeKey` config, `<script>` tag snippets, server-side jobs posting to
`/track` or `/batch`, `/pixel` URLs carrying `?wk=`, and any connected
integration.

#### Rotate in the dashboard

Generate the new key under the workspace's keys screen.

#### Redeploy every caller

Cached JavaScript bundles and CDN-served snippets are the usual thing people
forget — a stale bundle keeps sending the dead key until the cache turns
over.

> **Warning**
>
> Rotation takes effect immediately — the previous key is deactivated before the
> new one is issued, so there is **no grace period**. Events presented with the old
> key are rejected with a `401`.
>
> The browser SDK treats a `401` as permanent, stops retrying, and moves the event
> to its local queue, so some events may still drain once the new key ships. Don't
> rely on that: the queue holds at most 100 events and is per-browser. Roll the new
> key out promptly.

## Integration write keys

Inbound integrations get their **own** write keys, separate from your
first-party SDK key. They authenticate a different endpoint:

* **SDK / first-party keys** → `/track`, `/batch`, `/pixel`, `/id`
* **Integration keys** → `/ingest/integration`

The separation is enforced, not conventional: posting to `/ingest/integration`
with a first-party SDK key returns **403**. Keeping them apart means a key
pasted into a third-party admin panel can never be confused with the one running
on your own site, and you can revoke either without touching the other.

Re-creating an integration source **rotates its key** — the previous one stops
working immediately, so re-paste the new one wherever the integration lives.

See [Destinations vs integrations](/documentation/core-concepts/destinations-vs-integrations) for what
each integration actually does.

## First-party domains

By default the SDK talks to `api.pivolio.com`. Adding a subdomain you own — say
`track.yourcompany.com` — makes collection first-party, which gets you past
ad-blocker domain lists and Safari's ITP restrictions on third-party cookies.

Setup is one **CNAME** record: the Name is your hostname, the Value is the
target Pivolio gives you. Once it resolves, the TLS certificate is issued
automatically. The domain moves through `pending` → `verified` → `active`, or
lands in `error` if the record can't be resolved.

Point the SDK's `apiUrl` at your own hostname once the domain is `active`.

#### [Events](/documentation/core-concepts/events)

What you can actually send through a write key.

#### [Quickstart](/documentation/get-started/quickstart)

Install the SDK and send your first event.