Getting Started

The Scrapely API allows you to send direct messages and fetch conversations programmatically. Use it to integrate Twitter DM automation into your own applications and workflows.

Base URL

text
https://app.scrapely.co/api/v1

Available Endpoints

Accounts
GET/accountsList your Twitter accounts
POST/accountsAdd a new account
DELETE/accountsRemove an account
PUT/accounts/cookiesUpdate account cookies
PUT/accounts/proxyUpdate account proxy
PATCH/accounts/pauseToggle account pause status
PATCH/accounts/pause-followupsToggle follow-ups pause status
PATCH/accounts/warmupToggle account warmup status
POST/accounts/reassign-pendingMove pending DMs to other accounts
GET/accounts/tagsGet account tags
PUT/accounts/tagsSet account tags
PATCH/accounts/tagsAdd or remove tags
PATCH/accounts/handleUpdate an account's handle
Cookie Extraction (Deprecated)
POST/get-cookiesQueue cookie extraction job (Deprecated)
GET/get-cookies/statusCheck extraction status (Deprecated)
Lead Scraping
POST/scraping-sourcesCreate a scraping source
POST/scraping-sources/commentersScrape tweet commenters
GET/scraping-sourcesList/check scraping status
GET/scraping-sources/:lead_source_id/leadsGet a lead source's leads
POST/import-leadsImport leads from JSON
Campaigns
POST/campaignsLaunch a DM campaign
POST/campaigns/importLaunch an import campaign
GET/campaignsList campaigns
GET/campaigns/analyticsGet campaign analytics
GET/campaigns/:campaign_name/leadsList a campaign's leads with custom_fields
POST/campaigns/pausePause or unpause a campaign
POST/campaigns/renameRename a campaign
POST/campaigns/settingsEdit campaign settings
POST/media/uploadUpload an image for DMs and follow-ups
POST/campaigns/add-lead-sourceAdd lead source to campaign
POST/campaigns/add-leadAdd lead by handle
Conversations
GET/conversationsFetch conversations
GET/conversations/statsDM seen & reply rates
POST/dm/sendSend a direct message
CRM
GET/crm/conversationsGet CRM conversations by tag
PATCH/crm/updateUpdate CRM data
People Database
POST/people/searchSearch Twitter/X profiles
GET/people/creditsCheck search credits
GET/people/schemaGet table schema
Enrichment
POST/enrich/twitterFind a person's Twitter profile
POST/enrich/emailFind email from Twitter handle
GET/enrich/profileGet X profile from handle
GET/enrichment/creditsCheck enrichment credits
Scheduled Tweets
POST/scheduled-tweetsSchedule a tweet
GET/scheduled-tweetsList scheduled tweets
DELETE/scheduled-tweetsCancel scheduled tweet
Follower Listeners
GET/follower-listenersList follower listeners
POST/follower-listenersWatch an account for new followers/following
PATCH/follower-listenersUpdate or pause a listener
DELETE/follower-listenersRemove a listener
GET/follower-listeners/eventsQuery detected new followers/following
Webhooks
GET/webhooksList webhook URLs, settings and events
POST/webhooksAdd a webhook URL
PATCH/webhooksSet, rotate or remove a webhook key
DELETE/webhooksRemove a webhook URL
PATCH/webhooks/settingsTurn webhooks on or off
POST/webhooks/testSend a sample event
Feedback
POST/feedbackReport a problem with an endpoint or MCP tool
Blocklist
GET/blocklistList blocked users
POST/blocklistAdd user to blocklist
DELETE/blocklistRemove user from blocklist

Quick Start

1. Generate an API key from your Settings page

2. Make your first API call:

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/accounts

OpenAPI Specification

Download our OpenAPI spec to generate typed clients for any language:

bash
# Generate TypeScript client
npx openapi-typescript-codegen --input https://app.scrapely.co/openapi.yaml --output ./api

# Generate Python client
pip install openapi-python-client
openapi-python-client generate --url https://app.scrapely.co/openapi.yaml

# Import into Postman
# File -> Import -> Link -> https://app.scrapely.co/openapi.yaml

Authentication

All API requests require authentication using an API key. Generate API keys from your Settings page.

X-API-Key Header (Recommended)

bash
curl -H "X-API-Key: sk_live_your_api_key" \
  https://app.scrapely.co/api/v1/accounts

Authorization Bearer Header

bash
curl -H "Authorization: Bearer sk_live_your_api_key" \
  https://app.scrapely.co/api/v1/accounts
Keep your API key secret!

Never expose your API key in client-side code or public repositories.


List Accounts

GET/accountsList your connected Twitter accounts

Returns a list of all active Twitter accounts. Use the account id when sending DMs.

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/accounts

Response

json
{
  "accounts": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "handle": "yourhandle",
      "name": "Your Name",
      "profile_image_url": "https://pbs.twimg.com/...",
      "is_active": true,
      "is_verified": true,
      "dms_sent": 1250,
      "pending_count": 340,
      "created_at": "2024-01-15T10:30:00Z",
      "cookies": [
        { "name": "auth_token", "value": "abc123...", "domain": ".x.com" },
        { "name": "ct0", "value": "xyz789...", "domain": ".x.com" }
      ],
      "proxy": "161.77.187.46:12323",
      "proxy_auth": "username:password",
      "is_paused": false,
      "followups_paused": false,
      "warmup_paused": false,
      "ai_setter_enabled": true
    }
  ]
}

Response Fields

FieldTypeDescription
is_pausedbooleanWhether DM sending is paused for this account
followups_pausedbooleanWhether follow-up messages are paused
warmup_pausedbooleanWhether account warmup is paused
ai_setter_enabledbooleanWhether AI setter is enabled for this account

Add Account

POST/accountsAdd a new Twitter account

Add a new Twitter account to your workspace. Requires cookies exported from your browser and a proxy configuration.

Request Body

ParameterTypeRequiredDescription
handlestringRequiredTwitter handle, with or without @, or an x.com profile URL
cookiesarrayRequiredArray of cookie objects (Chrome export format)
proxystringRequiredProxy URL (e.g., http://proxy.example.com:8080)
proxy_authstringRequiredProxy authentication (username:password)
xchat_pinstringRequiredX chat PIN code

Request

bash
curl -X POST \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "myhandle",
    "cookies": [
      {"name": "ct0", "value": "abc123...", "domain": ".twitter.com"},
      {"name": "auth_token", "value": "xyz789...", "domain": ".twitter.com"}
    ],
    "proxy": "http://proxy.example.com:8080",
    "proxy_auth": "username:password",
    "xchat_pin": "123456"
  }' \
  https://app.scrapely.co/api/v1/accounts

Response

json
{
  "success": true,
  "message": "Account created successfully",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "handle": "myhandle",
    "is_active": true,
    "created_at": "2024-01-15T10:30:00Z",
    "is_new": true
  }
}
Cookie Requirements

Cookies must include at minimum ct0 and auth_token. Use a browser extension like "EditThisCookie" to export cookies from Twitter.


Remove Account

DELETE/accountsRemove a Twitter account

Remove a Twitter account from your workspace. You can identify the account by either its UUID or Twitter handle.

Request Body

ParameterTypeRequiredDescription
account_idstringRequired*The account UUID to remove
handlestringRequired*Twitter handle to remove (with or without @)

*Either account_id or handle is required.

Request (by ID)

bash
curl -X DELETE \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000"
  }' \
  https://app.scrapely.co/api/v1/accounts

Request (by Handle)

bash
curl -X DELETE \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "myhandle"
  }' \
  https://app.scrapely.co/api/v1/accounts

Response

json
{
  "success": true,
  "message": "Account removed successfully",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "handle": "myhandle"
  }
}
Warning: This action is permanent

Removing an account will permanently delete it from your workspace. All associated data including DM history will be retained, but the account will no longer be available for sending messages.


Update Cookies

PUT/accounts/cookiesUpdate cookies for an existing account

Update cookies for an existing account when the session expires. This will also reactivate the account if it was disabled.

Request Body

ParameterTypeRequiredDescription
account_idstringRequiredThe account UUID
cookiesarrayRequiredArray of cookie objects

Request

bash
curl -X PUT \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "cookies": [
      {"name": "ct0", "value": "new_value...", "domain": ".twitter.com"},
      {"name": "auth_token", "value": "new_token...", "domain": ".twitter.com"}
    ]
  }' \
  https://app.scrapely.co/api/v1/accounts/cookies

Response

json
{
  "success": true,
  "message": "Cookies updated successfully",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "handle": "myhandle",
    "is_active": true
  }
}

Update Proxy

PUT/accounts/proxyUpdate proxy settings for an existing account

Update the proxy configuration for an existing Twitter account. Each account uses its own proxy to avoid detection.

Request Body

ParameterTypeRequiredDescription
account_idstringRequiredThe account UUID
proxystringRequiredProxy URL (e.g., "161.77.187.46:12323")
proxy_authstringRequiredProxy authentication (username:password)

Request

bash
curl -X PUT \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "proxy": "161.77.187.46:12323",
    "proxy_auth": "username:password"
  }' \
  https://app.scrapely.co/api/v1/accounts/proxy

Response

json
{
  "success": true,
  "message": "Proxy updated successfully",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "handle": "myhandle",
    "proxy": "161.77.187.46:12323",
    "proxy_auth": "username:password"
  }
}

Toggle Pause Status

PATCH/accounts/pausePause or unpause DM sending for an account

Pause or unpause DM sending for a specific Twitter account. When paused, the account will not send any DMs until unpaused.

Request Body

ParameterTypeRequiredDescription
account_idstringRequiredThe account UUID
is_pausedbooleanRequiredSet to true to pause, false to unpause

Request

bash
curl -X PATCH \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "is_paused": true
  }' \
  https://app.scrapely.co/api/v1/accounts/pause

Response

json
{
  "success": true,
  "message": "Account paused",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "handle": "myhandle",
    "is_paused": true
  }
}

Toggle Follow-ups Pause Status

PATCH/accounts/pause-followupsPause or unpause follow-up messages for an account

Pause or unpause follow-up messages for a specific Twitter account. When paused, the account will not send any follow-up messages until unpaused.

Request Body

ParameterTypeRequiredDescription
account_idstringRequiredThe account UUID
followups_pausedbooleanRequiredSet to true to pause, false to unpause

Request

bash
curl -X PATCH \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "followups_paused": true
  }' \
  https://app.scrapely.co/api/v1/accounts/pause-followups

Response

json
{
  "success": true,
  "message": "Follow-ups paused",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "handle": "myhandle",
    "followups_paused": true
  }
}

Reassign Pending DMs

POST/accounts/reassign-pendingMove pending DMs off some accounts and spread them across others

Move DMs that haven't been sent yet from one or more accounts to others, split evenly. This is the same as "Reassign pending DMs" in the dashboard. Use it when an account is paused, banned or falling behind. To share an account's DMs with others while it keeps its part, list it in both from_account_ids and to_account_ids. GET /accounts shows each account's pending_count.

Send dry_run: true first to see what would move without changing anything. Limited to 10 calls per minute.

Request Body

ParameterTypeRequiredDescription
from_account_idsstring[]RequiredAccount UUIDs to take pending DMs from (up to 100)
to_account_idsstring[]RequiredAccount UUIDs to give them to, split evenly (up to 1,000)
dry_runbooleanOptionalSet to true to only see what would move. Default false

Request

bash
curl -X POST \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_account_ids": ["550e8400-e29b-41d4-a716-446655440000"],
    "to_account_ids": [
      "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
      "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
    ],
    "dry_run": true
  }' \
  https://app.scrapely.co/api/v1/accounts/reassign-pending

Response

json
{
  "success": true,
  "dry_run": true,
  "moved_count": 340,
  "message": "Would move 340 pending DMs (nothing changed)",
  "accounts": [
    { "id": "550e8400-e29b-41d4-a716-446655440000", "handle": "tiredaccount", "change": -340 },
    { "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "handle": "freshaccount1", "change": 170 },
    { "id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8", "handle": "freshaccount2", "change": 170 }
  ]
}

Toggle Warmup

PATCH/accounts/warmupPause or resume warmup for an account

Pause or resume warmup for a specific Twitter account. When warmup is paused, the account will not receive or send warmup DMs until resumed.

Request Body

FieldTypeDescription
account_idstringThe account UUID to update
warmup_pausedbooleanSet to true to pause warmup, false to resume

Example Request

bash
curl -X PATCH \
  -H "X-API-Key: sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "warmup_paused": true
  }' \
  https://app.scrapely.co/api/v1/accounts/warmup

