Big News :Get Lifetime 50% OFF on every planJoin Now

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.

    1

    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.

    2

    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.

    3

    Handle errors

    Check the HTTP status code before processing the response:

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

Getting started

Authentication

Every request is authenticated with your API key as a bearer token:

Authorization: Bearer aw_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
HeaderRequiredNotes
AuthorizationYesBearer <apiKey> — the only supported auth mechanism
X-Api-KeyFallback onlyAccepted if Authorization can't be set by your tooling; prefer Authorization
Content-TypeFor POST/PATCH bodiesapplication/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.

ScopeRequired for
leads:readGET /v1/leads, GET /v1/leads/{id}
leads:writePOST /v1/leads, PATCH /v1/leads/{id}
campaigns:readGET /v1/campaigns, GET /v1/campaigns/{id}
campaigns:writePOST /v1/campaigns, PATCH /v1/campaigns/{id}
campaigns:startPOST /v1/campaigns/{id}/{start|pause|resume|stop}
analytics:readGET /v1/analytics, GET /v1/campaigns/{id}/analytics
emails:readGET /v1/emails, GET /v1/emails/{id}
emails:sendPOST /v1/emails/send
account:readGET /v1/me
account:writePOST /v1/company
webhooks:readGET /v1/webhooks
webhooks:writePOST /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.

LayerLimitScope
Per-API-key60 requests / minute (default)Each individual key has its own independent budget
Stage-wide (API Gateway)25 requests/sec steady-state, burst 50Shared 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

FieldTypeDescription
leadIdstringDeterministic hash of userId:email — same email always maps to the same id
namestringEmpty string if not provided
emailstringLowercased, normalized
phone, company, location, linkedin, websitestringEmpty string if not provided
leadTypestring"api" for API-created leads, "manual" for dashboard-created
metadataobjectFree-form key/value map — any unrecognized field sent on create/update lands here
campaignIdsstring[]Every campaign this lead has been linked into
createdAtstring (ISO 8601)
sourcestring | null"public_api" for API-created leads, null/other for dashboard-created

Campaign

FieldTypeDescription
campaignIdstring (UUID)
namestring
statusstringinactive | 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.
campaignTypestringAI | AUTOMATION
campaignMotostringMARKETING | OUTREACH
campaignTagstringFree-text label
triggerobject{ type, delayHours, startAt }
scheduleobject{ days[], timezoneType, timezone, dailyLimit, timeWindow:{start,end} }
analyticsAnalyticsSee Analytics object
usageobject{ leadCount } at minimum
emailIndexstring | nullThe connected mailbox this campaign sends from. Must be set (via POST/PATCH) before start will work
followUpEmailIndexstring | nullConnected mailbox used for follow-up rounds; defaults to emailIndex if never set
createdAtstring (ISO 8601)

Analytics

FieldTypeDescription
emailsSentnumber
opens, uniqueOpensnumberTotal opens vs. distinct recipients who opened
clicks, uniqueClicksnumberSame distinction for link clicks
repliesnumber
bouncesnumber
unsubscribesnumber
lastActivityAtstring (ISO 8601) | nullMost recent engagement event of any kind

Email

FieldTypeDescription
emailIdstring
to, cc, bcc, replyTostring | null
fromstring | nullThe connected mailbox the email was/will be sent from
subjectstring | null
bodystring | nullRaw content (HTML or plain text depending on source)
statusstringPriority order bounced > unsubscribed > replied > clicked > opened > <base lifecycle status>
replyobject | null{ subject, body, from } if the recipient replied, else null
generatedAt, scheduledAt, sentAtstring (ISO 8601) | nullLifecycle timestamps
campaignId, leadIdstring | nullnull for one-off sends not tied to a campaign/lead
tagstring | nullFree-text label, one-off sends only
sourcestring"single" (one-off send) or "history" (campaign-originated)

Account

FieldTypeDescription
userIdstringThe unique identifier for the authenticated account
email, namestringAccount owner's identity
company_websitestring | nullsnake_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_intelligenceobject | nullSame snake_case naming note as company_website. Onboarding Q&A + campaign questions
emailConnectarray[{ email, displayName }] — connected sending mailboxes
usageobject{ containerCount, leadCount }
acceptedTermsboolean

Webhook

FieldTypeDescription
webhookIdstring (UUID)
urlstringAlways https:// — enforced at creation
eventsstring[]Subscribed event types, or ["*"] for all
statusstringAlways "active" today (no pause/disable action — delete + recreate instead)
createdAtstring (ISO 8601)
secretstringOnly present in the POST create response, exactly once. Never returned again — store it immediately
secretPrefixstringFirst 8 chars of the secret, returned on GET list instead of the full secret

