API

Build on 8Leads

A REST API and an MCP server over your accounts, leads, campaigns, inbox and results.

Quick start

  1. Sign up and start a Growth or Agency plan. The 14-day trial includes the API and needs a card.
  2. In the dashboard, open Settings → API keys and create a key. Copy it now, because it's only shown once.
  3. Check that the key works:
curl https://8leads.io/api/v1/me \
  -H "Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx"
{
  "data": {
    "workspace": { "id": "cmw2bdzek000040dj190ach70", "name": "Acme Outreach" },
    "plan": "GROWTH",
    "planStatus": "TRIALING",
    "scopes": ["read", "write"]
  }
}

Then add your first lead:

curl -X POST https://8leads.io/api/v1/prospects \
  -H "Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "instagramHandle": "@my.first.lead", "fullName": "First Lead" }'

To explore without writing code, import https://8leads.io/api/v1/openapi into Postman and add your key as a bearer token.

What you can test first

These work before you connect an Instagram account: leads, lists, tags, imports, the blocklist, labels, webhooks (POST /webhooks/{id}/test sends a signed test event), and building a campaign with its sequence and message preview.

Activating a campaign, sending a DM and receiving replies all need a connected account. Follow the full walkthrough when you're ready.

Base URL

All endpoints live under https://8leads.io/api/v1. Requests and responses are JSON.

Plans

The REST API and webhooks are included in the Growth plan and above, including during a Growth or Agency trial. On other plans every API request returns 403 with the code plan_upgrade_required. The MCP server below is available on every plan.

Authentication

A workspace owner or admin creates an API key under Settings, then you send it as a bearer token. Keys start with 8l_live_, belong to one workspace, and are shown once.

Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx

Scopes

A key carries read, write, or per-resource scopes such as leads:write or campaigns:read. write includes read, and a resource's :write includes its :read. The resources are leads, lists, tags, custom-fields, campaigns, enrollments, accounts, inbox, analytics, imports, blocklist, webhooks, lead-builder and team. A key missing a scope gets a 403 with the code insufficient_scope. Sending a DM needs inbox:write.

Rate limits

Each key can make 120 requests per minute, counted on each server instance. Responses to authenticated requests include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 also sends Retry-After. This limit is separate from the per-account daily and hourly send caps.

Errors

Errors share one shape. message names the first problem and, on a validation error, details lists every field.

{
  "error": {
    "code": "bad_request",
    "message": "instagramHandle: Required",
    "details": { "formErrors": [], "fieldErrors": { "instagramHandle": ["Required"] } }
  }
}
  • 400: invalid input, or a request the current state can't allow
  • 401: missing, invalid, revoked or expired key
  • 403: missing scope, a plan without API access, plan ended, or plan limit reached
  • 404: not found in this workspace
  • 409: duplicate, such as a lead handle that already exists
  • 429: over the rate limit, or an account at its send cap

Once a plan ends, reads keep working and anything that creates or sends returns 403.

Full walkthrough

This takes a workspace from nothing to a running campaign. Set your key first:

export API=https://8leads.io/api/v1
export AUTH="Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx"
export JSON="Content-Type: application/json"

1. Connect an Instagram account

Follow Connecting an Instagram account below. New accounts start in WARMING unless you pass "warmupEnabled": false.

2. Load leads

Create a list, then push leads one at a time or in bulk. Any key in attributes becomes a merge tag. In an import, so does any mapped name that isn't a standard field.

curl -X POST "$API/lists" -H "$AUTH" -H "$JSON" -d '{ "name": "Austin gyms" }'

curl -X POST "$API/prospects" -H "$AUTH" -H "$JSON" -d '{
  "instagramHandle": "@maya.builds",
  "fullName": "Maya Builds",
  "company": "Acme",
  "attributes": { "city_rank": "12" },
  "listId": "LIST_ID"
}'

curl -X POST "$API/imports" -H "$AUTH" -H "$JSON" -d '{
  "listId": "LIST_ID",
  "mapping": { "handle": "instagramHandle", "name": "fullName", "rank": "city_rank" },
  "rows": [ { "handle": "imp_one", "name": "One Person", "rank": "3" } ]
}'

3. Create the campaign

POST /campaigns takes only a name and starts with a default four-step sequence. Replace it, then set the sending window and accounts. A sequence has 1 to 20 steps, and each step type is MESSAGE, FOLLOW, LIKE, VIEW_STORY, LIKE_STORY or WAIT. Pause a campaign before changing its sequence.

curl -X POST "$API/campaigns" -H "$AUTH" -H "$JSON" -d '{ "name": "Austin gyms Q4" }'

