TL;DR

Complete REST API documentation for Superkabe v1 — endpoints for leads, campaigns, mailboxes, domains, validation, and webhooks. Authentication is via Bearer API key; all payloads are JSON; all timestamps are ISO 8601 UTC.

Superkabe API Documentation v1

Complete reference for the Superkabe public REST API. Every endpoint, every field, every error code. Used by the dashboard UI, the MCP server, and any third-party integration you build.

Base URL (production)
https://api.superkabe.com/api/v1
Base URL (local / staging)
http://localhost:4000/api/v1
Format
JSON in · JSON out · UTF-8
Transport
HTTPS only (TLS 1.2+)

1. Overview

The Superkabe v1 API is a resource-oriented REST API that exposes every core capability of the platform to programmatic callers. All requests use HTTPS, send and receive JSON, and are scoped to a single organization via the bearer token used for authentication.

Resource model

The API is organized around eight top-level resources:

ResourceRepresentsBase path
AccountYour organization, tier, and usage counters/account
LeadsProspects you ingest and validate/leads
CampaignsNative sequencer campaigns with multi-step sequences and variants/campaigns
RepliesInbound messages + outbound replies via connected mailboxes/replies
ValidationEmail validation analytics/validation
MailboxesConnected sending accounts (Gmail, Microsoft 365, SMTP)/mailboxes
DomainsSending domains with health, recovery phase, and DNSBL state/domains

Versioning policy

The version segment (/api/v1) is part of every URL. Breaking changes always trigger a new major version (/api/v2); the previous major is supported for at least 12 months after the new one ships. Additive changes (new fields, new endpoints, new optional parameters) do not break the contract and can appear at any time.

Forward-compatibility contract

Your client must ignore unknown fields in responses and unknown error codes it does not yet handle. Do not rely on field ordering in JSON objects. Do not rely on array ordering unless the endpoint explicitly documents an order.

2. Authentication

Every request must include an Authorization header containing a bearer token. Two token types are accepted:

  1. API key — long-lived, issued from the dashboard, scoped to a single organization with a bounded set of permissions. Prefix sk_live_ for production, sk_test_ for staging.
  2. JWT access token — short-lived, issued by the login flow, used by the dashboard itself. Carries the full scope of the logged-in user's role.
Clientcurl / Node / PythonorgContext middleware1. parse Authorization2. try JWT verify3. else hash + lookup ApiKey4. attach orgId + scopes5. rate-limit per keyv1 controllerrequireScope()Postgresorg-scoped queryresponse{ success, data }request
Fig 1. Every v1 request passes through orgContext, which resolves the bearer token to an orgId + scopes tuple. The controller enforces scopes per endpoint before hitting Postgres.

2.1 Creating an API key

  1. Sign in to the dashboard.
  2. Open API & MCP from the sidebar.
  3. Click Create API key, give it a descriptive name (e.g. "Claude Desktop" or "Zapier production").
  4. Choose the exact set of scopes you want the key to carry. Default to least privilege.
  5. (Optional) Set an expiry date. Keys without an expiry live until manually revoked.
  6. Copy the full key from the confirmation screen. It is shown once. The dashboard stores only a SHA-256 hash + the first 8 characters for display.

Treat API keys as secrets

Never commit keys to source control, never log them, never embed them in a browser bundle. If a key leaks, revoke it immediately from API & MCP → Revoke; every request using the revoked key will return 401 within seconds.

2.2 Sending the Authorization header

# API key
Authorization: Bearer sk_live_abc123...

# JWT access token
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

The server detects which kind of token was sent by trying JWT verification first; if the signature check fails, it falls back to an API-key lookup (hash, then a row in the ApiKey table). Unrecognized tokens return a 401.

2.3 Key storage model

Each API key row stores:

  • id — ULID, not used for auth
  • key_prefix — the first 8 characters of the key, shown in the dashboard list so you can identify a key without seeing the secret
  • key_hash — SHA-256 hash of the full key; the only way to verify a request token
  • name — human label
  • scopes — an array of permission strings (see §3)
  • last_used_at — updated on each successful request
  • expires_at — optional, enforced at verify time
  • revoked_at — when present, every request fails 401

