Build on the Sendvanta API
A REST API for your whole outbound operation: create and launch campaigns, import and verify leads, read replies, and monitor deliverability — from your own code. Included on the Pro plan.
Authentication
The API is available on the Pro plan. An owner or admin creates keys in the console under Settings → Integrations → API keys. The secret (svk_…) is shown exactly once — store it like a password. Send it as a bearer token on every request:
curl https://api.sendvanta.com/campaigns \
-H "Authorization: Bearer $SENDVANTA_API_KEY"A key is bound to the workspace it was created in — no workspace header is needed, and a key can never reach another workspace. Revoke keys any time from the same settings page; revocation is immediate. Each workspace can hold up to 10 active keys, so you can issue one per integration and rotate them independently.
Conventions
- Base URL:
https://api.sendvanta.com. Requests and responses are JSON — sendContent-Type: application/jsonon writes. - Ids are UUIDs. Timestamps are ISO 8601 in UTC. Campaign send windows are evaluated in the campaign's own timezone.
- List endpoints paginate with
limitandoffsetquery parameters and return atotalalongside the rows where shown. - Email bodies support template variables:
{{first_name}},{{last_name}},{{company}},{{title}}, custom import fields, and fallbacks like{{first_name|there}}. Every sequence must end with a step containing{{unsubscribe_url}}.
Errors
Errors share one shape: { "error": string, "details"?: … }.
| Status | error | When |
|---|---|---|
| 400 | validation | Malformed body or query. details carries the field-level issues. |
| 401 | unauthenticated | Missing, unknown, or revoked API key. |
| 402 | insufficient_credits | Verification credit balance is empty. |
| 403 | plan_upgrade_required | The workspace is no longer on the Pro plan. Keys are kept, not deleted — access resumes on upgrade. |
| 403 | forbidden | The route is not available over the API (billing, members, credentials, key management), or the role can't perform it. |
| 404 | not_found | The resource doesn't exist in this workspace. |
| 409 | conflict / blockers | State conflict — e.g. activating with preflight blockers returns { error, blockers: [...] }. |
| 429 | rate_limited | Over 120 requests/min for this key. Honor Retry-After. |
| 501 | *_not_configured | The feature isn't configured on the server (AI, verification, spam check). |
Rate limits
120 requests per minute per key. Every key-authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until the window resets). Exceeding the limit returns 429 with a Retry-After header — back off and retry. Need sustained higher volume? Email support@sendvanta.com.
Quickstart: launch a campaign end to end
# 1. Create a campaign
curl -X POST https://api.sendvanta.com/campaigns \
-H "Authorization: Bearer $SENDVANTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Roofers — Texas", "timezone": "America/Chicago"}'
# 2. Add steps (repeat per follow-up, bumping stepOrder and delayHours)
curl -X PUT https://api.sendvanta.com/campaigns/$CAMPAIGN_ID/steps \
-H "Authorization: Bearer $SENDVANTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"stepOrder": 1, "subject": "Quick question, {{first_name|there}}",
"bodyText": "…\n\n{{unsubscribe_url}}", "delayHours": 0}'
# 3. Assign mailboxes, then enroll a lead list
curl -X PUT https://api.sendvanta.com/campaigns/$CAMPAIGN_ID/mailboxes \
-H "Authorization: Bearer $SENDVANTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mailboxIds": ["…"]}'
curl -X POST https://api.sendvanta.com/campaigns/$CAMPAIGN_ID/enroll \
-H "Authorization: Bearer $SENDVANTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"listId": "…"}'
# 4. Preflight, then launch
curl https://api.sendvanta.com/campaigns/$CAMPAIGN_ID/preflight \
-H "Authorization: Bearer $SENDVANTA_API_KEY"
curl -X POST https://api.sendvanta.com/campaigns/$CAMPAIGN_ID/activate \
-H "Authorization: Bearer $SENDVANTA_API_KEY"Campaigns
/campaignsList campaigns, newest first, each with enrollment counts by status.
Response
[
{
"id": "…", "name": "Roofers — Texas", "status": "active",
"timezone": "America/Chicago", "daysOfWeek": [1,2,3,4,5],
"sendWindowStart": "09:00", "sendWindowEnd": "17:00", "dailyCap": 200,
"trackOpens": true, "trackClicks": true, "createdAt": "…",
"stats": { "active": 118, "replied": 9, "completed": 41, "stopped": 3,
"unsubscribed": 2, "bounced": 4, "paused": 0 }
}
]/campaignsCreate a draft campaign.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string | 1–120 characters. |
timezone | string | IANA timezone the send window is evaluated in. Default "America/New_York". |
daysOfWeek | integer[] | Send days, 0 = Sunday … 6 = Saturday. Default [1,2,3,4,5]. |
sendWindowStart | "HH:MM" | Default "09:00". |
sendWindowEnd | "HH:MM" | Default "17:00". |
dailyCap | integer | Max sends per day for the campaign (1–50,000). Default 200. |
startDate | "YYYY-MM-DD" | Optional earliest send date. |
trackOpens | boolean | Open pixel. Default false — tracking is opt-in. |
trackClicks | boolean | Click-through link rewriting. Default false. |
- The response is the created campaign object. Campaigns always start as drafts — add steps, assign mailboxes, enroll leads, then activate.
/campaigns/:idOne campaign with its sequence, assigned mailboxes, and stats.
Response
{ "campaign": { … }, "steps": [ … ], "mailboxes": [ … ], "stats": { … } }/campaigns/:idUpdate settings. Accepts any subset of the POST /campaigns fields.
- Only draft or paused campaigns can be edited; otherwise 409.
/campaigns/:id/stepsCreate or update a sequence step (upsert keyed on stepOrder).
Request body
| Field | Type | Description |
|---|---|---|
stepOrderrequired | integer | 1-based position in the sequence (1–20). |
subjectrequired | string | ≤500 chars. May be empty when threadMode is reply_to_previous. |
bodyTextrequired | string | Plain-text body, ≤50,000 chars. Supports template variables. |
bodyHtml | string | Optional HTML body, ≤100,000 chars. |
delayHoursrequired | integer | Hours after the previous step (0–2160). Step 1 uses 0. |
threadMode | "new_thread" | "reply_to_previous" | Default "new_thread". |
aiVariationsEnabled | boolean | Generate ~4 AI wording variants rotated at send time for deliverability. Default false. |
- The final step's body must contain {{unsubscribe_url}} — activation preflight blocks otherwise.
- AI variants regenerate when the step content changes and preserve the exact template-variable set. They do not count against the AI generation quota.
/campaigns/:id/steps/:stepIdRemove a step from the sequence.
/campaigns/:id/mailboxesReplace the campaign's sending-mailbox assignment.
Request body
| Field | Type | Description |
|---|---|---|
mailboxIdsrequired | uuid[] | 1–100 mailbox ids. Replaces the whole set. |
/campaigns/:id/enrollEnroll leads from a list and/or by explicit ids.
Request body
| Field | Type | Description |
|---|---|---|
listId | uuid | Enroll every eligible lead in the list. The list stays connected: leads added to it later are auto-enrolled. |
leadIds | uuid[] | Explicit leads (≤50,000). |
- Skips suppressed addresses, leads already in an active sequence anywhere in the workspace, and leads whose email verification came back invalid.
Response
{ "enrolled": 482, "skipped": { "suppressed": 3, "duplicate": 11, "invalid": 6, "unverified": 0 } }/campaigns/:id/preflightLaunch-readiness check without launching.
- Blockers include: no steps, no healthy assigned mailbox, no eligible leads, unresolved template variables, missing unsubscribe link.
Response
{
"ok": false,
"blockers": ["Final step is missing {{unsubscribe_url}}"],
"warnings": [],
"eligibleLeads": 482, "excludedLeads": 20,
"mailboxes": 3, "dailyCapacity": 96, "firstSendAt": "…"
}/campaigns/:id/activateLaunch the campaign. Runs preflight first; 409 with the blocker list if not ready.
/campaigns/:id/pausePause sending. In-flight scheduled messages hold.
/campaigns/:id/resumeResume a paused campaign.
/campaigns/:id/seed-testSend step 1 to your registered seed inboxes for an inbox-placement check. Results land in GET /seed-tests with a launch-readiness verdict.
/campaigns/:id/messagesSent-email log for the campaign, newest first (excludes test sends).
Query parameters
| Field | Type | Description |
|---|---|---|
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
Response
{
"messages": [{
"id": "…", "toEmail": "…", "subject": "…", "status": "sent", "stepOrder": 1,
"sentAt": "…", "openCount": 2, "clickCount": 1, "verifiedVisitCount": 1,
"engagementLevel": "verified_visit", "clickVerdict": "likely_human", "leadId": "…"
}],
"total": 1284
}/campaigns/:id/scheduleSend-volume forecast in the campaign timezone.
Query parameters
| Field | Type | Description |
|---|---|---|
view | "day" | "week" | Default "day": 24 hourly buckets. "week": 7 daily buckets. |
date | "YYYY-MM-DD" | Anchor date. Defaults to today in the campaign timezone. |
Response
{
"view": "day", "timezone": "America/Chicago", "date": "2026-09-15",
"window": { "sendWindowStart": "09:00", "sendWindowEnd": "17:00", "daysOfWeek": [1,2,3,4,5], "dailyCap": 200 },
"totals": { "scheduled": 96, "sent": 41 },
"buckets": [{ "key": "09", "scheduled": 12, "sent": 12 }, …]
}/campaigns/:id/list-sourcesLists connected to the campaign (recorded at enrollment; drive auto-enrollment of later additions).
/enrollmentsList enrollments across campaigns.
Query parameters
| Field | Type | Description |
|---|---|---|
campaignId | uuid | Filter to one campaign. |
status | string | active | replied | unsubscribed | bounced | completed | stopped | paused. |
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
/enrollments/:id/stopStop a lead's sequence. Cancels the pending scheduled message.
Request body
| Field | Type | Description |
|---|---|---|
reason | string | Optional note stored on the enrollment. |
AI generation
Draft sequences and individual steps with AI. Each successful generation (full sequence or one step) counts once against the plan's monthly quota — 10 per month on Pro. The generator only writes cold-email campaign copy; off-topic prompts return refused: true.
/ai/campaign-generateDraft a single email or a full sequence.
Request body
| Field | Type | Description |
|---|---|---|
productDetailrequired | string | What you're selling (≤3,000 chars). |
prompt | string | Extra guidance (≤2,000 chars). |
goal | string | e.g. "book a demo". |
focusArea | string | Audience / vertical to focus on. |
websiteDetail | string | Context about your site or offer. |
links | string[] | Up to 10 URLs to weave in. |
numberOfSequences | integer | Steps to draft, 1–6. Default 3. |
mode | "single" | "sequence" | Default "sequence". |
format | "plain" | "richtext" | "html" | Body format. Default "richtext". |
- Each email is shaped to PUT straight to /campaigns/:id/steps.
- Gates: 403 plan_upgrade_required · 403 ai_quota_exceeded · 501 ai_not_configured · 502 ai_generation_failed.
Response
{
"refused": false,
"emails": [{ "subject": "…", "bodyText": "…", "bodyHtml": "…", "delayHours": 0 }, …],
"format": "richtext",
"remaining": 7
}/ai/step-generateWrite or rewrite one step, using sibling steps for voice continuity.
Request body
| Field | Type | Description |
|---|---|---|
promptrequired | string | What the step should say or how to change it. |
format | "plain" | "richtext" | "html" | Default "richtext". |
isFirst | boolean | Whether this is the opening email. Default false. |
current | { subject?, body? } | Existing content when revising; omit to write fresh. |
otherSteps | array | Sibling steps [{ stepOrder, subject, bodyText }] for context (≤20). |
/ai/usageThis month's AI generation quota.
Response
{ "plan": "pro", "limit": 10, "used": 3, "remaining": 7 }Leads & lists
/lead-listsAll lists, newest first.
/lead-listsCreate a list.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string | 1–120 characters. |
/lead-lists/importBulk-import leads into a list. Synchronous: normalizes, validates syntax, dedupes against the payload and the workspace, and checks suppressions.
Request body
| Field | Type | Description |
|---|---|---|
listIdrequired | uuid | Target list. |
fieldMappingrequired | object | CSV column → field. Fields: "email", "first_name", "last_name", "company", "title", "phone", or "custom:<name>". |
rowsrequired | object[] | Parsed rows as { column: value } records. |
- Returns the import record including per-row errors (rowErrors). Import size is limited by your plan.
/imports/:idOne import's result record.
/leadsSearch leads.
Query parameters
| Field | Type | Description |
|---|---|---|
listId | uuid | Filter to a list. |
status | string | Lead status filter. |
search | string | Matches email, first/last name, company. |
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
- verificationStatus: valid | invalid | risky | unknown, null = never verified. verificationReason carries the underlying cause (catch_all, role_account, inbox_full, disabled, …).
Response
{
"leads": [{
"id": "…", "email": "…", "firstName": "…", "company": "…",
"verificationStatus": "valid", "verificationReason": null, "verifiedAt": "…"
}],
"total": 5210
}/leadsAdd a single lead.
Request body
| Field | Type | Description |
|---|---|---|
emailrequired | string | The address. |
listId | uuid | List to add to. |
firstName | string | |
lastName | string | |
company | string | |
title | string | |
phone | string |
/leads/suppressSuppress addresses — they are never emailed again, and any active sequences stop immediately.
Request body
| Field | Type | Description |
|---|---|---|
emailsrequired | string[] | 1–10,000 addresses. |
reason | "manual" | "unsubscribe" | "import" | Default "manual". |
/suppressionsThe workspace suppression list.
Query parameters
| Field | Type | Description |
|---|---|---|
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
Email verification
Verify addresses before sending. 1 credit = 1 address; Pro includes 60,000 credits, more come in $20 / 10,000 packs (purchased in the console — billing is not reachable over the API). Results are cached for 30 days; cached hits are free. Invalid leads are automatically excluded at enrollment and at send time.
/verification/verifyVerify one address and update the matching lead.
Request body
| Field | Type | Description |
|---|---|---|
emailrequired | string | Address to verify. |
force | boolean | Bypass the 30-day cache. Default false. |
- Gates in order: 403 plan_upgrade_required · 501 verification_not_configured · 402 insufficient_credits · 502 verification_failed (credit refunded).
/lead-lists/:id/verifyStart a background job over the list's unverified leads. Returns the job (201).
- 409 verification_in_progress if a job is live · 400 nothing_to_verify · 402 when the balance is 0.
/verification/jobs/:idJob progress.
Response
{
"status": "running", "totalLeads": 4100, "processed": 1240,
"validCount": 1010, "invalidCount": 105, "riskyCount": 88, "unknownCount": 37,
"creditsUsed": 1240, "errorCount": 0, "stopReason": null
}/verification/jobsRecent jobs, newest first.
Query parameters
| Field | Type | Description |
|---|---|---|
listId | uuid | Filter to one list. |
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
/verification/creditsCredit balance and plan allowance.
Response
{ "plan": "pro", "balance": 54210, "includedForPlan": 60000, "purchasedTotal": 0, "usedTotal": 5790, "pack": { "credits": 10000, "priceUsd": 20 } }Mailboxes & warm-up
Read-only over the API: connecting mailboxes and editing credentials stays in the console by design.
/mailboxesAll mailboxes with status, allowance, and health. Never returns credentials.
Response
[{
"id": "…", "provider": "smtp", "email": "…", "senderName": "…",
"status": "active", "dailyCap": 50, "dailyAllowance": 32, "sentToday": 12,
"minGapMinutes": 8, "healthScore": 86, "healthBand": "healthy",
"warmupEnabled": true, "warmupDailyTarget": 6
}]/mailboxes/:id/healthHealth-score snapshots over time.
/mailboxes/:id/usageSend volume for the mailbox.
/mailboxes/:id/warmupWarm-up status and recent network activity.
- peerEmail is null for cross-tenant network peers.
Response
{
"enabled": true, "dailyTarget": 6, "sentToday": 4, "poolSize": 212,
"recent": [{ "direction": "sent", "peerEmail": null, "status": "replied", "sentAt": "…" }],
"counts": { "sent7d": 38, "received7d": 41, "replied7d": 12 }
}/warmup/overviewWorkspace warm-up summary: pool size, enabled mailboxes, 7-day counts.
Deliverability
/reputation/overviewThe layered reputation board: every mailbox, sending domain, DKIM domain, and IP with score, band, and DNS auth status.
- Bands drive action: healthy 80–100 · reduce 60–79 · pause 40–59 · disable <40.
Response
{
"mailboxes": [{ "id": "…", "email": "…", "score": 86, "band": "healthy", "reasonCodes": [] }],
"domains": [{ "id": "…", "domain": "…", "score": 74, "band": "reduce",
"spf": "pass", "dkim": "pass", "dmarc": "warn", "spamComplaintRate": null }],
"ips": [ … ], "dkimDomains": [ … ]
}/reputation/mailbox/:idOne mailbox's score, contributing signals over 7d/30d windows, and recent provider events.
/reputation/domain/:idSending-domain detail: SPF/DKIM/DMARC results with per-record explanations, Postmaster signals, child DKIM/IP entities.
/provider-eventsClassified SMTP failures: throttles, blocks, spam rejections, auth failures.
Query parameters
| Field | Type | Description |
|---|---|---|
mailboxId | uuid | Filter to one mailbox. |
kind | string | throttle | block | spam_reject | auth_fail | rate_limit | restricted | invalid_recipient. |
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
/seed-testsInbox-placement results from your seed inboxes.
Query parameters
| Field | Type | Description |
|---|---|---|
campaignId | uuid | Filter to one campaign's runs. |
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
/spam-checkSpamAssassin-score a draft before sending. Template variables are scored as realistic sample text.
Request body
| Field | Type | Description |
|---|---|---|
subject | string | ≤500 chars. |
bodyText | string | Plain-text body. |
bodyHtml | string | Optional HTML body. |
Response
{ "score": 1.2, "threshold": 5, "isSpam": false, "rules": ["HTML_IMAGE_RATIO_02"] }Inbox & replies
/conversationsReply conversations, most recent activity first.
Query parameters
| Field | Type | Description |
|---|---|---|
disposition | string | Filter, e.g. "interested" for positive replies. |
since | ISO date | Only conversations with activity after this instant. |
campaignId | uuid | Filter to one campaign. |
limit | integer | Page size. Sensible default and cap apply. |
offset | integer | Rows to skip. Defaults to 0. |
Response
{
"conversations": [{
"id": "…", "subject": "…", "disposition": "interested", "lastMessageAt": "…",
"lead": { "email": "…", "firstName": "…", "lastName": "…" }
}],
"total": 26
}/conversations/:idOne conversation with its message events.
Response
{ "conversation": { … }, "events": [ … ] }Analytics & account
/analytics/overviewWorkspace funnel totals. Excludes test sends; opens/clicks are directional (bot-filtered verdicts included).
Query parameters
| Field | Type | Description |
|---|---|---|
from | ISO date | Window start. |
to | ISO date | Window end. |
campaignId | uuid | Filter to one campaign. |
mailboxId | uuid | Filter to one mailbox. |
Response
{
"scheduled": 1400, "attempted": 1290, "sent": 1284, "failed": 4, "bounced": 22,
"replied": 31, "unsubscribed": 6, "opened": 507, "clicked": 96,
"verifiedVisits": 41, "engaged": 17,
"clickVerdicts": { "likelyHuman": 44, "uncertain": 39, "likelyBot": 13 },
"byCampaign": [ … ]
}/campaigns/:id/analyticsPer-step funnel for one campaign: sent → opened → clicked → replied, plus bounced/failed per step.
/account/usagePlan limits and live usage for the workspace.
Response
{
"plan": "pro", "billingStatus": "active",
"limits": { "maxUsers": null, "maxMailboxes": 2000, "maxActiveLeads": null,
"monthlyEmailQuota": 200000, "maxSendsPerDay": 8000, "maxImportRows": null,
"aiGenerationsPerMonth": 10, "verificationCreditsIncluded": 60000 },
"usage": { "activeLeads": 5210, "mailboxes": 8, "sendsToday": 412, "sendsThisMonth": 9310,
"aiGenerationsUsed": 3, "verificationCreditsBalance": 54210, "verificationCreditsUsed": 5790 }
}What keys can — and can't — do
API keys cover everything documented above: campaigns, AI generation, leads, verification, deliverability, replies, and analytics. Billing, team management, mailbox connection and credentials, and API-key management itself are never reachable with a key — those stay in the console. Every API action is recorded in your workspace audit log. If the workspace leaves the Pro plan, keys stop working until you upgrade again (they are not deleted).
Prefer working conversationally? The same capabilities are available through the Sendvanta connector for Claude, ChatGPT, and Grok — no code required, on every paid plan.
Support
Questions, missing endpoints, or higher-volume needs? Email support@sendvanta.com. See also our Privacy Policy and Terms of Service.