Response

json
{
  "success": true,
  "message": "Warmup paused",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "handle": "myhandle",
    "warmup_paused": true
  }
}

Get Account Tags

GET/accounts/tagsGet tags for one or all accounts

Retrieve the tags assigned to a specific account, or get tags for all active accounts in your workspace.

Query Parameters

ParameterTypeRequiredDescription
account_idstringOptionalAccount UUID. Omit to get tags for all accounts.

Request (single account)

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/accounts/tags?account_id=550e8400-e29b-41d4-a716-446655440000"

Response (single account)

json
{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",
  "handle": "myhandle",
  "tags": ["lead-gen", "warm-account", "high-priority"]
}

Response (all accounts)

json
{
  "accounts": [
    {
      "account_id": "550e8400-e29b-41d4-a716-446655440000",
      "handle": "myhandle",
      "tags": ["lead-gen", "warm-account"]
    },
    {
      "account_id": "660f9511-f30c-52e5-b827-557766551111",
      "handle": "otherhandle",
      "tags": ["outbound"]
    }
  ]
}

Set Account Tags

PUT/accounts/tagsReplace all tags for an account

Replace the entire tag list for an account. This overwrites any existing tags. Tags are automatically normalized to lowercase alphanumeric with hyphens.

Request Body

ParameterTypeRequiredDescription
account_idstringRequiredThe account UUID
tagsstring[]RequiredArray of tag strings. Pass [] to clear all tags.

Request

bash
curl -X PUT \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "tags": ["lead-gen", "warm-account", "high-priority"]
  }' \
  https://app.scrapely.co/api/v1/accounts/tags

Response

json
{
  "success": true,
  "message": "Tags updated",
  "account_id": "550e8400-e29b-41d4-a716-446655440000",
  "handle": "myhandle",
  "tags": ["lead-gen", "warm-account", "high-priority"]
}

Add/Remove Account Tags

PATCH/accounts/tagsAdd or remove individual tags

Add and/or remove individual tags without replacing the entire list. Useful for incrementally updating tags on an account.

Request Body

ParameterTypeRequiredDescription
account_idstringRequiredThe account UUID
addstring[]OptionalTags to add (duplicates are ignored)
removestring[]OptionalTags to remove

Request

bash
curl -X PATCH \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "add": ["vip", "enterprise"],
    "remove": ["warm-account"]
  }' \
  https://app.scrapely.co/api/v1/accounts/tags

Response

json
{
  "success": true,
  "message": "Tags updated",
  "account_id": "550e8400-e29b-41d4-a716-446655440000",
  "handle": "myhandle",
  "tags": ["lead-gen", "high-priority", "vip", "enterprise"]
}

Update Account Handle

PATCH/accounts/handleUpdate an account's handle

Update the X handle stored for one of your accounts, e.g. after you renamed it on X. This only changes the handle in Scrapely; it doesn't rename the account on X. The handle is saved lowercase, without the @.

Request Body

FieldTypeRequiredDescription
account_idstringYesThe account UUID (from List Accounts)
handlestringYesThe new handle, with or without @

Request

bash
curl -X PATCH "https://app.scrapely.co/api/v1/accounts/handle" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"account_id": "550e8400-e29b-41d4-a716-446655440000", "handle": "@new_handle"}'

Response

json
{
  "success": true,
  "message": "Handle updated successfully",
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "old_handle": "old_handle",
    "new_handle": "new_handle"
  }
}

Returns 404 if the account isn't an active account in your workspace, and 409 if another account in your workspace already uses that handle.


Extract Cookies (Deprecated)

POST/get-cookiesQueue a cookie extraction job
Endpoint Deprecated

This endpoint is temporarily unavailable. Cookie extraction functionality is being redesigned. Please check back later or contact support for updates.

Previously: Submit account credentials and receive the session cookies for a Twitter account. Jobs are processed sequentially. Processing typically takes 1-2 minutes.


Check Extraction Status (Deprecated)

GET/get-cookies/statusCheck cookie extraction job status
Endpoint Deprecated

This endpoint is temporarily unavailable. Cookie extraction functionality is being redesigned. Please check back later or contact support for updates.

Previously: Poll this endpoint to check the status of a cookie extraction job and retrieve the results when completed.


Create Scraping Source

POST/scraping-sourcesCreate a new lead source by scraping Twitter

Create a lead source by scraping followers and/or following from Twitter accounts. Credits are consumed based on the number of leads scraped.

Request Body

ParameterTypeRequiredDescription
namestringRequiredName for the lead source
sourcesarrayRequiredArray of scraping targets
filtersobjectOptionalFilter configuration to filter scraped leads

Source Object

ParameterTypeDescription
handlestringTwitter handle to scrape
followersbooleanScrape followers
followingbooleanScrape following

All followers/following will be scraped automatically. Credits are deducted as leads are imported.

Filters Object

Apply filters to scraped leads in real-time during the scraping process. Only leads matching your filter criteria will be saved and count against your credits.

ParameterTypeDescription
includeKeywordsarrayArray of keyword groups. Each group is an array of keywords. Leads must match keywords in their bio/description.
excludeKeywordsarrayArray of keyword groups to exclude. Leads matching these keywords will be filtered out.
includeWithinLogicstring"AND" or "OR". Logic within each include keyword group. Default: "AND"
includeBetweenLogicstring"AND" or "OR". Logic between include keyword groups. Default: "OR"
excludeWithinLogicstring"AND" or "OR". Logic within each exclude keyword group. Default: "AND"
excludeBetweenLogicstring"AND" or "OR". Logic between exclude keyword groups. Default: "OR"
locationExcludeKeywordsarrayArray of location keywords to exclude (e.g., country names).
followersobject{ min: number, max: number }. Only keep leads who have this many followers themselves.
followingobject{ min: number, max: number }. Only keep leads who are following this many accounts.
hasWebsitebooleantrue = only leads with website, false = only leads without website, null = no filter.
isBlueVerifiedbooleantrue = only Blue Verified leads, false = only non-verified leads, null = no filter.
dmsOpenbooleantrue = only leads with DMs open, false = only leads with DMs closed, null = no filter.
lastActiveHoursnumberOnly keep leads active within this many hours. E.g. 24 = active in last 24 hours. null = no filter.
excludeThirdWorldCountriesbooleantrue = automatically exclude ~80 third world countries + Africa from lead locations. Merges with any existing locationExcludeKeywords.

Request

bash
curl -X POST \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Tech Founders Q1",
    "sources": [
      {
        "handle": "paulg",
        "followers": true
      },
      {
        "handle": "naval",
        "followers": true,
        "following": true
      }
    ],
    "filters": {
      "includeKeywords": [["founder", "ceo"], ["startup"]],
      "includeBetweenLogic": "OR",
      "includeWithinLogic": "AND",
      "excludeKeywords": [["hiring", "recruiter"]],
      "followers": { "min": 100, "max": 50000 },
      "hasWebsite": true,
      "dmsOpen": true,
      "lastActiveHours": 24
    }
  }' \
  https://app.scrapely.co/api/v1/scraping-sources

Response

json
{
  "success": true,
  "lead_source_id": "uuid-here",
  "name": "Tech Founders Q1",
  "jobs_created": 3,
  "message": "Scraping all followers/following. Credits deducted as leads are imported.",
  "jobs": [
    {
      "id": "job-uuid-1",
      "type": "followers",
      "handle": "paulg",
      "status": "pending"
    },
    {
      "id": "job-uuid-2",
      "type": "followers",
      "handle": "naval",
      "status": "pending"
    },
    {
      "id": "job-uuid-3",
      "type": "following",
      "handle": "naval",
      "status": "pending"
    }
  ]
}

Scrape Tweet Commenters

POST/scraping-sources/commentersScrape commenters from tweets

Scrape people who commented on or replied to specific tweets. Great for targeting engaged audiences around specific topics or viral posts. Credits are consumed based on the number of leads scraped.

Request Body

ParameterTypeRequiredDescription
namestringRequiredName for the lead source
tweetsarrayRequiredArray of tweet objects to scrape commenters from
filtersobjectOptionalFilter configuration (same as Create Source filters)

Tweet Object

FieldTypeDescription
urlstringFull tweet URL (must contain /status/<tweet_id>)
max_leadsnumberMaximum commenters to scrape from this tweet

Example Request

bash
curl -X POST "https://app.scrapely.co/api/v1/scraping-sources/commenters" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Viral Tweet Commenters",
    "tweets": [
      {
        "url": "https://x.com/elonmusk/status/1234567890",
        "max_leads": 500
      },
      {
        "url": "https://x.com/naval/status/9876543210",
        "max_leads": 300
      }
    ],
    "filters": {
      "includeKeywords": [["founder", "CEO"]],
      "followers": { "min": 1000 }
    }
  }'

Response

json
{
  "success": true,
  "lead_source_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Viral Tweet Commenters",
  "jobs_created": 2,
  "total_max_leads": 800,
  "jobs": [
    {
      "id": "job-uuid-1",
      "type": "commenters",
      "tweet_url": "https://x.com/elonmusk/status/1234567890",
      "max_leads": 500,
      "status": "pending"
    },
    {
      "id": "job-uuid-2",
      "type": "commenters",
      "tweet_url": "https://x.com/naval/status/9876543210",
      "max_leads": 300,
      "status": "pending"
    }
  ]
}
Checking Progress

Use GET /scraping-sources?id=LEAD_SOURCE_ID to check the status of your commenters scraping jobs. The lead source will show as "completed" when all tweet jobs finish.


Check Scraping Status

GET/scraping-sourcesList scraping sources or get status

Check the status of your scraping jobs. Pass an id parameter to get detailed status for a specific source.

Query Parameters

ParameterTypeDescription
idstringGet specific source by ID
limitnumberResults per page (default: 20, max: 100)
offsetnumberPagination offset

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/scraping-sources?id=uuid-here"

Response

json
{
  "scraping_source": {
    "id": "uuid-here",
    "name": "Tech Founders Q1",
    "status": "processing",
    "leads_imported": 4200,
    "total_leads": 9000,
    "profiles_processed": 8500,
    "progress": 47,
    "filters": {
      "includeKeywords": [["CTO", "VP Engineering", "Head of Eng"]],
      "excludeKeywords": [["recruiter", "hiring", "student"]],
      "includeWithinLogic": "OR",
      "includeBetweenLogic": "AND",
      "excludeWithinLogic": "OR",
      "excludeBetweenLogic": "AND",
      "followers": { "min": "250", "max": "50000" },
      "following": { "min": "", "max": "" },
      "hasWebsite": null,
      "isBlueVerified": true,
      "dmsOpen": true,
      "lastActiveHours": 24,
      "locationExcludeKeywords": "India, Nigeria, ..."
    },
    "handles": [
      {
        "handle": "paulg",
        "followers": {
          "id": 1234,
          "status": "completed",
          "leads_imported": 5000,
          "profiles_processed": 45000,
          "max_leads": 5000
        },
        "following": null
      }
    ],
    "created_at": "2024-01-15T10:30:00Z"
  }
}
Filters

The filters field returns the original filter criteria used when the source was created. Returns null if no filters were applied.

Status Values
  • pending - Job queued, not started
  • processing - Currently scraping
  • completed - Successfully finished
  • failed - Error occurred

Get Lead Source Leads

GET/scraping-sources/:lead_source_id/leadsGet a lead source's leads

Get the leads in one of your lead sources (a scrape or import), newest first, with optional filters. Uses cursor pagination: pass the next_cursor from one response as cursor in the next request until has_more is false.

Query Parameters

ParameterTypeRequiredDescription
limitnumberNoLeads per page (default 100, max 1000)
cursorstringNoThe next_cursor from the previous response
min_followersnumberNoOnly leads with at least this many followers
has_websitestringNotrue to only return leads with a website
bio_containsstringNoComma-separated keywords; only leads whose bio contains at least one of them
exclude_keywordsstringNoComma-separated keywords; leave out leads whose bio contains any of them

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/scraping-sources/LEAD_SOURCE_ID/leads?limit=100&min_followers=500&bio_contains=founder,ceo"

