Agent skill
instantly-api
Operate Instantly.ai (cold email) end-to-end via API and MCP, create, modify/edit, delete, activate/pause, push leads, and analyze campaigns.
Filed under Outbound email.
From OneGTM/gtm-skills · 9 skill entries · 0 · pushed 2026-09-30
What it does when it runs
Operate Instantly.ai (cold email) end-to-end via API and MCP, create, modify/edit, delete, activate/pause, push leads, and analyze campaigns. Master reference for every operation, exact variable + spintax syntax, the required-variable preflight guard, campaign/sequence/schedule/sender/lead payloads, what's API-only vs MCP-only, rate limits, and workarounds. Use whenever creating, editing, deleting, pushing leads to, activating, or analyzing Instantly campaigns. Pairs with `instantly-copywriter` (which owns the copy itself).
Automated analysis of the skill and the 1 file bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.
- Keys and connectors you must supply
- API_KEY
- INSTANTLY_API_KEY
- Hosts it reaches
- api.instantly.ai
- developer.instantly.ai
- Tool permissions it declares
- No
allowed-toolsin the frontmatter. It does act, so it runs under whatever permissions your session already grants. - Actions present in the files
- shell
Install it
View source on GitHub ↗git clone --depth 1 --filter=blob:none --sparse https://github.com/OneGTM/gtm-skills.git /tmp/gtm-skills git -C /tmp/gtm-skills sparse-checkout set "skills/instantly-api" mkdir -p ~/.claude/skills/instantly-api cp -R "/tmp/gtm-skills/skills/instantly-api/." ~/.claude/skills/instantly-api/
Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗
Or take the whole library
This repo ships a .claude-plugin manifest, so Claude Code can install all 9 skills at once. Plugin skills are invoked as /<plugin>:<skill>, so they never collide with your own.
/plugin marketplace add OneGTM/gtm-skills /plugin
The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.
Before you install: this skill will not complete its job on a bare agent. It needs API_KEY, INSTANTLY_API_KEY, which you have to obtain separately.
The skill
Source on GitHub ↗Reproduced in full from OneGTM/gtm-skills/blob/36da828338f8062c90f6a4a8ea8b918e4160f1e5/skills/instantly-api/SKILL.md, which is licensed MIT (repository). 3,266 words, 27 headings.
Instantly API + MCP (Shared)
Everything you can and cannot do with Instantly, the optimal way to do each thing, and the workarounds. This skill owns mechanics (endpoints, payloads, variables, limits). The instantly-copywriter skill owns copy (2x2 structure, naming, humanization). Use both together: copywriter writes the sequence, this skill pushes it.
Sibling skill: heyreach-api (LinkedIn). Keep them separate, different auth, different variable syntax, different everything.
Live-validated 2026-06-10 against a live production workspace. Confirmed: Bearer auth;
GET /campaigns+ cursor pagination;GET /accounts;GET /campaigns/{id}(sequence/step/variant shape, blanksubject:""threading, scheduledays/timing/IANA tz,{{firstName}}/{{companyName}}/{{accountSignature}}+ no-space{{RANDOM|...}}with nested vars); create/activate/pause/delete lifecycle; bulk lead add with dedup + custom_variables. Edge cases proven: Cloudflare1010UA ban (§9.1), bodyless-Content-Type400 (§9.1),timezone:America/New_York→400,daily_limit:100000accepted, nested custom_var→400, 1001-lead→400, activate-with-no-senders/leads→200 (no API prereq enforcement). Untested-but-documented: live-campaign mid-flight edit propagation, warmup background jobs.
Silent failure modes (check these FIRST when something "worked" but didn't)
These return success / no error yet do the wrong thing, the expensive ones:
| Symptom | Cause | Fix |
|---|---|---|
| Every call 403s with a valid key | default Python-urllib UA is Cloudflare-banned (err 1010) | set a User-Agent (curl/requests are fine) |
| Bodyless POST/DELETE → 400 "Body cannot be empty" | sent Content-Type: application/json with no body | omit Content-Type when there's no body (/activate, /pause, DELETE) |
| Push "succeeds" with 0 uploaded | wrong field name (company vs company_name, contacts vs leads …) | use V2 names (§3/§4); see provider field matrix |
| Email renders a blank/half-broken line | lead missing a {{variable}} the copy uses | preflight (§4.1) before push |
daily_limit silently capped at 50 | using MCP create_campaign | use raw API (no cap) |
| Spintax not rotating | bare `{a | b}(Instantly needs{{RANDOM |
| Upload counts don't add up | intra-batch duplicate emails collapse silently | dedupe emails before push |
timezone rejected (400) | used America/New_York (not in enum) | use America/Detroit etc. |
MCP list_accounts/list_campaigns dumps huge blob | full objects inline → overflows result cap | use raw API pagination |
Quickest safety net: put every fix above into one small
instantly.pyclient and import it everywhere.
0. Golden rules
- Prefer the raw V2 API over MCP and over Deepline. It's cleaner, has no wrapper caps (e.g. MCP caps
daily_limitat 50; the API has no such cap), is cheaper, and gives full field control. Drop to MCP only for quick reads/one-offs or when you have no key handy. See §8 for the decision table. - Variable names are case-sensitive and silently fail. Wrong field name = 0 leads pushed, no error. Get them exactly right (§3, §4).
- Multiple Instantly MCP servers are usually connected, one per client workspace (often named
<Client>_Instantly_MCP), plus shared/other ones. Always select the server bound to the workspace you're operating in; the rest point at other portals (using the wrong one = wrong account). Confirm via a quicklist_campaigns/get_server_infoif unsure. - Three integration methods use different field names (V2 API vs Deepline vs MCP). See the provider notes for the full mapping table. This skill documents the V2 API shapes (the preferred path).
- Run the required-variable preflight guard before every push (§4.1). Never enroll a lead missing a variable the copy uses (
email,first_name,company_name, any{{custom}}), Instantly renders the token blank/fallback, not an error. Upload only leads that pass; surface the rest.
1. Auth & base URL
- Base:
https://api.instantly.ai/api/v2, every endpoint is under/api/v2/.... - Auth: HTTP Bearer. Header:
Authorization: Bearer <API_KEY>. - Key: app.instantly.ai → Settings → Integrations → API Keys → Create (pick scopes; shown once). Client key lives in that client's
.envasINSTANTLY_API_KEY. - Scopes:
<resource>:<action>where action ∈read|create|update|delete|all, plus wildcards (all:all= full). E.g. create campaign needscampaigns:create. - Paid plan required, campaign create etc. return
402on a free workspace. - OpenAPI spec (machine-readable, best source for anything unlisted):
https://api.instantly.ai/openapi/api_v2.json. Docs have clean markdown twins at<page>.md; index athttps://developer.instantly.ai/llms.txt.
2. Operation map (what's possible)
| Area | Operation | Method + path | Notes |
|---|---|---|---|
| Campaign | Create | POST /campaigns | name + campaign_schedule required. §5 |
| Get / List | GET /campaigns/{id} · GET /campaigns | ||
| Update | PATCH /campaigns/{id} | Partial; same mutable fields as create | |
| Delete | DELETE /campaigns/{id} | ✅ Instantly HAS delete (HeyReach does not) | |
| Activate (start/resume) | POST /campaigns/{id}/activate | No body. Prereqs in §5.5 | |
| Pause | POST /campaigns/{id}/pause | No body | |
| Register variables | POST /campaigns/{id}/variables | {variables:["firstName",...]} | |
| Duplicate / Share / Export | dedicated endpoints | ||
| Sending status (why not sending) | issue-tracking endpoint | Decodes not_sending_status | |
| Leads | Bulk add | POST /leads/add | ≤1000/call. campaign_id XOR list_id. §4 |
| Create single | POST /leads | ||
| List | POST /leads/list | POST by design (complex filters) | |
| Move to campaign/list | move-leads endpoint | ||
| Update / interest status / merge / delete | dedicated endpoints | ||
| Lead lists | Create / Get / List / Patch / Delete | POST /lead-lists etc. | Reusable lead pools |
| Accounts | List / Get | GET /accounts | status, provider_code, tag_ids filters |
| Create / Patch / Pause / Resume / Delete | dedicated | ||
| Warmup enable/disable | → background job, poll GET /background-jobs/{id} | ||
| Analytics | Campaign(s) | GET /campaigns/analytics?id= or ?ids= | §7 |
| Daily | GET /campaigns/analytics/daily | ||
| Overview / steps | dedicated | ||
| Emails | List / Get / Reply / Forward / Send test / Count unread | GET /emails etc. | List = 20 req/min |
| Verification | Single / on-import | POST /email-verification / verify_leads_on_import:true | Async (background job) |
| Webhooks | Full CRUD + test | dedicated |
What's NOT possible via API: A/B winner is auto-picked only if you set auto_variant_select (no manual "promote variant" call documented); some UI-only campaign settings; reading the rendered (spintax-resolved) body of a sent email isn't exposed as a template.
3. Variables & personalization: EXACT syntax
Instantly uses double curly braces, camelCase, and is case-sensitive. {{firstName}} works; {{FirstName}}, {{first_name}}, {firstName} do not.
Standard / built-in tokens (use in subject + body)
{{firstName}} {{lastName}} {{companyName}} {{email}}
{{website}} {{phone}} {{jobTitle}} {{personalization}}
{{accountSignature}} ← sender signature; put at END of every email, never hardcode a name
Note the snake_case/camelCase split: lead input fields are snake_case (
first_name,company_name,job_title); they render as camelCase tokens ({{firstName}},{{companyName}},{{jobTitle}}).
Custom variables
Any key in a lead's custom_variables object becomes a token: custom_variables:{ "icebreaker": "..." } → {{icebreaker}}. Values must be string/number/boolean/null (no nested objects/arrays). Adding a custom variable to even one lead updates the whole campaign so all leads can carry it.
Fallbacks ("if empty, use X"): pipe inside the braces
{{firstName | there}} → "there" when firstName is empty
{{companyName | your company}}
{{firstName | lastName | for you}} ← chains: tries firstName, then lastName, then "for you"
Spintax: {{RANDOM|...}} (NOT bare {a|b})
{{RANDOM|Hi {{firstName}}|Hey {{firstName}}|{{firstName}}}},
- Double braces, leading
RANDOMkeyword, pipe-separated options. Bare{option1|option2}fails silently, always use{{RANDOM|...}}. - Use NO spaces around the pipes / after RANDOM,
{{RANDOM|a|b}}. This is the form used in live production campaigns (verified). The spaced{{RANDOM | a | b}}appears in Instantly's help center but is not what's proven in production here; stick with the no-space form. - Allowed in subject and body.
- Nests with variables/fallbacks:
{{RANDOM|Quick question {{firstName|there}}|A thought for {{companyName}}}}(confirmed live, variables nest inside spintax options). - Copy minimums, greeting/CTA patterns, and the 2x2 thread structure are owned by
instantly-copywriter, follow that skill when writing; this skill just confirms the engine syntax.
4. Leads: bulk add (the workhorse)
POST /api/v2/leads/add, the preferred way to enroll leads. 10-100× faster than single-create.
- Provide
campaign_idORlist_id, never both. leads: 1-1000 per call. Withcampaign_id, each lead needsemail. Withlist_id, each needs ≥1 ofemail/first_name/last_name.- Dedup flags:
skip_if_in_campaign,skip_if_in_list,skip_if_in_workspace(workspace overrides the others). Default toskip_if_in_campaign: true. - Lead fields (snake_case):
email,first_name,last_name,company_name,job_title,phone,website,personalization,custom_variables(flat object). - Response:
total_sent,leads_uploaded,in_blocklist,duplicated_leads,skipped_count,invalid_email_count,remaining_in_plan, always log these. - Safe batch size: 950 (under the 1000 ceiling). Verify on import with
verify_leads_on_import: true(creates a background job).
Verbatim payload + push loop → references/recipes.md.
4.1 Required-variable preflight guard (run before EVERY push: mandatory)
Never enroll a lead missing a variable the campaign copy uses. A missing {{firstName}}/{{companyName}}/{{custom}} doesn't error, Instantly just renders it blank (or its {{x | fallback}} if you set one), so a thin lead silently ships a broken/generic email. The guard catches it.
What to require: for each lead going into a campaign, email (required by the API anyway), plus every token the copy references: standard ones map to lead fields ({{firstName}}→first_name, {{companyName}}→company_name, {{jobTitle}}→job_title, …) and custom ones must exist in custom_variables. Extract the {{…}} tokens from your sequence (subjects + bodies), strip RANDOM/spintax and any | fallback, then require the remaining names. A token that has a | fallback is optional (the fallback covers it); a bare token is required.
How: split leads into ok / missing, push only ok, and report missing (count + sample + which field each lacks). Don't silently proceed. See preflight() in references/recipes.md. Applies to API, MCP, and Deepline pushes alike, none validate copy variables for you.
5. Campaigns & sequences
5.1 Create: required + key fields
POST /api/v2/campaigns. Required: name, campaign_schedule. Unknown fields are rejected (additionalProperties:false).
Common fields: sequences (copy, §5.3), email_list (sending account emails) or email_tag_list (sending-account tag UUIDs), daily_limit, email_gap (minutes), random_wait_max (minutes), stop_on_reply, stop_on_auto_reply, open_tracking, link_tracking, text_only, first_email_text_only, daily_max_leads, auto_variant_select:{trigger:"reply_rate"|"click_rate"|"open_rate"}, cc_list, bcc_list, pl_value, is_evergreen, provider_routing_rules.
5.2 Schedule object (campaign_schedule)
campaign_schedule:
start_date / end_date: "YYYY-MM-DD" | null
schedules: [ # ≥1; multiple blocks allowed
{ name: "My Schedule",
timing: { from: "09:00", to: "17:00" }, # 24h HH:MM
days: { "0":false,"1":true,...,"6":false }, # "0"=SUNDAY … "6"=SATURDAY
timezone: "America/Detroit" } ] # IANA enum (use America/Detroit, NOT America/New_York)
Days are 0-6 with
"0"= Sunday. Timezone must be from the IANA enum; Deepline specifically requiresAmerica/DetroitnotAmerica/New_York.
5.3 Sequence / steps / variants (multi-step + A/B = the "2x2")
sequences: [ # ⚠ ONLY sequences[0] is used
{ steps: [
{ type: "email", delay: 2, delay_unit: "days", # delay = wait BEFORE the next email
variants: [ # multiple variants = A/B
{ subject: "Hello {{firstName}}", body: "<p>...{{accountSignature}}</p>" },
{ subject: "Quick question {{firstName}}", body: "<p>...</p>", v_disabled: false }
] },
{ type: "email", delay: 2, delay_unit: "days",
variants: [ { subject: "", body: "<p>...</p>" } ] } # subject "" = threaded reply
] } ]
typeonly supports"email".delay_unit∈minutes|hours|days(defaultdays), all three verified accepted.- Delay semantics (verified, docs verbatim): a step's
delayis "the delay value before sending the NEXT email", i.e. the gap after this step. So: email 1 sends immediately on enrollment; step 0'sdelay= gap to email 2; the last step'sdelayis unused (no next email). (pre_delaywould delay a first email but is subsequence-only, ignored on normal campaigns.) ⚠ This is the opposite of HeyReach, where the delay is the wait before each node, don't carry the mental model across platforms. - A/B / 2x2: multiple objects in
variants[]; disable one withv_disabled:true. 2 steps × 2 variants = the standard 2x2. - Blank subject (
"") = threaded follow-up on the same thread (emails 2 & 4 in the 2x2). bodyaccepts HTML.
5.4 Set-at-create vs patch-only vs read-only
- Set at create or PATCH: all copy/schedule/tracking/limit/sender fields above.
- Read-only (never settable):
id,status,not_sending_status,core_variables,custom_variables, timestamps. - Not a body field: activation,
statusis read-only, so you must call/activateor/pause(§5.5). You cannot "create active"; create → add leads → activate.
5.5 Activate / pause
POST /campaigns/{id}/activate, flipsstatusto 1 (Active); also resumes a paused campaign.- The API does NOT enforce prereqs (verified: activating a campaign with zero senders and zero leads returns 200 and status→1). It just won't send, the reason surfaces in
not_sending_status(§5.6). The "needs ≥1 sender/≥1 lead/sequence/schedule" rule is an MCP-wrapper/UI guard, not an API gate. So in practice still add senders+leads+sequence before activating, but don't expect the API to stop you.
- The API does NOT enforce prereqs (verified: activating a campaign with zero senders and zero leads returns 200 and status→1). It just won't send, the reason surfaces in
POST /campaigns/{id}/pause, flipsstatusto 2.- ⚠ Both are bodyless POSTs, do NOT send
Content-Type: application/jsonwith an empty body (see §9 gotcha), or you get400 "Body cannot be empty...".
5.6 Status / not-sending codes (read-only)
status: 0 Draft · 1 Active · 2 Paused · 3 Completed · 4 Running Subsequences · −1 Accounts Unhealthy · −2 Bounce Protect · −99 Suspended.
not_sending_status: 1 Outside schedule · 2 Waiting for a lead · 3 Daily limit reached · 4 All accounts hit daily limit · 99 Error.
6. Sending accounts / mailboxes
GET /accounts, filters:search(by domain),status,provider_code(1 IMAP·2 Google·3 Microsoft·4 AWS·8 AirMail),tag_ids.- Bind accounts to a campaign via the campaign's
email_list(array of account emails) oremail_tag_list(tag UUIDs), there is no separate "attach" endpoint. - Per-account
daily_limit,signature, warmup config live on the account object. Warmup enable/disable runs as a background job.
7. Analytics
GET /campaigns/analytics?id={uuid}or?ids=a&ids=b(+start_date/end_date). Returns per-campaign:leads_count,contacted_count,emails_sent_count,open_count(_unique),reply_count(_unique),link_click_count(_unique),bounced_count,unsubscribed_count,completed_count,total_opportunities,total_opportunity_value.GET /campaigns/analytics/daily, per-day series.
8. API vs MCP vs Deepline: when to use which
| Need | Use | Why |
|---|---|---|
| Bulk lead push, campaign create/patch, anything production | V2 API | No wrapper caps, full fields, cheapest, scriptable |
daily_limit > 50 | V2 API (must) | MCP wrapper caps daily_limit at 50, email_gap at 1-1440 |
| Quick read (list campaigns, get analytics, check a lead) | MCP ok | Convenient, no script |
| Inside a Deepline play/CSV flow | Deepline tool | But mind the 100-row batch cap (>100 returns empty {}, silent) and different field names (contacts, company, personalized_body, custom_fields) |
Always pick the right MCP server for the client (multiple Instantly MCP servers are usually connected, match the server to the client's workspace). MCP create_campaign also can't exceed the daily_limit cap and is slower, prefer API.
⚠ MCP enumeration tools overflow the tool-result token cap. list_accounts / list_campaigns return the full object for every record inline (observed ~99k and ~161k chars), they blow past the result limit and get auto-spilled to a file you then have to parse. For any enumeration, use the raw API with pagination (limit=100 + starting_after) instead, or expect the overflow and parse the saved file. Single-record reads (get_campaign, get_account) are fine inline.
9. Rate limits & gotchas
- 100 req/sec and 6,000 req/min, shared across V1+V2 per workspace (all keys combined). Either limit →
429. Batch in groups of ~100 with a 2s pause; run automations 2-4×/day, not continuously. GET /emails(list) = 20/min. Send-test = 10/min.sequencesonly uses index [0], put all steps insequences[0].steps. Extra array elements are ignored.- Custom-variable values are scalar only (string/number/bool/null).
- Pagination:
limit(1-100) +starting_aftercursor (next_starting_afterin response). List leads isPOST /leads/list, not GET. - Editing a live campaign: PATCH works while Active, but changes generally affect only not-yet-sent steps; build copy/schedule/senders/leads before
/activatewhen possible. - Wrong field name = silent 0-lead push. No error. Re-check §3/§4 names if a push "succeeds" with 0 uploaded.
9.1 Two infra gotchas that will waste your afternoon (both verified live)
- Cloudflare blocks the default
Python-urllibUser-Agent →403, Cloudflare error code1010(browser-signature ban). Reads and writes fail with 403 even with a valid key. Verified results:Python-urllib/x→ 403;curl/8.4.0→ 200;Mozilla/5.0→ 200;python-requestsdefault UA → 200 (NOT banned). So:requestsworks out of the box;urllibdoes not, give urllib aUser-Agentheader (e.g.curl/8.4.0). The recipes use curl-via-subprocess (carries curl's UA), so they're immune. (403 here is the UA ban, NOT a rate limit,429is the rate limit; and403 code 1010from a burst of writes is the sticky write-throttle, a third distinct 403, space writes a few seconds apart.) - Bodyless POST/DELETE +
Content-Type: application/json→400 "Body cannot be empty when content-type is set to 'application/json'". Affects/activate,/pause, andDELETE /campaigns/{id}and/lead-lists/{id}. Fix: omit theContent-Typeheader when there is no body (or send a non-empty body). Verified: same DELETE 400s with the header, 200s without it.
9.2 Verified validation edges
campaign_scheduleis required → 400 if missing.timezonemust be in the IANA enum:America/New_York→ 400 ("must be equal to one of the allowed values"); useAmerica/Detroit(Eastern) etc.dayskeys are loosely validated, a bogus"7"is accepted (200) and ignored; only"0","6"matter.daily_limit: 100000accepted (no API ceiling; MCP-only caps at 50).custom_variablesvalues must be scalar, a nested object/array value → 400.- Bulk add 1001 leads → 400; cap is exactly 1000.
- Dedup works: re-adding with
skip_if_in_list/skip_if_in_campaignreturnsskipped_countup,leads_uploaded: 0. leads/addresponse counts don't always sum to input. Intra-batch duplicate emails are collapsed silently and are NOT reflected induplicated_leads(e.g. 374 in → uploaded + reported-dupes < 374, with the remainder unaccounted). If you need exact counts, dedupe your own email list before pushing.- Two
sequences[]elements are both stored (not stripped at create); only index [0] is used at send time. - All of these create-time fields echo back verified:
email_gap,random_wait_max,daily_limit,stop_on_reply,stop_on_auto_reply,open_tracking,link_tracking,stop_for_company,text_only,auto_variant_select:{trigger},email_list. PATCH /campaigns/{id}updatesname/daily_limit/etc. on a draft and returns the updated object (200, verified).POST /campaigns/{id}/variables→ 200 (verified).not_sending_statusis NOT a field onGET /campaigns/{id}(verified absent on a draft). The §5.6 codes come from the dedicated sending-status / issue-tracking endpoint, not the campaign object, query that endpoint to learn why a campaign isn't sending.
10. Workarounds / hacky moves
- Need daily_limit > 50: can't via MCP, hit the raw API.
- Threaded follow-ups: set the follow-up step's variant
subject: ""→ sends as a reply on the prior thread (this is how emails 2 & 4 of the 2x2 stay in-thread). - Auto-pick the best variant: set
auto_variant_select.triggerinstead of babysitting A/B. - Stop sending to a whole company on one reply:
stop_for_company: true. - Reuse leads across campaigns without re-uploading: put them in a lead-list (
list_id) and move/assign, rather than re-pushing. - Diagnose "why isn't it sending": read
not_sending_status(§5.6) or the issue-tracking endpoint before touching config. - Clean re-push after a bad field map: delete the campaign (
DELETE /campaigns/{id}, Instantly allows it) and recreate, rather than fighting half-loaded leads.
11. Files
- Build a small
instantly.pyclient with the UA fix, bodyless handling, andpreflight()baked in (prefer this over hand-rolling). references/recipes.md, verbatim, copy-paste working payloads (auth helper, bulk lead push, create campaign with 2x2, PATCH sequence, activate, analytics).- Cross-refs:
instantly-copywriter(copy + naming + spintax minimums), the provider notes (V2 vs Deepline vs MCP field-name matrix).
Files bundled with it
These load only when the skill asks for them, so they cost nothing until it runs.
Other skills for the same job
Different authors, same problem. Matched on the words in the skill name, across every library in the catalogue except this one.
- instantly-api by Cold-IQ · 1
- prospeo-search-api by growthenginenowoslawski · 739
- smartlead-api by growthenginenowoslawski · 739
- extruct-api by extruct-ai · 109
- gtm-api-linkedin by gtm-api · 89
- ai-api-developer-gtm by 0xF4ng · 6
- ga4-api-reporting by Ad-Superpowers · 5
- prospeo-search-api by timyakubson · 3
Need help setting it up?
This page tells you what instantly-api does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.
Book a call →The directory stays free. There is nothing gated behind this.