3. Scopes

Scopes are permission strings attached to an API key. They follow the pattern resource:action. Each endpoint enforces exactly one scope; missing the scope returns 403 Missing required scope: X.

JWT tokens (used by the dashboard) skip the scope check — they always have full access because the scope is implied by the user's role.

ScopeGatesRisk tier
account:readGET /accountlow
leads:readGET /leads, GET /leads/:idlow
leads:writePOST /leads/bulkmedium
validation:readGET /validation/resultslow
validation:triggerPOST /leads/validate (consumes credits)medium
campaigns:readGET /campaigns, GET /campaigns/:idlow
campaigns:writePOST /campaigns, PATCH /campaigns/:id, launch, pausehigh
reports:readGET /campaigns/:id/reportlow
replies:readGET /campaigns/:id/replieslow
replies:sendPOST /replies (sends a real email)high
mailboxes:readGET /mailboxeslow
domains:readGET /domainslow

Least-privilege recommendation

A key that only reads reports should carry reports:read and nothing else. If an agent only lists campaigns, grant campaigns:read but not campaigns:write. High-risk scopes (campaigns:write, replies:send, validation:trigger) should live on separate keys so they can be revoked independently.

4. Response format

Every response uses a uniform envelope:

// Success
{
 "success": true,
 "data": { ... }
 // Some list endpoints also include "meta": { total, page, limit, totalPages }
}

// Error
{
 "success": false,
 "error": "Human-readable message"
}

List endpoints that support pagination add a meta object alongside data. Pagination parameters are documented per-endpoint.

Timestamps

All timestamps are ISO 8601 strings in UTC, e.g. "2026-04-24T14:32:10.000Z". Clients should parse them as UTC and convert to the user's timezone for display.

Identifiers

All resource IDs are strings. Native sequencer resources use UUIDs; legacy platform-synced resources carry the upstream platform's ID verbatim. Never parse or type-assert IDs — treat them as opaque.

Nulls vs omitted fields

Optional fields that have no value are returned as null, not omitted. A field that is entirely absent from a response was not requested or is not applicable to that resource type.

5. Errors & status codes