EmailAccount

FieldTypeDescription
emailstringThe connected mailbox address
providerstringgmail | outlook | zoho | yahoo | other
displayNamestringShown in the dashboard's sender picker; defaults to the email address
smtpHost, smtpPortstring, numberResolved automatically for gmail/outlook/zoho/yahoo, or as given for other
imapHost, imapPortstring, numberSame resolution rules as SMTP
connectedAtstring (ISO 8601)

API reference

Leads

POST/v1/leadsleads:write

Create/dedup leads, optionally link to a campaign

Request fields

FieldTypeDescription
leadsarrayArray of lead objects. Omit and pass lead fields at the top level for a single lead
leads[].email *stringMust pass RFC-5321 format or that lead is skipped (not a hard error)
leads[].name, .phone, .company, .location, .linkedin, .websitestringStandard fields
any other field on a lead objectAuto-promoted into that lead's metadata
campaignIdstringIf given, every created/existing lead is also linked into this campaign. Validated to exist first — fails fast with 404
forceUpdatebooleantrue = 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

FieldTypeDescription
totalProcessednumberCount of lead entries in the request
leadsCreatednumberNewly created leads
leadsUpdatednumberExisting leads updated (only if forceUpdate:true)
leadsDuplicateSkippednumberExisting leads left untouched
createdLeadIdsstring[]IDs of newly created leads
duplicateLeadsarray[{ email, existingLeadId }]
invalidEmailSkippednumberCount of malformed emails skipped
skippedInvalidEmailsstring[]The actual malformed email strings
campaignIdstring | nullEcho of the request field
campaignLinksAddednumberNew campaign links created

Errors

StatusCodeMeaning
400NO_VALID_LEADSEvery lead in the request had a malformed email
404CAMPAIGN_NOT_FOUNDBad campaignId
GET/v1/leadsleads:read

List leads (paginated)

Request fields

FieldTypeDescription
limitnumberMax 100. Default 20
lastKeystringOpaque 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

FieldTypeDescription
leadsLead[]Newest-first
countnumberItems on this page
hasMorebooleanWhether another page exists
nextCursorstring | nullPass as ?lastKey= to fetch the next page
GET/v1/leads/{id}leads:read

Get 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

StatusCodeMeaning
404LEAD_NOT_FOUNDBad/unowned lead id
PATCH/v1/leads/{id}leads:write

Update a lead

Request fields

FieldTypeDescription
name, phone, company, location, linkedin, website, leadTypestringAll optional, allowlisted
metadataobjectMerged key-by-key, existing keys not in the request are preserved
emailImmutable 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

StatusCodeMeaning
400EMAIL_IMMUTABLEAttempted to change email
404LEAD_NOT_FOUNDBad/unowned lead id

API reference

Campaigns

POST/v1/campaignscampaigns:write

Create a campaign

Request fields

FieldTypeDescription
name *string
campaignTypestringAI | AUTOMATION (default AUTOMATION)
campaignMotostringMARKETING | OUTREACH (default MARKETING)
campaignTagstringFree-text label (default "")
trigger.typestringIMMEDIATE | AFTER_HOURS | CUSTOM_DATETIME (default IMMEDIATE)
schedule.daysstring[]e.g. ["MON","TUE",...] (default [])
schedule.dailyLimitnumberDefault 100
schedule.timeWindow.start / .endstring HH:mmDefault 09:00 / 18:00
campaignQuestionsobjectFree-form key/value map (default {})
emailIndexstringA 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
followUpEmailIndexstringSame 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

FieldTypeDescription
campaignCampaign201 Created, status:"inactive" initially

Errors

StatusCodeMeaning
400INVALID_SENDERemailIndex/followUpEmailIndex isn't a real connected mailbox on this account — response includes connectedEmails with the real list
GET/v1/campaignscampaigns:read

List campaigns (paginated)

Request fields

FieldTypeDescription
limitnumberDefault 20, max 100
lastKeystringPagination cursor

Example request

curl -X GET "https://api.outbird.dev/v1/campaigns" \
  -H "Authorization: Bearer aw_live_YOUR_API_KEY"

Response fields

FieldTypeDescription
campaignsCampaign[]
count, hasMore, nextCursorSame pagination shape as GET /v1/leads
GET/v1/campaigns/{id}campaigns:read

Get a single campaign

Example request

curl -X GET "https://api.outbird.dev/v1/campaigns/{id}" \
  -H "Authorization: Bearer aw_live_YOUR_API_KEY"

Errors

