⚡ Instantly v2 API Reference · Battle-tested

IEXDG × Instantly
API Reference

The full operating reference for the Instantly v2 cold-email API behind Campaign 05. Every endpoint, plus the dozen gotchas we paid for in 403s, bounces, and one campaign that silently went live before it should have.

Prepared by Dove Web Consulting · for the IEXDG operations bench · v1.0 · 2026-06-10

v2
API version
12
hard-won laws
3
live campaigns
🔗 GHL
live sync
01 · Foundations

Base URL & authentication

Instantly v2 is a REST API. V1 is deprecated and shares no compatibility, so always build on v2.

ItemValue
Base URLhttps://api.instantly.ai · all v2 paths under /api/v2
Auth headerAuthorization: Bearer <api_key> (scoped keys, create/revoke in the Instantly UI)
Key storage (IEXDG)source/secrets/instantly_api_key.txt · 68-char base64, prefix NjY4Ym
Acceptapplication/json
Content-Typeapplication/json on every body EXCEPT DELETE /leads (see Law 8)

🚨 Law 1 (the one that wastes hours): the Chrome User-Agent is mandatory

The API sits behind Cloudflare. Without a browser User-Agent header you get 403 / error code 1010 (browser-signature ban), and the failure is often swallowed as "returned 0 results." Send a real Chrome UA on every call, exactly like the GHL pattern.

# every request, no exceptions
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36
Law 2 · Key-file parsing. When reading the key file, skip #-comment lines. The first raw line may be a comment, and passing it as the token returns 401 Invalid authorization header.
02 · The receipts

The 12 hard-won laws

Each of these cost a real failure. They are not in the official docs. This table is the most valuable part of this reference.

#LawWhy / symptom
1Send a Chrome User-Agent on every callCloudflare 403 / code 1010 otherwise, often swallowed as "0 results"
2Skip # comment lines when reading the key file401 Invalid authorization header
3Timezone enum: use America/Detroit for Eastern, NOT America/New_YorkNew_York is rejected by the schedule enum; Detroit is the accepted ET value
4Email body must be <div>-wrapped lines with NO literal newlinesThe body sanitizer strips bare text and <br>\n; blank line = <div><br></div>
5CTA links: wrap in an <a href> with a phrase as the anchor text, period OUTSIDE the tagCopy ending "[link]." became a bare iexdg.com/clarity. with the period inside the auto-link, 302 to /404, served the generic "Contact Us" page. 0 clicks on 51 sends.
6POST /campaigns/{id}/activate needs an empty OBJECT body {}An empty/no body with content-type json is rejected
7⚠ Adding leads can AUTO-FLIP a campaign from paused to active (status 1) and it starts sendingA campaign that was status 3 (no audience) flipped to status 1 the moment a populated audience + a live schedule existed. Re-check status after every lead load; pause explicitly if not ready.
8DELETE /leads/{id} must send NO Content-Type header and NO bodyOtherwise 400 FST_ERR_CTP_EMPTY_JSON_BODY ("body must be null")
9POST /leads/list (it is POST, not GET); keep limit ≤ ~100limit 200 returns a 400; body {campaign, limit}; results under items[]
10For the true lead count, read leads_count from analytics, not /leads/list/leads/list can 400 or lag and read 0 while leads exist; analytics is authoritative
11Email verification is ASYNC: poll until status leaves pendingFirst POST often returns pending; re-POST to poll. 0.25 credits each. See Verification.
12Status integers: 1=active, 2=paused, 3=completed/empty, -1=errorThe print label "(0=paused)" in old scripts is wrong; paused reads back as 2
03 · Catalog

Endpoint reference

The endpoints IEXDG uses are marked LIVE. Full v2 catalog from developer.instantly.ai included for completeness.

Campaigns

MethodPathUse
GET/api/v2/campaignslist campaigns
GET/api/v2/campaigns/{id}get one (status, email_list, sequences) live
POST/api/v2/campaignscreate (we create PAUSED) live
PATCH/api/v2/campaigns/{id}update sequences/schedule live
POST/api/v2/campaigns/{id}/activatestart/resume · body must be {} live
POST/api/v2/campaigns/{id}/pausepause live
GET/api/v2/campaigns/analytics?id={id}leads_count, emails_sent_count, open_count, reply_count, bounced_count live
GET/api/v2/campaigns/{id}/steps/analyticsper-step analytics
POST/api/v2/campaigns/{id}/duplicatecopy a campaign
DEL/api/v2/campaigns/{id}delete

Leads

MethodPathUse
POST/api/v2/leadsadd one · body {campaign,email,first_name,last_name,company_name} live
POST/api/v2/leads/listlist · body {campaign,limit≤100}items[] live
DEL/api/v2/leads/{id}remove · NO content-type, NO body live
POST/api/v2/leads/bulk-addadd up to 1000 to campaign/list
POST/api/v2/leads/movemove leads between campaigns/lists
PATCH/api/v2/leads/{id}update a lead

Email verification