Response

json
{
  "leads": [
    {
      "twitter_handle": "janedoe",
      "lead_source_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "name": "Jane Doe",
      "bio": "Founder @acme | Building cool stuff",
      "followers_count": 4500,
      "following_count": 320,
      "location": "San Francisco, CA",
      "website": "https://acme.com",
      "is_blue_verified": false
    }
  ],
  "pagination": {
    "next_cursor": "eyJjIjoiMjAyNi0xMC0xMlQxNTowMDowMFoiLCJpIjoiYWJjIn0",
    "has_more": true,
    "count": 100
  }
}

Returns 404 if the lead source isn't yours, and 400 for an invalid cursor or min_followers.


Import Leads

POST/import-leadsImport leads from a JSON array

Import leads directly via JSON instead of scraping. Creates a lead source with your leads ready to use in campaigns. Maximum 5,000 leads per request — for larger imports, split into multiple requests using the same lead source name within 10 minutes.

Request Body

ParameterTypeRequiredDescription
namestringRequiredName for the lead source
leadsarrayRequiredArray of lead objects (max 5,000)

Lead Object

FieldTypeRequiredDescription
twitter_handlestringRequiredTwitter handle (with or without @)
namestringOptionalDisplay name
descriptionstringOptionalBio or description
follower_countnumberOptionalNumber of followers
following_countnumberOptionalNumber of accounts following
websitestringOptionalWebsite URL
locationstringOptionalLocation

Example Request

bash
curl -X POST "https://app.scrapely.co/api/v1/import-leads" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Imported Leads",
    "leads": [
      {
        "twitter_handle": "elonmusk",
        "name": "Elon Musk",
        "description": "CEO of Tesla & SpaceX",
        "follower_count": 150000000,
        "website": "https://tesla.com"
      },
      {
        "twitter_handle": "naval",
        "name": "Naval Ravikant",
        "description": "Angel investor",
        "follower_count": 2000000
      }
    ]
  }'

Response

json
{
  "success": true,
  "lead_source_id": "550e8400-e29b-41d4-a716-446655440000",
  "lead_source_name": "My Imported Leads",
  "imported_count": 2,
  "skipped_count": 0,
  "invalid_count": 0
}
Chunked Uploads

For imports larger than 5,000 leads, send multiple requests using the same name within 10 minutes. Subsequent requests will append leads to the same lead source.

Duplicates

Handles that appear twice in a request, or that you already have in your leads, are skipped and counted in skipped_count. Rows without a valid handle are counted in invalid_count.


Launch Campaign

POST/campaignsLaunch a DM campaign

Launch a DM campaign targeting leads from your scraping sources. Supports A/B testing with multiple message variants. Maximum 25,000 leads per campaign.

Request Body

ParameterTypeRequiredDescription
namestringRequiredCampaign name
messagestringRequired*Message text (supports {{firstName}})
message_variantsarrayOptionalMessage variants for A/B testing (max 50): array of { message, media_url?, followups? }. media_url attaches an image to the first DM
lead_source_idsarrayRequiredLead source IDs to target
account_idsarrayOptionalAccount IDs to send from (defaults to all)
followupsarrayOptionalFollow-up messages. Each object has wait_time (number), wait_unit ("seconds", "minutes", "hours", "days", or "weeks"), message (string), and optional media_url (string) to attach an image.
max_leadsnumberOptionalMaximum leads to target (max 25,000)
enable_followbooleanOptionalFollow the lead's profile before DM (default: false)
enable_likebooleanOptionalLike the lead's recent post before DM (default: false)
enable_commentbooleanOptionalComment on the lead's post before DM (default: false)
comment_templatestringOptionalAI personalization template for auto-generated comments

*Either message or message_variants is required.

Images: to attach an image, set media_url on a message variant (first DM) or a follow-up. The top-level message field has no image option, so use message_variants: [{ message, media_url }] instead. Any http(s) link works, but we recommend getting one from Upload Media first: the image is downloaded when each DM sends, and if an outside link stops working, that DM goes out without the image. An invalid media_url returns 400.

Request

bash
curl -X POST \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Outreach Campaign Q1",
    "message_variants": [
      {
        "message": "Hey {{firstName}}, I saw your work and wanted to connect!",
        "media_url": "https://your-project.supabase.co/storage/v1/object/public/campaign-media/api/123/1760300000000_ab12cd.png",
        "followups": [
          {"wait_time": 3, "wait_unit": "days", "message": "Just bumping this up!"}
        ]
      },
      {
        "message": "Hi {{firstName}}! Quick question about your business..."
      }
    ],
    "lead_source_ids": ["uuid-1", "uuid-2"],
    "enable_follow": true,
    "enable_like": true,
    "enable_comment": true,
    "comment_template": "Great insights on {{topic}}!"
  }' \
  https://app.scrapely.co/api/v1/campaigns

Response

json
{
  "success": true,
  "campaign_name": "Outreach Campaign Q1",
  "jobs_created": 1500,
  "leads_targeted": 1500,
  "message_variants": 2,
  "accounts_used": 2,
  "distribution": [
    {"account_id": "uuid", "handle": "account1", "jobs": 750},
    {"account_id": "uuid", "handle": "account2", "jobs": 750}
  ]
}

Launch Import Campaign

POST/campaigns/importLaunch a campaign from your own lead list

Launch a campaign from your own list of leads with custom data columns — the same as Import Campaign in the dashboard. Each lead carries its own custom_fields, which fill the {{variables}} in your message, follow-ups and comments.

Request Body

ParameterTypeRequiredDescription
namestringRequiredCampaign name
messagestringRequired*Message template. {{columnName}} is replaced from each lead's custom_fields; {{Hi|Hey}} spintax is supported
message_variantsarrayOptionalMessage variants for A/B testing (max 50): array of { message, media_url?, followups? }. Each lead gets a random variant. A variant without its own followups uses the top-level followups. media_url attaches an image to the first DM
leadsarrayRequiredLeads to message (max 5,000 per request) — see below
followupsarrayOptionalArray of { wait_time, wait_unit, message, media_url? }. Messages can use the same variables. wait_unit: seconds, minutes, hours, days, weeks. media_url attaches an image
account_idsarrayOptionalAccount IDs to send from (defaults to all active accounts)
enable_followbooleanOptionalFollow the lead before DMing (default: false)
enable_likebooleanOptionalLike the lead's recent post before DMing (default: false)
enable_commentbooleanOptionalComment on the lead's post before DMing (default: false)
comment_templatestringOptionalComment for every lead (variables allowed). A lead's own comment overrides it
send_window_startstringOptionalStart of sending hours, HH:MM 24h (e.g. "09:00")
send_window_endstringOptionalEnd of sending hours, HH:MM 24h (e.g. "17:00")
send_timezonestringOptionalIANA timezone (e.g. "America/New_York"). Required with a send window
send_daysarrayOptionalDays to send on: 0=Sun, 1=Mon, ..., 6=Sat

*Either message or message_variants is required.

Images: to attach an image, set media_url on a message variant (first DM) or a follow-up. The top-level message field has no image option, so use message_variants: [{ message, media_url }] instead. Any http(s) link works, but we recommend getting one from Upload Media first: the image is downloaded when each DM sends, and if an outside link stops working, that DM goes out without the image. An invalid media_url returns 400.

Lead Object

ParameterTypeRequiredDescription
twitter_handlestringRequiredTwitter handle, @handle, or x.com profile URL
custom_fieldsobjectOptional{ columnName: value } — used by {{columnName}} in your templates
commentstringOptionalComment for this lead (when enable_comment is true)

Example Request

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/import" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Founders Import",
    "message": "{{Hey|Hi}} {{firstName}}, loved what {{company}} is doing!",
    "leads": [
      { "twitter_handle": "naval", "custom_fields": { "firstName": "Naval", "company": "AngelList" } },
      { "twitter_handle": "https://x.com/paulg", "custom_fields": { "firstName": "Paul", "company": "YC" } }
    ],
    "followups": [
      { "wait_time": 3, "wait_unit": "days", "message": "Any thoughts, {{firstName}}?" }
    ],
    "enable_follow": true,
    "send_window_start": "09:00",
    "send_window_end": "17:00",
    "send_timezone": "America/New_York",
    "send_days": [1, 2, 3, 4, 5]
  }'

Response

json
{
  "success": true,
  "campaign_name": "Founders Import",
  "jobs_created": 2,
  "skipped_count": 0,
  "invalid_count": 0,
  "message_variants": 1,
  "accounts_used": 2
}
Chunked Uploads

Send at most 5,000 leads (and under 4 MB) per request. For larger lists, send multiple requests with the same name within 10 minutes — each one appends to the same campaign.

Duplicates & Retries

Handles that appear twice in a request, or are already in this campaign, are skipped and counted in skipped_count. If a request fails, it is safe to send it again. Rows without a valid handle are counted in invalid_count.


List Campaigns

GET/campaignsList campaigns with basic stats

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/campaigns?limit=10"

Response

json
{
  "campaigns": [
    {
      "name": "Outreach Campaign Q1",
      "total": 1500,
      "completed": 1200,
      "pending": 250,
      "dms_closed": 40,
      "already_contacted": 10,
      "progress": 80
    }
  ],
  "pagination": {
    "limit": 10,
    "offset": 0,
    "count": 1,
    "total": 5
  }
}

Campaign Analytics

GET/campaigns/analyticsGet detailed campaign analytics

Get detailed analytics including reply rates, sentiment analysis, daily (24-hour) reply and positive reply counts, and per-variant A/B testing statistics. Returns the same data structure as the dashboard UI.

Query Parameters

ParameterTypeDescription
campaign_namestringFilter to specific campaign
account_idstringFilter to specific account

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/campaigns/analytics

Response

json
{
  "accounts": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "handle": "myhandle",
      "name": "My Account",
      "profile_image_url": "https://pbs.twimg.com/...",
      "is_paused": false,
      "dms_sent": 1500,
      "dms_sent_24hrs": 45,
      "replies_24hrs": 12,
      "positive_replies_24hrs": 8,
      "responses": 180,
      "response_rate": 12.0,
      "pending_count": 250,
      "sentiment": {
        "positive": 120,
        "negative": 15,
        "neutral": 45
      }
    }
  ],
  "campaigns": [
    {
      "account_id": "550e8400-e29b-41d4-a716-446655440000",
      "account_handle": "myhandle",
      "campaign_name": "Outreach Q1",
      "total": 1500,
      "completed": 1200,
      "pending": 250,
      "remaining": 250,
      "progress": 80,
      "reply_count": 150,
      "response_rate": 12.5
    }
  ],
  "campaignSentimentStats": [
    {
      "campaign_name": "Outreach Q1",
      "account_id": "550e8400-e29b-41d4-a716-446655440000",
      "sentiment": {
        "positive": 100,
        "negative": 12,
        "neutral": 38,
        "total": 150
      },
      "reply_count": 150
    }
  ],
  "campaignVariantStats": [
    {
      "campaign_name": "Outreach Q1",
      "account_id": "550e8400-e29b-41d4-a716-446655440000",
      "variants": [
        {
          "message": "Hey {{firstName}}, loved your recent post...",
          "sent": 600,
          "replied": 90,
          "reply_rate": 15.0
        },
        {
          "message": "Hi {{firstName}}, saw you're building...",
          "sent": 600,
          "replied": 60,
          "reply_rate": 10.0
        }
      ]
    }
  ],
  "totals": {
    "dms_sent": 1500,
    "dms_sent_24hrs": 45,
    "replies_24hrs": 12,
    "positive_replies_24hrs": 8,
    "responses": 180,
    "response_rate": 12.0,
    "positive": 120,
    "negative": 15,
    "neutral": 45
  }
}

Campaign Detail

GET/campaigns/:campaign_nameGet detailed analytics for a single campaign

Get comprehensive analytics for a single campaign — the same data shown on the dashboard campaign detail page. Includes KPI totals, per-account breakdown, individual reply records with profile data, campaign settings, and a performance time series.

URL Parameter

ParameterTypeDescription
campaign_namestringURL-encoded campaign name

Query Parameters

ParameterTypeDescription
periodstringTime range for time_series: "7d", "30d" (default), "3m", "12m", "all"

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/campaigns/Outreach%20Q1"

Response

