WarmblyDocs

Authentication

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 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 and a recent sign-in, and the key is capped to the scopes your role in that workspace allows.
  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.

Revoking leaves the key listed, with the time and reason it ended, and its request history intact. DELETE /v1/api-keys/:id/permanent removes the row for good, along with its usage logs, and is how the Delete key button under a revoked key in Settings > API keys works. It refuses a key that could still authenticate with a 409: revoke it first, so what ended the credential is on the record. A key past its expires_at can be deleted directly, since it already authenticates nothing.

Using your API key

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

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:

curl -X GET "https://api.warmbly.com/v1/me" \
  -H "Authorization: Bearer wmbly_abc123..."
{
  "user_id": "0b1f...",
  "email": "[email protected]",
  "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, with two differences: an OAuth token also acts within the approving member's current role, and it can never manage API keys or OAuth apps. See OAuth for the full flow.

Key security best practices

Keep Your Keys Secret

Never expose API keys in client-side code, public repositories, or logs. Treat them like passwords.

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 for a complete list.

A key acts for the member who created it. Every route checks the key's bits and that member's current role in the workspace, so a key never does more than its creator could do in the dashboard today: if the creator's role loses a permission, the key loses it too. When the creator leaves the workspace, the key stops working and answers 401 with code api_key_holder_left; create a new key from a current member.

Permission categories

CategoryDescription
ReadView resources (emails, campaigns, contacts, etc.)
WriteCreate and modify resources
BulkPerform bulk operations
SpecialAdvanced features (realtime, webhooks, API key management)

Example: read-only key

A read-only API key might have these permissions:

{
  "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".

{
  "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:

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

A restricted key only reaches those mailboxes and their mail, and naming any other mailbox answers 403:

  • Unibox reads. GET /unibox lists only their conversations, GET /unibox/thread returns only their messages, GET /unibox/count and GET /unibox/overview count only them, and GET /unibox/:id, thread labels and agent drafts are refused or left out for mail in any other mailbox.
  • Unibox writes. Reply, compose (an automatic pick chooses among the listed mailboxes), drafts, agent-draft approval and discard, PATCH /unibox/seen, PATCH /unibox/folder and snoozes refuse a message or conversation with mail in another mailbox. Marking a whole folder read (folder on PATCH /unibox/seen) is refused, since it reaches every mailbox.
  • Campaigns and analytics. A sender pool (senders on POST /campaigns, PUT /campaigns/:id/senders) may only name listed mailboxes, GET /analytics/accounts lists only them, and GET /analytics/accounts/:id refuses any other. The key's campaigns use an explicit sender list (sender_strategy explicit): a pool resolved from mailbox tags, setting email_tag_ids or email_tags, or starting a campaign whose pool is not an explicit list of the key's mailboxes answers 403 with code api_key_mailbox_limited.
  • Advisor. Applying or undoing a fix is refused when the fix changes another mailbox.
  • AI tools. /ai/tools and /mcp act across the whole workspace, so a restricted key is refused there with code api_key_mailbox_limited. Use the REST endpoints, which apply the limit.

Key expiration

Set an expiration date for temporary access:

{
  "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 (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 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
{
  "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
{
  "error": "Forbidden",
  "message": "You don't have access to this feature."
}

Next steps

On this page