MethodPathUse
POST/api/v2/email-verificationverify · body {email} · async, poll · 0.25 credits live
GET/api/v2/email-verification/{email}check status

Email accounts (sending inboxes)

MethodPathUse
GET/api/v2/accountslist inboxes · warmup_score, status live
POST/api/v2/accounts/enable-warmupstart warmup
GET/api/v2/accounts/{id}/warmup-analyticswarmup performance
GET/api/v2/accounts/{id}/vitalsaccount health test

Other groups

Emails (/api/v2/emails, 20 req/min cap), Lead Lists (/api/v2/lead-lists), Subsequences (/api/v2/campaign-subsequences), Webhooks (/api/v2/webhooks + event-types), Block List, Custom Tags, Lead Labels, Inbox Placement Tests, SuperSearch Enrichment, Workspace, API Keys, Background Jobs.

04 · The flow

Campaign lifecycle (the safe order)

This is the order that does not bounce inboxes or send before you mean to.

  1. CREATE paused      POST /campaigns                  status 0/draft, schedule + sequence set
  2. VERIFY each email  POST /email-verification {email} poll until verified | invalid
  3. LOAD only valid    POST /leads {campaign,email,...} // invalid never gets loaded
  4. RE-CHECK STATUS      GET /campaigns/{id}              // Law 7: adding leads can auto-activate
  5. PAUSE if not ready POST /campaigns/{id}/pause {}    // hold until sign-off
  6. ACTIVATE on go     POST /campaigns/{id}/activate {} // empty OBJECT body
  7. MONITOR            GET /campaigns/analytics?id=     sent / open / reply / bounce
The verify-then-load gate is the bounce shield. Run every derived address through /email-verification and load only verified / accept_all. This caught NAM, Feeding America, and Catholic Charities as invalid before they touched a warmed inbox.
05 · Deliverability

Email verification, in detail

Async. The first POST usually returns pending. Re-POST the same email to poll the cached result.

verification_statusLoad?Meaning
verifiedYESmailbox confirmed deliverable
accept_all / catch_all trueCAUTIONdomain accepts everything; will not hard-bounce but mailbox unconfirmed
invalidNOwould bounce; never load
pendingWAITstill checking; poll again in ~7s

catch_all is a separate boolean from the status. Credits: 0.25 per check; the workspace pool was ~949 at last read.

06 · Our build

IEXDG Campaign 05

Three voices, three warmed inboxes on iexdg-team.com, ET business-hours schedule, daily cap 20, stop_on_reply on.

CampaignIDSenderVoice
05A Executive-Peer537becfb-a607-4f45-bfbe-89ffbdf42d80drdnicole@iexdg-team.comleader-to-leader (CHRO/C-suite)
05B Partnershipsb6ce458e-1fc9-451f-beef-08de839db8b0partnerships@iexdg-team.com"we" partnerships (associations, member orgs)
05C Practitionere38c3302-a72e-495d-8bf7-728966d8f19ainfo@iexdg-team.compractitioner-to-practitioner (HR/L&D)

Build + repair: source/scripts/outreach/build_campaign05.py (sequences, schedule, the --fix --commit re-push). Lead load: source/scripts/outreach/load_campaign05_leads.py. Verified-lead records: source/data/campaign05_*_verified.csv. CTA on every sequence: <a href="https://iexdg.com/clarity">pick a time</a>.

07 · Copy/paste

Reusable snippets

def call(method, path, body=None, ctype=True):
    """Instantly v2 call. Chrome UA mandatory. ctype=False for DELETE /leads."""
    h = {"Authorization": f"Bearer {KEY}",
         "User-Agent": "Mozilla/5.0 ... Chrome/124.0 Safari/537.36",
         "Accept": "application/json"}
    if body is not None and ctype: h["Content-Type"] = "application/json"
    # urllib request to https://api.instantly.ai/api/v2 + path ...

# verify (poll) -> load only valid
status = call("POST", "/email-verification", {"email": em})  # poll while pending
if status in ("verified", "accept_all"):
    call("POST", "/leads", {"campaign": CID, "email": em, "first_name": fn, "last_name": ln, "company_name": co})

# delete a lead: NO content-type, NO body
call("DELETE", f"/leads/{lead_id}", body=None, ctype=False)

# activate: empty OBJECT body
call("POST", f"/campaigns/{CID}/activate", {})
# then ALWAYS verify it read back status 1
status = call("GET", f"/campaigns/{CID}")["status"]
08 · Connect to GoHighLevel

Instantly × GoHighLevel — the verified live build

This is no longer a plan. As of 2026-06-16 the connector is live and a full automation is active: every Campaign 05 reply auto-creates or updates a GoHighLevel contact, tagged instantly-05a. Everything below was driven and confirmed end-to-end, not read off a help page.

🚨 Reality vs the help doc: it is OAuth and an API key

The Instantly help article implies "paste an API key and you are done." That is not what actually happens. Connecting a HighLevel account fires a full OAuth handshake in a popup against marketplace.gohighlevel.com, the OAuth broker is Nango (api.nango.dev/oauth/callback), and the Instantly API key is only requested at the very last install screen. You need BOTH: the GHL OAuth login (with 2FA) and the Instantly key with leads:create + accounts:read scope.