json
{
  "campaign_name": "Outreach Q1",
  "created_at": "2026-06-01T12:00:00.000Z",
  "totals": {
    "total": 1000,
    "completed": 692,
    "pending": 0,
    "dms_closed": 280,
    "already_contacted": 28,
    "replies": 29,
    "reply_rate": 4.2,
    "positive": 17,
    "negative": 2,
    "neutral": 10
  },
  "accounts": [
    {
      "twitter_account_id": "550e8400-...",
      "handle": "myhandle",
      "profile_image_url": "https://pbs.twimg.com/...",
      "total": 100,
      "completed": 80,
      "pending": 0,
      "replies": 5,
      "reply_rate": 6.3,
      "positive": 3,
      "negative": 1,
      "neutral": 1
    }
  ],
  "replies": [
    {
      "id": "550e8400-...",
      "replier_handle": "prospect_user",
      "replier_name": "Prospect Name",
      "replier_profile_image": "https://pbs.twimg.com/...",
      "reply_message": "Sure, send it over!",
      "reply_sentiment": "positive",
      "replied_at": "2026-06-15T14:30:00.000Z",
      "account_handle": "myhandle"
    }
  ],
  "settings": {
    "message_variants": [
      {
        "message": "Hey {{firstName}}, loved your recent post...",
        "followups": [
          {
            "wait_time": 3,
            "wait_unit": "days",
            "message": "Just following up..."
          }
        ]
      }
    ],
    "followups": [],
    "engagement_actions": {
      "enable_follow": true,
      "enable_like": true,
      "enable_comment": false,
      "comment_template": ""
    }
  },
  "time_series": [
    {
      "period_start": "2026-06-01",
      "period_label": "Jun 1",
      "dms_sent": 45,
      "responses": 3,
      "response_rate": 6.7
    }
  ]
}

Campaign Leads

GET/campaigns/:campaign_name/leadsList a campaign's leads with custom_fields

Every lead in a campaign with its status, sending account, reply info and custom_fields (the columns you imported with an import campaign). Paginated, oldest first. URL-encode the campaign name. Returns 404 if the campaign doesn't exist.

Query Parameters

ParameterTypeRequiredDescription
statusstringNoOnly leads with this status, e.g. pending, completed, dms_closed, needs_verified_sender
repliedbooleanNotrue for leads who replied, false for those who didn't
handlestringNoOne lead by X handle (case-insensitive)
limitnumberNoMax leads to return (default 100, max 1000)
offsetnumberNoPagination offset (default 0)

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/campaigns/Founders%20Import/leads?replied=true&limit=100"

Response

json
{
  "campaign_name": "Founders Import",
  "leads": [
    {
      "job_id": "000e2fb6-4d6c-4449-9605-8bb0b62b198e",
      "twitter_handle": "janedoe",
      "name": "Jane Doe",
      "custom_fields": {
        "first_name": "Jane",
        "company": "Acme",
        "booking_link": "https://cal.com/yourname/intro"
      },
      "status": "completed",
      "account_handle": "your_sender",
      "dm_sent_at": "2026-10-12T15:26:32.000Z",
      "replied": true,
      "reply_sentiment": "positive",
      "followups_sent": 1,
      "created_at": "2026-10-10T09:00:00.000Z"
    }
  ],
  "pagination": { "total": 1, "limit": 100, "offset": 0, "has_more": false }
}

Add Lead Source to Campaign

POST/campaigns/add-lead-sourceAdd a lead source to an existing campaign

Add leads from a lead source to an existing campaign. The new leads get exactly the campaign's settings: message variants (with their images), follow-ups, engagement actions, location filters and send window. Duplicate leads (already in the campaign) are automatically skipped. Lead sources have no custom fields, so adding one to an import campaign that uses {{columns}} returns a warnings list (see Add Lead to Campaign).

Request Body

ParameterTypeRequiredDescription
campaign_namestringRequiredName of the existing campaign
lead_source_idstringRequiredUUID of the lead source to add

Example Request

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/add-lead-source" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "Outreach Campaign Q1",
    "lead_source_id": "550e8400-e29b-41d4-a716-446655440000"
  }'

Response

json
{
  "success": true,
  "campaign_name": "Outreach Campaign Q1",
  "jobs_created": 450,
  "duplicates_skipped": 50
}
How it works

The new leads are distributed round-robin across the same accounts used in the campaign. Message variants and followup sequences are inherited from the existing campaign configuration. Leads whose Twitter handle already exists in the campaign are automatically deduplicated.


Add Lead to Campaign

POST/campaigns/add-leadAdd leads directly by Twitter handle

Add one or more leads directly to an existing campaign by Twitter handle — no lead source required. The leads get exactly the campaign's settings: message variants (with their images), follow-ups, engagement actions, location filters and send window. Duplicates are automatically skipped.

Request Body

ParameterTypeRequiredDescription
campaign_namestringRequiredName of the existing campaign
twitter_handlestringOne requiredSingle Twitter handle to add
twitter_handlesarrayOne requiredArray of Twitter handles to add (no limit; send very large lists in chunks of a few thousand)
leadsarrayOne requiredUse instead of twitter_handles to give each lead its own data: array of { twitter_handle, custom_fields?, comment? }. For import campaigns, custom_fields fill the {{columns}} in its messages (same as Launch Import Campaign), and comment is that lead's comment when the campaign comments per lead.

Single Lead Example

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/add-lead" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "Outreach Campaign Q1",
    "twitter_handle": "naval"
  }'

Batch Example

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/add-lead" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "Outreach Campaign Q1",
    "twitter_handles": ["naval", "elonmusk", "paulg"]
  }'

Import Campaign Example

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/add-lead" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "Founders Import",
    "leads": [
      { "twitter_handle": "janedoe", "custom_fields": { "first_name": "Jane", "company": "Acme" } },
      { "twitter_handle": "johnsmith", "custom_fields": { "first_name": "John", "company": "Globex" } }
    ]
  }'

Response

json
{
  "success": true,
  "campaign_name": "Outreach Campaign Q1",
  "jobs_created": 3,
  "duplicates_skipped": 0
}

Warnings for Import Campaigns

If the campaign is an import campaign and some added leads don't have values for the {{columns}} its messages use, the leads are still added and the response includes a warning, since those messages can't be filled in:

json
{
  "success": true,
  "campaign_name": "Founders Import",
  "jobs_created": 2,
  "duplicates_skipped": 0,
  "warnings": [
    "2 lead(s) were added to an import campaign without values for {{first_name}}, {{company}}. Their messages can't fill those columns. Pass custom_fields for each lead to fill them."
  ],
  "missing_columns": ["first_name", "company"],
  "leads_missing_columns": 2
}
No lead source needed

This endpoint creates DM jobs directly without requiring a lead source. Use it when you want to quickly add specific handles to a campaign. For bulk imports from a list, consider using POST /import-leads + POST /campaigns/add-lead-source instead.


Pause / Unpause Campaign

POST/campaigns/pausePause or unpause a campaign

Pauses or resumes DM sending for a specific campaign. Paused campaigns will not send any new DMs until unpaused.

Request Body

FieldTypeRequiredDescription
campaign_namestringYesCampaign name
is_pausedbooleanYestrue to pause, false to unpause

Example

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/pause" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"campaign_name": "Summer Outreach", "is_paused": true}'

Rename Campaign

POST/campaigns/renameRename a campaign

Renames a campaign. Its DMs, follow-ups, stats and paused state all carry over to the new name. After renaming, use the new name in every other request — the old name will return "not found".

Request Body

FieldTypeRequiredDescription
campaign_namestringYesCurrent campaign name
new_namestringYesNew name, 1-200 characters. Can't match another of your campaigns, including deleted ones (returns 409).

Example

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/rename" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"campaign_name": "Summer Outreach", "new_name": "Summer Outreach v2"}'

Edit Campaign Settings

POST/campaigns/settingsEdit campaign settings

Update settings for an existing campaign. All fields are optional — only the fields you include will be updated. Changes apply to all pending DM jobs in the campaign.

Request Body

FieldTypeRequiredDescription
campaign_namestringYesCampaign name
message_variantsarrayNoUpdated message variants: array of { message, media_url?, followups? }. media_url attaches an image to the first DM; per-variant followups can have one too. Replaces the current variants, so include media_url on every variant that should keep its image.
followupsarrayNoUpdated global follow-up messages. Each object: wait_time (number), wait_unit ("seconds", "minutes", "hours", "days", or "weeks"), message (string), optional media_url (string) to attach an image.
enable_followbooleanNoFollow the lead before DMing
enable_likebooleanNoLike the lead's post before DMing
enable_commentbooleanNoComment on the lead's post before DMing
comment_templatestringNoAI personalization template for comments
send_window_startstringNoStart time in HH:MM 24h format (e.g. "09:00")
send_window_endstringNoEnd time in HH:MM 24h format (e.g. "17:00")
send_timezonestringNoIANA timezone (e.g. "America/Los_Angeles")
send_daysarrayNoDays of week to send (0=Sun, 1=Mon, ... 6=Sat)

Example

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/settings" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "Summer Outreach",
    "send_window_start": "09:00",
    "send_window_end": "17:00",
    "send_timezone": "America/Los_Angeles",
    "send_days": [1, 2, 3, 4, 5],
    "enable_follow": true,
    "enable_like": true
  }'

Upload Media

POST/media/uploadUpload an image for DMs and follow-ups

Uploads an image and returns a media_url. Pass it as media_url on a message variant (the first DM) or a follow-up in Launch Campaign, Launch Import Campaign or Edit Settings. Send either a public image link, which we download and store a copy of, or the image itself as base64. Images can be JPEG, PNG, GIF or WebP, up to 5MB. The type is checked from the file contents, not the name.

Request Body

FieldTypeRequiredDescription
urlstringOne ofPublic http(s) link to an image. We download it (following redirects, 15 second timeout) and store a copy. Links to localhost or private networks are refused.
datastringOne ofBase64-encoded image. A data:image/...;base64, prefix is fine. Requests are capped at 4MB, so keep the image under about 3MB before encoding.

Send exactly one of url or data. Sending both or neither returns 400.

Example

bash
# From a public link
curl -X POST "https://app.scrapely.co/api/v1/media/upload" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/images/offer.png"}'

# From a local file (base64)
curl -X POST "https://app.scrapely.co/api/v1/media/upload" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"data\": \"$(base64 < offer.png | tr -d '\n')\"}"

Response

json
{
  "success": true,
  "media_url": "https://your-project.supabase.co/storage/v1/object/public/campaign-media/api/123/1760300000000_ab12cd.png",
  "mime_type": "image/png",
  "size_bytes": 248133
}

Using the Image

bash
curl -X POST "https://app.scrapely.co/api/v1/campaigns/settings" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "Summer Outreach",
    "message_variants": [
      {
        "message": "Hey {{firstName}}, here is the deck I mentioned",
        "media_url": "MEDIA_URL_FROM_UPLOAD",
        "followups": [
          {"wait_time": 3, "wait_unit": "days", "message": "Did you get a chance to look?", "media_url": "MEDIA_URL_FROM_UPLOAD"}
        ]
      }
    ]
  }'
Why Upload First

Campaign endpoints accept any http(s) link as media_url, but the image is downloaded when each DM sends. If an outside link stops working, that DM goes out without the image. Uploading here stores a copy that stays available.

Errors

Returns 400 with a message when both or neither of url and data are given, the file isn't a JPEG, PNG, GIF or WebP, the image is over 5MB or empty, or the link can't be downloaded (the message includes the reason, such as the HTTP status or a timeout).


Fetch Conversations

GET/conversationsFetch DM conversations

Returns conversations with lead info. Pass a conversation_id to get a single conversation with all messages and complete lead data.

The lead (lead for a single conversation, receiver in lists) includes custom_fields: the columns you imported for that person with an import campaign, plus custom_fields_campaign, the campaign they came from. If they were in several, the fields come from the campaign that messaged them from this conversation's account (the most recent one if there are several), otherwise from their most recent campaign. Leads without imported columns get {} and null.

Every conversation has an origin: the campaign DM the lead replied to. campaign_name is the campaign, variant is the exact message they got (names filled in), and variant_template / variant_index say which of the campaign's variants it was, so you can group replies by variant. variant_template matches the variants in Campaign Analytics, which already gives sent, replies and reply rate per variant. All four are null until the lead replies.

