---
title: "API key security"
description: "Choose the narrowest scope, keep test and live keys apart, store secrets outside your repository, and rotate on a schedule."
---

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

A Mailfully API key is a bearer credential: whoever holds the secret can do everything the key's scope allows.

## Scope every key down

Four scopes can be minted onto an API key; the three `manage:*` scopes are session-only and never reach a key. Mint the narrowest one the calling code needs.

| 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 |

An analytics dashboard that only renders charts needs `read:analytics` and nothing else: mint that scope alone rather than reusing a `send` key that could also dispatch mail. See [Authentication](/concepts/authentication) for how scopes are checked and what a scope mismatch returns.

## Keep test and live apart

A key's secret starts `mf_live_` or `mf_test_`, and that prefix partitions your message data: messages, events, analytics rollups, and the keys themselves are scoped to both your org and the key's environment. A test key cannot read or send as live. The partition covers data, not configuration: templates, domains, webhooks, and suppressions are shared by both environments, so a leaked test key carrying the default `send` scope can still change live templates and domains. [Test and live environments](/concepts/authentication#test-and-live-environments) says exactly where the boundary falls, and [Test mode](/guides/test-mode) covers how test sends behave.

## Where to keep the secret

Store a key's plaintext in an environment variable or a secret manager, and load it from there at runtime. Never commit it to source, never embed it in a client-side bundle, and never write it to a log line: a value that reaches a browser tab is exposed to anyone who opens developer tools, which is the exact failure [Calling the API from a browser](/troubleshooting/browser-requests) walks through.

## Rotate on a schedule

Rotating mints a new secret and keeps the old one authenticating for a 24-hour grace window, so a deploy that carries the new secret can overlap with instances still running the old one. After the window closes, the old secret fails with `403 invalid_api_key` ("This API key has been revoked.").

Rotation requires `manage:keys`, which is a dashboard-session scope no API key can hold, so it runs from the dashboard's API keys page rather than from your application. A curl call against the endpoint authenticates with a session token, not an API key:

```bash
curl -X POST https://api.mailfully.com/v1/api-keys/key_01J1F8Z3N9WXYQ5T2V4B6C8D0E/rotate \
  -H "Authorization: Bearer <session token>"
```

See [Rotate an API key](/api-reference/api-keys/rotate) for the full request and response contract.

## Revoke when you must

Revoking is immediate rather than graced: an active key is stamped `revoked_at` right away and rejected on its very next call, with `403 invalid_api_key` ("This API key has been revoked."). Calling revoke again on an already-revoked key is safe and keeps the original timestamp.

## If a key leaks

Whether to rotate or revoke first, and in which order, changes how long an exposed key stays live. See [A leaked API key](/troubleshooting/leaked-api-key) for the containment steps.
