# Authentication

A bearer token in one header, a `User-Agent` in another. Both are required on every request.

## Where to send requests

The base URL:

```text
https://api.rasket.com
```

HTTPS only. The same routes also answer on a deployment's own host, with the `/api/v1` prefix written out:

On a deployment host:

```text
https://<deployment-host>/api/v1
```

## The key

Send your API key as a bearer token. Tokens start with `rk_`.

The Authorization header:

```text
Authorization: Bearer rk_live_2f7a9c1d8e3b5074a6c2f019d4b83e5a
```

A key belongs to exactly one team and can reach nothing outside it. It can also be narrowed further — see [API keys](https://www.rasket.com/docs/api-reference/api-keys) for the two permissions and for restricting a key to a single domain.

> Never call this API from a browser. A key in client-side code is a key you have published. Send from your server, and let your server hold the credential.

## OAuth access tokens

An app a team has connected through [OAuth](https://www.rasket.com/docs/oauth) holds a second kind of bearer credential: an access token starting `rko_`. It goes in the same header as a key and belongs to one team in the same way, but it is authorised by the **scopes** the team approved rather than by a key's permission — and no scope reaches API keys, the member list, a billing change or the OAuth grants.

- An expired or revoked token is `401 invalid_api_key`, with `WWW-Authenticate: Bearer realm="rasket", error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource"`.
- A route the token's scopes do not reach is `403 invalid_permission`, with `WWW-Authenticate: Bearer error="insufficient_scope"` and a `scope=` naming the one needed — left out where no scope would do.
- An `rk_` key is refused by the MCP endpoint, which takes OAuth tokens only.

## Scopes

A token carries some of these 28 scopes, in this order — the order the consent page lists them in. A `full_access` key reaches everything a scope reaches.

| Scope | What it reaches |
| --- | --- |
| `emails:send` | Send an email or a batch. |
| `emails:read` | Retrieve, list, update and cancel sent email; attachments, shares and metrics; read received mail. |
| `domains:read` | Every read under /domains. |
| `domains:write` | Add, verify, update and delete a domain. |
| `templates:read` | Retrieve and list templates. |
| `templates:write` | Create, update, publish, duplicate and delete templates. |
| `contacts:read` | Retrieve and list contacts, contact properties and imports. |
| `contacts:write` | Create, update and delete contacts and their properties. |
| `segments:read` | Retrieve and list segments and their members. |
| `segments:write` | Create, update and delete segments; add and remove members. |
| `topics:read` | Retrieve and list topics. |
| `topics:write` | Create, update and delete topics. |
| `broadcasts:read` | Retrieve and list broadcasts, their recipients and clicked links. |
| `broadcasts:write` | Create, update, send, cancel and delete broadcasts. |
| `suppressions:read` | Retrieve and list suppressions. |
| `suppressions:write` | Add, remove and batch-change suppressions. Reading them needs suppressions:read. |
| `webhooks:read` | Retrieve and list webhooks, the events delivered to them and the events parked while one was off. |
| `webhooks:write` | Create, update, rotate and delete webhooks; replay an event and deliver a parked backlog. Reading them needs webhooks:read. |
| `logs:read` | Retrieve and list request logs. |
| `automations:read` | Retrieve and list automations, their versions and their runs. |
| `automations:write` | Create, update, publish, duplicate, stop and delete automations. |
| `events:read` | Retrieve and list custom event definitions. |
| `events:write` | Create, update and delete custom event definitions. |
| `events:send` | Send a custom event, which can start an automation. |
| `team:read` | Read the team: its plan and limits, sender identity, AI assist and SSO summary. |
| `team:write` | Change the team's sender identity, and turn AI assist on or off. |
| `billing:read` | Read the plan, usage, add-ons and invoices. |
| `ai:use` | Suggest subject lines, draft a body and diagnose an email with AI assist, spending the team's AI credits. |

No scope maps to eight permission groups, so no grant of scopes ever unlocks them: `api_keys`, `team`, `members`, `billing`, `operator`, `oauth_grants`, `mcp` and `emails`. These are group names, not scopes — `team` is the dashboard session's own surface, `members` the member list, `billing` every billing change, `emails` deleting received mail, and `mcp` the MCP endpoint, where each tool asks for its own scope.

Every operation in the [OpenAPI document](https://www.rasket.com/openapi.json) carries `x-rasket-permission` — the weakest key permission that reaches it — and `x-rasket-scope` — the scope a token needs, or `null` — both generated from the route's own authorization, so they cannot disagree with what the API enforces.

## The User-Agent

A `User-Agent` header is mandatory on every request. Missing or empty, the request is refused before any authentication work happens:

The missing User-Agent response:

```text
HTTP/1.1 403 Forbidden

{
  "statusCode": 403,
  "name": "validation_error",
  "message": "Missing User-Agent header.",
  "code": 1010
}
```

`code: 1010` appears on this response and on no other, so you can match it exactly. Name your client something identifiable — `acme-billing/1.0` is the shape we use in every example — because when something is wrong with your traffic, that string is how we find you.

- Most HTTP clients set a `User-Agent` by default. Some, including bare `fetch` in certain runtimes, do not.
- In a browser the header is set by the browser and cannot be overridden — one more reason the call belongs on your server.

## A complete request

List domains:

```sh
curl -X GET "https://api.rasket.com/domains" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/domains",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

## What each refusal means

| When | Status | name |
| --- | --- | --- |
| No Authorization header, or one that is not Bearer followed by a token | `401` | `missing_api_key` |
| A Bearer token we do not recognise: a mistyped or malformed key, or an expired or revoked OAuth access token | `401` | `invalid_api_key` |
| A sending_access key on an endpoint that is not sending | `401` | `restricted_api_key` |
| The key has been deleted, which revokes it | `403` | `restricted_api_key` |
| The key or the team is suspended | `403` | `suspended_api_key` |
| The key lacks a scope this endpoint needs | `403` | `invalid_permission` |
| Sending from a domain this key is not allowed to use, or one that is not verified | `403` | `validation_error` |
| No User-Agent header | `403` | `validation_error` |

`restricted_api_key` deliberately appears at both `401` and `403`: `401` means this kind of key cannot reach this endpoint, `403` means this key is no longer active. The full vocabulary is on the [errors](https://www.rasket.com/docs/errors) page.
