---
title: "Create an API key"
method: POST
path: "/api/projects/{id}/api-keys"
tags: ["API Keys"]
---

# Create an API key

`POST /api/projects/{id}/api-keys`

## Request

- Base URL: `https://api.sendly.now`
- URL: `https://api.sendly.now/api/projects/{id}/api-keys`
- Auth: one of:
  - API key in cookie `better-auth.session_token`
  - OAuth 2 (authorization code, authorization URL `https://app.sendly.now/api/auth/oauth2/authorize`, token URL `https://app.sendly.now/api/auth/oauth2/token`, scopes: `api-keys:write`)

Mint a new API key on the project. The token is NOT returned — the response carries the key's metadata plus a one-time `revealUrl` that only a signed-in dashboard session can open.

**Scope attenuation:** a key created with a delegated credential (an OAuth token or another API key) may not carry a scope that credential does not itself hold. A request that asks for more is refused with `400 SCOPE_ESCALATION` rather than quietly narrowed, so the mistake is reported where it was made instead of surfacing later as an unexplained 403. Naming only `legacyGrantPreset` counts as asking for every scope that preset implies.

Requires the `api-keys:write` scope — Create, rotate, and revoke API keys — these keep working even after you disconnect this app.

## Path parameters

- `id` string, required: Project id.

## Request body

- CreateApiKeyBody
  - `domainId` string, uuid, nullable
  - `legacyGrantPreset` 'FULL' | 'SENDING_ONLY'
  - `mode` 'LIVE' | 'TEST': `LIVE` (default) or `TEST`. A test key can only send from the project's sandbox address, which accepts only the project's own verified account addresses as recipients, so it cannot reach a customer. Its sends are real sends down the same pipeline and are marked `sentInTestMode`. Fixed at creation.
  - `name` string, required
  - `scopes` string[]: The explicit grant the new key will carry. Omitted ⇒ materialised from `legacyGrantPreset`. A `SENDING_ONLY` key may carry only `emails:send`.

## Response `201`

API key created; the secret is behind the reveal link.

- object
  - `data` object, required: An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created.
    - `createdAt` string, date-time, required: ISO 8601 datetime string
    - `domainId` string, uuid, nullable, required
    - `id` string, uuid, required
    - `lastFour` string, required: Last 4 characters of the token — the only fragment of the secret that survives creation.
    - `lastUsedAt` string, date-time, nullable, required: ISO 8601 datetime string
    - `legacyGrantPreset` 'FULL' | 'SENDING_ONLY', required: The coarse preset the key was minted under. It picks the token prefix (`sk_` vs `pk_`) and the rate-limit window, and it is the key's authority ONLY while `scopes` is empty — which is true just of keys minted before scopes existed. Read `scopes` to learn what a key can do.
    - `mode` 'LIVE' | 'TEST', required: Where mail sent with this key may go. `LIVE` sends from your verified domains. `TEST` can only send from this project's sandbox address, whose recipient list is your own verified account addresses, so a test key cannot reach a customer whatever it is asked to do. Fixed at creation.
    - `name` string, required
    - `projectId` string, uuid, required
    - `revokedAt` string, date-time, nullable, required: Set once the key is revoked. Revoked keys are NOT filtered out of list/get responses.
    - `scopes` string[], required: The explicit scope grant this key carries. Empty on a key minted before this column existed — that row's grant is derived from `legacyGrantPreset` instead at request time.
    - `revealExpiresAt` string, date-time, required: When the reveal link stops working. Create or rotate again to get a new one.
    - `revealUrl` string, uri, required: A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
  - `success` true, required

## Other responses

- `400`: Validation error
- `401`: Unauthorized — missing or invalid auth
- `403`: Forbidden — insufficient permissions or project disabled
- `429`: Rate limit or billing limit exceeded
- `500`: Internal server error

---

[API](https://skmtc.dev/sendly/apis/sendly-api.md) · [All operations](https://skmtc.dev/sendly/apis/sendly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sendly/apis/sendly-api/revisions/ce7cfb7f7c7e?raw)
