AutoWorkx Public API
Server-to-server API reference for Leads, Campaigns, Analytics, Emails and Account — authentication, scopes, rate limits, and every endpoint with request/response examples.
Getting started
Introduction
Use the AutoWorkx Public API to build and manage integrations — manage leads and campaigns, read analytics, and send email from your own backend or integration.
What you can build
- Create, list and update leads, with automatic dedup by email.
- Create and manage campaigns, and start/pause/resume/stop them.
- Read account-wide and per-campaign analytics.
- Send or schedule one-off emails, and read outbox/inbox history.
Before you start
You need an AutoWorkx account and an API key. Create one from the API Keys page — when you create a key, select only the scopes your integration requires. Then follow the quickstart below to send your first request.
Prefer to import this into Postman, Insomnia, or generate a client? Download the OpenAPI 3.0 spec.
Getting started
Quickstart
Create an API key and make your first request in a few minutes.
400Validation failure — malformed input or a missing required field.401The API key is missing, invalid, expired, or doesn't have the required scope.404The resource doesn't exist, or isn't owned by this key's account.429Per-key rate limit exceeded — honor the Retry-After header before retrying.
Create an API key
Open the API Keys page and create a key. Select only the scopes your integration needs. Copy the key when it's displayed — you cannot view it again afterward.
Send your first request
Create a lead using your new API key:
curl -X POST "https://api.outbird.dev/v1/leads" \
-H "Authorization: Bearer aw_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{"leads":[{"email":"jane@acme.com","name":"Jane Doe"}]}'Replace the placeholder with the key you created. A successful request returns a JSON response describing what was created.
Handle errors
Check the HTTP status code before processing the response:
Getting started
Authentication
Every request is authenticated with your API key as a bearer token:
Authorization: Bearer aw_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
| Header | Required | Notes |
|---|---|---|
| Authorization | Yes | Bearer <apiKey> — the only supported auth mechanism |
| X-Api-Key | Fallback only | Accepted if Authorization can't be set by your tooling; prefer Authorization |
| Content-Type | For POST/PATCH bodies | application/json required on any request with a JSON body |
Keep your API key secure. Never expose it in client-side code or commit it to source control. There is no session, no cookie, and no CORS on this API.
Getting started
Scopes
When you create a key, select only the scopes your integration requires.
| Scope | Required for | |
|---|---|---|
| leads:read | GET /v1/leads, GET /v1/leads/{id} | |
| leads:write | POST /v1/leads, PATCH /v1/leads/{id} | |
| campaigns:read | GET /v1/campaigns, GET /v1/campaigns/{id} | |
| campaigns:write | POST /v1/campaigns, PATCH /v1/campaigns/{id} | |
| campaigns:start | POST /v1/campaigns/{id}/{start|pause|resume|stop} | |
| analytics:read | GET /v1/analytics, GET /v1/campaigns/{id}/analytics | |
| emails:read | GET /v1/emails, GET /v1/emails/{id} | |
| emails:send | POST /v1/emails/send | |
| account:read | GET /v1/me | |
| account:write | POST /v1/company | |
| webhooks:read | GET /v1/webhooks | |
| webhooks:write | POST /v1/webhooks, DELETE /v1/webhooks/{id} |
A request with a key missing the required scope is rejected before your integration's logic ever runs.
Getting started
Rate limits
Requests are limited both per key and across your whole account.
| Layer | Limit | Scope |
|---|---|---|
| Per-API-key | 60 requests / minute (default) | Each individual key has its own independent budget |
| Stage-wide (API Gateway) | 25 requests/sec steady-state, burst 50 | Shared across all keys combined |
Build exponential backoff into any automated integration. When the per-key limit is hit, every endpoint returns a real 429 with {"error":{"code":"RATE_LIMITED","message":"...","retryAfterSeconds":60}} and a Retry-After: 60 response header — honor that header before retrying rather than retrying immediately or on a fixed short interval.
API reference
Data models
Shared object shapes returned across multiple endpoints.
Lead
| Field | Type | Description |
|---|---|---|
| leadId | string | Deterministic hash of userId:email — same email always maps to the same id |
| name | string | Empty string if not provided |
| string | Lowercased, normalized | |
| phone, company, location, linkedin, website | string | Empty string if not provided |
| leadType | string | "api" for API-created leads, "manual" for dashboard-created |
| metadata | object | Free-form key/value map — any unrecognized field sent on create/update lands here |
| campaignIds | string[] | Every campaign this lead has been linked into |
| createdAt | string (ISO 8601) | — |
| source | string | null | "public_api" for API-created leads, null/other for dashboard-created |
Campaign
| Field | Type | Description |
|---|---|---|
| campaignId | string (UUID) | — |
| name | string | — |
| status | string | inactive | starting | running | active | paused | scheduled | completed | failed | stopped. NOTE: starting/running/active are currently treated as the SAME "in progress" state — don't branch on which one you see, check for any of the three. |
| campaignType | string | AI | AUTOMATION |
| campaignMoto | string | MARKETING | OUTREACH |
| campaignTag | string | Free-text label |
| trigger | object | { type, delayHours, startAt } |
| schedule | object | { days[], timezoneType, timezone, dailyLimit, timeWindow:{start,end} } |
| analytics | Analytics | See Analytics object |
| usage | object | { leadCount } at minimum |
| emailIndex | string | null | The connected mailbox this campaign sends from. Must be set (via POST/PATCH) before start will work |
| followUpEmailIndex | string | null | Connected mailbox used for follow-up rounds; defaults to emailIndex if never set |
| createdAt | string (ISO 8601) | — |
Analytics
| Field | Type | Description |
|---|---|---|
| emailsSent | number | — |
| opens, uniqueOpens | number | Total opens vs. distinct recipients who opened |
| clicks, uniqueClicks | number | Same distinction for link clicks |
| replies | number | — |
| bounces | number | — |
| unsubscribes | number | — |
| lastActivityAt | string (ISO 8601) | null | Most recent engagement event of any kind |
| Field | Type | Description |
|---|---|---|
| emailId | string | — |
| to, cc, bcc, replyTo | string | null | — |
| from | string | null | The connected mailbox the email was/will be sent from |
| subject | string | null | — |
| body | string | null | Raw content (HTML or plain text depending on source) |
| status | string | Priority order bounced > unsubscribed > replied > clicked > opened > <base lifecycle status> |
| reply | object | null | { subject, body, from } if the recipient replied, else null |
| generatedAt, scheduledAt, sentAt | string (ISO 8601) | null | Lifecycle timestamps |
| campaignId, leadId | string | null | null for one-off sends not tied to a campaign/lead |
| tag | string | null | Free-text label, one-off sends only |
| source | string | "single" (one-off send) or "history" (campaign-originated) |
Account
| Field | Type | Description |
|---|---|---|
| userId | string | The unique identifier for the authenticated account |
| email, name | string | Account owner's identity |
| company_website | string | null | snake_case — inconsistent with the rest of this API (everything else is camelCase). Known naming inconsistency, not a typo; kept as-is for backward compatibility. |
| company_intelligence | object | null | Same snake_case naming note as company_website. Onboarding Q&A + campaign questions |
| emailConnect | array | [{ email, displayName }] — connected sending mailboxes |
| usage | object | { containerCount, leadCount } |
| acceptedTerms | boolean | — |
Webhook
| Field | Type | Description |
|---|---|---|
| webhookId | string (UUID) | — |
| url | string | Always https:// — enforced at creation |
| events | string[] | Subscribed event types, or ["*"] for all |
| status | string | Always "active" today (no pause/disable action — delete + recreate instead) |
| createdAt | string (ISO 8601) | — |
| secret | string | Only present in the POST create response, exactly once. Never returned again — store it immediately |
| secretPrefix | string | First 8 chars of the secret, returned on GET list instead of the full secret |
EmailAccount
| Field | Type | Description |
|---|---|---|
| string | The connected mailbox address | |
| provider | string | gmail | outlook | zoho | yahoo | other |
| displayName | string | Shown in the dashboard's sender picker; defaults to the email address |
| smtpHost, smtpPort | string, number | Resolved automatically for gmail/outlook/zoho/yahoo, or as given for other |
| imapHost, imapPort | string, number | Same resolution rules as SMTP |
| connectedAt | string (ISO 8601) | — |
API reference
Leads
/v1/leadsleads:writeCreate/dedup leads, optionally link to a campaign
Request fields
| Field | Type | Description |
|---|---|---|
| leads | array | Array of lead objects. Omit and pass lead fields at the top level for a single lead |
| leads[].email * | string | Must pass RFC-5321 format or that lead is skipped (not a hard error) |
| leads[].name, .phone, .company, .location, .linkedin, .website | string | Standard fields |
| any other field on a lead object | — | Auto-promoted into that lead's metadata |
| campaignId | string | If given, every created/existing lead is also linked into this campaign. Validated to exist first — fails fast with 404 |
| forceUpdate | boolean | true = overwrite name/phone/company on an existing (duplicate-email) lead. Default false = skip duplicates silently |
Example request
curl -X POST "https://api.outbird.dev/v1/leads" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{ "email": "jane@acme.com", "name": "Jane Doe", "company": "Acme", "phone": "+1234567890" }
],
"campaignId": "optional-existing-campaign-id",
"forceUpdate": false
}'Response fields
| Field | Type | Description |
|---|---|---|
| totalProcessed | number | Count of lead entries in the request |
| leadsCreated | number | Newly created leads |
| leadsUpdated | number | Existing leads updated (only if forceUpdate:true) |
| leadsDuplicateSkipped | number | Existing leads left untouched |
| createdLeadIds | string[] | IDs of newly created leads |
| duplicateLeads | array | [{ email, existingLeadId }] |
| invalidEmailSkipped | number | Count of malformed emails skipped |
| skippedInvalidEmails | string[] | The actual malformed email strings |
| campaignId | string | null | Echo of the request field |
| campaignLinksAdded | number | New campaign links created |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | NO_VALID_LEADS | Every lead in the request had a malformed email |
| 404 | CAMPAIGN_NOT_FOUND | Bad campaignId |
/v1/leadsleads:readList leads (paginated)
Request fields
| Field | Type | Description |
|---|---|---|
| limit | number | Max 100. Default 20 |
| lastKey | string | Opaque cursor from a previous response's nextCursor |
Example request
curl -X GET "https://api.outbird.dev/v1/leads" \ -H "Authorization: Bearer aw_live_YOUR_API_KEY"
Response fields
| Field | Type | Description |
|---|---|---|
| leads | Lead[] | Newest-first |
| count | number | Items on this page |
| hasMore | boolean | Whether another page exists |
| nextCursor | string | null | Pass as ?lastKey= to fetch the next page |
/v1/leads/{id}leads:readGet a single lead
Example request
curl -X GET "https://api.outbird.dev/v1/leads/{id}" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Response example
{ "lead": { "leadId": "...", "email": "jane@acme.com", "..." : "..." } }Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | LEAD_NOT_FOUND | Bad/unowned lead id |
/v1/leads/{id}leads:writeUpdate a lead
Request fields
| Field | Type | Description |
|---|---|---|
| name, phone, company, location, linkedin, website, leadType | string | All optional, allowlisted |
| metadata | object | Merged key-by-key, existing keys not in the request are preserved |
| — | Immutable via PATCH (it's the dedup key) — 400 EMAIL_IMMUTABLE if included |
Example request
curl -X PATCH "https://api.outbird.dev/v1/leads/{id}" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "New Name", "phone": "+1...", "company": "...", "location": "...",
"linkedin": "...", "website": "...", "leadType": "...", "metadata": { "custom_field": "x" } }'Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | EMAIL_IMMUTABLE | Attempted to change email |
| 404 | LEAD_NOT_FOUND | Bad/unowned lead id |
API reference
Campaigns
/v1/campaignscampaigns:writeCreate a campaign
Request fields
| Field | Type | Description |
|---|---|---|
| name * | string | — |
| campaignType | string | AI | AUTOMATION (default AUTOMATION) |
| campaignMoto | string | MARKETING | OUTREACH (default MARKETING) |
| campaignTag | string | Free-text label (default "") |
| trigger.type | string | IMMEDIATE | AFTER_HOURS | CUSTOM_DATETIME (default IMMEDIATE) |
| schedule.days | string[] | e.g. ["MON","TUE",...] (default []) |
| schedule.dailyLimit | number | Default 100 |
| schedule.timeWindow.start / .end | string HH:mm | Default 09:00 / 18:00 |
| campaignQuestions | object | Free-form key/value map (default {}) |
| emailIndex | string | A connected mailbox address from GET /v1/me's emailConnect[], or its numeric array index. Required before start will succeed — can also be set later via PATCH. 400 INVALID_SENDER if not a real connected mailbox |
| followUpEmailIndex | string | Same rules as emailIndex; defaults to emailIndex internally if never set |
Example request
curl -X POST "https://api.outbird.dev/v1/campaigns" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q3 Outreach",
"campaignType": "AUTOMATION",
"campaignMoto": "OUTREACH",
"campaignTag": "",
"trigger": { "type": "IMMEDIATE" },
"schedule": { "days": ["MON","TUE","WED","THU","FRI"], "dailyLimit": 100,
"timeWindow": { "start": "09:00", "end": "18:00" } },
"campaignQuestions": {},
"emailIndex": "you@yourdomain.com"
}'Response fields
| Field | Type | Description |
|---|---|---|
| campaign | Campaign | 201 Created, status:"inactive" initially |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_SENDER | emailIndex/followUpEmailIndex isn't a real connected mailbox on this account — response includes connectedEmails with the real list |
/v1/campaignscampaigns:readList campaigns (paginated)
Request fields
| Field | Type | Description |
|---|---|---|
| limit | number | Default 20, max 100 |
| lastKey | string | Pagination cursor |
Example request
curl -X GET "https://api.outbird.dev/v1/campaigns" \ -H "Authorization: Bearer aw_live_YOUR_API_KEY"
Response fields
| Field | Type | Description |
|---|---|---|
| campaigns | Campaign[] | — |
| count, hasMore, nextCursor | — | Same pagination shape as GET /v1/leads |
/v1/campaigns/{id}campaigns:readGet a single campaign
Example request
curl -X GET "https://api.outbird.dev/v1/campaigns/{id}" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | CAMPAIGN_NOT_FOUND | Bad/unowned campaign id |
/v1/campaigns/{id}campaigns:writeUpdate a campaign
Allowlisted body fields: name, campaignTag, campaignMoto, emailIndex, followUpEmailIndex (schedule/trigger internals still aren't PATCHable via v1). This is how to fix a campaign that was created before emailIndex was set — PATCH { emailIndex: "you@yourdomain.com" }, then retry start.
Request fields
| Field | Type | Description |
|---|---|---|
| emailIndex, followUpEmailIndex | string | Runs the same connected-mailbox validation as POST /v1/campaigns — a bad value returns 400 INVALID_SENDER, nothing is written |
Example request
curl -X PATCH "https://api.outbird.dev/v1/campaigns/{id}" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | CAMPAIGN_NOT_FOUND | Bad/unowned campaign id |
| 400 | INVALID_SENDER | emailIndex/followUpEmailIndex isn't a real connected mailbox on this account — response includes connectedEmails with the real list |
/v1/campaigns/{id}/startcampaigns:startStart a campaign
Invokes the real campaign-start pipeline (same as the dashboard) — begins drafting/sending real emails to every lead in the campaign. Requires emailIndex to be set first (via POST/PATCH) or it fails with 400.
Example request
curl -X POST "https://api.outbird.dev/v1/campaigns/{id}/start" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Errors
| Status | Code | Meaning |
|---|---|---|
| 409 | INVALID_STATE_TRANSITION | Campaign must be in one of [running, active, starting] to perform this action |
| 400 | — | "No email_index set on campaign" — set it via PATCH /v1/campaigns/{id} with { emailIndex: "<a connected address from GET /v1/me>" }, then retry start (no need to recreate the campaign) |
/v1/campaigns/{id}/pausecampaigns:startPause a campaign
running/active/starting → paused. Simple status transition only in this version — does not do the dashboard's full cleanup.
Example request
curl -X POST "https://api.outbird.dev/v1/campaigns/{id}/pause" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Errors
| Status | Code | Meaning |
|---|---|---|
| 409 | INVALID_STATE_TRANSITION | Wrong campaign status for this action |
/v1/campaigns/{id}/resumecampaigns:startResume a paused campaign
paused → running. Simple status transition only.
Example request
curl -X POST "https://api.outbird.dev/v1/campaigns/{id}/resume" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Errors
| Status | Code | Meaning |
|---|---|---|
| 409 | INVALID_STATE_TRANSITION | Wrong campaign status for this action |
/v1/campaigns/{id}/stopcampaigns:startStop a campaign
Any active state → stopped. Simple status transition only.
Example request
curl -X POST "https://api.outbird.dev/v1/campaigns/{id}/stop" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Errors
| Status | Code | Meaning |
|---|---|---|
| 409 | INVALID_STATE_TRANSITION | Wrong campaign status for this action |
API reference
Analytics
/v1/analyticsanalytics:readAccount-wide analytics
Example request
curl -X GET "https://api.outbird.dev/v1/analytics" \ -H "Authorization: Bearer aw_live_YOUR_API_KEY"
Response example
{
"analytics": { "emailsSent":0,"opens":0,"uniqueOpens":0,"clicks":0,"uniqueClicks":0,
"replies":0,"bounces":0,"unsubscribes":0,"lastActivityAt":null },
"emailStats": { "total":0,"scheduled":0,"sent":0,"failed":0,"stopped":0,
"generationFailed":0,"lowScore":0,"opened":0,"clicked":0,"replied":0,"bounced":0,
"unsubscribed":0 },
"usage": { "containerCount":1,"leadCount":2 }
}/v1/campaigns/{id}/analyticsanalytics:readSingle-campaign analytics
Example request
curl -X GET "https://api.outbird.dev/v1/campaigns/{id}/analytics" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Response example
{ "campaignId":"...", "name":"...", "status":"...", "analytics": {"...":"..."}, "usage": {"leadCount":1} }Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | CAMPAIGN_NOT_FOUND | Campaign doesn't exist / isn't owned by this key |
API reference
Emails (outbox + inbox + send)
/v1/emailsemails:readList emails
Request fields
| Field | Type | Description |
|---|---|---|
| status | string | Lifecycle: Scheduled|Sent|Failed|Stopped|GenerationFailed|LowScore. Engagement: Opened|Clicked|Replied|Bounced|Unsubscribed. Omit for the full outbox |
| box | "inbox" | Convenience shorthand for ?status=Replied |
| limit | number | Default 20, max 100 |
| lastKey | string | Pagination cursor |
Example request
curl -X GET "https://api.outbird.dev/v1/emails" \ -H "Authorization: Bearer aw_live_YOUR_API_KEY"
Response fields
| Field | Type | Description |
|---|---|---|
| emails | Email[] | — |
| count, status, hasMore, nextCursor | — | status echoes the applied filter, "all" if none |
/v1/emails/{id}emails:readGet a single email
Example request
curl -X GET "https://api.outbird.dev/v1/emails/{id}" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | EMAIL_NOT_FOUND | Bad/unowned email id |
/v1/emails/sendemails:sendSend/schedule a one-off email
Real send pipeline — a real email was sent and delivered during testing. The only endpoint with Idempotency-Key support today: pass an Idempotency-Key header (any client-generated unique string, e.g. a UUID) to make retries safe — a retry with the same key after the first completed returns the exact original response verbatim (Idempotency-Replayed: true header, no new email sent); a retry while the first is still executing gets 409 IDEMPOTENCY_KEY_IN_PROGRESS. Claims expire after 24 hours.
Request fields
| Field | Type | Description |
|---|---|---|
| to * | string | Recipient email address |
| subject * | string | — |
| html * | string | HTML body content |
| emailIndex | string | Required unless the account has exactly one connected mailbox. Numeric position ("0") or the literal connected address — from GET /v1/me → emailConnect[] |
| tag | string | Free-text label for later filtering via GET /v1/emails |
| leadId | string | Associates this send with an existing lead |
| scheduleAt | string (ISO 8601) | Omit to send immediately. If given, must be in the future |
| Idempotency-Key | header | Optional but recommended — any client-generated unique string (e.g. a UUID). Makes retries of this exact request safe from duplicate sends |
Example request
curl -X POST "https://api.outbird.dev/v1/emails/send" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "jane@acme.com",
"subject": "Quick question",
"html": "<p>Hi Jane...</p>",
"emailIndex": "your-connected@gmail.com",
"tag": "api-outreach",
"leadId": "optional-lead-id",
"scheduleAt": "2026-09-01T10:00:00.000Z"
}'Response fields
| Field | Type | Description |
|---|---|---|
| message | string | "Email sent" |
| emailId | string | Use this with GET /v1/emails/{id} to check status later |
| to, subject, tag, leadId | — | Echo of the request |
| scheduledTimeUTC | string | null | null for an immediate send |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | — | "emailIndex (which connected inbox to send from) is required" — the account hasn't connected a sending mailbox yet. Connect one via POST /v1/email-accounts, then retry |
| 409 | IDEMPOTENCY_KEY_IN_PROGRESS | A request with this Idempotency-Key is still executing — do not retry yet |
API reference
Account
/v1/meaccount:readGet account info
Example request
curl -X GET "https://api.outbird.dev/v1/me" \ -H "Authorization: Bearer aw_live_YOUR_API_KEY"
Response example
{
"userId": "...", "email": "...", "name": "...",
"company_website": "https://...", "company_intelligence": { "...": "..." },
"emailConnect": [ { "email": "...", "displayName": "..." } ],
"usage": { "containerCount": 1, "leadCount": 2 }, "acceptedTerms": true
}/v1/companyaccount:writeSet/refresh company URL + trigger intelligence extraction
Persists the URL then invokes the same company-intelligence pipeline the dashboard onboarding uses.
Request fields
| Field | Type | Description |
|---|---|---|
| url * | string | A fully-qualified URL |
Example request
curl -X POST "https://api.outbird.dev/v1/company" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com" }'Errors
| Status | Code | Meaning |
|---|---|---|
| 200 | — | { onboarding_q_and_a, campaign_questions } — data ready |
| 202 | — | Still scraping — poll again after a few seconds |
| 422 | — | Bad/unreachable URL (DNS failure, connection refused) — user input problem |
| 502 | — | Genuine scrape failure (site reachable but couldn't be scraped) |
API reference
Email Accounts
Connect/list/disconnect the sending mailboxes used by POST /v1/emails/send and emailIndex/followUpEmailIndex on campaigns. Credentials are live-verified against the real provider before anything is saved — a real SMTP login (+ a real test email send), then a real IMAP login. No plaintext password is ever returned in any response.
- Known SMTP/IMAP hosts are resolved automatically for gmail, outlook, zoho, and yahoo — you never need to supply smtpHost for those. zoho tries both smtp.zoho.in and smtp.zoho.com.
- gmail/outlook/yahoo/zoho require a provider app password, not your normal login password — every major provider requires 2FA to be enabled before it will issue one.
- Reconnecting the same address (e.g. after rotating an app password) is idempotent — it replaces the existing entry for that email rather than creating a duplicate.
/v1/email-accountsaccount:writeConnect a mailbox
Request fields
| Field | Type | Description |
|---|---|---|
| provider * | string | gmail | outlook | zoho | yahoo | other |
| email * | string | The mailbox address itself |
| password (alias appPassword) * | string | Not your normal login password for gmail/outlook/yahoo/zoho — generate a provider app password and use that. For provider:"other", this is the mailbox's real SMTP/IMAP password |
| smtpHost, smtpPort | string, number | Required when provider:"other" — your mail server's SMTP host/port |
| secure | boolean | provider:"other" only (default false) |
| displayName | string | Shown in the dashboard's sender picker; defaults to the email address |
| reference | string | Free-text label for your own bookkeeping |
Example request
curl -X POST "https://api.outbird.dev/v1/email-accounts" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "provider": "gmail", "email": "you@yourdomain.com", "password": "<app password>" }'Response example
{ "message": "gmail account connected successfully", "email": "you@yourdomain.com", "totalAccounts": 2 }Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | SMTP_AUTH_FAILED | Real provider rejected the credentials — message includes the raw SMTP error (almost always: not using an app password, or IMAP/SMTP access disabled at the provider). Nothing is saved |
| 401 | IMAP_AUTH_FAILED | IMAP access failed — message includes the raw IMAP error |
| 400 | INVALID_PROVIDER | provider not one of gmail|outlook|zoho|yahoo|other |
| 400 | MISSING_EMAIL / MISSING_PASSWORD / MISSING_SMTP_CONFIG | Required field missing (smtpHost/smtpPort only required for provider:"other") |
/v1/email-accountsaccount:readList connected mailboxes
Passwords/app-passwords are never included — this endpoint only ever returns connection metadata.
Example request
curl -X GET "https://api.outbird.dev/v1/email-accounts" \ -H "Authorization: Bearer aw_live_YOUR_API_KEY"
Response example
{ "emailAccounts": [ { "email": "...", "provider": "gmail", "displayName": "...", "smtpHost": "...", "smtpPort": 465, "imapHost": "...", "imapPort": 993, "connectedAt": "..." } ] }/v1/email-accounts/{email}account:writeDisconnect a mailbox
Removes a connected sending mailbox. If it's still in use by an active campaign, the request is rejected unless you tell it how to handle those campaigns (see the action query parameter below).
Request fields
| Field | Type | Description |
|---|---|---|
| email * | string (path) | The connected mailbox address, URL-encoded (e.g. %40 for @) |
| action | string (query) | Only needed if the mailbox is in use by an active campaign (409 CAMPAIGNS_ACTIVE). One of: cancel (abort, mailbox stays connected), migrate (requires targetEmail, reassigns those campaigns first, then disconnects), continue (disconnect anyway; those campaigns fail at their next send until reassigned) |
| targetEmail | string (query) | Required when action=migrate — another connected mailbox address to reassign the affected campaigns to |
Example request
curl -X DELETE "https://api.outbird.dev/v1/email-accounts/{email}" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Response example
{ "email": "you@yourdomain.com", "status": "disconnected" }Errors
| Status | Code | Meaning |
|---|---|---|
| 409 | CAMPAIGNS_ACTIVE | Mailbox is used by an active campaign (running/active/starting/paused/scheduled) — response includes affectedCampaigns and connectedAccounts; retry with ?action=cancel, ?action=migrate&targetEmail=..., or ?action=continue |
API reference
Webhooks
Subscribe a URL to be notified in near-real-time when events happen on your account, instead of polling GET /v1/emails / GET /v1/campaigns/{id}. Delivery is asynchronous (via an internal queue) — expect a delay of a few seconds between the event happening and your endpoint receiving the POST, not instant delivery.
- Event types: campaign.started, campaign.paused, campaign.resumed, campaign.stopped, email.sent, email.opened, email.clicked, email.replied, email.bounced (or "*" to subscribe to all of them).
- Max 10 webhooks per account (400 WEBHOOK_LIMIT_REACHED beyond that).
- Your endpoint must respond with a 2xx status within 8 seconds, or the delivery is treated as failed. Failed deliveries are automatically retried (up to 5 attempts total with backoff); after that they are dropped.
- Every delivery carries a stable eventId (the SAME on every redelivery of one logical event) and a per-attempt deliveryId (different on every retry) — dedupe on eventId / X-AutoWorkx-Event-Id, since a delivery can still arrive more than once (retry, or two webhooks subscribed to "*" both matching).
Delivery format
{ "event": "email.sent", "eventId": "3fae2d1c-6c9e-4c1a-9c1a-2f0e6f6f6f6f", "deliveryId": "a1b2c3d4-...", "timestamp": "2026-08-24T08:48:23.964Z", "data": { "emailId": "...", "campaignId": "...", "leadId": "...", "to": "..." } }| Header | Value | |
|---|---|---|
| Content-Type | application/json | |
| X-AutoWorkx-Event | The event type string, e.g. email.sent | |
| X-AutoWorkx-Event-Id | Stable identifier for this logical event occurrence — same on every redelivery. Use this to dedupe | |
| X-AutoWorkx-Delivery-Id | Unique identifier for this specific HTTP POST attempt — different on every retry | |
| X-AutoWorkx-Delivery-Attempt | 1 on the first attempt, incrementing on each redelivery (max 5) | |
| X-AutoWorkx-Signature | sha256=<hex> — HMAC-SHA256 of the exact raw request body, keyed with your webhook's secret |
Verify the signature before trusting a payload
const crypto = require("crypto");
const expected = "sha256=" + crypto.createHmac("sha256", YOUR_WEBHOOK_SECRET).update(rawBody).digest("hex");
if (expected !== req.headers["x-autoworkx-signature"]) return res.status(401).end();Data payload per event type
| Event | data shape | |
|---|---|---|
| campaign.started | { campaignId, name, followUpIndex } | |
| campaign.paused / campaign.resumed | { campaignId } | |
| campaign.stopped | { campaignId, removedEmails, stoppedLeads } | |
| email.sent | { emailId, campaignId, leadId, to } | |
| email.opened | { emailId, campaignId, uniqueOpen } | |
| email.clicked | { emailId, campaignId, uniqueClick } | |
| email.replied | { emailId, campaignId, leadId } | |
| email.bounced | { emailId, campaignId, leadId, bounceType } |
/v1/webhookswebhooks:writeRegister a webhook
Request fields
| Field | Type | Description |
|---|---|---|
| url * | string | Must be https:// — plain http:// is rejected with 400 INVALID_URL |
| events * | string[] | Array of event types, or ["*"]. Empty array → 400 MISSING_EVENTS |
Example request
curl -X POST "https://api.outbird.dev/v1/webhooks" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-server.example.com/webhooks/autoworkx", "events": ["email.sent", "campaign.started"] }'Response example
{
"webhookId": "...", "url": "...", "events": ["email.sent", "campaign.started"],
"status": "active", "createdAt": "...",
"secret": "<64 hex chars — SHOWN ONLY THIS ONCE>",
"warning": "Store this secret now — it will not be shown again. Use it to verify the X-AutoWorkx-Signature header on incoming deliveries."
}Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | MISSING_URL / INVALID_URL | Missing, non-https://, or unparseable url |
| 400 | MISSING_EVENTS / INVALID_EVENTS | Empty or unrecognized events entries |
| 400 | WEBHOOK_LIMIT_REACHED | Already have 10 webhooks on this account |
/v1/webhookswebhooks:readList your webhooks
Example request
curl -X GET "https://api.outbird.dev/v1/webhooks" \ -H "Authorization: Bearer aw_live_YOUR_API_KEY"
Response example
{ "webhooks": [ { "webhookId": "...", "url": "...", "events": ["..."], "status": "active", "secretPrefix": "..." } ] }/v1/webhooks/{id}webhooks:writeRemove a webhook
Example request
curl -X DELETE "https://api.outbird.dev/v1/webhooks/{id}" \
-H "Authorization: Bearer aw_live_YOUR_API_KEY"Response example
{ "webhookId": "...", "status": "deleted" }Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | WEBHOOK_NOT_FOUND | Bad/unowned webhook id |
API reference
Status codes
Every HTTP status code returned anywhere in this API.
| Status | Meaning | Example body |
|---|---|---|
| 200 | Successful GET/POST (non-creating) | — |
| 201 | Resource created (POST /v1/leads, /v1/campaigns, /v1/emails/send) | — |
| 400 | Validation failure — malformed input, missing required field | {"error":{"code":"<CODE>","message":"..."},"requestId":"..."} |
| 401 | Missing/malformed/expired/revoked API key, or wrong scope for the route | {"message":"Unauthorized"} (from the authorizer, before any endpoint runs) |
| 403 | Reserved / observed in one rare authorizer edge case | {"message":"Forbidden"} |
| 404 | Resource doesn't exist, or isn't owned by this key's account | {"error":{"code":"<X>_NOT_FOUND"},"requestId":"..."} |
| 405 | Method not allowed on that path | {"error":{"code":"METHOD_NOT_ALLOWED"},"requestId":"..."} |
| 409 | Campaign action attempted from an incompatible status, or a duplicate in-flight Idempotency-Key | {"error":{"code":"INVALID_STATE_TRANSITION"},"requestId":"..."} |
| 422 | POST /v1/company only — bad/unreachable URL (user input, not a bug) | — |
| 429 | Per-key rate limit exceeded — includes a Retry-After header, honor it before retrying | {"error":{"code":"RATE_LIMITED","message":"...","retryAfterSeconds":60},"requestId":"..."} |
| 500 | Server error — the requestId/X-Request-Id on this response identifies it for support | {"error":{"code":"INTERNAL_ERROR"},"requestId":"..."} |
| 502 | POST /v1/company only — genuine scrape failure | — |
Quick reference
| Method | Path | Scope |
|---|---|---|
| POST | /v1/leads | leads:write |
| GET | /v1/leads | leads:read |
| GET | /v1/leads/{id} | leads:read |
| PATCH | /v1/leads/{id} | leads:write |
| POST | /v1/campaigns | campaigns:write |
| GET | /v1/campaigns | campaigns:read |
| GET | /v1/campaigns/{id} | campaigns:read |
| PATCH | /v1/campaigns/{id} | campaigns:write |
| POST | /v1/campaigns/{id}/start | campaigns:start |
| POST | /v1/campaigns/{id}/pause | campaigns:start |
| POST | /v1/campaigns/{id}/resume | campaigns:start |
| POST | /v1/campaigns/{id}/stop | campaigns:start |
| GET | /v1/analytics | analytics:read |
| GET | /v1/campaigns/{id}/analytics | analytics:read |
| GET | /v1/emails | emails:read |
| GET | /v1/emails/{id} | emails:read |
| POST | /v1/emails/send | emails:send |
| GET | /v1/me | account:read |
| POST | /v1/company | account:write |
| POST | /v1/email-accounts | account:write |
| GET | /v1/email-accounts | account:read |
| DELETE | /v1/email-accounts/{email} | account:write |
| POST | /v1/webhooks | webhooks:write |
| GET | /v1/webhooks | webhooks:read |
| DELETE | /v1/webhooks/{id} | webhooks:write |