TLSHTTPS onlyCORSorigin checkRate limitper-key bucketAuthJWT or API keyScope checkper endpointControllerorg-scoped401 / 403 / 429 fail-fast at any gate200 / 201 / 204 response on success
Fig 2. Request lifecycle — each gate is enforced before the controller runs. Failure at any stage returns the matching HTTP status and short-circuits.
StatusMeaningTypical causeClient action
200OKSuccessful read
201CreatedResource created (e.g. new campaign)Read data.id
204No contentSuccessful action with no body
400Bad requestMissing / malformed fieldFix input, do not retry
401UnauthorizedMissing / invalid / revoked tokenRe-issue a key, do not retry
403ForbiddenScope check failed or feature-gate (tier)Grant scope or upgrade tier
404Not foundResource does not exist in your orgVerify ID, do not retry
409ConflictState conflict (e.g. can't PATCH an active campaign)Change state, then retry
429Too many requestsRate limit exceededBackoff, respect Retry-After
500Server errorUncaught exceptionRetry with exponential backoff
502 / 503 / 504UpstreamTransient infra (DB, Redis, ESP)Retry with exponential backoff

Retry strategy

// Pseudocode — safe retry loop
async function call(req, attempt = 0) {
 const res = await fetch(req);
 if (res.ok) return res.json();

 const status = res.status;
 const retriable = status === 429 || status >= 500;
 if (!retriable || attempt >= 5) throw await res.json();

 const retryAfter = Number(res.headers.get('Retry-After')) || null;
 const backoff = retryAfter ? retryAfter * 1000 : Math.min(2 ** attempt * 500, 30_000);
 await new Promise(r => setTimeout(r, backoff + Math.random() * 250));
 return call(req, attempt + 1);
}

6. Rate limits

Rate limits are per API key, using a token-bucket algorithm backed by Redis. The current defaults are:

  • Sustained: 300 requests / minute
  • Burst: 60 requests / 10 seconds
  • Bulk write endpoints: 10 requests / minute (POST /leads/bulk, POST /leads/validate)

Every response carries three headers you should pay attention to:

X-RateLimit-Limit: 300 # requests per window
X-RateLimit-Remaining: 247 # requests left in current window
X-RateLimit-Reset: 1713968400 # unix seconds when the window resets

A 429 response carries a Retry-After header in seconds. Your client must honor it.

7. Endpoints

7.1 Account

Get account info

GET/account

Scope: account:read

Returns organization metadata, subscription tier, usage counters, and plan limits. Use this first to understand what the current key can do.

Response
{
 "success": true,
 "data": {
 "id": "org_abc",
 "name": "Acme Corp",
 "slug": "acme",
 "tier": "growth", // trial | starter | growth | scale | enterprise
 "status": "active", // active | past_due | canceled | trialing
 "usage": {
 "leads": 12483,
 "domains": 14,
 "mailboxes": 32
 },
 "limits": {
 "leads": 50000,
 "domains": 25,
 "mailboxes": 50,
 "monthly_sends": 150000,
 "validation_credits": 50000
 }
 }
}

7.2 Leads

Bulk import leads

POST/leads/bulk

Scope: leads:write

Import up to 5,000 leads per request. Duplicates are detected case-insensitively on email within the organization and are skipped (not errored). Newly created leads enter with status held and validation_status: null — validation is a separate step.

Request body
{
 "leads": [
 {
 "email": "jane.doe@acme.com", // required
 "persona": "VP Engineering", // optional, default "general"
 "source": "apollo", // optional, default "api"
 "lead_score": 85 // optional integer 0-100, default 50
 }
 ]
}
Response
{
 "success": true,
 "data": {
 "total": 3,
 "created": 2,
 "duplicates": 1,
 "errors": 0,
 "results": [
 { "email": "jane.doe@acme.com", "id": "lead_xyz", "status": "created" },
 { "email": "bob@acme.com", "id": "lead_abc", "status": "duplicate" },
 { "email": "carol@new.com", "id": "lead_def", "status": "created" }
 ]
 }
}

Per-row status values: created duplicate rejected (missing/invalid email) error (row-level DB failure).

Trigger validation

POST/leads/validate

Scope: validation:trigger

Queues validation for existing leads. Provide either lead_ids or emails. Validation runs asynchronously; poll GET /leads and check validation_status. Consumes one credit per non-cached domain.

// Option A
{ "lead_ids": ["lead_xyz", "lead_abc"] }

// Option B
{ "emails": ["jane@acme.com", "bob@acme.com"] }
{
 "success": true,
 "data": {
 "queued": 2,
 "lead_ids": ["lead_xyz", "lead_abc"],
 "message": "Validation has been queued. Poll GET /api/v1/leads to check results."
 }
}

List leads

GET/leads

Scope: leads:read

Query parameters
NameTypeDefaultNotes
pageinteger1≥ 1
limitinteger501 to 100
statusstringheld · active · paused · blocked
validation_statusstringvalid · risky · invalid · unknown · pending
searchstringcase-insensitive email substring
{
 "success": true,
 "data": [
 {
 "id": "lead_xyz",
 "email": "jane@acme.com",
 "persona": "VP Engineering",
 "status": "active",
 "lead_score": 85,
 "source": "apollo",
 "source_platform": "sequencer",
 "validation_status": "valid",
 "validation_score": 95,
 "is_catch_all": false,
 "is_disposable": false,
 "emails_sent": 3,
 "emails_opened": 2,
 "emails_clicked": 1,
 "emails_replied": 0,
 "last_activity_at": "2026-04-24T14:32:10.000Z",
 "created_at": "2026-04-20T10:00:00.000Z"
 }
 ],
 "meta": { "total": 12483, "page": 1, "limit": 50, "totalPages": 250 }
}

Get lead by ID

GET/leads/:id

Scope: leads:read. Returns the full Lead row (every column). 404 if the lead does not belong to your organization.

7.3 Campaigns (native sequencer)

Native sequencer only

v1 campaign endpoints operate on campaigns where source_platform = 'sequencer'. Legacy campaigns synced from Smartlead / Instantly / EmailBison / Reply.io are not exposed through v1 — they live on platform-specific routes.

Create campaign

POST/campaigns

Scope: campaigns:write

Creates a campaign in draft status with its sequence steps, variants, and (optionally) an initial lead set. Assigned leads are passed through the health gate:

  • RED leads are blocked and excluded from the campaign (counted as leads_blocked).
  • YELLOW leads are added with status paused — safe for operator review.
  • GREEN leads are added with status active.
Request body
{
 "name": "Q2 cold outbound - technical founders",
 "steps": [
 {
 "subject": "Quick question about {{company}}",
 "body_html": "<p>Hi {{first_name}}, ...</p>",
 "body_text": "Hi {{first_name}}, ...",
 "delay_days": 0,
 "delay_hours": 0,
 "variants": [
 { "label": "A", "subject": "Quick question", "body_html": "...", "weight": 50 },
 { "label": "B", "subject": "Following up on {{company}}", "body_html": "...", "weight": 50 }
 ]
 },
 {
 "subject": "Following up",
 "body_html": "<p>Hey {{first_name}}, ...</p>",
 "delay_days": 3
 }
 ],
 "lead_ids": ["lead_xyz", "lead_abc"],
 "schedule": {
 "timezone": "America/New_York", // IANA TZ, default UTC
 "start_time": "09:00", // default 09:00
 "end_time": "17:00", // default 17:00
 "days": ["mon","tue","wed","thu","fri"], // default Mon-Fri
 "daily_limit": 50, // default 50
 "send_gap_minutes": 17 // default 17 (natural spacing)
 }
}
Response (201 Created)
{
 "success": true,
 "data": {
 "id": "cmp_abc123",
 "name": "Q2 cold outbound - technical founders",
 "status": "draft",
 "steps_count": 2,
 "leads_assigned": 2,
 "leads_blocked": 0
 }
}

Template variables

Subject lines and bodies can reference lead fields using {{variable}} syntax. Supported variables: first_name, last_name, email, company, persona. Unresolved variables render as the empty string.

List campaigns

GET/campaigns

Scope: campaigns:read

{
 "success": true,
 "data": [
 {
 "id": "cmp_abc123",
 "name": "Q2 cold outbound",
 "status": "active", // draft | active | paused | completed | archived
 "daily_limit": 50,
 "schedule_timezone": "America/New_York",
 "leads_count": 1200,
 "steps_count": 4,
 "created_at": "2026-04-20T10:00:00.000Z",
 "updated_at": "2026-04-24T14:00:00.000Z"
 }
 ]
}

Get campaign details

GET/campaigns/:id

Scope: campaigns:read. Returns the campaign, every sequence step with its variants, and the first 100 assigned leads (leads[]).

Update campaign

PATCH/campaigns/:id

Scope: campaigns:write

Only updatable in draft or paused state — PATCH on an active campaign returns 400 "Cannot update an active campaign. Pause it first." Updatable fields:

{
 "name": "New name",
 "daily_limit": 100,
 "send_gap_minutes": 20,
 "schedule_timezone": "UTC",
 "schedule_start_time": "08:00",
 "schedule_end_time": "18:00",
 "schedule_days": ["mon","tue","wed","thu","fri","sat"]
}

Sequence steps and variants are not editable through this endpoint — rebuild the campaign or edit through the dashboard.

Launch campaign

POST/campaigns/:id/launch

Scope: campaigns:write

Sets status to active and stamps launched_at. The sendQueueService picks up active campaigns on its next 60s tick. Preconditions enforced by the server:

  • Campaign is not already active → 400
  • Campaign has at least one sequence step → else 400
  • Campaign has at least one assigned lead → else 400
{
 "success": true,
 "data": { "id": "cmp_abc123", "status": "active", "leads": 1200, "steps": 4 }
}

Pause campaign

POST/campaigns/:id/pause

Scope: campaigns:write. Returns 400 if the campaign is not currently active. In-flight sends within the current dispatcher cycle may still complete; subsequent cycles skip the campaign.

Get campaign report

GET/campaigns/:id/report

Scope: reports:read

{
 "success": true,
 "data": {
 "campaign_id": "cmp_abc123",
 "campaign_name": "Q2 cold outbound",
 "status": "active",
 "total_leads": 1200,
 "lead_status_breakdown": {
 "active": 850,
 "paused": 120,
 "completed": 210,
 "unsubscribed": 20
 },
 "emails_sent": 2300,
 "replies": 47,
 "reply_rate": "2.04%",
 "created_at": "2026-04-20T10:00:00.000Z"
 }
}

Get campaign replies

GET/campaigns/:id/replies

Scope: replies:read

Returns up to the 100 most recently replied-on threads. Each thread is collapsed to its most recent inbound message; use POST /replies with the returned thread_id to respond.

{
 "success": true,
 "data": {
 "total_replies": 47,
 "replies": [
 {
 "thread_id": "thr_123",
 "contact_email": "prospect@bigcorp.com",
 "contact_name": "Alex Prospect",
 "subject": "Re: Quick question about BigCorp",
 "body_text": "Happy to chat — how about Thursday at 2pm?",
 "received_at": "2026-04-24T14:32:10.000Z"
 }
 ]
 }
}

7.4 Replies

Send a reply

POST/replies

Scope: replies:send

Sends an outbound message on an existing thread through the originally connected mailbox. The thread's mailbox must have connection_status = active, otherwise returns 400.

Request body
{
 "thread_id": "thr_123",
 "body_html": "<p>Thursday at 2pm works — sending a calendar invite now.</p>",
 "body_text": "Thursday at 2pm works — sending a calendar invite now."
}

Provide body_html or body_text, or both. At least one is required. If both are provided, multipart/alternative is constructed automatically.

{
 "success": true,
 "data": {
 "message_id": "msg_456",
 "thread_id": "thr_123",
 "from": "sender@yourdomain.com",
 "to": "prospect@bigcorp.com",
 "status": "sent"
 }
}

7.5 Validation analytics

GET/validation/results

Scope: validation:read

Organization-wide validation rollup. Use this for dashboards; for per-lead results call GET /leads with validation_status filters.

{
 "success": true,
 "data": {
 "total_validated": 12483,
 "status_breakdown": {
 "valid": 9812,
 "risky": 1402,
 "invalid": 980,
 "unknown": 289
 }
 }
}

7.6 Mailboxes

GET/mailboxes

Scope: mailboxes:read

Every sending account connected to your organization — Gmail, Microsoft 365, SMTP, and shadow mailboxes for connected platforms. Sorted alphabetically by email.

{
 "success": true,
 "data": [
 {
 "id": "mbx_abc",
 "email": "sender@yourdomain.com",
 "status": "active", // active | paused | quarantine | restricted | healing
 "source_platform": "sequencer", // sequencer | smartlead | instantly | emailbison | replyio
 "smtp_status": "ok", // ok | degraded | failing | unknown
 "imap_status": "ok",
 "total_sent_count": 1204,
 "hard_bounce_count": 3,
 "warmup_status": "active", // off | active | paused
 "warmup_reputation": 92, // 0 - 100
 "recovery_phase": null, // null when healthy; paused | quarantine | restricted_send | warm_recovery
 "resilience_score": 85 // 0 - 100
 }
 ]
}

7.7 Domains

GET/domains

Scope: domains:read

{
 "success": true,
 "data": [
 {
 "id": "dom_abc",
 "domain": "yourdomain.com",
 "status": "active",
 "source_platform": "sequencer",
 "total_sent_lifetime": 24103,
 "total_opens": 8912,
 "total_clicks": 1204,
 "total_replies": 187,
 "aggregated_bounce_rate_trend": 1.8, // last 24h bounce-rate %
 "warning_count": 0,
 "recovery_phase": null,
 "resilience_score": 90
 }
 ]
}

8. Idempotency

Read endpoints (GET) are naturally idempotent. Write endpoints that you want to retry safely — particularly POST /leads/bulk, POST /campaigns, and POST /replies — accept an optional header:

Idempotency-Key: <any unique string, max 255 chars>

If two requests arrive with the same key from the same API key within 24 hours, the server returns the cached response from the first request. This lets you retry a timed-out POST without fear of creating the campaign twice.

9. Webhooks (inbound)

Separate from the v1 API, Superkabe exposes HMAC-signed inbound webhook endpoints for Clay, sending platforms, and billing. See API & Webhook Integration for the full list, payload formats, signature verification, and event types.

10. SDK examples

cURL

# List active campaigns
curl -s https://api.superkabe.com/api/v1/campaigns \
 -H "Authorization: Bearer $SUPERKABE_API_KEY"

# Create a one-step campaign with two leads
curl -s https://api.superkabe.com/api/v1/campaigns \
 -H "Authorization: Bearer $SUPERKABE_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "name": "April outbound",
 "steps": [{ "subject": "Quick question", "body_html": "<p>Hi {{first_name}}</p>" }],
 "lead_ids": ["lead_xyz", "lead_abc"]
 }'