Query Parameters

ParameterTypeRequiredDescription
conversation_idstringOptionalGet single conversation with all messages & lead info
account_idstringOptionalFilter by Twitter account
handlestringOptionalFilter by receiver handle
campaign_namestringOptionalOnly leads who replied to a DM from this campaign
limitnumberOptionalResults per page (max 100)
cursorstringOptionalPagination cursor (recommended)
include_messagesbooleanOptionalInclude full message history (for list view)
Cursor Pagination (Recommended)

Use cursor instead of offset for reliable pagination. The next_cursor in the response can be passed as the cursor parameter.

List Conversations

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/conversations?limit=10"

List Response

json
{
  "conversations": [
    {
      "conversation_id": "123456789-987654321",
      "account_handle": "yourhandle",
      "receiver": {
        "screen_name": "prospect",
        "name": "John Doe",
        "bio": "Founder @ Startup",
        "website": "https://example.com",
        "location": "San Francisco",
        "followers_count": 5000
      },
      "origin": {
        "campaign_name": "October launch",
        "variant": "hey John, quick question about Startup...",
        "variant_template": "hey {{firstName}}, quick question about {{company}}...",
        "variant_index": 1
      },
      "last_message": {
        "text": "Thanks for reaching out!",
        "time": 1709234567000,
        "is_sent": false
      },
      "is_unread": true,
      "seen_at": "2026-07-29T08:07:00.000Z"
    }
  ],
  "pagination": {
    "limit": 10,
    "count": 10,
    "has_more": true
  },
  "next_cursor": "eyJ0IjoxNzA5MjM0NTY3MDAwLCJpIjoiYWJjMTIzIn0"
}

Get Single Conversation

Pass conversation_id to get full details:

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/conversations?conversation_id=123456789-987654321"

Single Conversation Response

json
{
  "conversation": {
    "id": "uuid-here",
    "conversation_id": "123456789-987654321",
    "account_handle": "yourhandle",
    "lead": {
      "user_id": "987654321",
      "screen_name": "prospect",
      "name": "John Doe",
      "profile_image_url": "https://pbs.twimg.com/...",
      "profile_banner_url": "https://pbs.twimg.com/...",
      "bio": "Founder @ Startup | Building cool things",
      "location": "San Francisco, CA",
      "website": "https://example.com",
      "followers_count": 5000,
      "following_count": 1200,
      "verified": false
    },
    "crm": {
      "notes": "Interested in enterprise plan",
      "deal_value": 5000.00,
      "deal_currency": "USD"
    },
    "stats": {
      "message_count": 8,
      "is_unread": false,
      "last_message_time": 1709234567000,
      "last_message_text": "Sounds great, let's schedule a call!",
      "last_message_is_sent": false,
      "seen_at": "2026-07-29T08:07:00.000Z"
    },
    "messages": [
      {
        "text": "Hey! Saw your work on...",
        "time": 1709200000000,
        "sender": "123456789",
        "isSent": true
      },
      {
        "text": "Thanks for reaching out!",
        "time": 1709210000000,
        "sender": "987654321",
        "isSent": false
      }
    ],
    "created_at": "2024-02-28T10:00:00.000Z",
    "updated_at": "2024-02-28T12:30:00.000Z"
  }
}

Conversation Stats

GET/conversations/statsAggregate DM stats: sent, seen, and reply rates

Returns aggregate stats for DMs sent, seen (read receipts), and replies over a given period. Use this to measure your DM open rate and reply rate.

Query Parameters

ParameterTypeRequiredDescription
sincestringOptionalISO 8601 date — only include DMs sent on or after this date
untilstringOptionalISO 8601 date — only include DMs sent on or before this date
account_idstringOptionalFilter to a specific Twitter account
campaign_namestringOptionalFilter to a specific campaign

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/conversations/stats?since=2026-07-01"

Response

json
{
  "period": {
    "since": "2026-07-01T00:00:00.000Z",
    "until": null
  },
  "dms_sent": 1000,
  "seen": 231,
  "seen_rate": 0.231,
  "replied": 59,
  "reply_rate": 0.027
}
About Seen Rate

The seen count includes both recipients who opened your DM (read receipts) and those who replied — since a reply means they saw it. The seen_rate is seen / dms_sent.


Send DM

POST/dm/sendSend a direct message

Send a direct message to an existing conversation.

Request Body

ParameterTypeRequiredDescription
conversation_idstringRequiredThe conversation ID
messagestringRequiredMessage text (max 10,000 chars)
account_idstringRequiredTwitter account ID to send from

Request

bash
curl -X POST \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "123456789-987654321",
    "message": "Hey! Just following up.",
    "account_id": "550e8400-e29b-41d4-a716-446655440000"
  }' \
  https://app.scrapely.co/api/v1/dm/send

Response

json
{
  "success": true,
  "message_id": "1234567890123456789",
  "conversation_id": "123456789-987654321",
  "account_handle": "yourhandle"
}

Get CRM Conversations

GET/crm/conversationsGet conversations organized by CRM tags

Fetch conversations organized by CRM tags like "interested", "negative", "booked", etc. Each receiver includes custom_fields and custom_fields_campaign, the same as Fetch Conversations.

Query Parameters

ParameterTypeDescription
tagstringFilter to specific tag
limitnumberConversations per tag (default: 20, max: 100)
include_messagesbooleanInclude full message history

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/crm/conversations

Response

json
{
  "columns": {
    "interested_reply": {
      "conversations": [
        {
          "id": "conv-key",
          "conversation_id": "123456-789012",
          "account_handle": "myhandle",
          "tag": "interested_reply",
          "receiver": {
            "screen_name": "lead_handle",
            "name": "Interested Lead",
            "bio": "CEO at...",
            "followers_count": 10000
          },
          "crm": {
            "notes": "Interested in demo",
            "deal_value": 5000,
            "deal_currency": "USD"
          },
          "last_message": {
            "text": "Yes, I'd love to learn more!",
            "time": "2024-01-15T14:30:00Z"
          }
        }
      ],
      "count": 1
    },
    "negative_reply": {
      "conversations": [],
      "count": 0
    }
  },
  "custom_tags": [
    {"id": "uuid", "name": "Hot Lead", "color": "#ff0000", "key": "custom_uuid"}
  ],
  "base_tags": [
    "unread", "interested_reply", "negative_reply", "neutral_reply",
    "engaged", "calendlyd", "booked", "not_interested", "not_qualified"
  ]
}

Update CRM Data

PATCH/crm/updateUpdate notes, deal value, or tags

Update CRM data for a conversation including notes, deal value, and tags.

Request Body

ParameterTypeRequiredDescription
conversation_idstringRequiredThe conversation ID
account_handlestringRequiredThe account handle
notesstringOptionalNotes for the conversation
deal_valuenumberOptionalDeal value
deal_currencystringOptionalCurrency code (default: USD)
tagstringOptionalTag to set (null to remove)

Request

bash
curl -X PATCH \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "123456-789012",
    "account_handle": "myhandle",
    "notes": "Interested in enterprise plan. Follow up Monday.",
    "deal_value": 10000,
    "deal_currency": "USD",
    "tag": "interested_reply"
  }' \
  https://app.scrapely.co/api/v1/crm/update

Response

json
{
  "success": true,
  "updated": {
    "conversation_id": "123456-789012",
    "account_handle": "myhandle",
    "notes": "Interested in enterprise plan. Follow up Monday.",
    "deal_value": 10000,
    "deal_currency": "USD",
    "tag": "interested_reply"
  }
}


Check People Credits

GET/people/creditsCheck your search credit balance and reset date

Check how many People search credits you have remaining. Count queries are free. Each row returned from a search costs 1 credit. Credits reset monthly.

Example Request

bash
curl https://app.scrapely.co/api/v1/people/credits \
  -H "X-API-Key: YOUR_API_KEY"

Response

json
{
  "credits_remaining": 9500,
  "credits_total": 10000,
  "resets_at": "2026-10-01T00:00:00Z"
}

Get People Schema

GET/people/schemaGet the table schema with column names and types

Returns the full list of available columns and their data types in the People database.

Example Request

bash
curl https://app.scrapely.co/api/v1/people/schema \
  -H "X-API-Key: YOUR_API_KEY"

Response

json
{
  "columns": [
    { "name": "rest_id", "type": "String" },
    { "name": "twitter_handle", "type": "String" },
    { "name": "name", "type": "String" },
    { "name": "description", "type": "String" },
    { "name": "follower_count", "type": "UInt64" },
    { "name": "following_count", "type": "UInt64" },
    { "name": "location", "type": "String" },
    { "name": "website", "type": "String" },
    { "name": "profile_image_url", "type": "String" },
    { "name": "profile_banner_url", "type": "String" },
    { "name": "updated_at", "type": "DateTime" },
    { "name": "resolved_website", "type": "String" },
    { "name": "resolved_bio_url", "type": "String" },
    { "name": "resolved_location_url", "type": "String" }
  ]
}

Find Twitter Profile

POST/enrich/twitterFind a person's Twitter/X profile from their name and company

Find a person's Twitter/X profile from their first name, last name, and company name. Uses search + AI matching to identify the best profile match. Results are cached for 24 hours.

Typical response time is 5-12 seconds for uncached lookups.

Request Body

FieldTypeRequiredDescription
firstNamestringYesPerson's first name
lastNamestringYesPerson's last name
companyNamestringYesPerson's company name

Example Request

bash
curl -X POST https://app.scrapely.co/api/v1/enrich/twitter \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "John",
    "lastName": "Doe",
    "companyName": "Acme Corp"
  }'

Response (Match Found)

json
{
  "matched": true,
  "confidence": 0.92,
  "cached": false,
  "twitter": {
    "handle": "johndoe",
    "user_id": "123456789",
    "name": "John Doe",
    "bio": "Founder @ Acme | Building cool stuff",
    "profile_url": "https://x.com/johndoe",
    "profile_image_url": "https://pbs.twimg.com/profile_images/.../photo_400x400.jpg",
    "followers_count": 5000,
    "following_count": 800,
    "verified": false,
    "location": "San Francisco, CA"
  }
}

Response (No Match)

json
{
  "matched": false,
  "confidence": 0.0,
  "cached": false,
  "reason": "no_results"
}

Response Fields

FieldTypeDescription
matchedbooleanWhether a matching profile was found
confidencenumberAI confidence score (0.0 to 1.0)
cachedbooleanWhether the result was served from cache
twitterobjectProfile data (only present when matched is true)
reasonstringWhy no match was returned: no_results, low_confidence, or invalid_input

Find Email from Handle

POST/enrich/emailFind a person's email address from their Twitter/X handle

Given a Twitter handle, this endpoint finds and verifies emails.

Typical response time is 5-10 seconds.

Request Body

FieldTypeRequiredDescription
handlestringYesTwitter/X handle (e.g. "elonmusk" or "@elonmusk")

Example Request

bash
curl -X POST https://app.scrapely.co/api/v1/enrich/email \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "alexhormozi"
  }'

Response (Email Found)

json
{
  "handle": "alexhormozi",
  "name": "Alex Hormozi",
  "firstName": "Alex",
  "lastName": "Hormozi",
  "fullName": "Alex Hormozi",
  "bio": "Founder @ Acquisition.com | Author of $100M Offers",
  "website": "https://acquisition.com",
  "followers": 500000,
  "domain": "acquisition.com",
  "companyHandle": "acquisition",
  "companyName": "Acquisition.com",
  "email": "alex.hormozi@acquisition.com",
  "emailValidity": "valid"
}

Response (No Email Found)

If a domain is found but no verified email, the response returns 200 with null email fields. If no company domain can be identified at all, the response returns 422.

json
{
  "handle": "someuser",
  "name": "Some User",
  "firstName": "Some",
  "lastName": "User",
  "fullName": "Some User",
  "bio": "Just vibing",
  "website": null,
  "followers": 120,
  "domain": null,
  "companyHandle": null,
  "companyName": null,
  "email": null,
  "emailValidity": null
}

Response Fields

