---
title: Authentication
description: Learn how to authenticate with the Warmbly API using API keys.
---

The Warmbly API uses API keys for authentication. Each API key has specific permissions that control what operations it can perform.

## API key format

API keys follow this format:

```
wmbly_<43-character-random-string>
```

The `wmbly_` prefix identifies the key as a Warmbly key. The remaining 43 characters are 32 random bytes encoded as base64url (no padding): 256 bits of entropy, so brute-forcing a valid key is computationally infeasible.

Keys are stored as a SHA-256 hash; the plaintext is shown exactly once on creation. To help you spot a key in the dashboard without exposing the secret, we store the first 8 characters (`key_prefix`, e.g. `wmbly_ab`) and the last 4 characters (`key_suffix`, e.g. `wxyz`). Render them as `wmbly_ab…wxyz`.

## Three ways to get a key

1. **The dashboard.** Settings > API keys, pick the scopes, copy the secret. This is the right path for a key a server will use.
2. **The CLI.** [`warmbly auth login`](/api/cli/) opens a browser approval and mints a key named for the machine that asked, then stores it at 0600. This is the right path for a key you will use yourself, and it is the only one that does not involve pasting a secret into a shell.
3. **The API.** `POST /v1/api-keys` with a key that carries `API_KEYS`. The secret is in that response and nowhere else.

All three produce the same thing: a `wmbly_` key with a scope bitmask, listed under Settings > API keys, revocable there.

### The CLI device flow

`warmbly auth login` uses a device-code handshake, so the terminal never handles your password and the browser never handles the key:

1. The CLI calls `POST /v1/auth/cli/code` with the scopes it wants and the machine's hostname. It gets back a `device_code` it keeps, a `user_code` it prints, and a `verification_uri_complete` it opens.
2. You approve at `app.warmbly.com/cli`, choosing which workspace the key belongs to. The approval is what mints the key, so it requires the `MANAGE_API_KEYS` organization permission.
3. The CLI polls `POST /v1/auth/cli/poll` and receives the key exactly once, on the first poll after approval.

Codes expire after ten minutes, both halves are per-IP rate limited, and the `device_code` is stored hashed. Anything else can drive the same flow: it is two public endpoints and a browser.

### Ending a key

`DELETE /v1/api-keys/:id` revokes any key in the workspace and needs the `API_KEYS` scope. `DELETE /v1/api-keys/self` revokes the key the call was made with and needs no scope at all, so a narrowly scoped credential can always end itself. This is what `warmbly auth logout` uses.

## Using your API key

Include your API key in the `Authorization` header of every request:

```bash
curl -X GET "https://api.warmbly.com/v1/api-keys" \
  -H "Authorization: Bearer wmbly_abc123..." \
  -H "Content-Type: application/json"
```

For mutation retries, include an `Idempotency-Key` header with a unique value per logical operation. Warmbly stores completed mutation responses for 24 hours per organization and key, then replays matching retries instead of performing the operation again.

## Verifying a credential

To check that a credential is valid and see who it belongs to, call `GET /v1/me`. It works with an API key, an OAuth access token, or a dashboard session, requires no specific permission, and returns the caller's identity:

```bash
curl -X GET "https://api.warmbly.com/v1/me" \
  -H "Authorization: Bearer wmbly_abc123..."
```

```json
{
  "user_id": "0b1f...",
  "email": "jane@acme.com",
  "name": "Jane Doe",
  "organization_id": "9a2c...",
  "organization_name": "Acme Inc",
  "auth_type": "api_key",
  "scopes": ["read_contacts", "write_contacts"]
}
```

`auth_type` is `api_key`, `oauth`, or `jwt`, and `scopes` lists the granted API scopes for key and OAuth callers (empty for dashboard sessions, which use organization roles instead). This is the right endpoint for an integration to validate a connection and render a label. The separate `GET /v1/auth/me` is session-only and is not reachable with an API key or OAuth token.

## OAuth access tokens

API keys authenticate your own scripts. If you are building an app that other people connect their Warmbly workspace to, use OAuth instead: the user grants your app scoped access and you receive a bearer **access token** (prefix `wmat_`). It goes in the same `Authorization: Bearer` header and is checked against the same permissions, so every endpoint below behaves identically whether you present an API key or an OAuth token. The difference is only how the credential is obtained. See [OAuth](/api/oauth/) for the full flow.

## Key security best practices

<Callout type="warn" title="Keep Your Keys Secret">
  Never expose API keys in client-side code, public repositories, or logs. Treat them like passwords.
</Callout>

### Do

- Store API keys in environment variables or secure secret managers
- Use different keys for development and production
- Restrict keys to only the permissions they need
- Set expiration dates for keys when possible
- Use IP allowlists to restrict key usage

### Don't

- Commit API keys to version control
- Share API keys via email or chat
- Use production keys in development
- Give keys more permissions than necessary

## Permissions

Each API key has a permissions bitmask that controls its capabilities. See the [Permissions Reference](/api/permissions/) for a complete list.

### Permission categories

| Category | Description |
|----------|-------------|
| **Read** | View resources (emails, campaigns, contacts, etc.) |
| **Write** | Create and modify resources |
| **Bulk** | Perform bulk operations |
| **Special** | Advanced features (realtime, webhooks, API key management) |

### Example: read-only key

A read-only API key might have these permissions:

```json
{
  "permissions": 31
}
```

This combines:
- `READ_EMAILS` (1)
- `READ_CAMPAIGNS` (2)
- `READ_CONTACTS` (4)
- `READ_UNIBOX` (8)
- `READ_ANALYTICS` (16)

Total: 1 + 2 + 4 + 8 + 16 = 31

## IP restrictions

You can restrict API keys to specific IPs or CIDR ranges. Entries can be bare IPs (v4 or v6) or CIDR blocks; an empty list means "any IP".

```json
{
  "name": "Production Server",
  "permissions": 688159,
  "allowed_ips": [
    "203.0.113.10",
    "203.0.113.11",
    "10.0.0.0/8",
    "2001:db8::/32"
  ]
}
```

Requests from outside every listed range are rejected with `403 Forbidden`. There's a soft cap of 64 entries per key.

## Email account restrictions

Limit API keys to specific email accounts:

```json
{
  "name": "Marketing Team",
  "permissions": 127,
  "allowed_email_accounts": [
    "550e8400-e29b-41d4-a716-446655440000",
    "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
  ]
}
```

## Key expiration

Set an expiration date for temporary access:

```json
{
  "name": "Contractor Access",
  "permissions": 31,
  "expires_at": "2027-12-31T23:59:59Z"
}
```

After expiration, the key returns `401 Unauthorized`.

## Using a credential with MCP

An [MCP client](/api/mcp/) (Claude Code, Claude Desktop, Cursor) authenticates to Warmbly with either an OAuth sign-in (the one-command path, no key to paste) or a static API key. For a key, send it as a bearer token to `https://api.warmbly.com/v1/mcp`; the client then sees exactly the tools the credential's scopes allow. See the [MCP server](/api/mcp/) page for both paths.

## Error responses

### 401 Unauthorized

Returned when:
- API key is missing
- API key is invalid
- API key has expired
- API key has been revoked

```json
{
  "error": "Unauthorized",
  "message": "Token not found."
}
```

### 403 Forbidden

Returned when:
- API key lacks required permissions
- Request IP is not in allowlist
- Email account is not in allowlist

```json
{
  "error": "Forbidden",
  "message": "You don't have access to this feature."
}
```

## Next steps

- [Endpoints](/api/endpoints/)
- [View all permissions](/api/permissions/)
- [Handle errors](/api/error-codes/)
