WarmblyDocs

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.

API key accepted

Emails

MethodPathAPI Permission
GET/emailsREAD_EMAILS
GET/emails/:idREAD_EMAILS
PATCH/emails/:idWRITE_EMAILS
PATCH/emails/tagsWRITE_EMAILS
PATCH/emails/:id/trackWRITE_EMAILS
GET/emails/:id/syncREAD_EMAILS
GET/emails/:id/behaviorREAD_EMAILS
PUT/emails/:id/behaviorWRITE_EMAILS
GET/emails/:id/behavior/planREAD_EMAILS
DELETE/emails/:idWRITE_EMAILS
POST/emails/:id/sendSEND_CAMPAIGNS

Campaigns and sequences

MethodPathAPI Permission
GET/campaignsREAD_CAMPAIGNS
GET/campaigns-overviewREAD_CAMPAIGNS
POST/campaignsWRITE_CAMPAIGNS
GET/campaigns/:idREAD_CAMPAIGNS
PATCH/campaigns/:idWRITE_CAMPAIGNS
DELETE/campaigns/:idWRITE_CAMPAIGNS
GET/campaigns/:id/advancedREAD_CAMPAIGNS
PATCH/campaigns/:id/advancedWRITE_CAMPAIGNS
GET/campaigns/:id/ab-variantsREAD_CAMPAIGNS
POST/campaigns/:id/ab-variantsWRITE_CAMPAIGNS
PATCH/campaigns/:id/ab-variants/:variantIdWRITE_CAMPAIGNS
DELETE/campaigns/:id/ab-variants/:variantIdWRITE_CAMPAIGNS
GET/campaigns/:id/ab-analysisREAD_ANALYTICS
POST/campaigns/:id/preflightSEND_CAMPAIGNS
POST/campaigns/:id/test-emailSEND_CAMPAIGNS
POST/campaigns/:id/startSEND_CAMPAIGNS
POST/campaigns/:id/stopSEND_CAMPAIGNS
GET/campaigns/:id/logsREAD_CAMPAIGNS
GET/POST/PATCH/DELETE/campaigns/:id/steps[/:sid]READ_CAMPAIGNS for GET, WRITE_CAMPAIGNS otherwise
PATCH/campaigns/:id/step-layoutWRITE_CAMPAIGNS
POST/generation/writeWRITE_CAMPAIGNS
POST/generation/editWRITE_CAMPAIGNS
POST/generation/ai-variableWRITE_CAMPAIGNS

Contacts

MethodPathAPI Permission
POST/contacts/searchREAD_CONTACTS
GET/contacts/custom-fieldsREAD_CONTACTS
POST/contactsWRITE_CONTACTS
DELETE/contactsBULK_CONTACTS
PATCH/contactsBULK_CONTACTS
PATCH/contacts/:idWRITE_CONTACTS
DELETE/contacts/:idWRITE_CONTACTS
GET/contacts/:id/notesREAD_CONTACTS
POST/contacts/:id/notesWRITE_CONTACTS
PATCH/contacts/:id/notes/:noteIdWRITE_CONTACTS
DELETE/contacts/:id/notes/:noteIdWRITE_CONTACTS
GET/contacts/:id/activitiesREAD_CONTACTS
GET/contacts/:id/dealsREAD_CRM
POST/contacts/:id/researchAI_RESEARCH
GET/contacts/:id/researchAI_RESEARCH
POST/contacts/research/batchAI_RESEARCH

GET /contacts/custom-fields lists the distinct custom-field keys used across your contacts (frequency-ranked), for building personalization pickers. It returns a flat string array under data, capped at 200 keys.

AI contact research charges credits (2 per run, billable even when it finds nothing) and only saves cited findings. See the AI contact research guide. The batch endpoint accepts up to 500 contact ids and drains in the background.

Unibox

