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
https://app.scrapely.co/api/v1Available Endpoints
/accountsList your Twitter accounts/accountsAdd a new account/accountsRemove an account/accounts/cookiesUpdate account cookies/accounts/proxyUpdate account proxy/accounts/pauseToggle account pause status/accounts/pause-followupsToggle follow-ups pause status/accounts/warmupToggle account warmup status/accounts/reassign-pendingMove pending DMs to other accounts/accounts/tagsGet account tags/accounts/tagsSet account tags/accounts/tagsAdd or remove tags/accounts/handleUpdate an account's handle/get-cookiesQueue cookie extraction job (Deprecated)/get-cookies/statusCheck extraction status (Deprecated)/scraping-sourcesCreate a scraping source/scraping-sources/commentersScrape tweet commenters/scraping-sourcesList/check scraping status/scraping-sources/:lead_source_id/leadsGet a lead source's leads/import-leadsImport leads from JSON/campaignsLaunch a DM campaign/campaigns/importLaunch an import campaign/campaignsList campaigns/campaigns/analyticsGet campaign analytics/campaigns/:campaign_name/leadsList a campaign's leads with custom_fields/campaigns/pausePause or unpause a campaign/campaigns/renameRename a campaign/campaigns/settingsEdit campaign settings/media/uploadUpload an image for DMs and follow-ups/campaigns/add-lead-sourceAdd lead source to campaign/campaigns/add-leadAdd lead by handle/conversationsFetch conversations/conversations/statsDM seen & reply rates/dm/sendSend a direct message/crm/conversationsGet CRM conversations by tag/crm/updateUpdate CRM data/people/searchSearch Twitter/X profiles/people/creditsCheck search credits/people/schemaGet table schema/enrich/twitterFind a person's Twitter profile/enrich/emailFind email from Twitter handle/enrich/profileGet X profile from handle/enrichment/creditsCheck enrichment credits/scheduled-tweetsSchedule a tweet/scheduled-tweetsList scheduled tweets/scheduled-tweetsCancel scheduled tweet/follower-listenersList follower listeners/follower-listenersWatch an account for new followers/following/follower-listenersUpdate or pause a listener/follower-listenersRemove a listener/follower-listeners/eventsQuery detected new followers/following/webhooksList webhook URLs, settings and events/webhooksAdd a webhook URL/webhooksSet, rotate or remove a webhook key/webhooksRemove a webhook URL/webhooks/settingsTurn webhooks on or off/webhooks/testSend a sample event/feedbackReport a problem with an endpoint or MCP tool/blocklistList blocked users/blocklistAdd user to blocklist/blocklistRemove user from blocklistQuick Start
1. Generate an API key from your Settings page
2. Make your first API call:
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/accountsOpenAPI Specification
Download our OpenAPI spec to generate typed clients for any language:
# 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.yamlAuthentication
All API requests require authentication using an API key. Generate API keys from your Settings page.
X-API-Key Header (Recommended)
curl -H "X-API-Key: sk_live_your_api_key" \
https://app.scrapely.co/api/v1/accountsAuthorization Bearer Header
curl -H "Authorization: Bearer sk_live_your_api_key" \
https://app.scrapely.co/api/v1/accountsNever expose your API key in client-side code or public repositories.
List Accounts
/accountsList your connected Twitter accountsReturns a list of all active Twitter accounts. Use the account id when sending DMs.
Request
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/accountsResponse
{
"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
| Field | Type | Description |
|---|---|---|
| is_paused | boolean | Whether DM sending is paused for this account |
| followups_paused | boolean | Whether follow-up messages are paused |
| warmup_paused | boolean | Whether account warmup is paused |
| ai_setter_enabled | boolean | Whether AI setter is enabled for this account |
Add Account
/accountsAdd a new Twitter accountAdd a new Twitter account to your workspace. Requires cookies exported from your browser and a proxy configuration.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| handle | string | Required | Twitter handle, with or without @, or an x.com profile URL |
| cookies | array | Required | Array of cookie objects (Chrome export format) |
| proxy | string | Required | Proxy URL (e.g., http://proxy.example.com:8080) |
| proxy_auth | string | Required | Proxy authentication (username:password) |
| xchat_pin | string | Required | X chat PIN code |
Request
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/accountsResponse
{
"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
}
}Cookies must include at minimum ct0 and auth_token. Use a browser extension like "EditThisCookie" to export cookies from Twitter.
Remove Account
/accountsRemove a Twitter accountRemove a Twitter account from your workspace. You can identify the account by either its UUID or Twitter handle.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| account_id | string | Required* | The account UUID to remove |
| handle | string | Required* | Twitter handle to remove (with or without @) |
*Either account_id or handle is required.
Request (by ID)
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/accountsRequest (by Handle)
curl -X DELETE \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"handle": "myhandle"
}' \
https://app.scrapely.co/api/v1/accountsResponse
{
"success": true,
"message": "Account removed successfully",
"account": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"handle": "myhandle"
}
}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 Proxy
/accounts/proxyUpdate proxy settings for an existing accountUpdate the proxy configuration for an existing Twitter account. Each account uses its own proxy to avoid detection.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| account_id | string | Required | The account UUID |
| proxy | string | Required | Proxy URL (e.g., "161.77.187.46:12323") |
| proxy_auth | string | Required | Proxy authentication (username:password) |
Request
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/proxyResponse
{
"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
/accounts/pausePause or unpause DM sending for an accountPause or unpause DM sending for a specific Twitter account. When paused, the account will not send any DMs until unpaused.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| account_id | string | Required | The account UUID |
| is_paused | boolean | Required | Set to true to pause, false to unpause |
Request
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/pauseResponse
{
"success": true,
"message": "Account paused",
"account": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"handle": "myhandle",
"is_paused": true
}
}Toggle Follow-ups Pause Status
/accounts/pause-followupsPause or unpause follow-up messages for an accountPause 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| account_id | string | Required | The account UUID |
| followups_paused | boolean | Required | Set to true to pause, false to unpause |
Request
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-followupsResponse
{
"success": true,
"message": "Follow-ups paused",
"account": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"handle": "myhandle",
"followups_paused": true
}
}Reassign Pending DMs
/accounts/reassign-pendingMove pending DMs off some accounts and spread them across othersMove 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| from_account_ids | string[] | Required | Account UUIDs to take pending DMs from (up to 100) |
| to_account_ids | string[] | Required | Account UUIDs to give them to, split evenly (up to 1,000) |
| dry_run | boolean | Optional | Set to true to only see what would move. Default false |
Request
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-pendingResponse
{
"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
/accounts/warmupPause or resume warmup for an accountPause 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
| Field | Type | Description |
|---|---|---|
| account_id | string | The account UUID to update |
| warmup_paused | boolean | Set to true to pause warmup, false to resume |
Example Request
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/warmupResponse
{
"success": true,
"message": "Warmup paused",
"account": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"handle": "myhandle",
"warmup_paused": true
}
}Update Account Handle
/accounts/handleUpdate an account's handleUpdate 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
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | Yes | The account UUID (from List Accounts) |
handle | string | Yes | The new handle, with or without @ |
Request
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
{
"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.
Create Scraping Source
/scraping-sourcesCreate a new lead source by scraping TwitterCreate a lead source by scraping followers and/or following from Twitter accounts. Credits are consumed based on the number of leads scraped.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Name for the lead source |
| sources | array | Required | Array of scraping targets |
| filters | object | Optional | Filter configuration to filter scraped leads |
Source Object
| Parameter | Type | Description |
|---|---|---|
| handle | string | Twitter handle to scrape |
| followers | boolean | Scrape followers |
| following | boolean | Scrape 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.
| Parameter | Type | Description |
|---|---|---|
| includeKeywords | array | Array of keyword groups. Each group is an array of keywords. Leads must match keywords in their bio/description. |
| excludeKeywords | array | Array of keyword groups to exclude. Leads matching these keywords will be filtered out. |
| includeWithinLogic | string | "AND" or "OR". Logic within each include keyword group. Default: "AND" |
| includeBetweenLogic | string | "AND" or "OR". Logic between include keyword groups. Default: "OR" |
| excludeWithinLogic | string | "AND" or "OR". Logic within each exclude keyword group. Default: "AND" |
| excludeBetweenLogic | string | "AND" or "OR". Logic between exclude keyword groups. Default: "OR" |
| locationExcludeKeywords | array | Array of location keywords to exclude (e.g., country names). |
| followers | object | { min: number, max: number }. Only keep leads who have this many followers themselves. |
| following | object | { min: number, max: number }. Only keep leads who are following this many accounts. |
| hasWebsite | boolean | true = only leads with website, false = only leads without website, null = no filter. |
| isBlueVerified | boolean | true = only Blue Verified leads, false = only non-verified leads, null = no filter. |
| dmsOpen | boolean | true = only leads with DMs open, false = only leads with DMs closed, null = no filter. |
| lastActiveHours | number | Only keep leads active within this many hours. E.g. 24 = active in last 24 hours. null = no filter. |
| excludeThirdWorldCountries | boolean | true = automatically exclude ~80 third world countries + Africa from lead locations. Merges with any existing locationExcludeKeywords. |
Request
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-sourcesResponse
{
"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
/scraping-sources/commentersScrape commenters from tweetsScrape 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Name for the lead source |
| tweets | array | Required | Array of tweet objects to scrape commenters from |
| filters | object | Optional | Filter configuration (same as Create Source filters) |
Tweet Object
| Field | Type | Description |
|---|---|---|
| url | string | Full tweet URL (must contain /status/<tweet_id>) |
| max_leads | number | Maximum commenters to scrape from this tweet |
Example Request
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
{
"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"
}
]
}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
/scraping-sourcesList scraping sources or get statusCheck the status of your scraping jobs. Pass an id parameter to get detailed status for a specific source.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| id | string | Get specific source by ID |
| limit | number | Results per page (default: 20, max: 100) |
| offset | number | Pagination offset |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/scraping-sources?id=uuid-here"Response
{
"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"
}
}The filters field returns the original filter criteria used when the source was created. Returns null if no filters were applied.
pending- Job queued, not startedprocessing- Currently scrapingcompleted- Successfully finishedfailed- Error occurred
Get Lead Source Leads
/scraping-sources/:lead_source_id/leadsGet a lead source's leadsGet 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
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Leads per page (default 100, max 1000) |
cursor | string | No | The next_cursor from the previous response |
min_followers | number | No | Only leads with at least this many followers |
has_website | string | No | true to only return leads with a website |
bio_contains | string | No | Comma-separated keywords; only leads whose bio contains at least one of them |
exclude_keywords | string | No | Comma-separated keywords; leave out leads whose bio contains any of them |
Request
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
{
"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
/import-leadsImport leads from a JSON arrayImport 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Name for the lead source |
| leads | array | Required | Array of lead objects (max 5,000) |
Lead Object
| Field | Type | Required | Description |
|---|---|---|---|
| twitter_handle | string | Required | Twitter handle (with or without @) |
| name | string | Optional | Display name |
| description | string | Optional | Bio or description |
| follower_count | number | Optional | Number of followers |
| following_count | number | Optional | Number of accounts following |
| website | string | Optional | Website URL |
| location | string | Optional | Location |
Example Request
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
{
"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
}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.
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
/campaignsLaunch a DM campaignLaunch 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Campaign name |
| message | string | Required* | Message text (supports {{firstName}}) |
| message_variants | array | Optional | Message variants for A/B testing (max 50): array of { message, media_url?, followups? }. media_url attaches an image to the first DM |
| lead_source_ids | array | Required | Lead source IDs to target |
| account_ids | array | Optional | Account IDs to send from (defaults to all) |
| followups | array | Optional | Follow-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_leads | number | Optional | Maximum leads to target (max 25,000) |
| enable_follow | boolean | Optional | Follow the lead's profile before DM (default: false) |
| enable_like | boolean | Optional | Like the lead's recent post before DM (default: false) |
| enable_comment | boolean | Optional | Comment on the lead's post before DM (default: false) |
| comment_template | string | Optional | AI 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
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/campaignsResponse
{
"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
/campaigns/importLaunch a campaign from your own lead listLaunch 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Campaign name |
| message | string | Required* | Message template. {{columnName}} is replaced from each lead's custom_fields; {{Hi|Hey}} spintax is supported |
| message_variants | array | Optional | Message 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 |
| leads | array | Required | Leads to message (max 5,000 per request) — see below |
| followups | array | Optional | Array 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_ids | array | Optional | Account IDs to send from (defaults to all active accounts) |
| enable_follow | boolean | Optional | Follow the lead before DMing (default: false) |
| enable_like | boolean | Optional | Like the lead's recent post before DMing (default: false) |
| enable_comment | boolean | Optional | Comment on the lead's post before DMing (default: false) |
| comment_template | string | Optional | Comment for every lead (variables allowed). A lead's own comment overrides it |
| send_window_start | string | Optional | Start of sending hours, HH:MM 24h (e.g. "09:00") |
| send_window_end | string | Optional | End of sending hours, HH:MM 24h (e.g. "17:00") |
| send_timezone | string | Optional | IANA timezone (e.g. "America/New_York"). Required with a send window |
| send_days | array | Optional | Days 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| twitter_handle | string | Required | Twitter handle, @handle, or x.com profile URL |
| custom_fields | object | Optional | { columnName: value } — used by {{columnName}} in your templates |
| comment | string | Optional | Comment for this lead (when enable_comment is true) |
Example Request
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
{
"success": true,
"campaign_name": "Founders Import",
"jobs_created": 2,
"skipped_count": 0,
"invalid_count": 0,
"message_variants": 1,
"accounts_used": 2
}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.
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
/campaignsList campaigns with basic statsRequest
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/campaigns?limit=10"Response
{
"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
/campaigns/analyticsGet detailed campaign analyticsGet 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
| Parameter | Type | Description |
|---|---|---|
| campaign_name | string | Filter to specific campaign |
| account_id | string | Filter to specific account |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/campaigns/analyticsResponse
{
"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
/campaigns/:campaign_nameGet detailed analytics for a single campaignGet 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
| Parameter | Type | Description |
|---|---|---|
| campaign_name | string | URL-encoded campaign name |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| period | string | Time range for time_series: "7d", "30d" (default), "3m", "12m", "all" |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/campaigns/Outreach%20Q1"Response
{
"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
/campaigns/:campaign_name/leadsList a campaign's leads with custom_fieldsEvery 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
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Only leads with this status, e.g. pending, completed, dms_closed, needs_verified_sender |
replied | boolean | No | true for leads who replied, false for those who didn't |
handle | string | No | One lead by X handle (case-insensitive) |
limit | number | No | Max leads to return (default 100, max 1000) |
offset | number | No | Pagination offset (default 0) |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/campaigns/Founders%20Import/leads?replied=true&limit=100"Response
{
"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
/campaigns/add-lead-sourceAdd a lead source to an existing campaignAdd 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| campaign_name | string | Required | Name of the existing campaign |
| lead_source_id | string | Required | UUID of the lead source to add |
Example Request
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
{
"success": true,
"campaign_name": "Outreach Campaign Q1",
"jobs_created": 450,
"duplicates_skipped": 50
}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
/campaigns/add-leadAdd leads directly by Twitter handleAdd 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| campaign_name | string | Required | Name of the existing campaign |
| twitter_handle | string | One required | Single Twitter handle to add |
| twitter_handles | array | One required | Array of Twitter handles to add (no limit; send very large lists in chunks of a few thousand) |
| leads | array | One required | Use 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
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
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
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
{
"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:
{
"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
}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
/campaigns/pausePause or unpause a campaignPauses or resumes DM sending for a specific campaign. Paused campaigns will not send any new DMs until unpaused.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
campaign_name | string | Yes | Campaign name |
is_paused | boolean | Yes | true to pause, false to unpause |
Example
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
/campaigns/renameRename a campaignRenames 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
| Field | Type | Required | Description |
|---|---|---|---|
campaign_name | string | Yes | Current campaign name |
new_name | string | Yes | New name, 1-200 characters. Can't match another of your campaigns, including deleted ones (returns 409). |
Example
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
/campaigns/settingsEdit campaign settingsUpdate 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
| Field | Type | Required | Description |
|---|---|---|---|
campaign_name | string | Yes | Campaign name |
message_variants | array | No | Updated 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. |
followups | array | No | Updated 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_follow | boolean | No | Follow the lead before DMing |
enable_like | boolean | No | Like the lead's post before DMing |
enable_comment | boolean | No | Comment on the lead's post before DMing |
comment_template | string | No | AI personalization template for comments |
send_window_start | string | No | Start time in HH:MM 24h format (e.g. "09:00") |
send_window_end | string | No | End time in HH:MM 24h format (e.g. "17:00") |
send_timezone | string | No | IANA timezone (e.g. "America/Los_Angeles") |
send_days | array | No | Days of week to send (0=Sun, 1=Mon, ... 6=Sat) |
Example
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
/media/uploadUpload an image for DMs and follow-upsUploads 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
| Field | Type | Required | Description |
|---|---|---|---|
url | string | One of | Public 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. |
data | string | One of | Base64-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
# 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
{
"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
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"}
]
}
]
}'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
/conversationsFetch DM conversationsReturns 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversation_id | string | Optional | Get single conversation with all messages & lead info |
| account_id | string | Optional | Filter by Twitter account |
| handle | string | Optional | Filter by receiver handle |
| campaign_name | string | Optional | Only leads who replied to a DM from this campaign |
| limit | number | Optional | Results per page (max 100) |
| cursor | string | Optional | Pagination cursor (recommended) |
| include_messages | boolean | Optional | Include full message history (for list view) |
Use cursor instead of offset for reliable pagination. The next_cursor in the response can be passed as the cursor parameter.
List Conversations
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/conversations?limit=10"List Response
{
"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:
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/conversations?conversation_id=123456789-987654321"Single Conversation Response
{
"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
/conversations/statsAggregate DM stats: sent, seen, and reply ratesReturns 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| since | string | Optional | ISO 8601 date — only include DMs sent on or after this date |
| until | string | Optional | ISO 8601 date — only include DMs sent on or before this date |
| account_id | string | Optional | Filter to a specific Twitter account |
| campaign_name | string | Optional | Filter to a specific campaign |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/conversations/stats?since=2026-07-01"Response
{
"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
}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
/dm/sendSend a direct messageSend a direct message to an existing conversation.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversation_id | string | Required | The conversation ID |
| message | string | Required | Message text (max 10,000 chars) |
| account_id | string | Required | Twitter account ID to send from |
Request
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/sendResponse
{
"success": true,
"message_id": "1234567890123456789",
"conversation_id": "123456789-987654321",
"account_handle": "yourhandle"
}Get CRM Conversations
/crm/conversationsGet conversations organized by CRM tagsFetch 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
| Parameter | Type | Description |
|---|---|---|
| tag | string | Filter to specific tag |
| limit | number | Conversations per tag (default: 20, max: 100) |
| include_messages | boolean | Include full message history |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/crm/conversationsResponse
{
"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
/crm/updateUpdate notes, deal value, or tagsUpdate CRM data for a conversation including notes, deal value, and tags.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| conversation_id | string | Required | The conversation ID |
| account_handle | string | Required | The account handle |
| notes | string | Optional | Notes for the conversation |
| deal_value | number | Optional | Deal value |
| deal_currency | string | Optional | Currency code (default: USD) |
| tag | string | Optional | Tag to set (null to remove) |
Request
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/updateResponse
{
"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"
}
}Search People
/people/searchSearch millions of Twitter/X profilesSearch the Scrapely People database — millions of Twitter/X profiles indexed and searchable by bio keywords, location, follower counts, website presence, and more.
Count-only queries are free. Row queries cost 1 credit per returned row.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
bio_contains | string[] | No | Bio must contain ANY of these keywords (case-insensitive) |
bio_contains_all | string[] | No | Bio must contain ALL of these keywords |
bio_excludes | string[] | No | Exclude profiles whose bio contains any of these |
name_contains | string[] | No | Name must contain ANY of these keywords |
location_contains | string[] | No | Location must contain ANY of these keywords |
location_excludes | string[] | No | Exclude profiles with location containing any of these |
handle_contains | string | No | Handle must contain this substring |
min_followers | number | No | Minimum follower count |
max_followers | number | No | Maximum follower count |
min_following | number | No | Minimum following count |
max_following | number | No | Maximum following count |
has_website | boolean | No | Filter by whether the profile has a website |
min_updated_at | string | No | Only profiles updated on or after this date (ISO 8601) |
max_updated_at | string | No | Only profiles updated on or before this date (ISO 8601) |
fields | string[] | No | Specific columns to return (defaults to all) |
count_only | boolean | No | Return only the count (free, no credits used) |
sort_by | string | No | follower_count, following_count, updated_at, twitter_handle, name |
sort_order | string | No | desc or asc |
limit | number | No | Max rows to return (max 1,000) |
offset | number | No | Pagination offset |
Example Request
curl -X POST https://app.scrapely.co/api/v1/people/search \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"bio_contains": ["founder", "ceo"],
"min_followers": 1000,
"has_website": true,
"sort_by": "follower_count",
"sort_order": "desc",
"limit": 50
}'Response
{
"rows": [
{
"rest_id": "123456789",
"twitter_handle": "johndoe",
"name": "John Doe",
"description": "Founder & CEO @ Acme Corp",
"website": "https://acme.com",
"location": "San Francisco, CA",
"follower_count": 25000,
"following_count": 1200,
"profile_image_url": "https://pbs.twimg.com/...",
"updated_at": "2026-08-30T12:00:00Z"
}
],
"total": 84320,
"credits_used": 50,
"credits_remaining": 9950
}Count-Only Example (Free)
curl -X POST https://app.scrapely.co/api/v1/people/search \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"bio_contains": ["saas", "b2b"],
"location_contains": ["new york"],
"count_only": true
}'Available Response Fields
rest_id, twitter_handle, name, description, website, location, follower_count, following_count, profile_image_url, profile_banner_url, updated_at, resolved_website, resolved_bio_url, resolved_location_url
Check People Credits
/people/creditsCheck your search credit balance and reset dateCheck 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
curl https://app.scrapely.co/api/v1/people/credits \
-H "X-API-Key: YOUR_API_KEY"Response
{
"credits_remaining": 9500,
"credits_total": 10000,
"resets_at": "2026-10-01T00:00:00Z"
}Get People Schema
/people/schemaGet the table schema with column names and typesReturns the full list of available columns and their data types in the People database.
Example Request
curl https://app.scrapely.co/api/v1/people/schema \
-H "X-API-Key: YOUR_API_KEY"Response
{
"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
/enrich/twitterFind a person's Twitter/X profile from their name and companyFind 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
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | Yes | Person's first name |
lastName | string | Yes | Person's last name |
companyName | string | Yes | Person's company name |
Example Request
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)
{
"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)
{
"matched": false,
"confidence": 0.0,
"cached": false,
"reason": "no_results"
}Response Fields
| Field | Type | Description |
|---|---|---|
matched | boolean | Whether a matching profile was found |
confidence | number | AI confidence score (0.0 to 1.0) |
cached | boolean | Whether the result was served from cache |
twitter | object | Profile data (only present when matched is true) |
reason | string | Why no match was returned: no_results, low_confidence, or invalid_input |
Find Email from Handle
/enrich/emailFind a person's email address from their Twitter/X handleGiven a Twitter handle, this endpoint finds and verifies emails.
Typical response time is 5-10 seconds.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
handle | string | Yes | Twitter/X handle (e.g. "elonmusk" or "@elonmusk") |
Example Request
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)
{
"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.
{
"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
| Field | Type | Description |
|---|---|---|
handle | string | The Twitter handle that was looked up |
name | string | Display name from the Twitter profile |
firstName | string | null | Extracted first name |
lastName | string | null | Extracted last name |
fullName | string | null | Combined first and last name |
bio | string | null | Twitter bio / description |
website | string | null | Website URL from the Twitter profile |
followers | number | Follower count |
domain | string | null | Company domain used for email lookup |
companyHandle | string | null | Twitter handle of the company, if found |
companyName | string | null | Name of the company |
email | string | null | The found email address, or null |
emailValidity | string | null | Verification result: valid, catchall, or unverified |
Get X Profile
/enrich/profileGet a public Twitter/X profile from a handleLook 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
| Parameter | Type | Required | Description |
|---|---|---|---|
handle | string | Yes | Twitter/X handle (e.g. "elonmusk" or "@elonmusk") |
Example Request
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/enrich/profile?handle=alexhormozi"Response
{
"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
| Field | Type | Description |
|---|---|---|
handle | string | The handle as shown on X |
user_id | string | null | X's numeric user ID |
name | string | Display name |
bio | string | Bio, with t.co short links replaced by their real destinations |
bio_urls | string[] | Links from the profile (real destinations, not t.co) |
profile_url | string | Link to the profile on X |
profile_image_url | string | null | Profile picture (400x400) |
followers_count | number | Follower count |
following_count | number | Following count |
verified | boolean | Has a verified (blue check) badge |
location | string | null | Location from the profile |
Check Enrichment Credits
/enrichment/creditsCheck enrichment creditsCheck your enrichment credit balance, used by Find Twitter Profile and Find Email from Handle.
Request
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/enrichment/creditsResponse
{
"enrichment_credits": 950,
"enrichment_credits_used": 50
}Schedule Tweet
/scheduled-tweetsSchedule a tweet to be posted at a specific timeSchedule 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| account_id | string | Required | The account UUID to post from |
| tweet_text | string | Required | The tweet content (max 280 characters) |
| scheduled_at | string | Required | ISO 8601 timestamp for when to post (must be in the future) |
Request
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-tweetsResponse
{
"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"
}
}pending- Waiting to be postedposted- Successfully postedfailed- Failed to post (check error_message)
List Scheduled Tweets
/scheduled-tweetsList all scheduled tweetsRetrieve a list of scheduled tweets for your accounts. Filter by account, status, or use pagination.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| account_id | string | Filter by specific account UUID |
| status | string | Filter by status: pending, posted, failed |
| limit | number | Max results (default: 50, max: 100) |
| offset | number | Pagination offset |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
"https://app.scrapely.co/api/v1/scheduled-tweets?status=pending&limit=10"Response
{
"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
/scheduled-tweetsCancel a pending scheduled tweetCancel a scheduled tweet before it gets posted. Only tweets with status "pending" can be cancelled.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| tweet_id | string | Required | The scheduled tweet UUID to cancel |
Request
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-tweetsResponse
{
"success": true,
"message": "Scheduled tweet cancelled",
"tweet_id": "7f3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
}You can only cancel tweets with status "pending". Once a tweet has been posted or failed, it cannot be cancelled.
List Follower Listeners
/follower-listenersList follower listenersFollower 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
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/follower-listenersResponse
{
"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
/follower-listenersWatch an account for new followers/followingStart 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
| Field | Type | Required | Description |
|---|---|---|---|
handle | string | Yes | X handle to watch: name, @name or an x.com profile URL |
listen_followers | boolean | No | Send new_follower events (default true) |
listen_following | boolean | No | Send new_following events (default true). At least one of the two must be true. |
Example
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)
{
"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
/follower-listenersUpdate or pause a listenerChange 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
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Listener id from List Follower Listeners |
listen_followers | boolean | No | Watch for new followers |
listen_following | boolean | No | Watch for new following |
is_active | boolean | No | false pauses the listener, true resumes it |
Example
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
/follower-listenersRemove a listenerStop watching an account and delete its listener. To stop temporarily instead, update it with is_active: false.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Listener id from List Follower Listeners |
Example
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
{
"success": true
}Query Listener Events
/follower-listeners/eventsQuery detected new followers/followingEvery 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
| Parameter | Type | Required | Description |
|---|---|---|---|
listener_id | string | No | Only events from this listener |
target_handle | string | No | Only events for this watched account (case-insensitive) |
type | string | No | followers (new_follower) or following (new_following) |
since | string | No | Detected on or after this ISO date or datetime, e.g. 2026-10-12 |
until | string | No | Detected on or before this ISO date or datetime |
handle | string | No | The person's X handle (exact, case-insensitive) |
search | string | No | Keyword in the person's name, handle or bio |
min_followers | number | No | Person has at least this many followers |
max_followers | number | No | Person has at most this many followers |
verified | boolean | No | true for verified only, false for unverified only |
limit | number | No | Max events to return (default 50, max 500) |
offset | number | No | Pagination offset (default 0) |
Request
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
{
"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
/webhooksList webhook URLs, settings and eventsReturns 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
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/webhooksResponse
{
"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
/webhooksAdd a webhook URLAdd 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
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The http(s) URL to send events to |
key | string | No | Your own secret, sent as the X-Webhook-Key header (up to 500 characters) |
generate_key | boolean | No | Generate a random key for you. Can't be combined with key. |
Example
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)
{
"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
/webhooksSet, rotate or remove a webhook keySet, 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
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The webhook URL, exactly as shown by List Webhooks |
key | string | One of | Your own new key |
generate_key | boolean | One of | Generate a new random key (returned once) |
remove_key | boolean | One of | Stop sending a key |
Example
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
/webhooksRemove a webhook URLStop sending events to a URL. To pause all webhooks instead, use Webhook Settings.
Example
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
{
"success": true,
"url": "https://example.com/scrapely-hook"
}Webhook Settings
/webhooks/settingsTurn webhooks on or offSame 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
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Turn reply, DM and account events on or off. Follower listener events (new_follower, new_following) are not affected; pause the listener instead. |
Example
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
/webhooks/testSend a sample eventSends 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
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
{
"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
/blocklistList blocked users with optional searchReturns your blocklist entries with resolved Twitter handles. Supports searching by handle and pagination.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| search | string | Optional | Filter by Twitter handle |
| limit | integer | Optional | Results per page (default 50, max 200) |
| offset | integer | Optional | Pagination offset (default 0) |
Request
curl -H "X-API-Key: YOUR_API_KEY" \
https://app.scrapely.co/api/v1/blocklist?search=johndoe&limit=50Response
{
"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
/blocklistAdd a user to the blocklistBlock 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| handle | string | Required | Twitter handle to block (e.g. "johndoe" or "@johndoe") |
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
curl -X POST \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"handle": "johndoe"}' \
https://app.scrapely.co/api/v1/blocklistResponse
{
"success": true,
"rest_id": "16979745"
}Remove from Blocklist
/blocklistRemove a user from the blocklistUnblock a user by their Twitter handle or user ID. The user will be able to receive DMs again in future campaigns.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| handle | string | Required | Twitter handle to unblock (e.g. "johndoe" or "@johndoe") |
Request
curl -X DELETE \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"handle": "johndoe"}' \
https://app.scrapely.co/api/v1/blocklistResponse
{
"success": true,
"cleared": 1
}Returns a 404 if the handle cannot be resolved or is not found in the blocklist.
Report a Problem
/feedbackReport a problem with an endpoint or MCP toolTell 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
| Field | Type | Required | Description |
|---|---|---|---|
tool | string | Yes | Endpoint or MCP tool name, e.g. launch_campaign (max 100 characters) |
what_happened | string | Yes | What you were trying to do and what went wrong (max 2,000 characters) |
error_message | string | No | The error text you got (max 2,000 characters) |
suggestion | string | No | What would have made it work or clearer (max 2,000 characters) |
source | string | No | api (default) or mcp |
Request
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)
{
"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
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad Request - Invalid parameters |
| 401 | Unauthorized - Invalid API key |
| 403 | Forbidden - Subscription not active |
| 429 | Rate Limited - Too many requests |
| 500 | Internal Server Error |
Error Response Format
{
"error": "Unauthorized",
"message": "Invalid API key"
}Rate Limits
API requests are rate limited to ensure fair usage.
Response Headers
Every response includes these headers:
X-Request-ID: req_m1abc123def456
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 105
X-RateLimit-Reset: 1709234567Every response includes an X-Request-ID header. Include this ID when contacting support for faster debugging.
Rate Limit Error
{
"error": "Rate limited",
"message": "Too many requests. Please try again later.",
"retry_after": 45
}- 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.
// Example: incoming webhook request headers
{
"Content-Type": "application/json",
"X-Webhook-Key": "your-webhook-secret-value"
}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
| Event | Description |
|---|---|
| new_reply | A prospect replied to your DM for the first time |
| reply_back_and_forth | A prospect sent a follow-up reply in an existing conversation |
| account_disconnected | A Twitter account was disconnected or paused |
| dm_seen | A lead opened and read your DM |
| new_follower | An account you added under Settings → Integrations → Follower Listeners gained a new follower (checked every 15 seconds) |
| new_following | An 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:
{
"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:
{
"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.
{
"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:
{
"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:
{
"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)
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:
{
"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.
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.
// 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.
- Open claude.ai/new#settings/customize-connectors
- Click + Add custom connector
- Set Name to
Scrapelyand paste this as the Remote MCP server URL:
https://mcp.scrapely.co/mcp- Click Connect, log in to Scrapely, and click Allow
- Open any conversation and start using Scrapely tools
Claude Code
One command — replace sk_live_... with your real key:
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:
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.
- Go to chatgpt.com → Settings → Apps → Advanced settings
- Turn on Developer mode and click Create app
- Name it
scrapely, leave Authentication set to OAuth, and paste this as the Connection URL:
https://mcp.scrapely.co/mcp- 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:
{
"mcpServers": {
"scrapely": {
"url": "https://mcp.scrapely.co/mcp",
"headers": {
"Authorization": "Bearer sk_live_your_key_here"
}
}
}
}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:
| Tool | Description |
|---|---|
| list_accounts | List all connected Twitter/X accounts |
| add_account | Add a new account (cookies + proxy) |
| remove_account | Remove an account by ID or handle |
| update_cookies | Refresh cookies for an existing account |
| toggle_pause | Pause/unpause DM sending |
| toggle_followups_pause | Pause/unpause follow-up messages |
| update_handle | Update an account's stored handle after renaming it on X |
| update_proxy | Update proxy settings for an account |
| get_account_tags | Get tags for accounts |
| set_account_tags | Replace all tags for an account |
| update_account_tags | Add or remove individual tags |
| toggle_warmup | Enable/disable warmup mode |
| create_scraping_source | Scrape followers/following with keyword and count filters |
| check_scraping_status | Check progress of scraping jobs |
| get_scraping_source_leads | Get leads from a scraping source |
| import_leads | Import your own list of leads (handles or x.com URLs) into a new lead source |
| launch_campaign | Launch a DM campaign with A/B testing, auto-follow/like/comment |
| launch_import_campaign | Launch a campaign from your own lead list with custom {{variables}}, send window and days |
| pause_campaign | Pause or unpause a campaign |
| get_campaign_leads | List a campaign's leads with status, reply info and custom_fields |
| add_leads_to_campaign | Add leads to a campaign by handle (any number; large lists are chunked automatically) |
| add_lead_source_to_campaign | Add every lead from a lead source to a campaign |
| upload_media | Upload an image (link, base64, or local file with npx) to attach to campaign DMs and follow-ups |
| rename_campaign | Rename a campaign (DMs, stats and pause state carry over) |
| edit_campaign | Edit a campaign's messages, follow-ups, engagement actions and send window |
| list_campaigns | List campaigns with stats |
| get_campaign_detail | Get detailed campaign info and settings |
| get_campaign_analytics | Detailed analytics: reply rates, sentiment, variant stats. Supports date range filtering. |
| fetch_conversations | Fetch conversations with optional message history |
| send_dm | Send a DM to an existing conversation |
| get_crm_data | Get conversations organized by CRM tags |
| update_crm | Update notes, deal value, and tags |
| list_listeners | List follower listeners (accounts watched for new followers/following) |
| add_listener | Watch an X account for new followers and/or following (webhook events) |
| update_listener | Switch followers/following on or off, or pause/resume a listener |
| get_listener_events | Query detected new followers/following with filters (watched handle, type, date range, handle, keyword, follower count, verified) |
| remove_listener | Stop watching an account and delete its listener |
| get_webhook_info | Get webhook event types and payload structure docs |
| enrich_twitter | Find a person's Twitter profile from name + company |
| enrich_email | Find a person's email from their Twitter handle |
| get_enrichment_credits | Check enrichment credit balance |
| search_people | Search the People database of X profiles by bio, location, follower counts and more |
| get_people_credits | Check your People search credit balance |
| get_people_schema | Get the People database columns and types |
| schedule_tweet | Schedule a tweet for later |
| list_scheduled_tweets | List scheduled tweets with filters |
| cancel_scheduled_tweet | Cancel a pending scheduled tweet |
| submit_feedback | Report a bug or confusing tool to the Scrapely team |
Example Prompts
Once connected, you can use natural language:
"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"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.
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
npm install -g scrapely-mcpThis 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
scrapely loginCreate 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
scrapely setup-agentOne 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, thenclaude). 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--yesanswers 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
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 doGood to know
- Asks before changes: commands that send DMs, spend credits, launch, pause or delete ask
Continue? [y/N]first. Add--yesto skip the question in scripts. - Scripts: add
--jsonto 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-mcpagain, thenscrapely setup-agentto refresh your agent's instructions.