Our key source/secrets/instantly_api_key.txt carries scopes:["all:all"] (confirmed via GET /api/v2/api-keys), which satisfies that requirement.

🔗 The connection handshake (what the popup really walks through)

1Click “Connect” Instantly → AutomationsLink HighLevel Account modal 2OAuth popup opens marketplace.gohighlevel.com/oauth/chooselocation 3Login + 2FA new tab app.gohighlevel.comcode emailed (espeakers) 4Install wizard openson the popup 5Next → Select account INTEGRAL EXPLORATIONDEVELOPMENT GROUP, LLC 6“Verify and install” paste Instantly API key(leads:create + accounts:read) 7Nango callback ✓ api.nango.dev/oauth/callbackAccount = “My Connection” 🔑 Domain-scoped cookies: the espeakers white-label session does NOT authenticate marketplace.gohighlevel.com, which is why step 3 needs its own login hop on app.gohighlevel.com. After one success, the saved session skips 2FA.
#ScreenWhat you do
1Instantly → Automations → action/trigger → Link HighLevel AccountClick Connect; a popup opens
2marketplace.gohighlevel.com/oauth/chooselocation“Please login to HighLevel” → click Login (opens a new tab)
3app.gohighlevel.com loginEmail + password → Verify Security Code (emailed) → tab auto-closes on success
4Install confirmationNext
5Select an accountChoose INTEGRAL EXPLORATION DEVELOPMENT GROUP, LLCVerify and install
6“Please add the details” — API KeyPaste the Instantly key → Verify and install
7Redirect to api.nango.dev/oauth/callback?code=…Done. The Automations Account field now shows My Connection

⚡ The live runtime flow

📧 Lead replies in any Campaign 05 inbox (05A / 05B / 05C) ⚡ Instantly Automation Trigger: Lead Replied campaign = All 🏢 GoHighLevel · Create Contact upsert by email + map fields tag: instantly-05a · loc N5N9…WeSc

The active automation — exact spec

PieceSetting
NameLead Replied to GHL Create Contact · automation id 019ed046-8ca7-7179-9327-7a18e49401d4
TriggerInstantly (built-in) → Lead Replied, campaign = All live
ActionHighLevel → Create Contact on account My Connection (loc N5N9WnYQGjQgzGQlWeSc) live
Field mapEmail ← Lead Email · First Name · Last Name · Phone · Company ← Company Name
Taginstantly-05a (literal; GHL lowercases it)
Statusactive · verified persistent across reload

What the connector actually exposes (verified labels)

DirectionExact options seen in the builder
Instantly triggersLead Replied · Lead Interest Status Changed · Campaign Finished for Lead · Subscriber Updated in Group · Lead Added to List
HighLevel actionsCreate Contact · Update Contact · Add Lead to Workflow  (no combined “Create/Update” — Create Contact upserts by email)
Action appsHighLevel, LeadConnector, HubSpot, Salesforce, Pipedrive, Attio, Zoho CRM, Slack, Mailchimp, HTTP Request, Condition, Delay, and more
Field tokens (Lead Replied)Lead Email, First Name, Last Name, Company Name, Phone, Website, LinkedIn, Campaign Id Label, plus the full reply payload

🚨 The four gotchas that cost real runs

  • You must SAVE, and it is hidden. Built nodes do NOT auto-persist; reopen and the canvas is empty. Save = the “…” (three-dot) menu top-right → Save. Same menu also names it (Settings) and holds Duplicate / History / Delete.
  • Activation is gated on the Test Step. The action shows “Action not tested” and the Paused toggle will not flip until you run the action’s Test Step — which actually creates the example contact in GHL. Because Create Contact upserts by email, repeated tests = one record. Delete the test contact afterward (see snippet).
  • Name it first. “…” → Settings → Name (required) → Edit to confirm.
  • Field map auto-advances. Filling the required fields jumps the panel to the Test tab on its own; do not blindly click a “Continue” that is no longer there.
✅ Cleanup snippet — delete the test contact.
# after the Test Step ran, remove the example contact from GHL
PIT = "pit-8e4c1579-...8b09a45"; CID = "<contact id from the Data Out tab>"
h = {"Authorization": f"Bearer {PIT}", "Version": "2021-07-28",
     "User-Agent": "Mozilla/5.0 ... Chrome/124.0 Safari/537.36"}
# DELETE https://services.leadconnectorhq.com/contacts/{CID}  -> {"succeeded": true}
Next (optional): add a second action, HighLevel Add Lead to Workflow, pointed at a GHL hot-reply workflow that alerts Dr. DNicole and routes to the Clarity Call. Not built yet, no workflow defined. Driver for the whole build: source/strategy/internal/_auto/inst_finalize.py.

Verified by direct, automated build · 2026-06-16 · Dove Web Consulting. Supersedes the 2026-06-15 help-doc summary, which mis-stated the auth model.