MethodPathAPI Permission
GET/uniboxREAD_UNIBOX
GET/unibox/countREAD_UNIBOX
GET/unibox/threadREAD_UNIBOX
GET/unibox/:idREAD_UNIBOX
PATCH/unibox/seenWRITE_UNIBOX
POST/unibox/replyWRITE_UNIBOX
POST/unibox/reply/draftREAD_UNIBOX
GET/unibox/compose/candidatesREAD_UNIBOX
POST/unibox/composeWRITE_UNIBOX
POST/unibox/compose/draftREAD_UNIBOX
GET/unibox/draftsREAD_UNIBOX
PUT/unibox/drafts/:idWRITE_UNIBOX
DELETE/unibox/drafts/:idWRITE_UNIBOX
GET/unibox/agent-draftsREAD_UNIBOX
POST/unibox/agent-drafts/:id/approveWRITE_UNIBOX
POST/unibox/agent-drafts/:id/discardWRITE_UNIBOX

POST /unibox/reply/draft returns an AI-drafted reply (it never sends) grounded in the thread, the contact, and your voice profile. It charges AI credits; see AI credits.

POST /unibox/compose/draft returns a grounded AI draft for a new email (it never sends): the recipient's contact record, correspondence history, and the workspace voice profile feed the prompt, and the response carries either text or a clarifying question plus a grounding report. Charges AI credits like the reply draft; see AI credits.

POST /unibox/compose sends a brand-new outbound email (not a reply). Omit email_account_id (or pass "auto") and the backend picks the best mailbox for the first recipient; the response reports account_id, account_email, auto, and a picked_reason. Recipients on the workspace suppression list are rejected with a 400 before anything queues. GET /unibox/compose/candidates?to=<address> returns every active mailbox scored for that recipient (conversation history, remaining daily budget, domain-auth health) plus the resolved contact and suppression state; see composing email.

The /unibox/drafts endpoints hold autosaved compose drafts, scoped to the calling user within the organization. The draft id is client-generated, so PUT is idempotent (safe for debounced autosave and retries); deleting a missing draft is a no-op.

PATCH /emails/:id accepts save_to_sent (boolean) on SMTP/IMAP mailboxes: when true, which is the default, the worker files a copy of each outbound message in the mailbox's Sent folder. It has no effect on Gmail and Outlook mailboxes, whose APIs file their own copy. See keeping a copy of sent mail.

GET /unibox and GET /unibox/thread return message previews: each row carries snippet, a one-line summary, not the message body. Read a full message with GET /unibox/:id, which returns body_plain plus body_html. The HTML is sanitized before it leaves the API (scripts, event handlers, embedded frames, and unsafe URL schemes are removed), so it is safe to render, and links carry target="_blank" with rel="noopener". body_truncated is true on the rare message whose stored body could not be read, where body_plain falls back to the snippet.

GET /unibox also accepts address=<value> (matches sender or recipient, for "every conversation with this person") and direction=sent|received (resolved against your own mailbox addresses).

The agent-drafts endpoints back the inbox agent: the agent drafts a suggested reply on an inbound human reply and holds it here for review. GET lists the pending drafts; approve sends one (optionally with an edited body) through the normal reply path and is safe to retry with an Idempotency-Key; discard dismisses it. Approving is the only path that sends.

Templates and CRM

MethodPathAPI Permission
GET/templates, /templates/:idREAD_TEMPLATES
POST/PATCH/DELETE/templates[/:id]WRITE_TEMPLATES
GET/crm/pipelines, /crm/pipelines/:idREAD_CRM
POST/PATCH/DELETE/crm/pipelines[/:id][/stages[/:stageId]]WRITE_CRM
GET/crm/deals, /crm/deals/:idREAD_CRM
POST/PATCH/DELETE/crm/deals[/:id]WRITE_CRM
GET/crm/tasks, /crm/tasks/:idREAD_CRM
POST/PATCH/DELETE/crm/tasks[/:id]WRITE_CRM

Analytics and audit