FieldTypeDescription
handlestringThe Twitter handle that was looked up
namestringDisplay name from the Twitter profile
firstNamestring | nullExtracted first name
lastNamestring | nullExtracted last name
fullNamestring | nullCombined first and last name
biostring | nullTwitter bio / description
websitestring | nullWebsite URL from the Twitter profile
followersnumberFollower count
domainstring | nullCompany domain used for email lookup
companyHandlestring | nullTwitter handle of the company, if found
companyNamestring | nullName of the company
emailstring | nullThe found email address, or null
emailValiditystring | nullVerification result: valid, catchall, or unverified

Get X Profile

GET/enrich/profileGet a public Twitter/X profile from a handle

Look up any public Twitter/X profile by handle: name, bio, links, follower counts, profile picture, verified status and location. Short t.co links are replaced with the real websites.

Free, no enrichment credits used. Counts toward your normal API rate limit. Typical response time is 2-3 seconds.

Query Parameters

ParameterTypeRequiredDescription
handlestringYesTwitter/X handle (e.g. "elonmusk" or "@elonmusk")

Example Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/enrich/profile?handle=alexhormozi"

Response

json
{
  "handle": "AlexHormozi",
  "user_id": "1417686048579018753",
  "name": "Alex Hormozi",
  "bio": "Co-Founder: https://acquisition.com/, https://skool.com/. Business Owners: Get your 2026 free scaling roadmap",
  "bio_urls": ["https://acquisition.com/", "https://skool.com/"],
  "profile_url": "https://x.com/AlexHormozi",
  "profile_image_url": "https://pbs.twimg.com/profile_images/1617408747080667136/C88UDPNZ_400x400.jpg",
  "followers_count": 1082680,
  "following_count": 195,
  "verified": true,
  "location": null
}

Returns 404 if no profile exists for the handle, and 502 if X couldn't be reached (safe to retry).

Response Fields

FieldTypeDescription
handlestringThe handle as shown on X
user_idstring | nullX's numeric user ID
namestringDisplay name
biostringBio, with t.co short links replaced by their real destinations
bio_urlsstring[]Links from the profile (real destinations, not t.co)
profile_urlstringLink to the profile on X
profile_image_urlstring | nullProfile picture (400x400)
followers_countnumberFollower count
following_countnumberFollowing count
verifiedbooleanHas a verified (blue check) badge
locationstring | nullLocation from the profile

Check Enrichment Credits

GET/enrichment/creditsCheck enrichment credits

Check your enrichment credit balance, used by Find Twitter Profile and Find Email from Handle.

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/enrichment/credits

Response

json
{
  "enrichment_credits": 950,
  "enrichment_credits_used": 50
}

Schedule Tweet

POST/scheduled-tweetsSchedule a tweet to be posted at a specific time

Schedule a tweet to be automatically posted from one of your accounts at a specified time. The tweet will be posted by the background worker when the scheduled time arrives.

Request Body

ParameterTypeRequiredDescription
account_idstringRequiredThe account UUID to post from
tweet_textstringRequiredThe tweet content (max 280 characters)
scheduled_atstringRequiredISO 8601 timestamp for when to post (must be in the future)

Request

bash
curl -X POST \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "tweet_text": "Just launched a new feature! Check it out",
    "scheduled_at": "2024-03-20T15:00:00Z"
  }' \
  https://app.scrapely.co/api/v1/scheduled-tweets

Response

json
{
  "success": true,
  "message": "Tweet scheduled successfully",
  "scheduled_tweet": {
    "id": "7f3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "account_handle": "myhandle",
    "tweet_text": "Just launched a new feature! Check it out",
    "scheduled_at": "2024-03-20T15:00:00.000Z",
    "status": "pending",
    "created_at": "2024-03-15T10:30:00.000Z"
  }
}
Tweet Status
  • pending - Waiting to be posted
  • posted - Successfully posted
  • failed - Failed to post (check error_message)

List Scheduled Tweets

GET/scheduled-tweetsList all scheduled tweets

Retrieve a list of scheduled tweets for your accounts. Filter by account, status, or use pagination.

Query Parameters

ParameterTypeDescription
account_idstringFilter by specific account UUID
statusstringFilter by status: pending, posted, failed
limitnumberMax results (default: 50, max: 100)
offsetnumberPagination offset

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/scheduled-tweets?status=pending&limit=10"

Response

json
{
  "scheduled_tweets": [
    {
      "id": "7f3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "account_id": "550e8400-e29b-41d4-a716-446655440000",
      "account_handle": "myhandle",
      "account_name": "My Account",
      "account_image": "https://pbs.twimg.com/...",
      "tweet_text": "Just launched a new feature!",
      "scheduled_at": "2024-03-20T15:00:00.000Z",
      "status": "pending",
      "posted_at": null,
      "error_message": null,
      "created_at": "2024-03-15T10:30:00.000Z"
    }
  ],
  "pagination": {
    "limit": 10,
    "offset": 0,
    "count": 1,
    "total": 5
  }
}

Cancel Scheduled Tweet

DELETE/scheduled-tweetsCancel a pending scheduled tweet

Cancel a scheduled tweet before it gets posted. Only tweets with status "pending" can be cancelled.

Request Body

ParameterTypeRequiredDescription
tweet_idstringRequiredThe scheduled tweet UUID to cancel

Request

bash
curl -X DELETE \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tweet_id": "7f3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
  }' \
  https://app.scrapely.co/api/v1/scheduled-tweets

Response

json
{
  "success": true,
  "message": "Scheduled tweet cancelled",
  "tweet_id": "7f3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
}
Note

You can only cancel tweets with status "pending". Once a tweet has been posted or failed, it cannot be cancelled.


List Follower Listeners

GET/follower-listenersList follower listeners

Follower listeners watch X accounts and send a webhook for every new follower (new_follower) or new account followed (new_following). Events go to the webhook URLs in Settings → Integrations; see Webhooks for the payload. These endpoints manage the same listeners as Settings → Integrations → Follower Listeners. You can have up to 25 listeners.

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/follower-listeners

Response

json
{
  "listeners": [
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "target_handle": "naval",
      "listen_followers": true,
      "listen_following": true,
      "is_active": true,
      "followers_checked_at": "2026-10-12T23:51:30.000Z",
      "following_checked_at": "2026-10-12T23:51:30.000Z",
      "last_error": null,
      "created_at": "2026-10-12T20:04:11.000Z"
    }
  ],
  "max": 25
}

Add Follower Listener

POST/follower-listenersWatch an account for new followers/following

Start watching an X account. The first check saves its current followers/following as a baseline, so you only get events for new ones after that. Returns 409 if you're already listening to that handle, and 400 once you have 25 listeners.

Request Body

FieldTypeRequiredDescription
handlestringYesX handle to watch: name, @name or an x.com profile URL
listen_followersbooleanNoSend new_follower events (default true)
listen_followingbooleanNoSend new_following events (default true). At least one of the two must be true.

Example

bash
curl -X POST "https://app.scrapely.co/api/v1/follower-listeners" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"handle": "naval", "listen_followers": true, "listen_following": false}'

Response (201)

json
{
  "listener": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "target_handle": "naval",
    "listen_followers": true,
    "listen_following": true,
    "is_active": true,
    "followers_checked_at": "2026-10-12T23:51:30.000Z",
    "following_checked_at": "2026-10-12T23:51:30.000Z",
    "last_error": null,
    "created_at": "2026-10-12T20:04:11.000Z"
  }
}

Update Follower Listener

PATCH/follower-listenersUpdate or pause a listener

Change what a listener watches, or pause and resume it. Only the fields you send are changed. Changes take effect within about 30 seconds.

Request Body

FieldTypeRequiredDescription
idstringYesListener id from List Follower Listeners
listen_followersbooleanNoWatch for new followers
listen_followingbooleanNoWatch for new following
is_activebooleanNofalse pauses the listener, true resumes it

Example

bash
curl -X PATCH "https://app.scrapely.co/api/v1/follower-listeners" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "is_active": false}'

Returns the updated listener, or 404 if the id isn't one of yours.


Remove Follower Listener

DELETE/follower-listenersRemove a listener

Stop watching an account and delete its listener. To stop temporarily instead, update it with is_active: false.

Request Body

FieldTypeRequiredDescription
idstringYesListener id from List Follower Listeners

Example

bash
curl -X DELETE "https://app.scrapely.co/api/v1/follower-listeners" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"}'

Response

json
{
  "success": true
}

Query Listener Events

GET/follower-listeners/eventsQuery detected new followers/following

Every new follower and new following your listeners detect is stored, even if your webhook was down or not set up. Use this to catch up on events you missed, or to search them. Results are newest first. Each event has the same shape as the new_follower / new_following webhook, with an id and detected_at. Events are kept after you remove a listener.

Query Parameters

ParameterTypeRequiredDescription
listener_idstringNoOnly events from this listener
target_handlestringNoOnly events for this watched account (case-insensitive)
typestringNofollowers (new_follower) or following (new_following)
sincestringNoDetected on or after this ISO date or datetime, e.g. 2026-10-12
untilstringNoDetected on or before this ISO date or datetime
handlestringNoThe person's X handle (exact, case-insensitive)
searchstringNoKeyword in the person's name, handle or bio
min_followersnumberNoPerson has at least this many followers
max_followersnumberNoPerson has at most this many followers
verifiedbooleanNotrue for verified only, false for unverified only
limitnumberNoMax events to return (default 50, max 500)
offsetnumberNoPagination offset (default 0)

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://app.scrapely.co/api/v1/follower-listeners/events?target_handle=naval&type=followers&since=2026-10-12&min_followers=1000"

Response

