Systems Lab

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.

activeNeeds a keyActs undeclared3,266 words

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-tools in the frontmatter. It does act, so it runs under whatever permissions your session already grants.
Actions present in the files
shell

Ask about instantly-api

Opens your assistant with this page's verified links already in the prompt.

Is this safe to install?ClaudeChatGPT
Adapt it to my stackClaudeChatGPT
What else do I need for it to workClaudeChatGPT
Rather ask a human? Talk to Cheetah
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.

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, blank subject:"" threading, schedule days/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: Cloudflare 1010 UA ban (§9.1), bodyless-Content-Type 400 (§9.1), timezone:America/New_York→400, daily_limit:100000 accepted, 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:

SymptomCauseFix
Every call 403s with a valid keydefault 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 bodyomit Content-Type when there's no body (/activate, /pause, DELETE)
Push "succeeds" with 0 uploadedwrong field name (company vs company_name, contacts vs leads …)use V2 names (§3/§4); see provider field matrix
Email renders a blank/half-broken linelead missing a {{variable}} the copy usespreflight (§4.1) before push
daily_limit silently capped at 50using MCP create_campaignuse raw API (no cap)
Spintax not rotatingbare `{ab}(Instantly needs{{RANDOM
Upload counts don't add upintra-batch duplicate emails collapse silentlydedupe emails before push
timezone rejected (400)used America/New_York (not in enum)use America/Detroit etc.
MCP list_accounts/list_campaigns dumps huge blobfull objects inline → overflows result capuse raw API pagination

Quickest safety net: put every fix above into one small instantly.py client and import it everywhere.

0. Golden rules

  1. Prefer the raw V2 API over MCP and over Deepline. It's cleaner, has no wrapper caps (e.g. MCP caps daily_limit at 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.
  2. Variable names are case-sensitive and silently fail. Wrong field name = 0 leads pushed, no error. Get them exactly right (§3, §4).
  3. 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 quick list_campaigns/get_server_info if unsure.
  4. 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).
  5. 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 .env as INSTANTLY_API_KEY.
  • Scopes: <resource>:<action> where action ∈ read|create|update|delete|all, plus wildcards (all:all = full). E.g. create campaign needs campaigns:create.
  • Paid plan required, campaign create etc. return 402 on 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 at https://developer.instantly.ai/llms.txt.

2. Operation map (what's possible)

AreaOperationMethod + pathNotes
CampaignCreatePOST /campaignsname + campaign_schedule required. §5
Get / ListGET /campaigns/{id} · GET /campaigns
UpdatePATCH /campaigns/{id}Partial; same mutable fields as create
DeleteDELETE /campaigns/{id}✅ Instantly HAS delete (HeyReach does not)
Activate (start/resume)POST /campaigns/{id}/activateNo body. Prereqs in §5.5
PausePOST /campaigns/{id}/pauseNo body
Register variablesPOST /campaigns/{id}/variables{variables:["firstName",...]}
Duplicate / Share / Exportdedicated endpoints
Sending status (why not sending)issue-tracking endpointDecodes not_sending_status
LeadsBulk addPOST /leads/add≤1000/call. campaign_id XOR list_id. §4
Create singlePOST /leads
ListPOST /leads/listPOST by design (complex filters)
Move to campaign/listmove-leads endpoint
Update / interest status / merge / deletededicated endpoints
Lead listsCreate / Get / List / Patch / DeletePOST /lead-lists etc.Reusable lead pools
AccountsList / GetGET /accountsstatus, provider_code, tag_ids filters
Create / Patch / Pause / Resume / Deletededicated
Warmup enable/disable→ background job, poll GET /background-jobs/{id}
AnalyticsCampaign(s)GET /campaigns/analytics?id= or ?ids=§7
DailyGET /campaigns/analytics/daily
Overview / stepsdedicated
EmailsList / Get / Reply / Forward / Send test / Count unreadGET /emails etc.List = 20 req/min
VerificationSingle / on-importPOST /email-verification / verify_leads_on_import:trueAsync (background job)
WebhooksFull CRUD + testdedicated

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 RANDOM keyword, 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_id OR list_id, never both.
  • leads: 1-1000 per call. With campaign_id, each lead needs email. With list_id, each needs ≥1 of email/first_name/last_name.
  • Dedup flags: skip_if_in_campaign, skip_if_in_list, skip_if_in_workspace (workspace overrides the others). Default to skip_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 requires America/Detroit not America/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
  ] } ]
  • type only supports "email". delay_unit ∈ minutes|hours|days (default days), all three verified accepted.
  • Delay semantics (verified, docs verbatim): a step's delay is "the delay value before sending the NEXT email", i.e. the gap after this step. So: email 1 sends immediately on enrollment; step 0's delay = gap to email 2; the last step's delay is unused (no next email). (pre_delay would 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 with v_disabled:true. 2 steps × 2 variants = the standard 2x2.
  • Blank subject ("") = threaded follow-up on the same thread (emails 2 & 4 in the 2x2).
  • body accepts 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, status is read-only, so you must call /activate or /pause (§5.5). You cannot "create active"; create → add leads → activate.

5.5 Activate / pause

  • POST /campaigns/{id}/activate, flips status to 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.
  • POST /campaigns/{id}/pause, flips status to 2.
  • ⚠ Both are bodyless POSTs, do NOT send Content-Type: application/json with an empty body (see §9 gotcha), or you get 400 "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) or email_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

NeedUseWhy
Bulk lead push, campaign create/patch, anything productionV2 APINo wrapper caps, full fields, cheapest, scriptable
daily_limit > 50V2 API (must)MCP wrapper caps daily_limit at 50, email_gap at 1-1440
Quick read (list campaigns, get analytics, check a lead)MCP okConvenient, no script
Inside a Deepline play/CSV flowDeepline toolBut 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.
  • sequences only uses index [0], put all steps in sequences[0].steps. Extra array elements are ignored.
  • Custom-variable values are scalar only (string/number/bool/null).
  • Pagination: limit (1-100) + starting_after cursor (next_starting_after in response). List leads is POST /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 /activate when 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)

  1. Cloudflare blocks the default Python-urllib User-Agent → 403, Cloudflare error code 1010 (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-requests default UA → 200 (NOT banned). So: requests works out of the box; urllib does not, give urllib a User-Agent header (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, 429 is the rate limit; and 403 code 1010 from a burst of writes is the sticky write-throttle, a third distinct 403, space writes a few seconds apart.)
  2. Bodyless POST/DELETE + Content-Type: application/json → 400 "Body cannot be empty when content-type is set to 'application/json'". Affects /activate, /pause, and DELETE /campaigns/{id} and /lead-lists/{id}. Fix: omit the Content-Type header 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_schedule is required → 400 if missing.
  • timezone must be in the IANA enum: America/New_York → 400 ("must be equal to one of the allowed values"); use America/Detroit (Eastern) etc.
  • days keys are loosely validated, a bogus "7" is accepted (200) and ignored; only "0", "6" matter.
  • daily_limit: 100000 accepted (no API ceiling; MCP-only caps at 50).
  • custom_variables values 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_campaign returns skipped_count up, leads_uploaded: 0.
  • leads/add response counts don't always sum to input. Intra-batch duplicate emails are collapsed silently and are NOT reflected in duplicated_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} updates name/daily_limit/etc. on a draft and returns the updated object (200, verified). POST /campaigns/{id}/variables → 200 (verified).
  • not_sending_status is NOT a field on GET /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.trigger instead 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.py client with the UA fix, bodyless handling, and preflight() 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.

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.