StatusCodeMeaning
404CAMPAIGN_NOT_FOUNDBad/unowned campaign id
PATCH/v1/campaigns/{id}campaigns:write

Update 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

FieldTypeDescription
emailIndex, followUpEmailIndexstringRuns 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

StatusCodeMeaning
404CAMPAIGN_NOT_FOUNDBad/unowned campaign id
400INVALID_SENDERemailIndex/followUpEmailIndex isn't a real connected mailbox on this account — response includes connectedEmails with the real list
POST/v1/campaigns/{id}/startcampaigns:start

Start 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

StatusCodeMeaning
409INVALID_STATE_TRANSITIONCampaign 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)
POST/v1/campaigns/{id}/pausecampaigns:start

Pause 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

StatusCodeMeaning
409INVALID_STATE_TRANSITIONWrong campaign status for this action
POST/v1/campaigns/{id}/resumecampaigns:start

Resume 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

StatusCodeMeaning
409INVALID_STATE_TRANSITIONWrong campaign status for this action
POST/v1/campaigns/{id}/stopcampaigns:start

Stop 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

StatusCodeMeaning
409INVALID_STATE_TRANSITIONWrong campaign status for this action

API reference

Analytics

GET/v1/analyticsanalytics:read

Account-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 }
}
GET/v1/campaigns/{id}/analyticsanalytics:read

Single-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

StatusCodeMeaning
404CAMPAIGN_NOT_FOUNDCampaign doesn't exist / isn't owned by this key

API reference

Emails (outbox + inbox + send)

GET/v1/emailsemails:read

List emails

Request fields

FieldTypeDescription
statusstringLifecycle: Scheduled|Sent|Failed|Stopped|GenerationFailed|LowScore. Engagement: Opened|Clicked|Replied|Bounced|Unsubscribed. Omit for the full outbox
box"inbox"Convenience shorthand for ?status=Replied
limitnumberDefault 20, max 100
lastKeystringPagination cursor

Example request

curl -X GET "https://api.outbird.dev/v1/emails" \
  -H "Authorization: Bearer aw_live_YOUR_API_KEY"

Response fields

FieldTypeDescription
emailsEmail[]
count, status, hasMore, nextCursorstatus echoes the applied filter, "all" if none
GET/v1/emails/{id}emails:read

Get a single email

Example request

curl -X GET "https://api.outbird.dev/v1/emails/{id}" \
  -H "Authorization: Bearer aw_live_YOUR_API_KEY"

Errors

StatusCodeMeaning
404EMAIL_NOT_FOUNDBad/unowned email id
POST/v1/emails/sendemails:send

Send/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

FieldTypeDescription
to *stringRecipient email address
subject *string
html *stringHTML body content
emailIndexstringRequired unless the account has exactly one connected mailbox. Numeric position ("0") or the literal connected address — from GET /v1/me → emailConnect[]
tagstringFree-text label for later filtering via GET /v1/emails
leadIdstringAssociates this send with an existing lead
scheduleAtstring (ISO 8601)Omit to send immediately. If given, must be in the future
Idempotency-KeyheaderOptional 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

FieldTypeDescription
messagestring"Email sent"
emailIdstringUse this with GET /v1/emails/{id} to check status later
to, subject, tag, leadIdEcho of the request
scheduledTimeUTCstring | nullnull for an immediate send

Errors

StatusCodeMeaning
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
409IDEMPOTENCY_KEY_IN_PROGRESSA request with this Idempotency-Key is still executing — do not retry yet

API reference

Account

GET/v1/meaccount:read

Get 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
}
POST/v1/companyaccount:write

Set/refresh company URL + trigger intelligence extraction

Persists the URL then invokes the same company-intelligence pipeline the dashboard onboarding uses.

Request fields

FieldTypeDescription
url *stringA 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

StatusCodeMeaning
200{ onboarding_q_and_a, campaign_questions } — data ready
202Still scraping — poll again after a few seconds
422Bad/unreachable URL (DNS failure, connection refused) — user input problem
502Genuine 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.
POST/v1/email-accountsaccount:write

Connect a mailbox

Request fields

FieldTypeDescription
provider *stringgmail | outlook | zoho | yahoo | other
email *stringThe mailbox address itself
password (alias appPassword) *stringNot 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, smtpPortstring, numberRequired when provider:"other" — your mail server's SMTP host/port
securebooleanprovider:"other" only (default false)
displayNamestringShown in the dashboard's sender picker; defaults to the email address
referencestringFree-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