curl -X PATCH "$API/campaigns/CAMPAIGN_ID" -H "$AUTH" -H "$JSON" -d '{
  "timezone": "America/Chicago",
  "sendingDays": [1, 2, 3, 4, 5],
  "businessHourStart": 540,
  "businessHourEnd": 1020
}'

curl -X PUT "$API/campaigns/CAMPAIGN_ID/sequence" -H "$AUTH" -H "$JSON" -d '{
  "steps": [
    { "stepType": "FOLLOW", "delayMinutes": 0 },
    { "stepType": "LIKE", "delayMinutes": 1440, "config": { "count": 2 } },
    { "stepType": "MESSAGE", "delayMinutes": 1440, "name": "Opener",
      "variants": [
        { "body": "Hi {{first_name}}, loved what you are doing at {{company}}.", "weight": 5 },
        { "body": "Hey {{first_name}}, quick question about {{company}}.", "weight": 5 }
      ] }
  ]
}'

curl -X PUT "$API/campaigns/CAMPAIGN_ID/accounts" -H "$AUTH" -H "$JSON" \
  -d '{ "accountIds": ["ACCOUNT_ID"] }'

Sequences run in a straight line, with no branches or spintax. Use variants to split test copy. A merge tag with no value renders as empty text, so check GET /merge-tags?campaignId=… to see how many leads each tag can fill.

4. Add leads and launch

Attach the list so leads added later join on their own, check the rendered copy, then activate. Activation needs at least one step, one account and one enrolled lead.

curl -X POST "$API/campaigns/CAMPAIGN_ID/lists" -H "$AUTH" -H "$JSON" \
  -d '{ "listId": "LIST_ID" }'

curl "$API/campaigns/CAMPAIGN_ID/preview?leads=3" -H "$AUTH"

curl -X POST "$API/campaigns/CAMPAIGN_ID/activate" -H "$AUTH"

5. Handle replies

Subscribe to replies, then answer from your own system.

curl -X POST "$API/webhooks" -H "$AUTH" -H "$JSON" -d '{
  "url": "https://yourapp.example/8leads",
  "events": ["reply.received", "message.sent", "account.challenged"]
}'

curl -X POST "$API/inbox/threads/THREAD_ID/reply" -H "$AUTH" -H "$JSON" \
  -d '{ "body": "Thanks Maya, does Tuesday work?" }'

Endpoints

The API covers daily use of the product:

  • Leads: list, create, get, update, delete, tag, add to lists, bulk actions, and bulk POST /imports.
  • Lead builder: build lists from a post's comments, hashtags, keywords or followers, and check lead credits. Paid plans only.
  • Lists, tags, custom fields, labels: list, create and delete. Lists can also be updated.
  • Campaigns: create, update, duplicate, preview, set the sequence and sending accounts, and activate, pause, resume, complete or archive.
  • Enrollments: enroll leads by id or from a list, attach a list so new leads join automatically, unenroll, and move between campaigns.
  • Accounts: connect and reconnect through a hosted login, list, update, pause, resume, remove, run health checks, send a DM to any handle, and read an account's messages.
  • Inbox: list and read conversations, reply, mark read, set status, assign, label and add notes.
  • Analytics: headline metrics, daily series, funnel, per-campaign and per-account performance, and recent activity.
  • Blocklist: suppress handles you never want contacted, one at a time or in bulk.
  • Webhooks: create, list, update, test and delete endpoints, and read each endpoint's delivery log.

Billing, inviting teammates, creating API keys and the first sign-up happen in the dashboard.

Leads, enrollments, the blocklist and webhook deliveries use ?page and ?pageSize (100 at most). The inbox and account messages use ?cursor and ?limit, and return nextCursor.

Example request

curl "https://8leads.io/api/v1/prospects?page=1&pageSize=25" \
  -H "Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx"

Example response

{
  "data": [
    {
      "id": "cmw2avh1d000340znci0gfylp",
      "instagramHandle": "maya.builds",
      "fullName": "Maya Builds",
      "firstName": "Maya",
      "company": "Acme",
      "status": "ACTIVE",
      "attributes": { "city_rank": "12" }
    }
  ],
  "pagination": { "page": 1, "pageSize": 25, "total": 1, "pageCount": 1 }
}

Connecting an Instagram account

The account owner signs in to Instagram themselves in a hosted browser that runs on the account's own residential proxy, so your code never handles the password. Use POST /accounts/{id}/reconnect the same way to sign an account back in.