# Launch it
curl -s -X POST https://api.superkabe.com/api/v1/campaigns/cmp_abc/launch \
 -H "Authorization: Bearer $SUPERKABE_API_KEY"

Node.js (fetch)

const API = 'https://api.superkabe.com/api/v1';
const KEY = process.env.SUPERKABE_API_KEY;

async function sk(method, path, body) {
 const res = await fetch(API + path, {
 method,
 headers: {
 'Authorization': `Bearer ${KEY}`,
 'Content-Type': 'application/json',
 },
 body: body ? JSON.stringify(body) : undefined,
 });
 const json = await res.json();
 if (!json.success) throw new Error(json.error);
 return json.data;
}

// Usage
const account = await sk('GET', '/account');
console.log(account.tier); // "growth"

const { id } = await sk('POST', '/campaigns', {
 name: 'April outbound',
 steps: [{ subject: 'Hi', body_html: '<p>Hey {{first_name}}</p>' }],
 lead_ids: ['lead_xyz'],
});
await sk('POST', `/campaigns/${id}/launch`);

Python (requests)

import os, requests

API = "https://api.superkabe.com/api/v1"
KEY = os.environ["SUPERKABE_API_KEY"]

class SuperkabeError(Exception): pass

def sk(method, path, body=None):
 r = requests.request(
 method, API + path,
 headers={"Authorization": f"Bearer {KEY}"},
 json=body,
 timeout=30,
 )
 data = r.json()
 if not data.get("success"):
 raise SuperkabeError(data.get("error", r.text))
 return data["data"]