json
{
  "events": [
    {
      "id": 1042,
      "event": "new_follower",
      "detected_at": "2026-10-12T15:26:32.000Z",
      "listener_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "target_handle": "naval",
      "target_url": "https://x.com/naval",
      "user": {
        "id": "987654321",
        "screen_name": "prospect",
        "name": "John Doe",
        "description": "Founder @acme | Building cool stuff",
        "location": "San Francisco, CA",
        "website": "https://acme.com",
        "followers_count": 4500,
        "following_count": 320,
        "verified": false,
        "profile_image_url": "https://pbs.twimg.com/profile_images/.../photo_normal.jpg",
        "created_at": "2019-04-01T10:00:00.000Z",
        "profile_url": "https://x.com/prospect"
      }
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0, "has_more": false }
}

List Webhooks

GET/webhooksList webhook URLs, settings and events

Returns the webhook URLs Scrapely sends events to, whether webhooks are turned on, and the list of events. These are the same webhooks as Settings → Integrations. Keys are never shown in full: key_preview shows only the last 4 characters (or just … for short keys). See Webhooks for the payloads.

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/webhooks

Response

json
{
  "enabled": true,
  "webhooks": [
    { "url": "https://example.com/scrapely-hook", "has_key": true, "key_preview": "…x9Qa" },
    { "url": "https://hooks.slack.com/services/T000/B000/XXXX", "has_key": false, "key_preview": null }
  ],
  "events": [
    { "name": "new_reply", "description": "A lead replied to your DM for the first time." },
    { "name": "account_disconnected", "description": "One of your X accounts was disconnected and needs attention." }
  ]
}

Add Webhook

POST/webhooksAdd a webhook URL

Add a URL to receive events. Same rules as Settings: http or https only, no private or internal addresses, and no duplicates (409). You can have up to 10 webhooks. Webhook URLs must start with https:// and use a domain name (like hooks.example.com), not an IP address. Optionally protect it with an X-Webhook-Key header: pass your own key, or generate_key: true to get a strong random one. A generated key is returned once in the response and never shown again, so save it.

Request Body

FieldTypeRequiredDescription
urlstringYesThe http(s) URL to send events to
keystringNoYour own secret, sent as the X-Webhook-Key header (up to 500 characters)
generate_keybooleanNoGenerate a random key for you. Can't be combined with key.

Example

bash
curl -X POST "https://app.scrapely.co/api/v1/webhooks" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/scrapely-hook", "generate_key": true}'

Response (201)

json
{
  "webhook": { "url": "https://example.com/scrapely-hook", "has_key": true, "key_preview": "…x9Qa" },
  "key": "whk_3q2v...x9Qa",
  "note": "Save this key now. It will not be shown again."
}

Change Webhook Key

PATCH/webhooksSet, rotate or remove a webhook key

Set, rotate or remove the X-Webhook-Key on a webhook you already added. Send exactly one of key, generate_key or remove_key. The new key is used for the next event right away, so update your endpoint first.

Request Body

FieldTypeRequiredDescription
urlstringYesThe webhook URL, exactly as shown by List Webhooks
keystringOne ofYour own new key
generate_keybooleanOne ofGenerate a new random key (returned once)
remove_keybooleanOne ofStop sending a key

Example

bash
curl -X PATCH "https://app.scrapely.co/api/v1/webhooks" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/scrapely-hook", "generate_key": true}'

Returns { webhook } (plus key and note when generated), or 404 if the URL isn't one of yours.


Remove Webhook

DELETE/webhooksRemove a webhook URL

Stop sending events to a URL. To pause all webhooks instead, use Webhook Settings.

Example

bash
curl -X DELETE "https://app.scrapely.co/api/v1/webhooks" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/scrapely-hook"}'

Response

json
{
  "success": true,
  "url": "https://example.com/scrapely-hook"
}

Webhook Settings

PATCH/webhooks/settingsTurn webhooks on or off

Same switch as Settings → Integrations. To choose which events send a webhook (for example back-and-forth replies), use Settings → Notifications. Returns the same response as List Webhooks.

Request Body

FieldTypeRequiredDescription
enabledbooleanYesTurn reply, DM and account events on or off. Follower listener events (new_follower, new_following) are not affected; pause the listener instead.

Example

bash
curl -X PATCH "https://app.scrapely.co/api/v1/webhooks/settings" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'

Test Webhook

POST/webhooks/testSend a sample event

Sends a sample new_reply event (with your X-Webhook-Key) to one saved webhook, or to all of them (up to the first 10) if you leave out url. Works even while webhooks are turned off. Limited to 10 tests per minute per account. Redirects are not followed: a 3xx answer counts as a failed delivery.

Example

bash
curl -X POST "https://app.scrapely.co/api/v1/webhooks/test" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Response

json
{
  "results": [
    { "url": "https://example.com/scrapely-hook", "ok": true, "status": 200 },
    { "url": "https://example.com/old-hook", "ok": false, "status": 404, "error": "HTTP 404" }
  ]
}

List Blocklist

GET/blocklistList blocked users with optional search

Returns your blocklist entries with resolved Twitter handles. Supports searching by handle and pagination.

Query Parameters

ParameterTypeRequiredDescription
searchstringOptionalFilter by Twitter handle
limitintegerOptionalResults per page (default 50, max 200)
offsetintegerOptionalPagination offset (default 0)

Request

bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://app.scrapely.co/api/v1/blocklist?search=johndoe&limit=50

Response

json
{
  "entries": [
    {
      "rest_id": "16979745",
      "handle": "johndoe",
      "added_by_account_id": "550e8400-e29b-41d4-a716-446655440000",
      "created_at": "2026-08-15T10:30:00.000Z"
    }
  ],
  "pagination": {
    "total": 2518,
    "limit": 50,
    "offset": 0
  }
}

Add to Blocklist

POST/blocklistAdd a user to the blocklist

Block a user by their Twitter handle or user ID. Blocked users are skipped across all accounts and campaigns. The handle must exist in your leads or past campaigns.

Request Body

ParameterTypeRequiredDescription
handlestringRequiredTwitter handle to block (e.g. "johndoe" or "@johndoe")
Note

The handle is resolved to a user ID from your leads or past campaigns. If the handle is not found, a 404 is returned. Duplicate entries return a 409 error.

Request

bash
curl -X POST \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"handle": "johndoe"}' \
  https://app.scrapely.co/api/v1/blocklist

Response

json
{
  "success": true,
  "rest_id": "16979745"
}

Remove from Blocklist

DELETE/blocklistRemove a user from the blocklist

Unblock a user by their Twitter handle or user ID. The user will be able to receive DMs again in future campaigns.

Request Body

ParameterTypeRequiredDescription
handlestringRequiredTwitter handle to unblock (e.g. "johndoe" or "@johndoe")

Request

bash
curl -X DELETE \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"handle": "johndoe"}' \
  https://app.scrapely.co/api/v1/blocklist

Response

json
{
  "success": true,
  "cleared": 1
}
Note

Returns a 404 if the handle cannot be resolved or is not found in the blocklist.


Report a Problem

POST/feedbackReport a problem with an endpoint or MCP tool

Tell the Scrapely team about an endpoint or MCP tool that failed or behaved differently than documented. AI agents can call this when a tool returns an error that looks like our bug. Never include cookies, proxy credentials, PINs or API keys; API keys are redacted automatically.

Request Body

FieldTypeRequiredDescription
toolstringYesEndpoint or MCP tool name, e.g. launch_campaign (max 100 characters)
what_happenedstringYesWhat you were trying to do and what went wrong (max 2,000 characters)
error_messagestringNoThe error text you got (max 2,000 characters)
suggestionstringNoWhat would have made it work or clearer (max 2,000 characters)
sourcestringNoapi (default) or mcp

Request

bash
curl -X POST "https://app.scrapely.co/api/v1/feedback" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "launch_campaign",
    "what_happened": "Launched a campaign with 3 variants; only the first variant was sent",
    "error_message": "None, returned success"
  }'

Response (201)

json
{
  "success": true,
  "duplicate": false
}

The same tool and error from your workspace within 24 hours is treated as a duplicate: you get 200 with "duplicate": true and it isn't filed again. Limited to 10 reports per minute per API key (429 after that).


Error Handling

The API uses conventional HTTP response codes.

HTTP Status Codes

CodeDescription
200Success
400Bad Request - Invalid parameters
401Unauthorized - Invalid API key
403Forbidden - Subscription not active
429Rate Limited - Too many requests
500Internal Server Error

Error Response Format

json
{
  "error": "Unauthorized",
  "message": "Invalid API key"
}

Rate Limits

API requests are rate limited to ensure fair usage.

120 requests
per minute per API key

Response Headers

Every response includes these headers:

text
X-Request-ID: req_m1abc123def456
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 105
X-RateLimit-Reset: 1709234567
Request IDs

Every response includes an X-Request-ID header. Include this ID when contacting support for faster debugging.

Rate Limit Error

json
{
  "error": "Rate limited",
  "message": "Too many requests. Please try again later.",
  "retry_after": 45
}
Best Practices
  • Implement exponential backoff when retrying
  • Cache responses when possible
  • Use webhooks for real-time updates

Webhooks

Webhooks allow you to receive real-time notifications when events occur in your Scrapely account. Configure your webhook URL in Settings, or with the API (see List Webhooks), to start receiving events.

Authentication

Each webhook can have an optional X-Webhook-Key header. When you add a webhook in Settings, type your own secret or click Generate, or leave it blank to send without one. If set, the header is sent with every webhook request so your endpoint can verify the request came from Scrapely.

json
// Example: incoming webhook request headers
{
  "Content-Type": "application/json",
  "X-Webhook-Key": "your-webhook-secret-value"
}
Security Recommendation

For your own endpoints, set a key and verify the X-Webhook-Key header before processing any webhook payload. Reject requests that don't include a valid key to prevent unauthorized parties from sending fake events to your server.

Slack and Discord

Slack (hooks.slack.com) and Discord (discord.com/api/webhooks/...) webhook URLs receive a readable message instead of the JSON payloads below, so you can post events straight into a channel. Discord messages are capped at 2,000 characters.

Webhook Events

EventDescription
new_replyA prospect replied to your DM for the first time
reply_back_and_forthA prospect sent a follow-up reply in an existing conversation
account_disconnectedA Twitter account was disconnected or paused
dm_seenA lead opened and read your DM
new_followerAn account you added under Settings → Integrations → Follower Listeners gained a new follower (checked every 15 seconds)
new_followingAn account you added under Follower Listeners followed someone new (checked every minute)
leads_exhausted (Deprecated)A campaign has run out of leads to contact (Temporarily unavailable)

Webhook Payload Format

Reply webhook payloads (new_reply and reply_back_and_forth) use a flat structure with all fields at the top level. Lead fields are prefixed with lead_.

new_reply Event

Sent when a prospect replies to your DM for the first time:

json
{
  "event": "new_reply",
  "timestamp": "2024-02-28T12:00:00.000Z",
  "is_back_and_forth": false,

  "conversation_id": "123456789-987654321",
  "unibox_url": "https://app.scrapely.co/unibox?conversation=123456789-987654321",

  "account_twitter_handle": "yourhandle",
  "account_id": "550e8400-e29b-41d4-a716-446655440000",

  "sender_id": "987654321",
  "sender_screen_name": "prospect",
  "message_text": "Hey! I'm interested, let's chat.",
  "message_time": "2024-02-28T12:00:00.000Z",
  "profile_url": "https://x.com/prospect",

  "sentiment": "positive",
  "response_count": 1,

  "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "lead_lead_id": "lead_abc123",
  "lead_twitter_handle": "prospect",
  "lead_name": "John Doe",
  "lead_description": "Founder @acme | Building cool stuff",
  "lead_follower_count": 4500,
  "lead_following_count": 320,
  "lead_location": "San Francisco, CA",
  "lead_website": "https://acme.com",
  "lead_replied": true,
  "lead_replied_at": "2024-02-28T12:00:00.000Z",
  "lead_reply_message": "Hey! I'm interested, let's chat.",
  "lead_reply_sentiment": "positive",
  "lead_created_at": "2024-02-20T08:00:00.000Z",
  "lead_dm_status": "sent",
  "campaign_name": "Founders Import",
  "message_sent": "Hey John, saw you're building Acme...",
  "custom_fields": {
    "first_name": "John",
    "company": "Acme",
    "booking_link": "https://cal.com/yourname/intro"
  },
  "custom_fields_campaign": "Founders Import"
}

reply_back_and_forth Event

Sent when a prospect sends a follow-up reply in an existing conversation. Same structure as new_reply, but sentiment and response_count are null:

json
{
  "event": "reply_back_and_forth",
  "timestamp": "2024-02-28T14:30:00.000Z",
  "is_back_and_forth": true,

  "conversation_id": "123456789-987654321",
  "unibox_url": "https://app.scrapely.co/unibox?conversation=123456789-987654321",

  "account_twitter_handle": "yourhandle",
  "account_id": "550e8400-e29b-41d4-a716-446655440000",

  "sender_id": "987654321",
  "sender_screen_name": "prospect",
  "message_text": "Sure, how about a call Thursday?",
  "message_time": "2024-02-28T14:30:00.000Z",
  "profile_url": "https://x.com/prospect",

  "sentiment": null,
  "response_count": null,

  "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "lead_lead_id": "lead_abc123",
  "lead_twitter_handle": "prospect",
  "lead_name": "John Doe",
  "lead_description": "Founder @acme | Building cool stuff",
  "lead_follower_count": 4500,
  "lead_following_count": 320,
  "lead_location": "San Francisco, CA",
  "lead_website": "https://acme.com",
  "lead_replied": true,
  "lead_replied_at": "2024-02-28T12:00:00.000Z",
  "lead_reply_message": "Hey! I'm interested, let's chat.",
  "lead_reply_sentiment": "positive",
  "lead_created_at": "2024-02-20T08:00:00.000Z",
  "lead_dm_status": "sent",
  "campaign_name": "Founders Import",
  "message_sent": "Hey John, saw you're building Acme...",
  "custom_fields": {
    "first_name": "John",
    "company": "Acme",
    "booking_link": "https://cal.com/yourname/intro"
  },
  "custom_fields_campaign": "Founders Import"
}

custom_fields: when you launch an import campaign, each lead can have extra columns, like first_name,company or a booking_link, that fill the {{variables}} in your messages. Those same columns are sent back here, keyed by your column names, so your automation can use them when the lead replies. custom_fields_campaign is the import campaign they came from. If the lead is in several, the fields come from the campaign that messaged them from the account they replied to (the most recent one if there are several), otherwise from their most recent campaign. Leads that weren't added through an import campaign get {} and null.

new_follower / new_following Events

Sent for each new follower of (new_follower) or new account followed by (new_following) a handle you're listening to. target_handle is the account you're listening to, user is the account that followed it or that it followed. Up to 25 listeners per workspace.