StatusCodeMeaning
401SMTP_AUTH_FAILEDReal 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
401IMAP_AUTH_FAILEDIMAP access failed — message includes the raw IMAP error
400INVALID_PROVIDERprovider not one of gmail|outlook|zoho|yahoo|other
400MISSING_EMAIL / MISSING_PASSWORD / MISSING_SMTP_CONFIGRequired field missing (smtpHost/smtpPort only required for provider:"other")
GET/v1/email-accountsaccount:read

List 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": "..." } ] }
DELETE/v1/email-accounts/{email}account:write

Disconnect 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

FieldTypeDescription
email *string (path)The connected mailbox address, URL-encoded (e.g. %40 for @)
actionstring (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)
targetEmailstring (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

StatusCodeMeaning
409CAMPAIGNS_ACTIVEMailbox 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": "..." } }
HeaderValue
Content-Typeapplication/json
X-AutoWorkx-EventThe event type string, e.g. email.sent
X-AutoWorkx-Event-IdStable identifier for this logical event occurrence — same on every redelivery. Use this to dedupe
X-AutoWorkx-Delivery-IdUnique identifier for this specific HTTP POST attempt — different on every retry
X-AutoWorkx-Delivery-Attempt1 on the first attempt, incrementing on each redelivery (max 5)
X-AutoWorkx-Signaturesha256=<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

Eventdata 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 }
POST/v1/webhookswebhooks:write

Register a webhook

Request fields

FieldTypeDescription
url *stringMust 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

StatusCodeMeaning
400MISSING_URL / INVALID_URLMissing, non-https://, or unparseable url
400MISSING_EVENTS / INVALID_EVENTSEmpty or unrecognized events entries
400WEBHOOK_LIMIT_REACHEDAlready have 10 webhooks on this account
GET/v1/webhookswebhooks:read

List 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": "..." } ] }
DELETE/v1/webhooks/{id}webhooks:write

Remove 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

StatusCodeMeaning
404WEBHOOK_NOT_FOUNDBad/unowned webhook id

API reference

Status codes

Every HTTP status code returned anywhere in this API.

StatusMeaningExample body
200Successful GET/POST (non-creating)
201Resource created (POST /v1/leads, /v1/campaigns, /v1/emails/send)
400Validation failure — malformed input, missing required field{"error":{"code":"<CODE>","message":"..."},"requestId":"..."}
401Missing/malformed/expired/revoked API key, or wrong scope for the route{"message":"Unauthorized"} (from the authorizer, before any endpoint runs)
403Reserved / observed in one rare authorizer edge case{"message":"Forbidden"}
404Resource doesn't exist, or isn't owned by this key's account{"error":{"code":"<X>_NOT_FOUND"},"requestId":"..."}
405Method not allowed on that path{"error":{"code":"METHOD_NOT_ALLOWED"},"requestId":"..."}
409Campaign action attempted from an incompatible status, or a duplicate in-flight Idempotency-Key{"error":{"code":"INVALID_STATE_TRANSITION"},"requestId":"..."}
422POST /v1/company only — bad/unreachable URL (user input, not a bug)
429Per-key rate limit exceeded — includes a Retry-After header, honor it before retrying{"error":{"code":"RATE_LIMITED","message":"...","retryAfterSeconds":60},"requestId":"..."}
500Server error — the requestId/X-Request-Id on this response identifies it for support{"error":{"code":"INTERNAL_ERROR"},"requestId":"..."}
502POST /v1/company only — genuine scrape failure

Quick reference

MethodPathScope
POST/v1/leadsleads:write
GET/v1/leadsleads:read
GET/v1/leads/{id}leads:read
PATCH/v1/leads/{id}leads:write
POST/v1/campaignscampaigns:write
GET/v1/campaignscampaigns:read
GET/v1/campaigns/{id}campaigns:read
PATCH/v1/campaigns/{id}campaigns:write
POST/v1/campaigns/{id}/startcampaigns:start
POST/v1/campaigns/{id}/pausecampaigns:start
POST/v1/campaigns/{id}/resumecampaigns:start
POST/v1/campaigns/{id}/stopcampaigns:start
GET/v1/analyticsanalytics:read
GET/v1/campaigns/{id}/analyticsanalytics:read
GET/v1/emailsemails:read
GET/v1/emails/{id}emails:read
POST/v1/emails/sendemails:send
GET/v1/meaccount:read
POST/v1/companyaccount:write
POST/v1/email-accountsaccount:write
GET/v1/email-accountsaccount:read
DELETE/v1/email-accounts/{email}account:write
POST/v1/webhookswebhooks:write
GET/v1/webhookswebhooks:read
DELETE/v1/webhooks/{id}webhooks:write