# Pull a report for every active campaign
campaigns = sk("GET", "/campaigns")
for c in campaigns:
 if c["status"] == "active":
 report = sk("GET", f"/campaigns/{c['id']}/report")
 print(c["name"], "-", report["reply_rate"])

11. Changelog

  • 2026-04-24 v1 endpoints operate on the unified Campaign table (post SendCampaign merge). No contract change for clients.
  • 2026-03-10 Added send_gap_minutes to campaign schedule.
  • 2026-02-01 POST /replies added with replies:send scope.
  • 2026-01-15 Initial v1 public release.

Next steps

  • Generate an API key in Dashboard → API & MCP.
  • Try the cURL examples above against /account.
  • Connect the Superkabe MCP server to drive the API from Claude.
  • Wire up inbound webhooks from Clay or your sending platform.

Frequently Asked Questions

Is there an OpenAPI spec?

+

Yes — /api/v1/openapi.json returns the full OpenAPI 3.1 document, which you can import into Postman, Insomnia, or any SDK generator.

How are errors formatted?

+

All error responses follow { error: { code, message, details? } } with conventional HTTP status codes. 4xx means client-fixable; 5xx means server-side and retryable with backoff.

Is there a sandbox environment?

+

Yes — api-sandbox.superkabe.com mirrors production endpoints with test credentials and simulated bounce/reply events for integration testing.