---
title: Authentication
description: How bearer API keys work, which scopes they can carry, and how to read 401 vs 403 responses.
---

> **For AI agents:** the complete documentation index is at [llms.txt](/docs/llms.txt). Append `.md` to any page URL for its markdown version.

Every request carries a credential in the `Authorization` header:

<Tabs>
  <Tab title="curl">

```bash
curl https://api.mailfully.com/v1/emails \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx"
```

  </Tab>
  <Tab title="Node">

```typescript
import { Mailfully } from "@mailfully/node";

const mailfully = new Mailfully({ apiKey: process.env.MAILFULLY_API_KEY ?? "" });

const { data, error } = await mailfully.emails.list();
```

  </Tab>
</Tabs>

Create keys at [dashboard.mailfully.com/api-keys](https://dashboard.mailfully.com/api-keys).

## Key format

API key secrets follow `mf_<env>_<body>`, where `<env>` is `live` or `test` and `<body>` is 43 base62 characters (`[0-9A-Za-z]`). A full secret is 51 characters.

Mailfully stores only a SHA-256 hash of the secret, its first 12 characters, and its last 4. The dashboard identifies each key by that prefix and last 4. The plaintext is shown exactly once, when the key is created or rotated. Copy it then; you cannot retrieve it later.

## Test and live environments

Each key belongs to one environment, and the environment separates your message data: messages, their events, analytics rollups, and the keys themselves are scoped to both your org and the key's environment. A `test` key never sees `live` messages: a live message id fetched with an `mf_test_` key returns `404 not_found`, indistinguishable from a message that never existed. Templates, domains, webhooks, and suppressions are shared resources: both environments of your org see the same ones.

Test sends are exempt from the verified-domain gate and never count against your monthly quota. The daily send cap applies to both environments.

## Scopes

Seven scopes exist. Four can be minted onto API keys; the three `manage:*` scopes are session-only, and an API key can never carry them.

| Scope | Unlocks | On API keys |
|---|---|---|
| `send` | `POST /v1/emails` and `/v1/emails/batch`, cancel and reschedule; template create, update, delete; domain create, verify, tracking; also satisfies email, template, and domain reads | Yes (the default) |
| `read:emails` | `GET /v1/emails`, `GET /v1/emails/{id}`, `GET /v1/emails/{id}/events`; also template and domain reads | Yes |
| `read:analytics` | Analytics reads | Yes |
| `read:suppressions` | Suppression list reads | Yes |
| `manage:keys` | API key list, create, revoke, rotate | No — session only |
| `manage:webhooks` | Webhook endpoint CRUD, deliveries, dead letters, replay | No — session only |
| `manage:suppressions` | Add and remove suppressions | No — session only |

Keys default to `["send"]` when you omit `scopes` at creation.

Read routes accept any one of their listed scopes: `GET /v1/emails` requires `send` **or** `read:emails`, so a send-only key can read the messages it sends; template and domain reads accept either scope the same way. Mint `read:*`-only keys for dashboards and reporting jobs that should never send.

<Note>
Webhook management, suppression add/remove, and API key management are dashboard-session operations today. Calling them with an `mf_` key returns `403 insufficient_scope` regardless of how the key was minted. Manage these at [dashboard.mailfully.com](https://dashboard.mailfully.com).
</Note>

Dashboard sessions get scopes from the member's role: owners and admins hold all seven, members hold everything except `manage:keys`, and viewers hold only the three `read:*` scopes.

## 401 vs 403

A 401 means the credential itself failed. A 403 means the credential is valid but not allowed to do this. Authentication runs before scope checks, so a bad credential always returns 401 even when a scope problem also exists.

| Status | `type` | When |
|---|---|---|
| 401 | `missing_api_key` | No `Authorization` header |
| 401 | `invalid_api_key` | Header is not `Bearer <token>`, or an `mf_` key that is unknown or too short |
| 401 | `invalid_session_token` | A bearer that does not start with `mf_` and fails session verification |
| 403 | `invalid_api_key` | A recognized key that has been revoked |
| 403 | `insufficient_scope` | Authenticated, but missing the scope the route requires |

Any bearer that does not start with `mf_` is treated as a dashboard session token, so a pasted value that lost its `mf_` prefix surfaces as `401 invalid_session_token` rather than `invalid_api_key`.

```json
{
  "error": {
    "type": "invalid_api_key",
    "message": "The provided API key is invalid."
  }
}
```

## Rotation

Rotating a key mints a new secret and keeps the old one authenticating for a 24-hour grace window. After the window the old key fails with `403 invalid_api_key` ("This API key has been revoked."). Deploy the new secret before the window closes.

Rotation requires `manage:keys`, so it is a dashboard-session operation — rotate from the dashboard's API keys page. See [Rotate an API key](/api-reference/api-keys/rotate) for the endpoint contract.
