Endpoint scope map
Every endpoint, which auth types it accepts, and which permission it requires.
This page is the source of truth for what an API key can and cannot reach. Every route in the backend falls into one of three buckets:
- API key accepted: works with both a JWT (dashboard user) and an API key with the listed permission.
- JWT only: never reachable via an API key. Used for billing, governance, websocket bootstrap, danger-zone destruction, the OAuth onboarding flow, and admin tools.
- Public: no auth required (webhooks with signature verification, health check, OAuth bouncer pages).
When an endpoint says "JWT permission: X / API permission: Y", the dual-auth middleware checks the relevant one based on which credential the caller used.
All paths below are relative to the versioned base URL https://api.warmbly.com/v1 (for example /campaigns is https://api.warmbly.com/v1/campaigns). See versioning for details.
| Method | Path | API Permission |
|---|
| GET | /emails | READ_EMAILS |
| GET | /emails/:id | READ_EMAILS |
| PATCH | /emails/:id | WRITE_EMAILS |
| PATCH | /emails/:id/track | WRITE_EMAILS |
| DELETE | /emails/:id | WRITE_EMAILS |
| POST | /emails/:id/send | SEND_CAMPAIGNS |
| Method | Path | API Permission |
|---|
| GET | /campaigns | READ_CAMPAIGNS |
| POST | /campaigns | WRITE_CAMPAIGNS |
| GET | /campaigns/:id | READ_CAMPAIGNS |
| PATCH | /campaigns/:id | WRITE_CAMPAIGNS |
| DELETE | /campaigns/:id | WRITE_CAMPAIGNS |
| GET | /campaigns/:id/advanced | READ_CAMPAIGNS |
| PATCH | /campaigns/:id/advanced | WRITE_CAMPAIGNS |
| GET | /campaigns/:id/ab-variants | READ_CAMPAIGNS |
| POST | /campaigns/:id/ab-variants | WRITE_CAMPAIGNS |
| PATCH | /campaigns/:id/ab-variants/:variantId | WRITE_CAMPAIGNS |
| DELETE | /campaigns/:id/ab-variants/:variantId | WRITE_CAMPAIGNS |
| GET | /campaigns/:id/ab-analysis | READ_ANALYTICS |
| POST | /campaigns/:id/preflight | SEND_CAMPAIGNS |
| POST | /campaigns/:id/test-email | SEND_CAMPAIGNS |
| POST | /campaigns/:id/start | SEND_CAMPAIGNS |
| POST | /campaigns/:id/stop | SEND_CAMPAIGNS |
| GET | /campaigns/:id/logs | READ_CAMPAIGNS |
| GET/POST/PATCH/DELETE | /campaigns/:id/steps[/:sid] | READ_CAMPAIGNS for GET, WRITE_CAMPAIGNS otherwise |
| PATCH | /campaigns/:id/step-layout | WRITE_CAMPAIGNS |
| Method | Path | API Permission |
|---|
| POST | /contacts/search | READ_CONTACTS |
| POST | /contacts | WRITE_CONTACTS |
| DELETE | /contacts | BULK_CONTACTS |
| PATCH | /contacts | BULK_CONTACTS |
| PATCH | /contacts/:id | WRITE_CONTACTS |
| DELETE | /contacts/:id | WRITE_CONTACTS |
| GET | /contacts/:id/notes | READ_CONTACTS |
| POST | /contacts/:id/notes | WRITE_CONTACTS |
| PATCH | /contacts/:id/notes/:noteId | WRITE_CONTACTS |
| DELETE | /contacts/:id/notes/:noteId | WRITE_CONTACTS |
| GET | /contacts/:id/activities | READ_CONTACTS |
| GET | /contacts/:id/deals | READ_CRM |
| Method | Path | API Permission |
|---|
| GET | /unibox | READ_UNIBOX |
| GET | /unibox/count | READ_UNIBOX |
| GET | /unibox/thread | READ_UNIBOX |
| GET | /unibox/:id | READ_UNIBOX |
| PATCH | /unibox/seen | WRITE_UNIBOX |
| POST | /unibox/reply | WRITE_UNIBOX |
| Method | Path | API Permission |
|---|
| GET | /templates, /templates/:id | READ_TEMPLATES |
| POST/PATCH/DELETE | /templates[/:id] | WRITE_TEMPLATES |
| GET | /crm/pipelines, /crm/pipelines/:id | READ_CRM |
| POST/PATCH/DELETE | /crm/pipelines[/:id][/stages[/:stageId]] | WRITE_CRM |
| GET | /crm/deals, /crm/deals/:id | READ_CRM |
| POST/PATCH/DELETE | /crm/deals[/:id] | WRITE_CRM |
| GET | /crm/tasks, /crm/tasks/:id | READ_CRM |
| POST/PATCH/DELETE | /crm/tasks[/:id] | WRITE_CRM |
| Method | Path | API Permission |
|---|
| GET | /analytics/* (dashboard, deliverability, warmup, campaigns, accounts, usage) | READ_ANALYTICS |
| GET | /audit-logs | READ_AUDIT_LOGS |
| Method | Path | API Permission |
|---|
| GET | /api-keys | API_KEYS |
| POST | /api-keys | API_KEYS |
| GET | /api-keys/permissions | API_KEYS |
| GET | /api-keys/:id | API_KEYS |
| PATCH | /api-keys/:id | API_KEYS |
| DELETE | /api-keys/:id | API_KEYS |
Registering and managing the OAuth apps your workspace owns. The flow itself (authorize, token, revoke) is listed under JWT only and Public below.
| Method | Path | API Permission |
|---|
| GET | /oauth/applications | API_KEYS |
| POST | /oauth/applications | API_KEYS |
| GET | /oauth/applications/:id | API_KEYS |
| PATCH | /oauth/applications/:id | API_KEYS |
| DELETE | /oauth/applications/:id | API_KEYS |
| POST | /oauth/applications/:id/rotate-secret | API_KEYS |
| GET | /oauth/applications/:id/webhook-secret | API_KEYS |
| POST | /oauth/applications/:id/webhook-secret/rotate | API_KEYS |
| GET | /oauth/applications/:id/webhook-endpoints | API_KEYS |
| GET | /oauth/applications/:id/webhook-deliveries | API_KEYS |
| Method | Path | API Permission |
|---|
| GET/PATCH | /outreach/settings | WRITE_CAMPAIGNS |
| POST | /deliverability/events | WRITE_CAMPAIGNS |
| GET | /tasks/dlq | SEND_CAMPAIGNS |
| POST | /tasks/dlq/:id/replay | SEND_CAMPAIGNS |
| GET/POST/PATCH/DELETE | /webhooks[/:id] | WEBHOOKS |
| POST | /webhooks/:id/rotate-secret | WEBHOOKS |
| POST | /webhooks/:id/verify | WEBHOOKS |
| GET | /webhooks/event-types | WEBHOOKS |
| GET | /webhooks/deliveries | WEBHOOKS |
| GET | /webhooks/:id/deliveries | WEBHOOKS |
| POST | /webhooks/deliveries/:deliveryId/redeliver | WEBHOOKS |
| GET | /webhooks/throttle-drops | WEBHOOKS |
| GET/POST/DELETE | /integrations/* | INTEGRATIONS |
| GET/POST/PATCH/DELETE | /automations[/:id] | INTEGRATIONS |
| PATCH | /automations/:id/layout | INTEGRATIONS |
| GET/POST/PATCH/DELETE | /warmup/routing[/:id] | WARMUP_ROUTING |
Mutating API requests may include an Idempotency-Key header. Warmbly stores the completed response for 24 hours per organization and key. Reusing the same key with the same method, route, query, and body replays the original response with X-Idempotent-Replayed: true; reusing the key with a different request returns 409 Conflict.
| Method | Path | API Permission |
|---|
| GET | /me | (any authenticated key) |
GET /me returns who the active credential belongs to: user_id, email, name, organization_id, organization_name, auth_type (api_key, oauth, or jwt), and the granted scopes. It requires no specific permission, so it is the right call for validating a connection and rendering a human-readable label. Unlike /auth/me (JWT only), it is reachable by API keys and OAuth tokens.
| Method | Path | API Permission |
|---|
| GET | /plans | (any authenticated key) |
| GET | /timezones | (any authenticated key) |
These never accept an API key. They depend on a human-bound session: billing flows, governance, OAuth onboarding, websocket bootstrap, and operational destruction.
POST /auth/login, /auth/login/confirm, /auth/register, /auth/register/confirm, /auth/refresh, /auth/reset-password, /auth/reset-password/confirm
POST /auth/logout, POST /auth/logout-all, GET /auth/me, PATCH /auth/me/onboarding
POST /auth/me/avatar, DELETE /auth/me/avatar
POST /emails/onboarding/oauth/start, POST /emails/onboarding/oauth/finish, POST /emails/onboarding/smtp-imap
GET /oauth/authorize/details, POST /oauth/authorize (the consent flow: a human approves a third-party app)
GET /oauth/authorized-apps, DELETE /oauth/authorized-apps/:id (apps the user has authorized)
POST /getaway (websocket bootstrap)
GET /realtime/info
GET /me/danger-zone, POST /me/danger-zone/delete, DELETE /me/danger-zone/delete
GET /invitations, POST /invitations/accept
- All of
/organization/* (create, switch, members, invitations, transfer ownership, avatar, danger zone)
- All of
/subscription/* (checkout, portal, cancel, change-plan, preview-change, enterprise-inquiry, discounts, referrals, etc.)
- All of
/admin/*
The referral program and billing discount history are part of /subscription/*, so they are JWT only and never accept an API key. There is no API permission scope for them; browser callers are gated by the org permission below. The discount validation and checkout endpoints are listed under Subscription and billing in the reference.
| Method | Path | JWT permission |
|---|
| GET | /subscription/referral | manage_billing |
| POST | /subscription/referral | manage_billing |
| GET | /subscription/referral/attributions | manage_billing |
| GET | /subscription/referral/earnings | manage_billing |
| GET | /subscription/discounts | manage_billing |
GET /health
POST /webhooks/github/releases (HMAC-SHA256 signature)
POST /webhook/stripe (Stripe signature)
POST /webhook/campaign, /webhook/email, /webhook/user-email (Google OIDC token from Cloud Tasks)
GET /addresses/google/callback, GET /addresses/outlook/callback (OAuth bouncer pages)
POST /oauth/token, POST /oauth/revoke (OAuth token endpoints, authenticated by the client's id and secret)
GET /.well-known/oauth-authorization-server (OAuth discovery metadata)
- When a route has both a JWT permission and an API permission listed, the dual-auth middleware checks the JWT user's organization role for browser callers and the key's permission bitmask for API key callers. They're independent gates: a user's role doesn't constrain what an API key can do beyond what was granted at creation.
- Every API key request is logged to
api_key_usage_logs with the endpoint pattern (e.g. /campaigns/:id), method, IP, user-agent, response status, and elapsed milliseconds. JWT requests are not logged here.
- Rate limit categories (
X-RateLimit-* headers) are scoped to the underlying user, not the key; every key issued by a user shares the user's quota.