MethodPathAPI Permission
GET/analytics/* (dashboard, deliverability, warmup, campaigns, accounts, usage)READ_ANALYTICS
GET/audit-logsREAD_AUDIT_LOGS

Advisor

Recommendations about deliverability, mailbox configuration, warmup, campaign performance, copy, and list quality. See the Advisor guide.

Reads are an analytics read of the workspace's sending posture. Applying or undoing a fix has no scope of its own: the change runs through the tool the fix actually uses and is refused if the caller lacks that tool's permission, so a key that can read recommendations cannot use them to make changes it could not make directly.

MethodPathAPI Permission
GET/advisor/recommendationsREAD_ANALYTICS
GET/advisor/summaryREAD_ANALYTICS
GET/advisor/settingsREAD_ANALYTICS
POST/advisor/refreshREAD_ANALYTICS
POST/advisor/recommendations/:id/snoozeREAD_ANALYTICS
POST/advisor/recommendations/:id/dismissREAD_ANALYTICS
POST/advisor/recommendations/:id/feedbackREAD_ANALYTICS
POST/advisor/recommendations/:id/applypermission of the underlying change
POST/advisor/recommendations/:id/undopermission of the underlying change

Changing Advisor settings (PATCH /advisor/settings) is JWT only, alongside the rest of organization settings. Silencing checks for a whole workspace is governance, and no read scope should be able to do it. The same applies to POST /advisor/recommendations/:id/agent-fix, which is JWT only and needs USE_AI: it acts as a named member and spends credits, and no API scope should let a key rewrite a workspace's campaigns unattended.

API key self-service

MethodPathAPI Permission
GET/api-keysAPI_KEYS
POST/api-keysAPI_KEYS
GET/api-keys/permissionsAPI_KEYS
GET/api-keys/:idAPI_KEYS
PATCH/api-keys/:idAPI_KEYS
DELETE/api-keys/:idAPI_KEYS

OAuth apps

Registering and managing the OAuth apps your workspace owns. The flow itself (authorize, token, revoke) is listed under JWT only and Public below.

MethodPathAPI Permission
GET/oauth/applicationsAPI_KEYS
POST/oauth/applicationsAPI_KEYS
GET/oauth/applications/:idAPI_KEYS
PATCH/oauth/applications/:idAPI_KEYS
DELETE/oauth/applications/:idAPI_KEYS
POST/oauth/applications/:id/rotate-secretAPI_KEYS
GET/oauth/applications/:id/webhook-secretAPI_KEYS
POST/oauth/applications/:id/webhook-secret/rotateAPI_KEYS
GET/oauth/applications/:id/webhook-endpointsAPI_KEYS
GET/oauth/applications/:id/webhook-deliveriesAPI_KEYS

Operations

MethodPathAPI Permission
GET/PATCH/outreach/settingsWRITE_CAMPAIGNS
POST/deliverability/eventsWRITE_CAMPAIGNS
GET/tasks/dlqSEND_CAMPAIGNS
POST/tasks/dlq/:id/replaySEND_CAMPAIGNS
GET/POST/PATCH/DELETE/webhooks[/:id]WEBHOOKS
POST/webhooks/:id/rotate-secretWEBHOOKS
POST/webhooks/:id/verifyWEBHOOKS
GET/webhooks/event-typesWEBHOOKS
GET/webhooks/deliveriesWEBHOOKS
GET/webhooks/:id/deliveriesWEBHOOKS
POST/webhooks/deliveries/:deliveryId/redeliverWEBHOOKS
GET/webhooks/throttle-dropsWEBHOOKS
GET/POST/DELETE/integrations/*INTEGRATIONS
GET/POST/PATCH/DELETE/automations[/:id]INTEGRATIONS
PATCH/automations/:id/layoutINTEGRATIONS
GET/POST/PATCH/DELETE/warmup/routing[/:id]WARMUP_ROUTING

Retry safety

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.

Identity

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

Reference data

MethodPathAPI Permission
GET/plans(any authenticated key)
GET/timezones(any authenticated key)

JWT only

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
  • GET /auth/config (public deployment capabilities: which sign-in methods this backend has enabled, whether a login code step follows, whether signups are open, whether the instance still needs claiming)

POST /auth/register accepts an optional invite field carrying an invitation token. On a deployment running DISABLE_REGISTRATION=invite_only it is what permits the signup, and the account is created inside the inviting organization rather than in a new one. The token must resolve to a live invitation whose email equals the submitted address, otherwise the request is refused with invitation_invalid. Omitting it on a closed deployment returns registration_invite_only or registration_closed. See error codes.

GET /auth/config gained two fields: invites_required (boolean, true when an invitation token is needed to create an account) and docs_url (string, the deployment's link to the accounts and access documentation, for a client to surface next to a refusal).

GET /auth/config also carries billing_enabled (boolean). It is false when the deployment runs with BILLING_PROVIDER=none, which is the self-host default: every feature is unlocked server-side, so the dashboard shows the workspace as self-hosted instead of on a free trial and hides the billing and referral pages. self_hosted alone does not imply this, because a self-hosted install may still run Stripe.

  • POST /auth/setup (first-run claim: exchanges the one-time token printed at boot for the owner account. Refused once any account exists)
  • GET /auth/providers, POST /auth/apple, POST /auth/google (native-app social sign-in)
  • POST /auth/oidc/begin, GET /auth/oidc/callback (generic OpenID Connect sign-in)
  • 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/*

Referrals and discounts

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.

MethodPathJWT permission
GET/subscription/referralmanage_billing
POST/subscription/referralmanage_billing
GET/subscription/referral/attributionsmanage_billing
GET/subscription/referral/earningsmanage_billing
GET/subscription/discountsmanage_billing

AI credits

Credit balance, top-up purchases, and the transaction log are part of /subscription/*, so they are JWT only and never accept an API key. Purchases are fulfilled only in the Stripe webhook, never in the checkout call. See the AI credits guide for what each action costs and the pack and reset rules. GET /subscription/credits carries unlimited (boolean): true on a deployment with BILLING_PROVIDER=none, where the ledger is bypassed and every balance and allowance field is zero and meaningless.

MethodPathJWT permission
GET/subscription/creditsmanage_billing
GET/subscription/credits/transactionsmanage_billing
POST/subscription/credits/checkoutmanage_billing

AI assistant

The dashboard AI assistant is JWT only: sessions are private to the member who started them. The message and approval runs stream over Server-Sent Events. Each tool the assistant runs is gated by the member's own organization permission bits, so the assistant can never do more than the member could by hand. See the AI assistant guide. (API-key and OAuth callers reach the same tools through the MCP server, gated by the AI_AGENT scope.)

MethodPathJWT permission
POST/ai/sessionsorganization member (use AI)
GET/ai/sessionsorganization member (use AI)
DELETE/ai/sessionsorganization member (use AI)
DELETE/ai/sessions/:idorganization member (use AI)
GET/ai/sessions/:id/messagesorganization member (use AI)
POST/ai/sessions/:id/messagesorganization member (use AI, SSE)
POST/ai/sessions/:id/approveorganization member (use AI, SSE)

AI skills

Org playbooks the AI features follow (see the AI skills guide). Gated on manage_settings for JWT callers, or the AI_AGENT scope for API keys.

MethodPathPermission
GET/ai/skillsmanage_settings / AI_AGENT
POST/ai/skillsmanage_settings / AI_AGENT
PATCH/ai/skills/:idmanage_settings / AI_AGENT
DELETE/ai/skills/:idmanage_settings / AI_AGENT

Connected MCP servers

External MCP servers whose tools the assistant can use (see Connect MCP tools). JWT-only and manage_settings-gated; bearer tokens are sealed with the org key and never returned.

MethodPathJWT permission
GET/ai/connectionsmanage_settings
POST/ai/connectionsmanage_settings
PATCH/ai/connections/:idmanage_settings
DELETE/ai/connections/:idmanage_settings
POST/ai/connections/:id/refreshmanage_settings

MCP server

POST /v1/mcp exposes the tool registry over the Model Context Protocol streamable-HTTP transport. It accepts an API key or an OAuth 2.1 access token (an unauthenticated request gets the RFC 9728 discovery challenge). Each tool is gated by its scope, tools/list reflects only what the credential allows, and send-class tools are never exposed. Per-key rate limits apply.

Public

  • 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, or PKCE for public clients)
  • POST /oauth/register (OAuth dynamic client registration, RFC 7591; open and per-IP rate-limited)
  • GET /.well-known/oauth-authorization-server, GET /.well-known/oauth-protected-resource (OAuth discovery metadata)

Notes

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

See also

On this page