# API keys and authentication

> Which key sends data, which key reads it, and how to call the API safely.

Orwel uses two kinds of keys, with different jobs:

| Key | Prefix | Can do | Where it lives |
| --- | --- | --- | --- |
| Production API key | `orwel_pk_` (older workspaces: `orwel_`) | **Only send data**: events, sessions, identify, conversions. | In your site's browser bundle. Safe to expose. |
| Development key | `orwel_dev_` | Same as the production API key, but everything is stored as test data. See [Dev mode](/docs/guides/dev-mode). | Local and staging builds. |
| API key | `orwel_sk_` | **Read and write workspace data** through the REST API and the MCP server, limited to the scopes you choose. | Your backend, or a browser app on an allowed origin. Never in a public repo. |

<Warning>
  The production API key cannot read anything. Calls to `/api/v1` or `/mcp` with `orwel_pk_` or `orwel_dev_` are rejected. To read data, create an API key.
</Warning>

## Sending data (production API key)

The [SDK](/docs/sdk/introduction) sends the production API key in the `X-Orwel-Key` header to the ingest endpoints (`/ingest`, `/ingest/batch`, `/visitor-resolve`). You find it in **Settings, API keys** in the dashboard, and in the install guide at [orwel.io/install](https://orwel.io/install).

```ts
orwel.init({ apiKey: process.env.NEXT_PUBLIC_ORWEL_KEY })
```

## Reading data (API keys)

API keys are optional. You only need one to build on Orwel: pull leads into your own dashboard, sync them to a warehouse, or connect an AI agent through [MCP](/docs/agents/mcp).

### Create a key

<Steps>
  <Step title="Open Settings, API keys">
    In the Orwel dashboard, go to **Settings** and open **API keys**.
  </Step>
  <Step title="Name it and pick scopes">
    Give the key a name you will recognize later (for example "Sales agent" or "Warehouse sync") and select only the scopes it needs.
  </Step>
  <Step title="Choose where it can be used">
    Add the allowed origins of the browser app that will call the API, or choose **server only**.
  </Step>
  <Step title="Copy it once">
    The full key (`orwel_sk_...`) is shown only once. Store it in an environment variable or a secret manager.
  </Step>
</Steps>

### Scopes

| Scope | Grants |
| --- | --- |
| `leads:read` | List and read leads. |
| `visitors:read` | Read visitors, identities, their activity and personalities. |
| `companies:read` | Read enriched companies. |
| `campaigns:read` | Read ad campaigns and their performance. |
| `conversions:read` | Read conversions. |
| `leads:write` | Create and update leads. |
| `lists:write` | Create visitor lists and add members to them. |
| `mcp` | Connect to the [MCP server](/docs/agents/mcp) for AI agents. |

A request outside the key's scopes gets `403`.

### Allowed origins or server only

- **Allowed origins:** the key only answers browser requests whose `Origin` is in the list (for example `https://app.yourcompany.com`). Any other site gets `403`.
- **Server only:** the key has no origins and is rejected from any browser. Use it from a backend, a cron job or an agent.

<Note>
  Origins stop other websites from using your key in a browser. They do not stop a server or `curl`, because those can send any `Origin`. If a key leaks, rotate it. That is why each key should carry the minimum scopes it needs.
</Note>

### Send the key

Send the API key as a Bearer token:

<CodeGroup>

```bash cURL
curl "https://api.orwel.io/api/v1/leads?limit=10" \
  -H "Authorization: Bearer $ORWEL_API_KEY"
```

```javascript JavaScript
const res = await fetch('https://api.orwel.io/api/v1/leads?limit=10', {
  headers: { Authorization: `Bearer ${process.env.ORWEL_API_KEY}` }
})
const { data } = await res.json()
```

```python Python
import os, requests

res = requests.get(
    "https://api.orwel.io/api/v1/leads",
    headers={"Authorization": f"Bearer {os.environ['ORWEL_API_KEY']}"},
    params={"limit": 10},
)
print(res.json())
```

</CodeGroup>

### Rotate or revoke

Each key in **Settings, API keys** shows when it was last used. Revoke a key there and it stops working immediately. To rotate without downtime, create the new key, deploy it, then revoke the old one.

## Errors

### 401 Unauthorized

The key is missing, mistyped, revoked, or it is a public or development key used against the API.

```json
{
  "error": "unauthorized",
  "message": "Invalid or missing API key"
}
```

### 403 Forbidden

The key is valid but not allowed for this request: the scope is missing, the request came from an origin that is not on the list, or the workspace subscription does not include the feature.

## Best practices

<AccordionGroup>
  <Accordion icon="lock" title="Keep API keys out of public code">
    `orwel_sk_` keys read customer data. Keep them in environment variables or a secret manager, never in a public repository, and only in a browser app when the key has allowed origins.
  </Accordion>
  <Accordion icon="filter" title="One key per use, minimum scopes">
    A warehouse sync needs `leads:read`, not `leads:write`. An agent that only answers questions does not need `lists:write`. Separate keys also make the "last used" column meaningful.
  </Accordion>
  <Accordion icon="rotate" title="Rotate on any doubt">
    If a key may have leaked, revoke it right away. The production API key in your site is not a secret and does not need rotation.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api-reference/introduction">
    Endpoints you can call with an API key.
  </Card>
  <Card title="MCP for agents" icon="robot" href="/agents/mcp">
    Connect Claude, Cursor or any MCP client to your workspace.
  </Card>
</CardGroup>