# 1. Open a hosted browser on the account's own proxy
curl -X POST https://8leads.io/api/v1/accounts/connect \
  -H "Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "username": "growthstudio", "country": "US" }'
# Returns accountId, browserSessionId and liveViewUrl

# 2. The account owner signs in to Instagram at liveViewUrl

# 3. Capture the login
curl -X POST https://8leads.io/api/v1/accounts/connect/finalize \
  -H "Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "ACCOUNT_ID", "browserSessionId": "SESSION_ID" }'

Sending from an account

Send a DM from any connected account to any handle, with no existing conversation needed. We find or create the lead and the thread, then record the outbound message. Blocklisted handles are refused, an account that isn't ACTIVE or WARMING is refused, and a 429 comes back once the account hits its daily or hourly cap. A message can be up to 2,000 characters. Read an account's messages from GET /accounts/{id}/messages, and filter with ?direction=INBOUND or OUTBOUND.

curl -X POST https://8leads.io/api/v1/accounts/ACCOUNT_ID/messages \
  -H "Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "recipientHandle": "@mayabuilds", "message": "Hi Maya, loved your latest post." }'

Webhooks

Create an endpoint with POST /webhooks, using a public https:// URL and the events you want. We POST each event to your URL. A background job sends deliveries about every 25 seconds, so expect a short delay.

Instagram doesn't push replies to us. We check each account's inbox at most every 45 minutes, for 3 days after the last message to a lead who hasn't replied yet, so a reply event can arrive 45 minutes or more after the lead writes back. GET /webhooks/events returns the full list of events, and lead.created fires for single leads, not for each row of an import.

{
  "event": "reply.received",
  "data": {
    "threadId": "cmw2axgde001940zbubcvuokd",
    "prospect": "@maya.builds",
    "sentiment": "POSITIVE",
    "campaignId": "cmw2aw3jw001y40zn8l5gme8i"
  },
  "timestamp": "2026-11-04T15:58:58.553Z"
}

Every delivery carries the event name in x-8leads-event and an HMAC-SHA256 signature in x-8leads-signature, as a hex string. To verify, recompute the HMAC over the raw request body with your whsec_… signing secret. You receive that secret once, when you create the endpoint.

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody must be the exact bytes we sent, before any JSON parsing.
function isFrom8Leads(rawBody, signatureHeader, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}

Reply with any 2xx within 10 seconds. Otherwise we retry with exponential backoff, starting at 30 seconds, for up to 6 attempts. After 15 failures in a row the endpoint is switched off, and PATCH /webhooks/{id} with { "enabled": true } turns it back on. GET /webhooks/{id}/deliveries shows each attempt, and POST /webhooks/{id}/test sends a signed test event right away.

Events

  • Leads: lead.created, lead.replied, reply.received, leads.imported (a lead build finished)
  • Sending: message.sent, step.completed, enrollment.completed, account.followed, posts.liked, story.viewed, story.liked
  • Campaigns: campaign.activated, campaign.paused, campaign.completed, campaign.archived
  • Accounts: account.connected, account.paused, account.disconnected, account.challenged (Instagram signed it out, so reconnect it)
  • Testing: webhook.test

Delivery is at-least-once and order isn't guaranteed, so make your handler safe to run twice.

Limits

  • Growth: 10 Instagram accounts, unlimited active campaigns, 25,000 leads.
  • Agency: 50 Instagram accounts, unlimited active campaigns, 500,000 leads.
  • 14-day trial: 1 Instagram account, 1 active campaign, 500 leads, and no lead builder until the paid plan starts.
  • Send caps: each account has a daily limit (up to 200) and an hourly limit (up to 50), lower during warm-up. effectiveDailyLimit and effectiveHourlyLimit on the account show what is in force.
  • Per call: 20,000 import rows, and 5,000 ids to enroll, unenroll or move.
  • Enrichment: POST /prospects/{id}/enrich isn't active yet and returns the lead unchanged.

Connect to Claude (MCP)

8Leads also runs a Model Context Protocol server, so you can connect a workspace to Claude and work in plain language: ask for your reply rate, search leads, work the inbox, send a DM, or set up and launch a campaign.

Add it as a connector with this URL, then sign in with your 8Leads account:

https://8leads.io/api/mcp

It's a per-user connection: everything Claude does runs as you, limited by your role in the workspace, and there's no API key to share. Revoke access under Settings, in the API keys section. If you belong to more than one workspace, ask Claude to list your workspaces and pick one.

Full reference

Every endpoint, field and scope is in the interactive reference. To use Postman or a code generator, import the API spec. In Postman: Import → Link → the spec URL.