A REST API and an MCP server over your accounts, leads, campaigns, inbox and results.
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.
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.
All endpoints live under https://8leads.io/api/v1. Requests and responses are JSON.
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.
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_xxxxxxxxxxxxxxxxA 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.
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 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 allow401: missing, invalid, revoked or expired key403: missing scope, a plan without API access, plan ended, or plan limit reached404: not found in this workspace409: duplicate, such as a lead handle that already exists429: over the rate limit, or an account at its send capOnce a plan ends, reads keep working and anything that creates or sends returns 403.
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"Follow Connecting an Instagram account below. New accounts start in WARMING unless you pass "warmupEnabled": false.
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" } ]
}'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.
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"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?" }'The API covers daily use of the product:
POST /imports.activate, pause, resume, complete or archive.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.
curl "https://8leads.io/api/v1/prospects?page=1&pageSize=25" \
-H "Authorization: Bearer 8l_live_xxxxxxxxxxxxxxxx"{
"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 }
}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" }'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." }'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.
lead.created, lead.replied, reply.received, leads.imported (a lead build finished)message.sent, step.completed, enrollment.completed, account.followed, posts.liked, story.viewed, story.likedcampaign.activated, campaign.paused, campaign.completed, campaign.archivedaccount.connected, account.paused, account.disconnected, account.challenged (Instagram signed it out, so reconnect it)webhook.testDelivery is at-least-once and order isn't guaranteed, so make your handler safe to run twice.
effectiveDailyLimit and effectiveHourlyLimit on the account show what is in force.POST /prospects/{id}/enrich isn't active yet and returns the lead unchanged.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/mcpIt'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.
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.