json
{
  "event": "new_follower",
  "timestamp": "2026-10-12T15:26:32.000Z",
  "listener_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "target_handle": "naval",
  "target_url": "https://x.com/naval",
  "user": {
    "id": "987654321",
    "screen_name": "prospect",
    "name": "John Doe",
    "description": "Founder @acme | Building cool stuff",
    "location": "San Francisco, CA",
    "website": "https://acme.com",
    "followers_count": 4500,
    "following_count": 320,
    "verified": false,
    "profile_image_url": "https://pbs.twimg.com/profile_images/.../photo_normal.jpg",
    "created_at": "2019-04-01T10:00:00.000Z",
    "profile_url": "https://x.com/prospect"
  }
}

account_disconnected Event

Sent when a Twitter account is disconnected:

json
{
  "event": "account_disconnected",
  "timestamp": "2024-02-28T12:00:00.000Z",
  "account": {
    "twitter_handle": "yourhandle",
    "account_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "reason": "cookies_expired"
}

dm_seen Event

Sent when a lead opens and reads your DM:

json
{
  "event": "dm_seen",
  "timestamp": "2024-02-28T13:15:00.000Z",

  "conversation_id": "123456789-987654321",
  "unibox_url": "https://app.scrapely.co/unibox?conversation=123456789-987654321",

  "account_twitter_handle": "yourhandle",
  "account_id": "550e8400-e29b-41d4-a716-446655440000",

  "recipient_id": "987654321",
  "recipient_screen_name": "prospect",
  "profile_url": "https://x.com/prospect",

  "seen_at": "2024-02-28T13:15:00.000Z",

  "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "lead_lead_id": "lead_abc123",
  "lead_twitter_handle": "prospect",
  "lead_name": "John Doe"
}

leads_exhausted Event (Deprecated)

Webhook Deprecated

This webhook is temporarily unavailable. The leads_exhausted webhook is being redesigned. Please check back later or contact support for updates.

Previously: Sent when a campaign runs out of leads:

json
{
  "event": "leads_exhausted",
  "timestamp": "2024-02-28T12:00:00.000Z",
  "data": {
    "campaign_id": "camp_abc123",
    "campaign_name": "SaaS Founders Outreach",
    "account_handle": "yourhandle",
    "total_sent": 500
  }
}

Responding to Webhooks

Your endpoint should return a 200 status code within 30 seconds. If we don't receive a successful response, we'll retry up to 3 times with exponential backoff.

Verifying Webhooks

Check the X-Webhook-Key header on every incoming request. Compare it against the secret shown in your Settings page. Reject any request where the header is missing or doesn't match.

javascript
// Example: verifying the webhook in Node.js
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

function handleWebhook(req, res) {
  const key = req.headers["x-webhook-key"];
  if (key !== WEBHOOK_SECRET) {
    return res.status(403).json({ error: "Forbidden" });
  }

  // Process the webhook payload
  const payload = req.body;
  console.log("Received event:", payload.event);
  res.status(200).json({ received: true });
}

MCP Setup

Connect Scrapely to your AI client via MCP (Model Context Protocol). Pick your client below and follow the steps. No install required — everything runs through our hosted server.

You need a Scrapely API key. Generate one from Settings.

Claude (claude.ai)

Add Scrapely as a connector. No install needed.

  1. Open claude.ai/new#settings/customize-connectors
  2. Click + Add custom connector
  3. Set Name to Scrapely and paste this as the Remote MCP server URL:
text
https://mcp.scrapely.co/mcp
  1. Click Connect, log in to Scrapely, and click Allow
  2. Open any conversation and start using Scrapely tools

Claude Code

One command — replace sk_live_... with your real key:

bash
claude mcp add --transport http --scope user scrapely https://mcp.scrapely.co/mcp \
  --header "Authorization: Bearer sk_live_your_key_here"

No Node.js or npm required. The --scope user flag makes it available across all your projects.

To rotate your key, remove and re-add:

bash
claude mcp remove scrapely --scope user
claude mcp add --transport http --scope user scrapely https://mcp.scrapely.co/mcp \
  --header "Authorization: Bearer sk_live_new_key_here"

ChatGPT

Works with ChatGPT Plus/Team with Developer mode enabled.

  1. Go to chatgpt.com → Settings → Apps → Advanced settings
  2. Turn on Developer mode and click Create app
  3. Name it scrapely, leave Authentication set to OAuth, and paste this as the Connection URL:
text
https://mcp.scrapely.co/mcp
  1. Click Create, then Sign in with scrapely and enter your API key

Cursor / VS Code / Codex

Add Scrapely to your MCP config with your API key as a Bearer token:

json
{
  "mcpServers": {
    "scrapely": {
      "url": "https://mcp.scrapely.co/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_your_key_here"
      }
    }
  }
}
Multiple workspaces

To manage multiple Scrapely workspaces, add each one with a different name and API key. For example in Claude Code: scrapely-client-a and scrapely-client-b.

Available Tools

Once connected, Scrapely exposes 48 tools that map to the Scrapely API endpoints documented above:

ToolDescription
list_accountsList all connected Twitter/X accounts
add_accountAdd a new account (cookies + proxy)
remove_accountRemove an account by ID or handle
update_cookiesRefresh cookies for an existing account
toggle_pausePause/unpause DM sending
toggle_followups_pausePause/unpause follow-up messages
update_handleUpdate an account's stored handle after renaming it on X
update_proxyUpdate proxy settings for an account
get_account_tagsGet tags for accounts
set_account_tagsReplace all tags for an account
update_account_tagsAdd or remove individual tags
toggle_warmupEnable/disable warmup mode
create_scraping_sourceScrape followers/following with keyword and count filters
check_scraping_statusCheck progress of scraping jobs
get_scraping_source_leadsGet leads from a scraping source
import_leadsImport your own list of leads (handles or x.com URLs) into a new lead source
launch_campaignLaunch a DM campaign with A/B testing, auto-follow/like/comment
launch_import_campaignLaunch a campaign from your own lead list with custom {{variables}}, send window and days
pause_campaignPause or unpause a campaign
get_campaign_leadsList a campaign's leads with status, reply info and custom_fields
add_leads_to_campaignAdd leads to a campaign by handle (any number; large lists are chunked automatically)
add_lead_source_to_campaignAdd every lead from a lead source to a campaign
upload_mediaUpload an image (link, base64, or local file with npx) to attach to campaign DMs and follow-ups
rename_campaignRename a campaign (DMs, stats and pause state carry over)
edit_campaignEdit a campaign's messages, follow-ups, engagement actions and send window
list_campaignsList campaigns with stats
get_campaign_detailGet detailed campaign info and settings
get_campaign_analyticsDetailed analytics: reply rates, sentiment, variant stats. Supports date range filtering.
fetch_conversationsFetch conversations with optional message history
send_dmSend a DM to an existing conversation
get_crm_dataGet conversations organized by CRM tags
update_crmUpdate notes, deal value, and tags
list_listenersList follower listeners (accounts watched for new followers/following)
add_listenerWatch an X account for new followers and/or following (webhook events)
update_listenerSwitch followers/following on or off, or pause/resume a listener
get_listener_eventsQuery detected new followers/following with filters (watched handle, type, date range, handle, keyword, follower count, verified)
remove_listenerStop watching an account and delete its listener
get_webhook_infoGet webhook event types and payload structure docs
enrich_twitterFind a person's Twitter profile from name + company
enrich_emailFind a person's email from their Twitter handle
get_enrichment_creditsCheck enrichment credit balance
search_peopleSearch the People database of X profiles by bio, location, follower counts and more
get_people_creditsCheck your People search credit balance
get_people_schemaGet the People database columns and types
schedule_tweetSchedule a tweet for later
list_scheduled_tweetsList scheduled tweets with filters
cancel_scheduled_tweetCancel a pending scheduled tweet
submit_feedbackReport a bug or confusing tool to the Scrapely team

Example Prompts

Once connected, you can use natural language:

text
"Show me all my connected accounts"

"Scrape followers of @naval and @paulg, filter for founders with 1k+ followers"

"Launch a campaign called Q1 Outreach targeting my Tech Founders source"

"What are my campaign analytics? Which variant is performing best?"

"Show me all interested replies in the CRM"

"Send a followup to conversation 123456-789012"

"Schedule a tweet from my main account for tomorrow at 9am"

"Find the email for @elonmusk"

"Pause DM sending on my backup account"
How it works

The MCP server translates natural language requests into Scrapely API calls. Every tool maps directly to an API endpoint documented above — same authentication, same payloads, same responses. Your API key is used as a Bearer token and never stored on our MCP server.


CLI & Agent Setup

Run Scrapely from your terminal, or let your coding agent (Claude Code, Codex, Cursor and others) run it for you. Every API action is a command, with readable tables or --json for scripts.

Quick setup: paste this into your agent

Paste this into Claude Code (or Codex, Cursor) and it walks you through install, login, agent setup and a first test. You log in yourself, so your API key never goes into the chat. Or follow the steps below.

text
Set up the Scrapely CLI for me so you can run Scrapely (my X/Twitter DM outreach tool) from here. Go step by step and tell me what you're doing.

1. Check Node.js with `node --version`. It needs 18.3 or newer. If it's missing or too old, stop and tell me how to install it for my OS.

2. Install the CLI: `npm install -g scrapely-mcp`. Then run `scrapely --version` to confirm. (Note: `npx scrapely` is a different package, never use it.) If the install fails with a permissions error (EACCES), explain the fix for my OS instead of using sudo.

3. Run `scrapely whoami --json`. If I'm not logged in, do NOT ask me for my API key and never let me paste it into this chat. Instead tell me to:
   - create an API key at https://app.scrapely.co/settings/integrations
   - type `! scrapely login` here and paste the key when asked (the input stays hidden)
   Then wait until I say I'm done, and run `scrapely whoami --json` again to confirm. If SCRAPELY_API_KEY is set in my environment with a different key, tell me, because it overrides my login.

4. Run `scrapely setup-agent` (without --yes) to preview the instructions it will install, show me which files it will write, and ask me to confirm. Only after I say yes, run `scrapely setup-agent --yes`.

5. To prove it works, run `scrapely campaigns list --json` and give me a short summary of my campaigns. This only reads data.

6. Finish by telling me: setup is done and only needed once, I should start a new session (`/exit`, then `claude`) so the Scrapely skill loads, and after that I can just ask things like "show me my Scrapely campaigns". Remind me that you'll always show me what you're about to do and wait for my OK before sending DMs, launching or pausing campaigns, or spending credits. Also tell me: when Scrapely announces a new CLI version in the changelog, I update with `npm install -g scrapely-mcp`, then run `scrapely setup-agent`.

1. Install

bash
npm install -g scrapely-mcp

This gives you the scrapely command. Requires Node.js 18.3 or newer. Note: npx scrapely is a different package. To try it without installing, use npx -p scrapely-mcp scrapely campaigns list.

2. Log in

bash
scrapely login

Create an API key in Settings → Integrations and paste it when asked (it stays hidden). Run scrapely whoami to check which key is in use.

3. Set up your AI agent

bash
scrapely setup-agent

One command teaches Claude Code, Codex, Cursor and other agents how to use Scrapely. It shows you the files it will write and asks first. Your agent will always show you what it's about to do and wait for your OK before sending DMs, launching campaigns or spending credits. Then just ask it: "show me my Scrapely campaigns".

  • Run it once. It installs into your home folder, so your agent knows Scrapely in every project from then on.
  • Then start a new agent session (in Claude Code: /exit, then claude). Agents only load new instructions when a session starts.
  • Running it from inside Claude Code? Type ! scrapely setup-agent --yes. Commands started with ! can't answer the "Continue?" question, so --yes answers it for you.
  • Run it again after updating the CLI, so your agent learns any new commands.
  • Also connected Scrapely through MCP? Your agent can use either. To make it use the CLI, say "use the scrapely CLI" in your request.

4. Try your first commands

bash
scrapely accounts list                          # your connected X accounts
scrapely campaigns list                         # your campaigns and how they're doing
scrapely enrich profile --handle alexhormozi    # any public X profile (free)
scrapely people search --bio-contains founder --min-followers 1000 --count-only   # free count
scrapely --help                                 # everything you can do

Good to know

  • Asks before changes: commands that send DMs, spend credits, launch, pause or delete ask Continue? [y/N] first. Add --yes to skip the question in scripts.
  • Scripts: add --json to any command for raw JSON.
  • Agencies: save one login per workspace with scrapely login --profile clientname, then pick it per command with --profile clientname.
  • Updating: run npm install -g scrapely-mcp again, then scrapely setup-agent to refresh your agent's instructions.