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.
https://api.superkabe.com/api/v1http://localhost:4000/api/v11. 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:
| Resource | Represents | Base path |
|---|---|---|
| Account | Your organization, tier, and usage counters | /account |
| Leads | Prospects you ingest and validate | /leads |
| Campaigns | Native sequencer campaigns with multi-step sequences and variants | /campaigns |
| Replies | Inbound messages + outbound replies via connected mailboxes | /replies |
| Validation | Email validation analytics | /validation |
| Mailboxes | Connected sending accounts (Gmail, Microsoft 365, SMTP) | /mailboxes |
| Domains | Sending 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:
- 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. - 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.
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
- Sign in to the dashboard.
- Open API & MCP from the sidebar.
- Click Create API key, give it a descriptive name (e.g. "Claude Desktop" or "Zapier production").
- Choose the exact set of scopes you want the key to carry. Default to least privilege.
- (Optional) Set an expiry date. Keys without an expiry live until manually revoked.
- 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 authkey_prefix— the first 8 characters of the key, shown in the dashboard list so you can identify a key without seeing the secretkey_hash— SHA-256 hash of the full key; the only way to verify a request tokenname— human labelscopes— an array of permission strings (see §3)last_used_at— updated on each successful requestexpires_at— optional, enforced at verify timerevoked_at— when present, every request fails401
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.
| Scope | Gates | Risk tier |
|---|---|---|
| account:read | GET /account | low |
| leads:read | GET /leads, GET /leads/:id | low |
| leads:write | POST /leads/bulk | medium |
| validation:read | GET /validation/results | low |
| validation:trigger | POST /leads/validate (consumes credits) | medium |
| campaigns:read | GET /campaigns, GET /campaigns/:id | low |
| campaigns:write | POST /campaigns, PATCH /campaigns/:id, launch, pause | high |
| reports:read | GET /campaigns/:id/report | low |
| replies:read | GET /campaigns/:id/replies | low |
| replies:send | POST /replies (sends a real email) | high |
| mailboxes:read | GET /mailboxes | low |
| domains:read | GET /domains | low |
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
| Status | Meaning | Typical cause | Client action |
|---|---|---|---|
| 200 | OK | Successful read | — |
| 201 | Created | Resource created (e.g. new campaign) | Read data.id |
| 204 | No content | Successful action with no body | — |
| 400 | Bad request | Missing / malformed field | Fix input, do not retry |
| 401 | Unauthorized | Missing / invalid / revoked token | Re-issue a key, do not retry |
| 403 | Forbidden | Scope check failed or feature-gate (tier) | Grant scope or upgrade tier |
| 404 | Not found | Resource does not exist in your org | Verify ID, do not retry |
| 409 | Conflict | State conflict (e.g. can't PATCH an active campaign) | Change state, then retry |
| 429 | Too many requests | Rate limit exceeded | Backoff, respect Retry-After |
| 500 | Server error | Uncaught exception | Retry with exponential backoff |
| 502 / 503 / 504 | Upstream | Transient 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
/accountScope: 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
/leads/bulkScope: 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
/leads/validateScope: 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
/leadsScope: leads:read
Query parameters
| Name | Type | Default | Notes |
|---|---|---|---|
| page | integer | 1 | ≥ 1 |
| limit | integer | 50 | 1 to 100 |
| status | string | — | held · active · paused · blocked |
| validation_status | string | — | valid · risky · invalid · unknown · pending |
| search | string | — | case-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
/leads/:idScope: 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
/campaignsScope: 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
/campaignsScope: 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
/campaigns/:idScope: campaigns:read. Returns the campaign, every sequence step with its variants, and the first 100 assigned leads (leads[]).
Update campaign
/campaigns/:idScope: 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
/campaigns/:id/launchScope: 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
/campaigns/:id/pauseScope: 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
/campaigns/:id/reportScope: 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
/campaigns/:id/repliesScope: 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
/repliesScope: 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
/validation/resultsScope: 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
/mailboxesScope: 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
/domainsScope: 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
Campaigntable (postSendCampaignmerge). No contract change for clients. - 2026-03-10 Added
send_gap_minutesto campaign schedule. - 2026-02-01
POST /repliesadded withreplies:sendscope. - 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.