Back to API Overview

Complete API Reference

All 239 endpoints across 26 sections

Authentication

User authentication and session management

1 endpoint
POST/api/auth/signup
Public

Sign Up

Register a new user account

Rate Limited: 5 requests per 15 minutes per IP address

Request Body

NameTypeRequiredDescription
emailstringRequiredUser email address
passwordstringRequiredPassword (minimum 8 characters)
namestringOptionalUser display name
termsAcceptedbooleanRequiredMust be true to accept terms
privacyAcceptedbooleanRequiredMust be true to accept privacy policy
marketingConsentbooleanOptionalOpt-in to marketing emails

Example Request

curl -X POST "https://scrappy.gg/api/auth/signup" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "your-password",
"termsAccepted": true,
"privacyAccepted": true
}'

Responses

201User created successfully
{
"success": true,
"data": {
"user": {
"id": "abc123",
"email": "user@example.com"
}
}
}
400Invalid input
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format"
}
}
429Rate limit exceeded
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many attempts"
}
}

Users

User profile and account management

2 endpoints
GET/api/users
Auth Required

Get Current User

Get the authenticated user's profile and statistics

Example Request

curl -X GET "https://scrappy.gg/api/users" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200User profile returned
{
"success": true,
"data": {
"user": {
"id": "abc123",
"email": "user@example.com",
"name": "John Doe",
"createdAt": "2024-01-01T00:00:00Z"
},
"stats": {
"sources": 10,
"jobs": 25,
"leads": 500
}
}
}
401Not authenticated
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required"
}
}
PATCH/api/users/password
Auth Required

Change Password

Update the authenticated user's password

Request Body

NameTypeRequiredDescription
currentPasswordstringRequiredCurrent password for verification
newPasswordstringRequiredNew password (minimum 8 characters)

Example Request

curl -X PATCH "https://scrappy.gg/api/users/password" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"currentPassword": "your-password",
"newPassword": "your-password"
}'

Responses

200Password updated
{
"success": true
}
400Invalid password
{
"success": false,
"error": {
"code": "INVALID_PASSWORD",
"message": "Current password is incorrect"
}
}

Data Export

GDPR subject access requests - export your data

3 endpoints
POST/api/users/export
Auth Required

Request Data Export

Request an export of all your data (JSON or CSV format)

Rate Limited: 3 requests per 1 week per user

Request Body

NameTypeRequiredDescription
formatstringOptionalExport format. JSON is the only format.Options: JSONDefault: JSON

Example Request

curl -X POST "https://scrappy.gg/api/users/export" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Export request created
{
"success": true,
"data": {
"requestId": "abc123",
"status": "PENDING",
"downloadExpiry": "2024-01-08T00:00:00Z"
}
}
429Rate limit exceeded
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Maximum 3 exports per week"
}
}
GET/api/users/export/status
Auth Required

Get Export Status

Check the status of all export requests

Example Request

curl -X GET "https://scrappy.gg/api/users/export/status" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Export status returned
{
"success": true,
"data": {
"exports": [
{
"requestId": "abc123",
"status": "COMPLETED",
"format": "JSON",
"createdAt": "2024-01-01T00:00:00Z",
"downloadExpiry": "2024-01-08T00:00:00Z"
}
]
}
}
GET/api/users/export/download
Auth Required

Download Export

Download a completed data export (7-day download window)

Query Parameters

NameTypeRequiredDescription
requestIdstringRequiredThe export request ID

Example Request

curl -X GET ?requestId=abc123"https://scrappy.gg/api/users/export/download" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200File download
{
"note": "Returns file attachment"
}
404Export not found or expired
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Export not found or expired"
}
}

Account Deletion

GDPR right to erasure - delete your account

4 endpoints
POST/api/users/deletion
Auth Required

Request Account Deletion

Request deletion of your account. A confirmation email will be sent.

Rate Limited: 1 requests per 1 day per user

Example Request

curl -X POST "https://scrappy.gg/api/users/deletion" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Deletion requested
{
"success": true,
"data": {
"expiresAt": "2024-01-02T00:00:00Z"
}
}
429Rate limit exceeded
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "One deletion request per day"
}
}
POST/api/users/deletion/confirm
Auth Required

Confirm Account Deletion

Confirm account deletion using the token from email. Starts 30-day grace period.

Request Body

NameTypeRequiredDescription
tokenstringRequiredConfirmation token from email (24-hour expiry)

Example Request

curl -X POST "https://scrappy.gg/api/users/deletion/confirm" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"token": "string"
}'

Responses

200Deletion confirmed
{
"success": true,
"data": {
"scheduledAt": "2024-01-31T00:00:00Z",
"gracePeriodDays": 30
}
}
400Invalid or expired token
{
"success": false,
"error": {
"code": "INVALID_TOKEN",
"message": "Token is invalid or expired"
}
}
POST/api/users/deletion/cancel
Auth Required

Cancel Account Deletion

Cancel a pending account deletion during the grace period

Example Request

curl -X POST "https://scrappy.gg/api/users/deletion/cancel" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Deletion cancelled
{
"success": true,
"message": "Account deletion cancelled"
}
400No pending deletion
{
"success": false,
"error": {
"code": "NO_PENDING_DELETION",
"message": "No deletion request to cancel"
}
}
GET/api/users/deletion/status
Auth Required

Get Deletion Status

Check the status of any pending account deletion

Example Request

curl -X GET "https://scrappy.gg/api/users/deletion/status" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Deletion status returned
{
"success": true,
"data": {
"status": "scheduled",
"scheduledAt": "2024-01-31T00:00:00Z",
"gracePeriodDays": 30
}
}

Projects

Organize sources and leads into projects

5 endpoints
GET/api/projects
Auth Required

List Projects

Get all projects for the authenticated user

Example Request

curl -X GET "https://scrappy.gg/api/projects" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Projects returned
{
"success": true,
"data": {
"projects": [
{
"id": "abc123",
"name": "My Project",
"description": "Project description",
"color": "#3b82f6",
"createdAt": "2024-01-01T00:00:00Z"
}
]
}
}
POST/api/projects
Auth Required

Create Project

Create a new project

Request Body

NameTypeRequiredDescription
namestringRequiredProject name
descriptionstringOptionalProject description
colorstringOptionalHex color code (e.g., #3b82f6)

Example Request

curl -X POST "https://scrappy.gg/api/projects" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name"
}'

Responses

201Project created
{
"success": true,
"data": {
"project": {
"id": "abc123",
"name": "My Project"
}
}
}
GET/api/projects/[id]
Auth Required

Get Project

Get a specific project by ID

Path Parameters

NameTypeRequiredDescription
idstringRequiredProject ID

Example Request

curl -X GET "https://scrappy.gg/api/projects/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Project returned
{
"success": true,
"data": {
"project": {
"id": "abc123",
"name": "My Project"
}
}
}
404Project not found
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Project not found"
}
}
PATCH/api/projects/[id]
Auth Required

Update Project

Update a project's details

Path Parameters

NameTypeRequiredDescription
idstringRequiredProject ID

Request Body

NameTypeRequiredDescription
namestringOptionalProject name
descriptionstringOptionalProject description
colorstringOptionalHex color code

Example Request

curl -X PATCH "https://scrappy.gg/api/projects/abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Project updated
{
"success": true,
"data": {
"project": {
"id": "abc123",
"name": "Updated Project"
}
}
}
403Not authorized
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "Not authorized to update this project"
}
}
DELETE/api/projects/[id]
Auth Required

Delete Project

Delete a project and all associated data

Path Parameters

NameTypeRequiredDescription
idstringRequiredProject ID

Example Request

curl -X DELETE "https://scrappy.gg/api/projects/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Project deleted
{
"success": true
}
403Not authorized
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "Not authorized to delete this project"
}
}

Sources

Manage data sources for lead scraping

10 endpoints
GET/api/sources
Auth Required

List Sources

Get all sources with optional filtering

Query Parameters

NameTypeRequiredDescription
statusstringOptionalFilter by statusOptions: DRAFT, ACTIVE, ARCHIVED
projectIdstringOptionalFilter by project ID

Example Request

curl -X GET "https://scrappy.gg/api/sources" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Sources returned
{
"success": true,
"data": {
"sources": [
{
"id": "abc123",
"name": "Example Source",
"url": "https://example.com",
"scrapeType": "STATIC",
"status": "ACTIVE"
}
]
}
}
POST/api/sources
Auth Required

Create Source

Create a new source for lead scraping

Request Body

NameTypeRequiredDescription
namestringRequiredSource name
urlstringRequiredURL to scrape
scrapeTypestringRequiredType of scrapingOptions: STATIC, DYNAMIC, API
selectorsobjectRequiredCSS/XPath selectors for data extraction
descriptionstringOptionalSource description
scheduleTypestringOptionalScraping scheduleOptions: MANUAL, HOURLY, DAILY, WEEKLY

Example Request

curl -X POST "https://scrappy.gg/api/sources" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"url": "https://example.com",
"scrapeType": "string",
"selectors": {}
}'

Responses

201Source created
{
"success": true,
"data": {
"source": {
"id": "abc123",
"name": "Example Source",
"status": "DRAFT"
}
}
}
GET/api/sources/[id]
Auth Required

Get Source

Get a specific source by ID

Path Parameters

NameTypeRequiredDescription
idstringRequiredSource ID

Example Request

curl -X GET "https://scrappy.gg/api/sources/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Source returned
{
"success": true,
"data": {
"source": {
"id": "abc123",
"name": "Example Source"
}
}
}
PATCH/api/sources/[id]
Auth Required

Update Source

Update a source's configuration

Path Parameters

NameTypeRequiredDescription
idstringRequiredSource ID

Request Body

NameTypeRequiredDescription
namestringOptionalSource name
urlstringOptionalURL to scrape
selectorsobjectOptionalCSS/XPath selectors

Example Request

curl -X PATCH "https://scrappy.gg/api/sources/abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Source updated
{
"success": true,
"data": {
"source": {
"id": "abc123",
"name": "Updated Source"
}
}
}
DELETE/api/sources/[id]
Auth Required

Delete Source

Delete a source

Path Parameters

NameTypeRequiredDescription
idstringRequiredSource ID

Example Request

curl -X DELETE "https://scrappy.gg/api/sources/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Source deleted
{
"success": true
}
POST/api/sources/[id]/activate
Auth Required

Activate Source

Activate a draft source to enable scraping

Path Parameters

NameTypeRequiredDescription
idstringRequiredSource ID

Example Request

curl -X POST "https://scrappy.gg/api/sources/abc123/activate" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Source activated
{
"success": true,
"data": {
"source": {
"id": "abc123",
"status": "ACTIVE"
}
}
}
400Already active
{
"success": false,
"error": {
"code": "ALREADY_ACTIVE",
"message": "Source is already active"
}
}
POST/api/sources/discover
Auth Required

Discover Sources (AI)

Use AI to discover relevant sources based on a search query

Request Body

NameTypeRequiredDescription
querystringRequiredSearch query (minimum 3 characters)
countnumberOptionalNumber of sources to discover (1-10)Default: 5

Example Request

curl -X POST "https://scrappy.gg/api/sources/discover" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "search term"
}'

Responses

200Sources discovered
{
"success": true,
"data": {
"sources": [
{
"name": "Example Directory",
"url": "https://example.com/directory",
"category": "Business Directory",
"estimatedLeads": 500,
"scrapeType": "STATIC",
"description": "A directory of local businesses",
"reasoning": "This directory contains relevant business listings"
}
]
}
}
POST/api/sources/generate
Auth Required

Generate Sources (AI)

Use AI to generate source suggestions based on a project description

Request Body

NameTypeRequiredDescription
projectDescriptionstringRequiredProject description (minimum 10 characters)
targetAudiencestringOptionalTarget audience for the leads
industrystringOptionalIndustry to focus on
locationstringOptionalGeographic location
countnumberOptionalNumber of sources to generate (1-5)Default: 3
autoSavebooleanOptionalAutomatically save generated sources to the projectDefault: false

Example Request

curl -X POST "https://scrappy.gg/api/sources/generate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectDescription": "string"
}'

Responses

200Sources generated
{
"success": true,
"data": {
"sources": [
{
"name": "Industry Directory",
"url": "https://example.com/directory",
"description": "Relevant directory for target industry",
"scrapeType": "STATIC",
"reasoning": "Matches project requirements"
}
]
}
}
400Invalid input
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Project description must be at least 10 characters"
}
}
POST/api/sources/batch
Auth Required

Batch Create Sources

Create multiple sources at once from AI suggestions or manual input

Request Body

NameTypeRequiredDescription
sourcesarrayRequiredArray of source objects to create (1 or more)
sources[].namestringRequiredSource name
sources[].urlstringRequiredURL to scrape
sources[].descriptionstringOptionalSource description
sources[].scrapeTypestringOptionalType of scrapingOptions: STATIC, DYNAMIC, APIDefault: DYNAMIC
sources[].selectorsobjectOptionalCSS/XPath selectors for data extraction
sources[].reasoningstringOptionalAI reasoning for why this source was suggested

Example Request

curl -X POST "https://scrappy.gg/api/sources/batch" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sources": [],
"sources[].name": "Example Name",
"sources[].url": "https://example.com"
}'

Responses

201Sources created
{
"success": true,
"data": {
"sources": [
{
"id": "abc123",
"name": "Example Source",
"status": "DRAFT"
}
],
"count": 1
}
}
400Invalid input
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "At least one source is required"
}
}
POST/api/sources/analyze
Auth Required

Analyze Source

Analyze project requirements from a URL or PDF file

Request Body

NameTypeRequiredDescription
filefileOptionalPDF file to analyze (mutually exclusive with url)
urlstringOptionalURL to analyze (mutually exclusive with file)

Example Request

curl -X POST "https://scrappy.gg/api/sources/analyze" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: multipart/form-data"

Responses

200Analysis results
{
"success": true,
"data": {
"requirements": {
"targetAudience": "Small businesses",
"industry": "Technology",
"location": "United States",
"suggestedSources": []
}
}
}
400Invalid input
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Either file or url is required"
}
}

Jobs

Scraping job management and monitoring

5 endpoints
GET/api/jobs
Auth Required

List Jobs

Get all scraping jobs

Example Request

curl -X GET "https://scrappy.gg/api/jobs" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Jobs returned
{
"success": true,
"data": {
"jobs": [
{
"id": "abc123",
"name": "Scrape Job",
"status": "COMPLETED",
"progress": 100,
"leadsFound": 150,
"createdAt": "2024-01-01T00:00:00Z"
}
]
}
}
POST/api/jobs
Auth Required

Create Job

Create and enqueue a new scraping job

Request Body

NameTypeRequiredDescription
namestringRequiredJob name
sourceIdstringRequiredSource ID to scrape

Example Request

curl -X POST "https://scrappy.gg/api/jobs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"sourceId": "abc123"
}'

Responses

201Job created
{
"success": true,
"data": {
"job": {
"id": "abc123",
"name": "Scrape Job",
"status": "PENDING"
}
}
}
GET/api/jobs/[id]
Auth Required

Get Job

Get job details and progress

Path Parameters

NameTypeRequiredDescription
idstringRequiredJob ID

Example Request

curl -X GET "https://scrappy.gg/api/jobs/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Job returned
{
"success": true,
"data": {
"job": {
"id": "abc123",
"status": "RUNNING",
"progress": 45,
"leadsFound": 67
}
}
}
DELETE/api/jobs/[id]
Auth Required

Cancel/Delete Job

Cancel a running job or delete a completed job

Path Parameters

NameTypeRequiredDescription
idstringRequiredJob ID

Example Request

curl -X DELETE "https://scrappy.gg/api/jobs/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Job cancelled/deleted
{
"success": true
}
POST/api/jobs/[id]/retry
Auth Required

Retry Job

Retry a failed job

Path Parameters

NameTypeRequiredDescription
idstringRequiredJob ID

Example Request

curl -X POST "https://scrappy.gg/api/jobs/abc123/retry" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Job retry started
{
"success": true,
"message": "Job retry started"
}
400Job cannot be retried
{
"success": false,
"error": {
"code": "INVALID_STATUS",
"message": "Only failed jobs can be retried"
}
}

Leads

Lead management and email validation

10 endpoints
GET/api/leads
Auth Required

List Leads

Get all leads with optional filtering

Query Parameters

NameTypeRequiredDescription
statusstringOptionalFilter by status
companystringOptionalFilter by company name
jobIdstringOptionalFilter by job ID

Example Request

curl -X GET "https://scrappy.gg/api/leads" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Leads returned
{
"success": true,
"data": {
"leads": [
{
"id": "abc123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"company": "Example Inc",
"status": "VALID"
}
]
}
}
POST/api/leads
Auth Required

Create Lead

Create a lead manually

Request Body

NameTypeRequiredDescription
sourceUrlstringRequiredSource URL where lead was found
firstNamestringOptionalFirst name
lastNamestringOptionalLast name
emailstringOptionalEmail address
phonestringOptionalPhone number
companystringOptionalCompany name
jobTitlestringOptionalJob title

Example Request

curl -X POST "https://scrappy.gg/api/leads" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceUrl": "https://example.com"
}'

Responses

201Lead created
{
"success": true,
"data": {
"lead": {
"id": "abc123",
"firstName": "John",
"lastName": "Doe"
}
}
}
GET/api/leads/[id]
Auth Required

Get Lead

Get lead details with job and source info

Path Parameters

NameTypeRequiredDescription
idstringRequiredLead ID

Example Request

curl -X GET "https://scrappy.gg/api/leads/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Lead returned
{
"success": true,
"data": {
"lead": {
"id": "abc123",
"email": "john@example.com"
}
}
}
PATCH/api/leads/[id]
Auth Required

Update Lead

Update a lead's information

Path Parameters

NameTypeRequiredDescription
idstringRequiredLead ID

Request Body

NameTypeRequiredDescription
firstNamestringOptionalFirst name
lastNamestringOptionalLast name
emailstringOptionalEmail address
statusstringOptionalLead status
tagsarrayOptionalTags for categorization
notesstringOptionalNotes about the lead

Example Request

curl -X PATCH "https://scrappy.gg/api/leads/abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Lead updated
{
"success": true,
"data": {
"lead": {
"id": "abc123"
}
}
}
DELETE/api/leads/[id]
Auth Required

Delete Lead

Delete a lead

Path Parameters

NameTypeRequiredDescription
idstringRequiredLead ID

Example Request

curl -X DELETE "https://scrappy.gg/api/leads/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Lead deleted
{
"success": true
}
POST/api/leads/[id]/validate-email
Auth Required

Validate Lead Email

Trigger email validation for a specific lead

Path Parameters

NameTypeRequiredDescription
idstringRequiredLead ID

Example Request

curl -X POST "https://scrappy.gg/api/leads/abc123/validate-email" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Validation result
{
"success": true,
"data": {
"status": "VALID",
"tier": "TIER_1",
"errors": [],
"flags": []
}
}
400No email on lead
{
"success": false,
"error": {
"code": "NO_EMAIL",
"message": "Lead has no email to validate"
}
}
DELETE/api/leads/bulk
Auth Required

Bulk Delete Leads

Soft-delete multiple leads at once (max 1000), by id list or by filter

Rate Limited: 10 requests per 1h per user

Request Body

NameTypeRequiredDescription
leadIdsarrayOptionalArray of lead IDs to delete (1-1000). Either this, or mode + filters
modestringOptional'filter' to delete every lead matching filters instead of an id list
filtersobjectOptionalLead filters (status, emailValidationStatus, company, jobId, search, dateFrom, dateTo) when mode is filter

Example Request

curl -X DELETE "https://scrappy.gg/api/leads/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Bulk delete result
{
"success": true,
"data": {
"deleted": 50,
"failed": 0,
"errors": []
}
}
PATCH/api/leads/bulk
Auth Required

Bulk Update Leads

Update multiple leads at once

Request Body

NameTypeRequiredDescription
leadIdsarrayRequiredArray of lead IDs to update
updatesobjectRequiredUpdates to apply (status, tags add/remove)

Example Request

curl -X PATCH "https://scrappy.gg/api/leads/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadIds": [],
"updates": {}
}'

Responses

200Bulk update result
{
"success": true,
"data": {
"updated": 50,
"failed": 0
}
}
POST/api/leads/validate-bulk
Auth Required

Bulk Validate Emails

Queue email validation for multiple leads (max 1000)

Request Body

NameTypeRequiredDescription
leadIdsarrayRequiredArray of lead IDs to validate (1-1000)

Example Request

curl -X POST "https://scrappy.gg/api/leads/validate-bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadIds": []
}'

Responses

200Validation queued
{
"success": true,
"data": {
"totalRequested": 100,
"totalQueued": 95,
"skipped": [
{
"id": "abc",
"reason": "No email"
}
]
}
}

Profile Types & Profiles

Custom entity schemas for structured data extraction, plus management of extracted profiles

11 endpoints
GET/api/profile-types
Auth Required

List Profile Types

Get all profile type schemas defined by the user.

Example Request

curl -X GET "https://scrappy.gg/api/profile-types" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Profile types returned
{
"success": true,
"data": {
"profileTypes": [
{
"id": "pt_123",
"name": "Private Lender",
"slug": "private-lender",
"fields": [],
"createdAt": "2024-01-01T00:00:00Z"
}
]
}
}
POST/api/profile-types
Auth Required

Create Profile Type

Define a new custom entity schema for structured data extraction.

Request Body

NameTypeRequiredDescription
namestringRequiredProfile type name
slugstringRequiredURL-safe identifier
descriptionstringOptionalSchema description
fieldsarrayRequiredJSON Schema field definitions

Example Request

curl -X POST "https://scrappy.gg/api/profile-types" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"slug": "string",
"fields": []
}'

Responses

201Profile type created
{
"success": true,
"data": {
"id": "pt_123",
"name": "Private Lender"
}
}
GET/api/profile-types/{id}
Auth Required

Get Profile Type

Get a profile type by ID.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile type ID

Example Request

curl -X GET "https://scrappy.gg/api/profile-types/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Profile type returned
{
"success": true,
"data": {}
}
PATCH/api/profile-types/{id}
Auth Required

Update Profile Type

Update a profile type's fields or metadata.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile type ID

Request Body

NameTypeRequiredDescription
namestringOptionalProfile type name
fieldsarrayOptionalUpdated field definitions

Example Request

curl -X PATCH "https://scrappy.gg/api/profile-types/{id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Profile type updated
{
"success": true,
"data": {}
}
DELETE/api/profile-types/{id}
Auth Required

Delete Profile Type

Delete a profile type schema.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile type ID

Example Request

curl -X DELETE "https://scrappy.gg/api/profile-types/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Deleted
{
"success": true
}
GET/api/profile-types/templates
Auth Required

List Built-in Templates

Get built-in profile type templates (e.g. Private Lender, Restaurant, SaaS Company).

Example Request

curl -X GET "https://scrappy.gg/api/profile-types/templates" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Templates returned
{
"success": true,
"data": {
"templates": [
{
"slug": "private-lender",
"name": "Private Lender",
"description": "Real estate lender schema"
},
{
"slug": "restaurant",
"name": "Restaurant",
"description": "Food & beverage schema"
}
]
}
}
GET/api/profiles
Auth Required

List Profiles

Get extracted profiles with optional filtering.

Query Parameters

NameTypeRequiredDescription
profileTypeIdstringOptionalFilter by profile type
statusstringOptionalFilter by statusOptions: NEW, VERIFIED, OUTDATED, ARCHIVED
pagenumberOptionalPage number
limitnumberOptionalItems per page (max: 100)

Example Request

curl -X GET "https://scrappy.gg/api/profiles" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Profiles returned
{
"success": true,
"data": {
"profiles": [
{
"id": "prof_123",
"profileTypeId": "pt_123",
"data": {},
"status": "NEW"
}
],
"total": 5
}
}
GET/api/profiles/{id}
Auth Required

Get Profile

Get a single profile by ID.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile ID

Example Request

curl -X GET "https://scrappy.gg/api/profiles/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Profile returned
{
"success": true,
"data": {}
}
PATCH/api/profiles/{id}
Auth Required

Update Profile

Update profile data or status.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile ID

Request Body

NameTypeRequiredDescription
dataobjectOptionalProfile field data
statusstringOptionalProfile statusOptions: NEW, VERIFIED, OUTDATED, ARCHIVED

Example Request

curl -X PATCH "https://scrappy.gg/api/profiles/{id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Profile updated
{
"success": true,
"data": {}
}
POST/api/profiles/export
Auth Required

Export Profiles

Export profiles of one type as json, csv or jsonl. Optionally filter by matching rule (includes matchScore + aiReasoning columns).

Request Body

NameTypeRequiredDescription
profileTypeIdstringRequiredProfile type to export
formatstringOptionalOutput formatOptions: json, csv, jsonlDefault: json
filtersarrayOptionalField filters to apply before export
ruleIdstringOptionalExport profiles matching a specific matching rule
matchedOnlybooleanOptionalWith ruleId, only profiles the rule matched (default true)

Example Request

curl -X POST "https://scrappy.gg/api/profiles/export" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileTypeId": "abc123"
}'

Responses

200Export returned in the requested format
{
"Content-Type": "text/csv"
}
GET/api/profiles/stats
Auth Required

Profile Stats

Get aggregate statistics across all profiles.

Example Request

curl -X GET "https://scrappy.gg/api/profiles/stats" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Stats returned
{
"success": true,
"data": {
"total": 150,
"byStatus": {
"NEW": 80,
"VERIFIED": 50,
"OUTDATED": 15,
"ARCHIVED": 5
},
"byProfileType": [
{
"profileTypeId": "pt_123",
"count": 90
}
]
}
}

Matching Engine

Rules-based and AI-powered filtering and scoring of profiles — FILTER, SCORED, and AI match types

8 endpoints
GET/api/matching/rules
Auth Required

List Matching Rules

Get all matching rules for the current user.

Query Parameters

NameTypeRequiredDescription
profileTypeIdstringOptionalFilter by profile type

Example Request

curl -X GET "https://scrappy.gg/api/matching/rules" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Rules returned
{
"success": true,
"data": {
"rules": [
{
"id": "rule_123",
"name": "High-value Leads",
"type": "SCORED",
"profileTypeId": "pt_123"
}
]
}
}
POST/api/matching/rules
Auth Required

Create Matching Rule

Create a new matching rule. Three types are supported: FILTER (boolean pass/fail), SCORED (weighted 0–100 score), and AI (LLM-powered evaluation using aiPrompt).

Request Body

NameTypeRequiredDescription
namestringRequiredRule name
typestringRequiredMatch typeOptions: FILTER, SCORED, AI
profileTypeIdstringRequiredProfile type this rule applies to
criteriaobjectOptionalMatching criteria (field conditions and weights). Can be empty {} for AI type.
aiPromptstringOptionalLLM prompt template for AI type rules. Use {{profile}} and {{profileType}} placeholders.
minimumScorenumberOptionalMinimum score threshold for SCORED type (0–100)

Example Request

curl -X POST "https://scrappy.gg/api/matching/rules" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"type": "string",
"profileTypeId": "abc123"
}'

Responses

201Rule created
{
"success": true,
"data": {
"id": "rule_123",
"name": "High-value Leads"
}
}
GET/api/matching/rules/{id}
Auth Required

Get Matching Rule

Get a matching rule by ID.

Path Parameters

NameTypeRequiredDescription
idstringRequiredRule ID

Example Request

curl -X GET "https://scrappy.gg/api/matching/rules/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Rule returned
{
"success": true,
"data": {}
}
PATCH/api/matching/rules/{id}
Auth Required

Update Matching Rule

Update a matching rule's criteria or metadata.

Path Parameters

NameTypeRequiredDescription
idstringRequiredRule ID

Request Body

NameTypeRequiredDescription
namestringOptionalRule name
criteriaobjectOptionalUpdated criteria
aiPromptstringOptionalUpdated AI prompt

Example Request

curl -X PATCH "https://scrappy.gg/api/matching/rules/{id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Rule updated
{
"success": true,
"data": {}
}
DELETE/api/matching/rules/{id}
Auth Required

Delete Matching Rule

Delete a matching rule.

Path Parameters

NameTypeRequiredDescription
idstringRequiredRule ID

Example Request

curl -X DELETE "https://scrappy.gg/api/matching/rules/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Rule deleted
{
"success": true
}
POST/api/matching/preview
Auth Required

Preview Matching

Test a matching rule against sample profiles before saving.

Request Body

NameTypeRequiredDescription
ruleIdstringOptionalExisting rule to preview
typestringOptionalRule type for inline previewOptions: FILTER, SCORED, AI
criteriaobjectOptionalInline criteria to test
aiPromptstringOptionalInline AI prompt to test
profileTypeIdstringOptionalProfile type to test against
sampleSizenumberOptionalNumber of sample profiles (default: 10)

Example Request

curl -X POST "https://scrappy.gg/api/matching/preview" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Preview results
{
"success": true,
"data": {
"matched": 7,
"notMatched": 3,
"results": [
{
"profileId": "prof_123",
"matched": true,
"score": 87,
"reasoning": "Revenue > $1M"
}
]
}
}
POST/api/matching/execute
Auth Required

Execute Matching Rule

Run a matching rule against all profiles and store results.

Request Body

NameTypeRequiredDescription
ruleIdstringRequiredRule ID to execute

Example Request

curl -X POST "https://scrappy.gg/api/matching/execute" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ruleId": "abc123"
}'

Responses

200Execution results
{
"success": true,
"data": {
"matched": 42,
"notMatched": 18,
"executionTimeMs": 1250
}
}
GET/api/matching/results
Auth Required

List Matching Results

Get stored matching results for a rule.

Query Parameters

NameTypeRequiredDescription
ruleIdstringRequiredRule ID
matchedbooleanOptionalFilter matched/unmatched
pagenumberOptionalPage number
limitnumberOptionalItems per page

Example Request

curl -X GET ?ruleId=abc123"https://scrappy.gg/api/matching/results" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Results returned
{
"success": true,
"data": {
"results": [
{
"profileId": "prof_123",
"matched": true,
"score": 87,
"matchedAt": "2024-01-15"
}
],
"total": 42
}
}

Accounts

Aggregated company/account view derived from lead data

3 endpoints
GET/api/accounts
Auth Required

List Accounts

Returns companies aggregated from lead data with enriched metadata. Accounts are computed dynamically — not stored as a separate entity.

Query Parameters

NameTypeRequiredDescription
searchstringOptionalSearch by company name or domain
projectIdstringOptionalFilter by project ID
pagenumberOptionalPage number (default: 1)
limitnumberOptionalItems per page (default: 20, max: 100)

Example Request

curl -X GET "https://scrappy.gg/api/accounts" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Accounts returned
{
"success": true,
"data": {
"accounts": [
{
"id": "acme.com",
"companyDomain": "acme.com",
"companyName": "Acme Corp",
"leadCount": 12,
"latestLeadAt": "2024-01-15T10:30:00Z",
"emailsFound": 10,
"linkedinUrl": "https://linkedin.com/company/acme"
}
],
"total": 42,
"page": 1,
"limit": 20
}
}
GET/api/accounts/{accountId}
Auth Required

Get Account

Get a single account with all associated leads.

Path Parameters

NameTypeRequiredDescription
accountIdstringRequiredCompany domain used as account identifier

Example Request

curl -X GET "https://scrappy.gg/api/accounts/{accountId}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Account returned
{
"success": true,
"data": {
"id": "acme.com",
"companyDomain": "acme.com",
"companyName": "Acme Corp",
"leadCount": 12,
"leads": []
}
}
POST/api/accounts/{accountId}/commonalities
Auth Required

Analyze LinkedIn Commonalities

Use AI (Perplexity) to analyze shared traits among leads at this account, optionally against a target profile. Requires PERPLEXITY_API_KEY. Accounts are read-only aggregates over leads — there is no update route.

Rate Limited: 10 requests per 1h per user

Path Parameters

NameTypeRequiredDescription
accountIdstringRequiredCompany domain

Request Body

NameTypeRequiredDescription
targetProfileobjectOptionalThe profile to compare the account's leads against (school, employers, interests)

Example Request

curl -X POST "https://scrappy.gg/api/accounts/{accountId}/commonalities" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Commonalities analysis
{
"success": true,
"data": {
"commonalities": [
"Alumni of MIT",
"Previously worked at Google",
"Focus on B2B SaaS"
],
"analyzedProfiles": 8
}
}
503PERPLEXITY_API_KEY not configured
{
"success": false,
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "AI service not configured"
}
}

Deal Campaigns

AI-powered deal flow management — discover sources, rank accounts, and generate matching rules

8 endpoints
GET/api/campaigns
Auth Required

List Campaigns

Get all deal campaigns for the current user.

Query Parameters

NameTypeRequiredDescription
statusstringOptionalFilter by statusOptions: ACTIVE, PAUSED, COMPLETED, ARCHIVED
projectIdstringOptionalFilter by project ID

Example Request

curl -X GET "https://scrappy.gg/api/campaigns" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Campaigns returned
{
"success": true,
"data": {
"campaigns": [
{
"id": "camp_123",
"name": "Q1 Outreach",
"status": "ACTIVE"
}
]
}
}
POST/api/campaigns
Auth Required

Create Campaign

Create a new deal campaign in the project named by the x-project-id header. No route-specific rate limit beyond the default mutation limit.

Request Body

NameTypeRequiredDescription
namestringRequiredCampaign name
descriptionstringOptionalCampaign description
targetIndustrystringOptionalTarget industry vertical
targetGeographystringOptionalTarget geography (e.g. 'US', 'New York')
targetCompanySizestringOptionalTarget company size (e.g. '50-200 employees')
dealParamsobjectOptionalCustom deal parameters (loan size, revenue threshold, etc.)
projectIdstringOptionalAssociate with a project

Example Request

curl -X POST "https://scrappy.gg/api/campaigns" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name"
}'

Responses

201Campaign created
{
"success": true,
"data": {
"id": "camp_123",
"name": "Q1 Outreach",
"status": "ACTIVE"
}
}
GET/api/campaigns/{id}
Auth Required

Get Campaign

Get a single campaign by ID.

Path Parameters

NameTypeRequiredDescription
idstringRequiredCampaign ID

Example Request

curl -X GET "https://scrappy.gg/api/campaigns/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Campaign returned
{
"success": true,
"data": {
"id": "camp_123"
}
}
PATCH/api/campaigns/{id}
Auth Required

Update Campaign

Update campaign fields.

Path Parameters

NameTypeRequiredDescription
idstringRequiredCampaign ID

Request Body

NameTypeRequiredDescription
namestringOptionalCampaign name
statusstringOptionalCampaign statusOptions: ACTIVE, PAUSED, COMPLETED, ARCHIVED

Example Request

curl -X PATCH "https://scrappy.gg/api/campaigns/{id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Campaign updated
{
"success": true,
"data": {
"id": "camp_123"
}
}
DELETE/api/campaigns/{id}
Auth Required

Delete Campaign

Permanently delete a campaign.

Path Parameters

NameTypeRequiredDescription
idstringRequiredCampaign ID

Example Request

curl -X DELETE "https://scrappy.gg/api/campaigns/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Campaign deleted
{
"success": true
}
POST/api/campaigns/{id}/discover-sources
Auth Required

Discover Sources for Campaign

Use AI to find relevant data sources based on the campaign's target parameters. Requires PERPLEXITY_API_KEY.

Path Parameters

NameTypeRequiredDescription
idstringRequiredCampaign ID

Example Request

curl -X POST "https://scrappy.gg/api/campaigns/{id}/discover-sources" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Sources suggested
{
"success": true,
"data": {
"sources": [
{
"name": "NYC Construction Directory",
"url": "https://example.com",
"reasoning": "High density of target companies"
}
]
}
}
POST/api/campaigns/{id}/suggest-matching
Auth Required

Suggest Matching Rules

Generate AI-suggested matching rules based on campaign deal parameters.

Path Parameters

NameTypeRequiredDescription
idstringRequiredCampaign ID

Example Request

curl -X POST "https://scrappy.gg/api/campaigns/{id}/suggest-matching" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Matching rules suggested
{
"success": true,
"data": {
"rules": [
{
"type": "FILTER",
"field": "jobTitle",
"operator": "contains",
"value": "CFO"
}
]
}
}
GET/api/campaigns/{id}/ranked-accounts
Auth Required

Ranked Accounts

Get accounts ranked by fit score for this campaign.

Path Parameters

NameTypeRequiredDescription
idstringRequiredCampaign ID

Example Request

curl -X GET "https://scrappy.gg/api/campaigns/{id}/ranked-accounts" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Ranked accounts returned
{
"success": true,
"data": {
"accounts": [
{
"companyDomain": "acme.com",
"companyName": "Acme Corp",
"score": 92,
"leadCount": 8
}
]
}
}

AI Chat Agent

Conversational AI assistant (Claude) that can query and manage leads, sources, and jobs via natural language

5 endpoints
GET/api/chat/conversations
Auth Required

List Conversations

Get all chat conversations for the current user.

Example Request

curl -X GET "https://scrappy.gg/api/chat/conversations" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Conversations returned
{
"success": true,
"data": {
"conversations": [
{
"id": "conv_123",
"title": "Lead search for NYC dentists",
"messageCount": 6,
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:05:00Z"
}
]
}
}
POST/api/chat/conversations
Auth Required

Create Conversation

Start a new chat conversation. No route-specific rate limit beyond the default mutation limit.

Request Body

NameTypeRequiredDescription
titlestringOptionalConversation title (auto-generated if omitted)

Example Request

curl -X POST "https://scrappy.gg/api/chat/conversations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

201Conversation created
{
"success": true,
"data": {
"id": "conv_123",
"title": "New Conversation",
"messages": []
}
}
GET/api/chat/conversations/{id}
Auth Required

Get Conversation

Get a conversation with its full message history.

Path Parameters

NameTypeRequiredDescription
idstringRequiredConversation ID

Example Request

curl -X GET "https://scrappy.gg/api/chat/conversations/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Conversation returned
{
"success": true,
"data": {
"id": "conv_123",
"title": "Lead search",
"messages": [
{
"role": "user",
"content": "Find leads in healthcare",
"createdAt": "..."
},
{
"role": "assistant",
"content": "I found 24 leads...",
"createdAt": "..."
}
]
}
}
DELETE/api/chat/conversations/{id}
Auth Required

Delete Conversation

Delete a conversation and all its messages.

Path Parameters

NameTypeRequiredDescription
idstringRequiredConversation ID

Example Request

curl -X DELETE "https://scrappy.gg/api/chat/conversations/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Conversation deleted
{
"success": true
}
POST/api/chat/conversations/{id}/messages
Auth Required

Send Message (Streaming)

Send a message to the AI agent and receive a streaming SSE response. The agent has 9 built-in tools: search leads, get lead, update lead, search sources, get source, search jobs, get job, create job, and get stats. Requires ANTHROPIC_API_KEY.

Rate Limited: 1000 requests per 1h per user

Path Parameters

NameTypeRequiredDescription
idstringRequiredConversation ID

Request Body

NameTypeRequiredDescription
contentstringRequiredUser message to send to the agent (1–10000 chars)
attachmentobjectOptionalOptional file: { fileName, fileType: 'pdf' | 'csv', data, preview? } — data is base64 for a PDF or the raw text for a CSV (≤7,000,000 chars, ~5MB); preview is optional { pageCount?, title? }

Example Request

curl -X POST "https://scrappy.gg/api/chat/conversations/{id}/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "string"
}'

Responses

200Server-Sent Events stream. Event types: text (delta), tool_start, tool_call (with input), tool_result, done, error.
{
"event: text": {
"delta": "I found "
},
"event: tool_start": {
"tool": "search_leads"
},
"event: tool_call": {
"tool": "search_leads",
"input": {
"query": "healthcare"
}
},
"event: tool_result": {
"tool": "search_leads",
"result": {
"count": 24
}
},
"event: done": {}
}
503ANTHROPIC_API_KEY not configured
{
"success": false,
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "AI chat not configured"
}
}

Email Verification

Email bounce tracking and spam trap detection

3 endpoints
POST/api/email-verification/bounce
Auth Required

Record Bounce

Record an email bounce event

Request Body

NameTypeRequiredDescription
emailstringRequiredEmail address that bounced
bounceTypestringRequiredType of bounceOptions: HARD, SOFT, SPAM
bounceReasonstringOptionalDetailed bounce reason
smtpCodestringOptionalSMTP error code

Example Request

curl -X POST "https://scrappy.gg/api/email-verification/bounce" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"bounceType": "string"
}'

Responses

200Bounce recorded
{
"success": true
}
POST/api/email-verification/spam-trap
Auth Required

Mark as Spam Trap

Mark an email as a known spam trap

Request Body

NameTypeRequiredDescription
emailstringRequiredEmail address
confidencenumberOptionalConfidence level 0-100Default: 100
trapTypestringOptionalType of spam trap
sourcestringOptionalSource of identification

Example Request

curl -X POST "https://scrappy.gg/api/email-verification/spam-trap" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'

Responses

200Spam trap marked
{
"success": true
}
GET/api/email-verification/stats
Auth Required

Get Verification Stats

Get email verification and bounce statistics

Query Parameters

NameTypeRequiredDescription
bouncesbooleanOptionalInclude recent bounces
bounceLimitnumberOptionalNumber of recent bouncesDefault: 100

Example Request

curl -X GET "https://scrappy.gg/api/email-verification/stats" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Stats returned
{
"success": true,
"data": {
"verificationStats": {
"total": 1000,
"valid": 850,
"invalid": 100,
"unknown": 50
},
"bounceStats": {
"hard": 50,
"soft": 30,
"spam": 20
},
"recentBounces": []
}
}

LinkedIn Integration

LinkedIn scraping, lead enrichment, profile validation, and commonalities analysis

7 endpoints
GET/api/linkedin/credentials
Auth Required

List LinkedIn Credentials

Get stored LinkedIn credentials with computed status (ACTIVE, RATE_LIMITED, SOFT_BANNED, INVALID), daily limit, and requests remaining.

Example Request

curl -X GET "https://scrappy.gg/api/linkedin/credentials" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Credentials returned
{
"success": true,
"data": {
"credentials": [
{
"id": "cred_123",
"email": "linkedin@example.com",
"status": "ACTIVE",
"dailyLimit": 80,
"requestsRemaining": 45
}
]
}
}
POST/api/linkedin/credentials
Auth Required

Add LinkedIn Credentials

Store LinkedIn account credentials for scraping.

Request Body

NameTypeRequiredDescription
emailstringRequiredLinkedIn account email
passwordstringRequiredLinkedIn account password (encrypted at rest)

Example Request

curl -X POST "https://scrappy.gg/api/linkedin/credentials" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "your-password"
}'

Responses

201Credentials added
{
"success": true,
"data": {
"id": "cred_123",
"email": "linkedin@example.com"
}
}
POST/api/linkedin/validate
Auth Required

Validate LinkedIn Profile

Validate that a LinkedIn profile URL is reachable and extract basic metadata.

Request Body

NameTypeRequiredDescription
profileUrlstringRequiredLinkedIn profile URL to validate

Example Request

curl -X POST "https://scrappy.gg/api/linkedin/validate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileUrl": "https://example.com"
}'

Responses

200Validation result
{
"success": true,
"data": {
"valid": true,
"name": "John Doe",
"headline": "CEO at Acme Corp"
}
}
POST/api/linkedin/enrich
Auth Required

Enrich Lead from LinkedIn

Enrich a lead's data by scraping their LinkedIn profile.

Request Body

NameTypeRequiredDescription
leadIdstringRequiredLead ID to enrich
profileUrlstringOptionalLinkedIn profile URL (uses lead.linkedin if omitted)

Example Request

curl -X POST "https://scrappy.gg/api/linkedin/enrich" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadId": "abc123"
}'

Responses

200Lead enriched
{
"success": true,
"data": {
"leadId": "lead_123",
"enriched": {
"jobTitle": "VP of Sales",
"company": "Acme Corp",
"location": "New York"
}
}
}
GET/api/linkedin/jobs
Auth Required

List LinkedIn Jobs

Get all LinkedIn scraping jobs.

Example Request

curl -X GET "https://scrappy.gg/api/linkedin/jobs" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Jobs returned
{
"success": true,
"data": {
"jobs": [
{
"id": "ljob_123",
"status": "COMPLETED",
"profilesScraped": 150
}
]
}
}
GET/api/linkedin/jobs/{id}
Auth Required

Get LinkedIn Job

Get a LinkedIn scraping job by ID.

Path Parameters

NameTypeRequiredDescription
idstringRequiredJob ID

Example Request

curl -X GET "https://scrappy.gg/api/linkedin/jobs/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Job returned
{
"success": true,
"data": {}
}
POST/api/linkedin/commonalities
Auth Required

Batch Commonalities Analysis

Analyze common traits across a batch of LinkedIn profiles using AI. Useful for identifying shared backgrounds, schools, companies, or interests. Requires PERPLEXITY_API_KEY.

Rate Limited: 10 requests per 1h per user

Request Body

NameTypeRequiredDescription
profileUrlsarrayRequiredArray of LinkedIn profile URLs to analyze
contextstringOptionalAdditional context for the analysis

Example Request

curl -X POST "https://scrappy.gg/api/linkedin/commonalities" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileUrls": []
}'

Responses

200Commonalities analysis
{
"success": true,
"data": {
"commonalities": [
"All attended Ivy League universities",
"Previously worked in investment banking",
"Active in real estate investing communities"
],
"analyzedProfiles": 12,
"confidence": 0.85
}
}
503PERPLEXITY_API_KEY not configured
{
"success": false,
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "AI service not configured"
}
}

Warmup Domains

Email domain warming system — 5-week progressive warmup for improved deliverability

7 endpoints
GET/api/warmup-domains
Auth Required

List Warmup Domains

Get all warmup domains for the current user.

Example Request

curl -X GET "https://scrappy.gg/api/warmup-domains" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Warmup domains returned
{
"success": true,
"data": {
"warmupDomains": [
{
"id": "wd_123",
"domain": "outreach.acme.com",
"status": "WARMING",
"currentDailyVolume": 25,
"targetDailyVolume": 100,
"weekNumber": 2,
"openRate": 0.42,
"bounceRate": 0.02
}
]
}
}
POST/api/warmup-domains
Auth Required

Create Warmup Domain

Register a new domain for warmup. The domain must have valid SPF and DKIM before warmup can start.

Request Body

NameTypeRequiredDescription
domainstringRequiredDomain to warm up (e.g. outreach.acme.com)
targetDailyVolumenumberOptionalTarget daily email volume after warmup (default: 100)

Example Request

curl -X POST "https://scrappy.gg/api/warmup-domains" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "string"
}'

Responses

201Warmup domain created
{
"success": true,
"data": {
"id": "wd_123",
"domain": "outreach.acme.com",
"status": "PENDING_DNS",
"spfValid": false,
"dkimValid": false
}
}
GET/api/warmup-domains/{id}
Auth Required

Get Warmup Domain

Get a warmup domain by ID with full metrics.

Path Parameters

NameTypeRequiredDescription
idstringRequiredWarmup domain ID

Example Request

curl -X GET "https://scrappy.gg/api/warmup-domains/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Warmup domain returned
{
"success": true,
"data": {
"id": "wd_123",
"domain": "outreach.acme.com",
"status": "WARMING",
"spfValid": true,
"dkimValid": true,
"dmarcValid": true,
"currentDailyVolume": 25,
"openRate": 0.42,
"replyRate": 0.08,
"bounceRate": 0.02,
"spamRate": 0.001
}
}
PATCH/api/warmup-domains/{id}
Auth Required

Update Warmup Domain

Update warmup domain settings (e.g. pause/resume).

Path Parameters

NameTypeRequiredDescription
idstringRequiredWarmup domain ID

Request Body

NameTypeRequiredDescription
statusstringOptionalOverride statusOptions: PAUSED, WARMING
targetDailyVolumenumberOptionalUpdated target daily volume

Example Request

curl -X PATCH "https://scrappy.gg/api/warmup-domains/{id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Domain updated
{
"success": true,
"data": {}
}
DELETE/api/warmup-domains/{id}
Auth Required

Delete Warmup Domain

Remove a warmup domain.

Path Parameters

NameTypeRequiredDescription
idstringRequiredWarmup domain ID

Example Request

curl -X DELETE "https://scrappy.gg/api/warmup-domains/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Domain deleted
{
"success": true
}
POST/api/warmup-domains/{id}/start
Auth Required

Start Warmup

Begin the 5-week progressive warmup schedule. Requires valid SPF and DKIM records. Warmup progresses: 5→10→25→50→100 emails/day. Auto-pauses if bounce rate >10% or spam rate >0.5%.

Path Parameters

NameTypeRequiredDescription
idstringRequiredWarmup domain ID

Example Request

curl -X POST "https://scrappy.gg/api/warmup-domains/{id}/start" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Warmup started
{
"success": true,
"data": {
"status": "WARMING",
"startedAt": "2024-01-15T10:00:00Z",
"estimatedCompletionWeeks": 5
}
}
400DNS not configured
{
"success": false,
"error": {
"code": "DNS_NOT_CONFIGURED",
"message": "SPF and DKIM records are required before warmup can start"
}
}
POST/api/warmup-domains/{id}/metrics
Auth Required

Update Warmup Metrics

Update sending metrics (open rate, reply rate, bounce rate, spam rate). Used to feed real delivery data back into the warmup monitor.

Path Parameters

NameTypeRequiredDescription
idstringRequiredWarmup domain ID

Request Body

NameTypeRequiredDescription
openRatenumberOptionalOpen rate (0–1)
replyRatenumberOptionalReply rate (0–1)
bounceRatenumberOptionalBounce rate (0–1). >0.10 triggers auto-pause.
spamRatenumberOptionalSpam rate (0–1). >0.005 triggers auto-pause.
emailsSentnumberOptionalEmails sent today

Example Request

curl -X POST "https://scrappy.gg/api/warmup-domains/{id}/metrics" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Metrics updated
{
"success": true,
"data": {
"status": "WARMING",
"bounceRateAlert": false,
"spamRateAlert": false
}
}

AngelSend Integration

Cold email outreach platform integration — export leads, send emails, AI personalization, and domain warmup

17 endpoints
GET/api/angelsend/status
Auth Required

Connection Status

Check if AngelSend is connected and return account info.

Example Request

curl -X GET "https://scrappy.gg/api/angelsend/status" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Status returned
{
"success": true,
"data": {
"connected": true,
"email": "user@example.com",
"plan": "pro"
}
}
GET/api/angelsend/credits
Auth Required

Get AngelSend Credits

Get the current AngelSend credit balance.

Example Request

curl -X GET "https://scrappy.gg/api/angelsend/credits" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Credits returned
{
"success": true,
"data": {
"credits": 5000,
"used": 1200
}
}
POST/api/angelsend/leads/export
Auth Required

Export Leads to AngelSend

Export selected leads to an AngelSend project for outreach.

Request Body

NameTypeRequiredDescription
leadIdsarrayRequiredArray of lead IDs to export
projectNamestringOptionalAngelSend project to export into

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/leads/export" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadIds": []
}'

Responses

200Leads exported
{
"success": true,
"data": {
"exported": 45,
"failed": 2
}
}
POST/api/angelsend/send-email
Auth Required

Send Single Email

Send an individual email via AngelSend.

Request Body

NameTypeRequiredDescription
tostringRequiredRecipient email address
subjectstringRequiredEmail subject
bodystringRequiredEmail body (HTML or plain text)
leadIdstringOptionalAssociated lead ID

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/send-email" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "string",
"subject": "string",
"body": "string"
}'

Responses

200Email sent
{
"success": true,
"data": {
"messageId": "msg_123"
}
}
POST/api/angelsend/send-emails
Auth Required

Send Bulk Emails

Send emails to multiple leads in one request.

Request Body

NameTypeRequiredDescription
emailsarrayRequiredArray of { to, subject, body, leadId } objects

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/send-emails" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": []
}'

Responses

200Bulk send results
{
"success": true,
"data": {
"sent": 48,
"failed": 2
}
}
POST/api/angelsend/generate-emails
Auth Required

Generate Email Copy

Use AI to generate personalized email copy for leads.

Request Body

NameTypeRequiredDescription
leadIdsarrayRequiredLead IDs to generate emails for
templatestringOptionalEmail template or campaign context
tonestringOptionalEmail toneOptions: professional, casual, friendly

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/generate-emails" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadIds": []
}'

Responses

200Generated emails
{
"success": true,
"data": {
"emails": [
{
"leadId": "lead_123",
"subject": "Quick question about Acme",
"body": "Hi John, ..."
}
]
}
}
POST/api/angelsend/personalize
Auth Required

Personalize Email

AI-personalize an existing email template for a specific lead.

Request Body

NameTypeRequiredDescription
leadIdstringRequiredLead ID
templatestringRequiredBase email template

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/personalize" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadId": "abc123",
"template": "string"
}'

Responses

200Personalized email
{
"success": true,
"data": {
"subject": "...",
"body": "..."
}
}
POST/api/angelsend/verify-emails
Auth Required

Verify Emails via AngelSend

Trigger email verification for leads through AngelSend.

Request Body

NameTypeRequiredDescription
leadIdsarrayRequiredLead IDs to verify

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/verify-emails" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadIds": []
}'

Responses

200Verification queued
{
"success": true
}
POST/api/angelsend/ai/analyze-intent
Auth Required

Analyze Reply Intent

Use AI to analyze the intent of a reply email (interested, not interested, objection, etc.).

Request Body

NameTypeRequiredDescription
emailBodystringRequiredReply email body to analyze
leadIdstringOptionalAssociated lead ID

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/ai/analyze-intent" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emailBody": "user@example.com"
}'

Responses

200Intent analysis
{
"success": true,
"data": {
"intent": "INTERESTED",
"confidence": 0.92,
"summary": "Lead expressed interest and asked for a call",
"suggestedAction": "Schedule a call"
}
}
POST/api/angelsend/ai/auto-reply
Auth Required

Generate Auto-Reply

Generate an AI-powered reply to an inbound email.

Request Body

NameTypeRequiredDescription
originalEmailstringRequiredThe original outbound email
replyEmailstringRequiredThe inbound reply to respond to
leadIdstringOptionalAssociated lead ID

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/ai/auto-reply" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"originalEmail": "user@example.com",
"replyEmail": "user@example.com"
}'

Responses

200Generated reply
{
"success": true,
"data": {
"subject": "Re: ...",
"body": "Thanks for getting back to me..."
}
}
POST/api/angelsend/ai/follow-ups
Auth Required

Generate Follow-up Sequence

Generate a multi-step follow-up email sequence for a campaign.

Request Body

NameTypeRequiredDescription
leadIdstringRequiredLead ID
initialEmailstringRequiredThe initial outbound email
stepsnumberOptionalNumber of follow-up steps (default: 3)

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/ai/follow-ups" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leadId": "abc123",
"initialEmail": "user@example.com"
}'

Responses

200Follow-up sequence
{
"success": true,
"data": {
"followUps": [
{
"dayOffset": 3,
"subject": "Following up",
"body": "Just checking in..."
},
{
"dayOffset": 7,
"subject": "Last try",
"body": "One more thought..."
}
]
}
}
POST/api/angelsend/import/apollo
Auth Required

Import from Apollo

Import leads from an Apollo.io CSV export into Scrappy.

Request Body

NameTypeRequiredDescription
filefileRequiredApollo CSV export file
projectIdstringOptionalTarget project ID

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/import/apollo" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: multipart/form-data" \
-d '{
"file": ""
}'

Responses

200Import results
{
"success": true,
"data": {
"imported": 200,
"duplicates": 15,
"errors": 2
}
}
POST/api/angelsend/import/hunter
Auth Required

Import from Hunter

Import leads from a Hunter.io CSV export into Scrappy.

Request Body

NameTypeRequiredDescription
filefileRequiredHunter CSV export file
projectIdstringOptionalTarget project ID

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/import/hunter" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: multipart/form-data" \
-d '{
"file": ""
}'

Responses

200Import results
{
"success": true,
"data": {
"imported": 100,
"duplicates": 5,
"errors": 0
}
}
POST/api/angelsend/warmup
Auth Required

Schedule Warmup

Schedule domain warmup sending via AngelSend.

Request Body

NameTypeRequiredDescription
domainstringRequiredDomain to warm up
dailyVolumenumberOptionalTarget daily send volume

Example Request

curl -X POST "https://scrappy.gg/api/angelsend/warmup" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "string"
}'

Responses

200Warmup scheduled
{
"success": true
}
GET/api/user/integrations/angelsend
Auth Required

Get User AngelSend Config

Get the current user's AngelSend API key configuration — whether a per-user key is set and whether the global environment variable applies. Change it with PUT on this same path; there is no PATCH.

Example Request

curl -X GET "https://scrappy.gg/api/user/integrations/angelsend" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Config returned
{
"success": true,
"data": {
"hasApiKey": true,
"isUsingGlobal": false
}
}
PUT/api/user/integrations/angelsend
Auth Required

Set User AngelSend Key

Set or remove the per-user AngelSend API key. { apiKey } (must start with sk_) is verified against AngelSend before it is stored and then overrides the global environment variable; { removeApiKey: true } removes it; a body with neither is 400 INVALID_INPUT, and a key AngelSend rejects is 400 INVALID_API_KEY.

Request Body

NameTypeRequiredDescription
apiKeystringOptionalPer-user AngelSend API key (sk_…), verified before it is stored
removeApiKeybooleanOptionaltrue removes the per-user key

Example Request

curl -X PUT "https://scrappy.gg/api/user/integrations/angelsend" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Saved
{
"success": true,
"data": {
"isConfigured": true,
"maskedApiKey": "sk_****a1b2",
"message": "AngelSend API key saved successfully"
}
}
200Removed
{
"success": true,
"data": {
"isConfigured": false,
"message": "AngelSend API key removed"
}
}
400Neither field sent
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "Either apiKey or removeApiKey must be provided"
}
}
400Key rejected by AngelSend
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid AngelSend API key. Please check your key and try again."
}
}
DELETE/api/user/integrations/angelsend
Auth Required

Remove User AngelSend Key

Remove the per-user AngelSend API key so the global environment variable applies again.

Example Request

curl -X DELETE "https://scrappy.gg/api/user/integrations/angelsend" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Key removed
{
"success": true,
"data": {
"message": "AngelSend API key removed"
}
}

Credits

Credit balance and purchasing

3 endpoints
GET/api/credits/balance
Auth Required

Get Credit Balance

Get the current user's credit balance

Example Request

curl -X GET "https://scrappy.gg/api/credits/balance" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Balance returned
{
"success": true,
"data": {
"balance": 500,
"configured": true,
"costs": {
"scrape": 1,
"emailValidation": 0.5,
"aiDiscovery": 5
}
}
}
GET/api/credits/pricing
Public

Get Pricing Tiers

Get available credit pricing tiers for purchase. Public — no authentication required.

Example Request

curl -X GET "https://scrappy.gg/api/credits/pricing"

Responses

200Pricing returned
{
"success": true,
"data": {
"tiers": [
{
"id": "starter",
"name": "Starter",
"credits": 500,
"price": 9
},
{
"id": "growth",
"name": "Growth",
"credits": 2000,
"price": 29
},
{
"id": "scale",
"name": "Scale",
"credits": 6000,
"price": 79
}
],
"configured": true
}
}
POST/api/credits/checkout
Auth Required

Create Checkout Session

Create a checkout session to purchase credits

Request Body

NameTypeRequiredDescription
pricingTierIdstringRequiredID of the pricing tier to purchase

Example Request

curl -X POST "https://scrappy.gg/api/credits/checkout" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pricingTierId": "abc123"
}'

Responses

200Checkout created
{
"success": true,
"data": {
"url": "https://checkout.stripe.com/..."
}
}
503Credits not configured
{
"success": false,
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Credits system not configured"
}
}

Markdown Exports

Export websites to markdown format

7 endpoints
GET/api/markdown-exports
Auth Required

List Exports

Get all markdown exports

Example Request

curl -X GET "https://scrappy.gg/api/markdown-exports" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Exports returned
{
"success": true,
"data": {
"exports": [
{
"id": "abc123",
"name": "Example Site",
"url": "https://example.com",
"status": "COMPLETED",
"pageCount": 25
}
]
}
}
POST/api/markdown-exports
Auth Required

Create Export

Create a new markdown export job

Request Body

NameTypeRequiredDescription
namestringRequiredExport name
urlstringRequiredURL to export
exportTypestringOptionalExport typeOptions: SINGLE_PAGE, FULL_SITEDefault: SINGLE_PAGE
maxPagesnumberOptionalMaximum pages to export (1-1000)Default: 100
followExternalbooleanOptionalFollow external links
useSitemapbooleanOptionalUse sitemap.xml for discovery

Example Request

curl -X POST "https://scrappy.gg/api/markdown-exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"url": "https://example.com"
}'

Responses

201Export created
{
"success": true,
"data": {
"export": {
"id": "abc123",
"status": "PENDING"
}
}
}
GET/api/markdown-exports/[id]
Auth Required

Get Export

Get export details

Path Parameters

NameTypeRequiredDescription
idstringRequiredExport ID

Example Request

curl -X GET "https://scrappy.gg/api/markdown-exports/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Export returned
{
"success": true,
"data": {
"export": {
"id": "abc123",
"status": "COMPLETED"
}
}
}
DELETE/api/markdown-exports/[id]
Auth Required

Delete Export

Delete an export

Path Parameters

NameTypeRequiredDescription
idstringRequiredExport ID

Example Request

curl -X DELETE "https://scrappy.gg/api/markdown-exports/abc123" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Export deleted
{
"success": true
}
GET/api/markdown-exports/[id]/pages
Auth Required

Get Export Pages

Get all pages from an export with their markdown content

Path Parameters

NameTypeRequiredDescription
idstringRequiredExport ID

Example Request

curl -X GET "https://scrappy.gg/api/markdown-exports/abc123/pages" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Pages returned
{
"success": true,
"data": {
"pages": [
{
"id": "page123",
"url": "https://example.com/page",
"title": "Page Title",
"markdown": "# Page Content..."
}
]
}
}
GET/api/markdown-exports/[id]/download
Auth Required

Download Export

Download export as ZIP or single markdown file

Path Parameters

NameTypeRequiredDescription
idstringRequiredExport ID

Query Parameters

NameTypeRequiredDescription
formatstringOptionalDownload formatOptions: zip, singleDefault: zip

Example Request

curl -X GET "https://scrappy.gg/api/markdown-exports/abc123/download" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200File download
{
"note": "Returns file attachment"
}
404No content
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Export has no content"
}
}
POST/api/markdown-exports/[id]/retry
Auth Required

Retry Export

Retry a failed export

Path Parameters

NameTypeRequiredDescription
idstringRequiredExport ID

Example Request

curl -X POST "https://scrappy.gg/api/markdown-exports/abc123/retry" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Export retry started
{
"success": true,
"message": "Export retry started"
}

Analytics

Pipeline overview, source performance, capacity planning, and credit burn rate metrics

4 endpoints
GET/api/analytics/pipeline-overview
Auth Required

Pipeline Overview

Get high-level pipeline metrics: lead counts by status, conversion rates, email validation rates, and recent activity.

Query Parameters

NameTypeRequiredDescription
projectIdstringOptionalFilter by project ID
daysnumberOptionalLookback window in days (default: 30)

Example Request

curl -X GET "https://scrappy.gg/api/analytics/pipeline-overview" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Pipeline overview returned
{
"success": true,
"data": {
"totalLeads": 1250,
"leadsByStatus": {
"NEW": 420,
"CONTACTED": 380,
"QUALIFIED": 210,
"UNQUALIFIED": 180,
"ARCHIVED": 60
},
"emailValidationRate": 0.84,
"newLeadsLast7Days": 145,
"conversionRate": 0.168
}
}
GET/api/analytics/sources
Auth Required

Source Analytics

Get performance metrics for each source: lead yield, success rate, and average run time.

Query Parameters

NameTypeRequiredDescription
projectIdstringOptionalFilter by project ID
daysnumberOptionalLookback window in days (default: 30)

Example Request

curl -X GET "https://scrappy.gg/api/analytics/sources" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Source analytics returned
{
"success": true,
"data": {
"sources": [
{
"sourceId": "src_123",
"sourceName": "NYC Dentist Directory",
"totalJobs": 12,
"successRate": 0.92,
"totalLeads": 340,
"avgLeadsPerJob": 28,
"avgRunTimeMs": 45000,
"lastRunAt": "2024-01-15T08:00:00Z"
}
]
}
}
GET/api/analytics/capacity
Auth Required

Capacity Planning

Get capacity metrics including active sources, scheduled jobs, job queue depth, and estimated throughput.

Example Request

curl -X GET "https://scrappy.gg/api/analytics/capacity" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Capacity data returned
{
"success": true,
"data": {
"activeSources": 24,
"scheduledJobsNext24h": 8,
"queueDepth": 3,
"estimatedLeadsPerDay": 450,
"workerUtilization": 0.35
}
}
GET/api/analytics/credit-burn-rate
Auth Required

Credit Burn Rate

Get credit usage trends and burn rate projection.

Query Parameters

NameTypeRequiredDescription
daysnumberOptionalLookback window in days (default: 30)

Example Request

curl -X GET "https://scrappy.gg/api/analytics/credit-burn-rate" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Credit burn rate returned
{
"success": true,
"data": {
"currentBalance": 8500,
"dailyBurnRate": 125,
"projectedDaysRemaining": 68,
"burnByCategory": {
"emailValidation": 80,
"aiFeatures": 30,
"scraping": 15
},
"usageTrend": [
{
"date": "2024-01-14",
"credits": 120
},
{
"date": "2024-01-15",
"credits": 135
}
]
}
}

Admin

Admin-only endpoints for platform management

6 endpoints
GET/api/admin/users
Admin Only

List All Users

Get all users with their statistics (admin only)

Example Request

curl -X GET "https://scrappy.gg/api/admin/users" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Users returned
{
"success": true,
"data": {
"users": [
{
"id": "abc123",
"email": "user@example.com",
"stats": {
"projects": 5,
"sources": 20,
"leads": 500,
"jobs": 50
}
}
],
"summary": {
"total": 100,
"active": 85,
"newThisWeek": 10
}
}
}
403Not admin
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "Admin access required"
}
}
POST/api/admin/users
Admin Only

Create User

Create a user with a default project (admin only). Validation failures answer 400 with { success: false, error: 'Validation failed', details } rather than the standard error object.

Request Body

NameTypeRequiredDescription
emailstringRequiredEmail address (≤254)
passwordstringRequiredPassword (8–128 chars)
namestringOptionalDisplay name (≤100); defaults to the local part of the email

Example Request

curl -X POST "https://scrappy.gg/api/admin/users" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "your-password"
}'

Responses

201User created
{
"success": true,
"data": {
"id": "abc123",
"email": "new@example.com",
"name": "new",
"createdAt": "2024-01-01T00:00:00Z"
}
}
409Email already taken
{
"success": false,
"error": "A user with this email already exists"
}
DELETE/api/admin/users
Admin Only

Delete User

Delete a user by id (admin only). Admin users cannot be deleted through the API.

Query Parameters

NameTypeRequiredDescription
idstringRequiredUser id to delete

Example Request

curl -X DELETE ?id=abc123"https://scrappy.gg/api/admin/users" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Deleted
{
"success": true
}
403Target is an admin
{
"success": false,
"error": "Cannot delete an admin user"
}
404Unknown user
{
"success": false,
"error": "User not found"
}
GET/api/admin/logs
Admin Only

Application Logs

Stored AppLog rows, newest first (admin only) — the first-party log store that outlives Fly's ~100-line buffer, so 'what happened at 03:00' is answerable a day later. Cursor-paginated: pass the previous page's nextCursor.

Query Parameters

NameTypeRequiredDescription
levelstringOptionalLog levelOptions: debug, info, metric, warn, error
servicestringOptionalService name, e.g. app, worker, steel (≤64)
hostnamestringOptionalMachine hostname (≤64)
qstringOptionalCase-insensitive substring of the message (≤200)
sincestringOptionalISO 8601 lower bound on timestamp
untilstringOptionalISO 8601 upper bound on timestamp
limitnumberOptionalRows per page (1–500)Default: 100
cursorstringOptionalId of the last row from the previous page

Example Request

curl -X GET "https://scrappy.gg/api/admin/logs" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Logs — nextCursor is null on the last page
{
"success": true,
"data": {
"logs": [
{
"id": "log_1",
"timestamp": "2026-09-24T03:00:00Z",
"level": "warn",
"service": "app",
"hostname": "832476",
"msg": "slow_operation",
"meta": {}
}
],
"nextCursor": "log_1"
}
}
GET/api/admin/metrics/summary
Admin Only

Get Metrics Summary

Get aggregated platform metrics (admin only). Note: Response does not use standard wrapper format.

Example Request

curl -X GET "https://scrappy.gg/api/admin/metrics/summary" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Metrics returned
{
"database": {
"users": 100,
"projects": 500,
"sources": 1000,
"leads": 50000,
"jobs": 2000
},
"queues": {
"waiting": {
"scraping": 5,
"emailValidation": 10
},
"active": {
"scraping": 2,
"emailValidation": 5
},
"failed": {
"scraping": 0,
"emailValidation": 1
}
},
"timestamp": "2024-01-01T00:00:00Z"
}
GET/api/admin/metrics/prometheus
Admin Only

Prometheus Query

Proxy Prometheus queries (admin only)

Query Parameters

NameTypeRequiredDescription
querystringRequiredPrometheus query string
typestringOptionalQuery typeOptions: instant, rangeDefault: instant
startstringOptionalStart time for range queries (ISO 8601)
endstringOptionalEnd time for range queries (ISO 8601)
stepstringOptionalQuery resolution stepDefault: 15s

Example Request

curl -X GET ?query=search term"https://scrappy.gg/api/admin/metrics/prometheus" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Prometheus query results
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {
"__name__": "up"
},
"value": [
1704067200,
"1"
]
}
]
}
}
400Missing query
{
"error": "Query parameter is required"
}

Webhooks

Manage outbound webhooks for event notifications and receive inbound webhooks from external services

8 endpoints
GET/api/user/webhooks
Auth Required

List Webhooks

Get all configured outbound webhooks for the current user.

Example Request

curl -X GET "https://scrappy.gg/api/user/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Webhooks returned
{
"success": true,
"data": {
"webhooks": [
{
"id": "wh_123",
"url": "https://your-app.com/webhook",
"events": [
"lead.created",
"lead.updated"
],
"enabled": true,
"createdAt": "2024-01-01T00:00:00Z"
}
]
}
}
POST/api/user/webhooks
Auth Required

Create Webhook

Register an outbound webhook endpoint. Events are signed with HMAC-SHA256 via the X-Scrappy-Signature header.

Request Body

NameTypeRequiredDescription
urlstringRequiredHTTPS endpoint to receive events
eventsarrayRequiredEvent types to subscribe to: JOB_STARTED, JOB_COMPLETED, JOB_FAILED, LEAD_CREATED, EXPORT_COMPLETED, BROWSER_LOGIN_LOST, BROWSER_SESSION_ENDED. BROWSER_LOGIN_LOST fires once per breakage while a session hydrated from a profile runs, when the vendor ends that login (checked each minute): data {sessionId, apiKeyId, clientRef, profileId, origin, path, reason: sign_in_path | password_form | auth_cookie_removed, detectedAt}. origin is scheme+host and path has no query or fragment; origin and path are null for auth_cookie_removed. It re-arms when a capture from the same session saves a login again. BROWSER_SESSION_ENDED fires once when a session ends: data {sessionId, apiKeyId, clientRef, profileId, reason: released | idle_timeout | max_age | orphaned | out_of_credits | browser_gone | not_resumed, endedAt}. Neither ever carries page text, cookie names or values, or query strings. Signed like every event: X-Scrappy-Signature is the hex HMAC-SHA256 of the raw JSON body with your secret.
secretstringOptionalSecret for HMAC signature verification (auto-generated if omitted)
enabledbooleanOptionalEnable the webhook on creation (default: true)

Example Request

curl -X POST "https://scrappy.gg/api/user/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"events": []
}'

Responses

201Webhook created
{
"success": true,
"data": {
"id": "wh_123",
"url": "https://your-app.com/webhook",
"secret": "whsec_abc123...",
"events": [
"lead.created"
]
}
}
GET/api/user/webhooks/{id}
Auth Required

Get Webhook

Get a webhook by ID including recent delivery history.

Path Parameters

NameTypeRequiredDescription
idstringRequiredWebhook ID

Example Request

curl -X GET "https://scrappy.gg/api/user/webhooks/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Webhook returned
{
"success": true,
"data": {
"id": "wh_123",
"url": "https://your-app.com/webhook",
"events": [
"lead.created"
],
"enabled": true,
"deliveries": [
{
"id": "del_456",
"event": "lead.created",
"status": "SUCCESS",
"responseCode": 200,
"deliveredAt": "2024-01-15T10:30:00Z"
}
]
}
}
PATCH/api/user/webhooks/{id}
Auth Required

Update Webhook

Update a webhook's URL, events, or enabled status.

Path Parameters

NameTypeRequiredDescription
idstringRequiredWebhook ID

Request Body

NameTypeRequiredDescription
urlstringOptionalUpdated endpoint URL
eventsarrayOptionalUpdated event list
enabledbooleanOptionalEnable or disable

Example Request

curl -X PATCH "https://scrappy.gg/api/user/webhooks/{id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Webhook updated
{
"success": true,
"data": {}
}
DELETE/api/user/webhooks/{id}
Auth Required

Delete Webhook

Delete a webhook endpoint.

Path Parameters

NameTypeRequiredDescription
idstringRequiredWebhook ID

Example Request

curl -X DELETE "https://scrappy.gg/api/user/webhooks/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Webhook deleted
{
"success": true
}
POST/api/user/webhooks/{id}/deliveries/{deliveryId}/retry
Auth Required

Retry Webhook Delivery

Manually retry a failed webhook delivery. Auto-retry uses 3 attempts with exponential backoff (5s / 25s / 125s).

Path Parameters

NameTypeRequiredDescription
idstringRequiredWebhook ID
deliveryIdstringRequiredDelivery ID to retry

Example Request

curl -X POST "https://scrappy.gg/api/user/webhooks/{id}/deliveries/{deliveryId}/retry" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Delivery retry queued
{
"success": true,
"data": {
"retryJobId": "job_789"
}
}
POST/api/webhooks/cred
Public

Cred.diy Webhook (Inbound)

Inbound webhook from cred.diy credits system. Verified via HMAC-SHA256 signature. Handles credit top-ups and subscription events.

Request Body

NameTypeRequiredDescription
x-cred-signatureheaderRequiredHMAC-SHA256 signature for verification
x-cred-eventheaderRequiredEvent type (e.g. credit.purchased, subscription.updated)

Example Request

curl -X POST "https://scrappy.gg/api/webhooks/cred" \
-H "Content-Type: application/json" \
-d '{
"x-cred-signature": "",
"x-cred-event": ""
}'

Responses

200Webhook received and processed
{
"received": true
}
401Invalid signature
{
"error": "Invalid signature"
}
POST/api/webhooks/resend
Public

Resend Email Webhook (Inbound)

Inbound webhook from Resend for transactional email delivery events (bounces, complaints, etc.).

Request Body

NameTypeRequiredDescription
svix-signatureheaderRequiredSvix signature for verification
typestringRequiredEvent type (e.g. email.bounced, email.complained)

Example Request

curl -X POST "https://scrappy.gg/api/webhooks/resend" \
-H "Content-Type: application/json" \
-d '{
"svix-signature": "",
"type": "string"
}'

Responses

200Webhook received
{
"received": true
}

Enterprise

Organizations, API keys, source templates, proxy profiles, suppression lists, and data destinations

32 endpoints
GET/api/organizations
Auth Required

List Organizations

Get all organizations the current user belongs to.

Example Request

curl -X GET "https://scrappy.gg/api/organizations" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Organizations returned
{
"success": true,
"data": {
"organizations": [
{
"id": "org_123",
"name": "Acme Corp",
"role": "OWNER",
"memberCount": 8
}
]
}
}
POST/api/organizations
Auth Required

Create Organization

Create a new organization.

Request Body

NameTypeRequiredDescription
namestringRequiredOrganization name
domainstringOptionalCompany domain (for SSO)

Example Request

curl -X POST "https://scrappy.gg/api/organizations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name"
}'

Responses

201Organization created
{
"success": true,
"data": {
"id": "org_123",
"name": "Acme Corp"
}
}
GET/api/organizations/{id}
Auth Required

Get Organization

Get organization details.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Example Request

curl -X GET "https://scrappy.gg/api/organizations/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Organization returned
{
"success": true,
"data": {}
}
GET/api/organizations/{id}/members
Auth Required

List Members

Get all members of an organization.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Example Request

curl -X GET "https://scrappy.gg/api/organizations/{id}/members" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Members returned
{
"success": true,
"data": {
"members": [
{
"userId": "u_123",
"email": "admin@acme.com",
"role": "OWNER",
"joinedAt": "2024-01-01"
}
]
}
}
POST/api/organizations/{id}/members
Auth Required

Invite Member

Invite an existing account to the organization by email (ADMIN role required). The account is NOT a member until it accepts the invitation, and until then it does not appear in the member list. The response is identical whether or not the address has an account, is already a member, or was already invited. Session only: an API key cannot call this.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Request Body

NameTypeRequiredDescription
emailstringRequiredEmail address to invite
rolestringOptionalRole the invitee gets on acceptingOptions: ADMIN, EDITOR, VIEWERDefault: VIEWER

Example Request

curl -X POST "https://scrappy.gg/api/organizations/{id}/members" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'

Responses

202Invitation recorded (if the address has an account)
{
"success": true,
"data": {
"invited": true,
"message": "If that email belongs to a Scrappy account, an invitation has been sent. They join once they accept it."
}
}
403Requester is not an ADMIN
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "Forbidden"
}
}
PATCH/api/organizations/{id}/members
Auth Required

Change Member Role

Change an active member's role (ADMIN role required). The owner and the last remaining admin cannot be demoted. Session only.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Request Body

NameTypeRequiredDescription
userIdstringRequiredMember's user id
rolestringRequiredNew roleOptions: ADMIN, EDITOR, VIEWER

Example Request

curl -X PATCH "https://scrappy.gg/api/organizations/{id}/members" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"userId": "abc123",
"role": "string"
}'

Responses

200Role changed
{
"success": true,
"data": {
"userId": "u_456",
"role": "EDITOR"
}
}
409Would leave the organization without an admin
{
"success": false,
"error": {
"code": "LAST_ADMIN",
"message": "An organization must keep at least one admin, and its owner cannot be removed or demoted."
}
}
DELETE/api/organizations/{id}/members
Auth Required

Remove Member

Remove a member (ADMIN role required, or a member removing themselves) by the userId query parameter, or withdraw a pending invitation by the email query parameter (ADMIN only; answers the same whether or not one existed). The owner and the last remaining admin cannot be removed. Session only.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Query Parameters

NameTypeRequiredDescription
userIdstringOptionalUser id to remove
emailstringOptionalInvited address whose pending invitation to withdraw

Example Request

curl -X DELETE "https://scrappy.gg/api/organizations/{id}/members" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Member removed
{
"success": true
}
409Would leave the organization without an admin
{
"success": false,
"error": {
"code": "LAST_ADMIN",
"message": "An organization must keep at least one admin, and its owner cannot be removed or demoted."
}
}
GET/api/organizations/invitations
Auth Required

My Invitations

Invitations waiting for the signed-in user's answer.

Example Request

curl -X GET "https://scrappy.gg/api/organizations/invitations" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Pending invitations
{
"success": true,
"data": [
{
"orgId": "org_123",
"orgName": "Acme",
"role": "VIEWER",
"invitedAt": "2026-10-01T12:00:00.000Z",
"invitedBy": {
"name": "Ada",
"email": "ada@acme.com"
}
}
]
}
POST/api/organizations/{id}/invitation
Auth Required

Accept or Decline an Invitation

The invitee's answer to a pending invitation. Accepting is what makes the caller a member. Session only.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Request Body

NameTypeRequiredDescription
actionstringRequiredThe answerOptions: accept, decline

Example Request

curl -X POST "https://scrappy.gg/api/organizations/{id}/invitation" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "string"
}'

Responses

200Answer recorded
{
"success": true,
"data": {
"action": "accept",
"member": true
}
}
404No pending invitation for the caller
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "No pending invitation to this organization"
}
}
GET/api/organizations/{id}/sso
Auth Required

SSO Configuration

Get the organization's SSO configuration (null when none is configured).

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Example Request

curl -X GET "https://scrappy.gg/api/organizations/{id}/sso" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200SSO config returned
{
"success": true,
"data": {
"orgId": "org_123",
"provider": "SAML",
"metadataUrl": "https://idp.example.com/metadata",
"enabled": true
}
}
POST/api/organizations/{id}/sso
Auth Required

Configure SSO

Set the organization's SSO provider (ADMIN role required). Returns the stored configuration.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Request Body

NameTypeRequiredDescription
providerstringRequiredIdentity provider protocolOptions: SAML, OIDC
metadataUrlstringOptionalIdP metadata URL (≤2048)
clientIdstringOptionalOIDC client id (≤500)
clientSecretstringOptionalOIDC client secret (≤10000)

Example Request

curl -X POST "https://scrappy.gg/api/organizations/{id}/sso" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "abc123"
}'

Responses

200SSO configured
{
"success": true,
"data": {
"orgId": "org_123",
"provider": "OIDC",
"enabled": true
}
}
GET/api/organizations/{id}/scim
Auth Required

SCIM Configuration

Get the organization's SCIM provisioning config (null when none). The bearer token is never returned.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Example Request

curl -X GET "https://scrappy.gg/api/organizations/{id}/scim" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200SCIM config returned
{
"success": true,
"data": {
"id": "scim_1",
"orgId": "org_123",
"enabled": true,
"lastSyncAt": null,
"createdAt": "2024-01-01T00:00:00Z"
}
}
POST/api/organizations/{id}/scim
Auth Required

Configure SCIM

Set the SCIM bearer token the identity provider will use (ADMIN role required). Returns the config without the token.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Request Body

NameTypeRequiredDescription
bearerTokenstringRequiredToken the IdP presents (20–500 chars)
enabledbooleanOptionalAccepted but not read by the route

Example Request

curl -X POST "https://scrappy.gg/api/organizations/{id}/scim" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"bearerToken": "string"
}'

Responses

200SCIM configured
{
"success": true,
"data": {
"id": "scim_1",
"orgId": "org_123",
"enabled": true
}
}
GET/api/organizations/{id}/defaults
Auth Required

Organization Defaults

Get the organization's default settings (branding).

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Example Request

curl -X GET "https://scrappy.gg/api/organizations/{id}/defaults" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Defaults returned
{
"success": true,
"data": {
"id": "org_123",
"brandingConfig": {
"primaryColor": "#3b82f6"
}
}
}
PATCH/api/organizations/{id}/defaults
Auth Required

Update Organization Defaults

Update the organization's default settings (ADMIN role required).

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Request Body

NameTypeRequiredDescription
brandingConfigobjectOptionalBranding key/values (≤50 keys, ≤16KB)

Example Request

curl -X PATCH "https://scrappy.gg/api/organizations/{id}/defaults" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Defaults updated
{
"success": true,
"data": {
"id": "org_123",
"brandingConfig": {
"primaryColor": "#3b82f6"
}
}
}
GET/api/organizations/{id}/ip-allowlist
Auth Required

IP Allowlist

List the organization's IP allowlist entries. Once any entry exists, org routes refuse requests from outside the allowlist.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Example Request

curl -X GET "https://scrappy.gg/api/organizations/{id}/ip-allowlist" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Allowlist returned
{
"success": true,
"data": [
{
"id": "ipa_1",
"cidr": "192.168.1.0/24",
"label": "Office"
}
]
}
POST/api/organizations/{id}/ip-allowlist
Auth Required

Add IP Allowlist Entry

Add a CIDR range to the allowlist (ADMIN role required).

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Request Body

NameTypeRequiredDescription
cidrstringRequiredIPv4 CIDR, e.g. 203.0.113.0/24
labelstringOptionalHuman-readable label (≤100)

Example Request

curl -X POST "https://scrappy.gg/api/organizations/{id}/ip-allowlist" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"cidr": "abc123"
}'

Responses

201Entry added
{
"success": true,
"data": {
"id": "ipa_2",
"cidr": "203.0.113.0/24",
"label": "VPN"
}
}
DELETE/api/organizations/{id}/ip-allowlist
Auth Required

Remove IP Allowlist Entry

Remove an allowlist entry by id (ADMIN role required). The entry is named by the entryId query parameter.

Path Parameters

NameTypeRequiredDescription
idstringRequiredOrganization ID

Query Parameters

NameTypeRequiredDescription
entryIdstringRequiredAllowlist entry id

Example Request

curl -X DELETE ?entryId=abc123"https://scrappy.gg/api/organizations/{id}/ip-allowlist" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Entry removed
{
"success": true
}
GET/api/api-keys
Auth Required

List API Keys

Get all API keys for programmatic access.

Example Request

curl -X GET "https://scrappy.gg/api/api-keys" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200API keys returned
{
"success": true,
"data": {
"apiKeys": [
{
"id": "key_123",
"name": "Production Key",
"prefix": "sk_prod_****",
"createdAt": "2024-01-01"
}
]
}
}
POST/api/api-keys
Auth Required

Create API Key

Generate a new API key. The full key is returned only once.

Request Body

NameTypeRequiredDescription
namestringRequiredKey name/description
scopesarrayOptionalPermission scopes, e.g. ['leads:read', 'sources:write', 'browser:read', 'browser:write']. Use browser:read + browser:write for the cloud browser / @willpschulz/scrappy. '*' grants full access.
expiresAtstringOptionalISO 8601 expiration date
projectIdstring | nullOptionalDefault project for browser sessions created with this key. A project supplied by the request overrides it.

Example Request

curl -X POST "https://scrappy.gg/api/api-keys" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name"
}'

Responses

201API key created — full key shown once
{
"success": true,
"data": {
"id": "key_123",
"name": "Production Key",
"key": "sk_prod_abc123...",
"prefix": "sk_prod_****"
}
}
DELETE/api/api-keys/{id}
Auth Required

Delete API Key

Revoke an API key.

Path Parameters

NameTypeRequiredDescription
idstringRequiredAPI key ID

Example Request

curl -X DELETE "https://scrappy.gg/api/api-keys/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Key revoked
{
"success": true
}
POST/api/mcp
Auth Required

Hosted MCP Server

Stateless Model Context Protocol endpoint (Streamable HTTP) for external AI agents. Authenticate with an sk_ API key (Authorization: Bearer). Registers the API-backed tools plus the browser_* cloud-browser toolset and send_chat_message; tool calls proxy to Scrappy's REST API as the key's user, scoped and rate-limited like any API call. POST only (GET/DELETE → 405).

Request Body

NameTypeRequiredDescription
jsonrpcstringRequiredMCP JSON-RPC 2.0 envelope (e.g. tools/list, tools/call)

Example Request

curl -X POST "https://scrappy.gg/api/mcp" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "string"
}'

Responses

200MCP JSON-RPC response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
"…",
"browser_create_session",
"send_chat_message"
]
}
}
401Missing/invalid API key
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Provide a Scrappy API key: Authorization: Bearer sk_..."
}
}
GET/api/source-templates
Auth Required

List Source Templates

Get reusable source configuration templates.

Example Request

curl -X GET "https://scrappy.gg/api/source-templates" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Templates returned
{
"success": true,
"data": {
"templates": [
{
"id": "tmpl_123",
"name": "LinkedIn Directory Scraper",
"category": "Social",
"usageCount": 42
}
]
}
}
GET/api/source-templates/marketplace
Auth Required

Template Marketplace

Browse publicly shared source templates from other users.

Example Request

curl -X GET "https://scrappy.gg/api/source-templates/marketplace" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Marketplace templates
{
"success": true,
"data": {
"templates": []
}
}
POST/api/source-templates
Auth Required

Create Source Template

Create a reusable source configuration template.

Request Body

NameTypeRequiredDescription
namestringRequiredTemplate name
descriptionstringOptionalTemplate description
configobjectRequiredSource configuration to templatize
isPublicbooleanOptionalShare on marketplace

Example Request

curl -X POST "https://scrappy.gg/api/source-templates" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"config": {}
}'

Responses

201Template created
{
"success": true,
"data": {
"id": "tmpl_123"
}
}
GET/api/proxy-profiles
Auth Required

List Proxy Profiles

Get configured proxy profiles for scraping.

Example Request

curl -X GET "https://scrappy.gg/api/proxy-profiles" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Proxy profiles returned
{
"success": true,
"data": {
"proxies": [
{
"id": "proxy_123",
"name": "US Residential",
"type": "RESIDENTIAL",
"country": "US",
"successRate": 0.94
}
]
}
}
POST/api/proxy-profiles
Auth Required

Create Proxy Profile

Add a proxy profile for use in scraping jobs.

Request Body

NameTypeRequiredDescription
namestringRequiredProfile name
typestringRequiredProxy typeOptions: RESIDENTIAL, DATACENTER, MOBILE
hoststringRequiredProxy host
portnumberRequiredProxy port
usernamestringOptionalProxy username
passwordstringOptionalProxy password (encrypted at rest)
countrystringOptionalCountry code (e.g. 'US')

Example Request

curl -X POST "https://scrappy.gg/api/proxy-profiles" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"type": "string",
"host": "string",
"port": 1
}'

Responses

201Proxy profile created
{
"success": true,
"data": {
"id": "proxy_123"
}
}
GET/api/suppression-list
Auth Required

List Suppression Lists

Get email suppression lists for compliance.

Example Request

curl -X GET "https://scrappy.gg/api/suppression-list" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Suppression lists returned
{
"success": true,
"data": {
"lists": [
{
"id": "sl_123",
"name": "Global Unsubscribes",
"entryCount": 1250
}
]
}
}
GET/api/suppression-list/{id}/entries
Auth Required

List Suppression Entries

Get entries in a suppression list.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSuppression list ID

Example Request

curl -X GET "https://scrappy.gg/api/suppression-list/{id}/entries" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Entries returned
{
"success": true,
"data": {
"entries": [
{
"email": "unsubscribed@example.com",
"addedAt": "2024-01-15"
}
]
}
}
GET/api/data-destinations
Auth Required

List Data Destinations

Get configured data sync destinations (webhooks, databases, CRMs).

Example Request

curl -X GET "https://scrappy.gg/api/data-destinations" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Destinations returned
{
"success": true,
"data": {
"destinations": [
{
"id": "dest_123",
"name": "HubSpot CRM",
"type": "WEBHOOK",
"enabled": true
}
]
}
}
POST/api/data-destinations
Auth Required

Create Data Destination

Configure a new data sync destination.

Request Body

NameTypeRequiredDescription
namestringRequiredDestination name
typestringRequiredDestination typeOptions: WEBHOOK, DATABASE, CRM
configobjectRequiredDestination-specific configuration
enabledbooleanOptionalEnable sync on creation (default: true)

Example Request

curl -X POST "https://scrappy.gg/api/data-destinations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name",
"type": "string",
"config": {}
}'

Responses

201Destination created
{
"success": true,
"data": {
"id": "dest_123"
}
}
POST/api/data-destinations/{id}/sync
Auth Required

Trigger Sync

Manually trigger a data sync to a destination.

Path Parameters

NameTypeRequiredDescription
idstringRequiredDestination ID

Request Body

NameTypeRequiredDescription
leadIdsarrayOptionalSpecific lead IDs to sync (syncs all if omitted)

Example Request

curl -X POST "https://scrappy.gg/api/data-destinations/{id}/sync" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Sync triggered
{
"success": true,
"data": {
"synced": 150,
"failed": 2
}
}

Cloud Browser

Create and drive cloud browser sessions (navigate, click, type, screenshot, scrape) and embed a live viewer. Backs @willpschulz/scrappy. Requires the browser:read / browser:write API-key scopes. Multi-tenant integrations: if one API key drives browsers for many of your own users, send X-Client-Ref: <your user id> on EVERY browser request. A request that carries it can only see and drive sessions, profiles and sleeping snapshots recorded for that same clientRef — anything else answers 404 exactly as if it did not exist, including sessions recorded with no clientRef at all. Create calls take the ref from the header when the body omits it, and reject a body clientRef that disagrees with the header (400). Requests without the header keep the key-wide view, so existing integrations are unaffected until they opt in.

65 endpoints
POST/api/browser/sessions
Auth Required

Create Session

Create a cloud browser session. Reaped after 30 minutes idle — any action resets the window, and the keepalive endpoint refreshes it without acting; a 12-hour absolute cap applies as a backstop. Scope browser:write. Capped at STEEL_MAX_SESSIONS_PER_USER (default 10) concurrent per user. screenWidth/screenHeight set the browser's real window and the screen it reports, so a page cannot see a viewport larger than its own screen. Egress: a session that names an end-user — by profileId, or by clientRef/X-Client-Ref — leaves through a dedicated static residential IP chosen from that identity, so the SAME end-user gets the SAME exit on every session, across restarts and after a pin is cleared. clientRef counts because a FIRST sign-in has no profile yet (the profile is created from the capture at the end), and that is the session whose exit matters most. A profile that already carries a pinned exit replays that pin. Anonymous sessions — no profileId and no clientRef — keep the rotating gateway, so a one-off cannot spend the reputation of an IP other people's logins depend on; their exit IP is not stable between sessions. Scrappy records the exit itself at capture time — you never supply it. Profiles captured before 2026-08-25 are pinned to a rotating exit and stay on it until re-captured: one fresh sign-in per platform moves them onto a static IP. If a session fails with UPSTREAM_UNAVAILABLE, retry with rotateEgress:true to keep the login but take a fresh route. When credits are configured, costs CRED_BROWSER_SESSION_COST up front (402 on insufficient balance) plus per-minute runtime metering.

Request Body

NameTypeRequiredDescription
screenWidthnumberOptionalViewport width in px (640–3840, default 1280)
screenHeightnumberOptionalViewport height in px (480–2160, default 720)
profileIdstringOptionalPersistent profile to hydrate — the session starts already logged in, and its rotated login is saved back on release
autoCapturebooleanOptionalDefault true. Pass false when you save the profile yourself with POST /profiles/:id/capture: Scrappy then never writes this session back into its profile automatically — no periodic refresh while live, and no capture on release, sleep, idle reap or out-of-credits. Automatic saves already refuse a page that shows a sign-in form, but you can judge 'signed in' better than a structural check can. Only literal false opts out.
clientRefstringOptionalYour own end-user reference (1–200 chars; an empty string is a 400). Recorded on the session and enforced: any later request that sends X-Client-Ref must name this same value to reach the session. Defaults to the X-Client-Ref header when omitted; must match it when both are present
projectIdstringOptionalProject to tag the session to (1–64 chars; an empty string is a 400). Takes precedence over the x-project-id header, which takes precedence over the API key's bound project
showCursorbooleanOptionalDraw the software mouse cursor into the page for the live viewer. Default true for a dashboard (cookie) session and FALSE for API-key callers — it is a div injected into the page DOM that an anti-bot script can find, and an API-driven session usually has nobody watching. Sending the field explicitly (either value) wins
rotateEgressbooleanOptionalReplay the profile's login through a fresh exit IP instead of its pinned one (default false). For retrying after UPSTREAM_UNAVAILABLE — the login is fine, the route is not. Do not set it routinely: the pin exists because some logins drop the moment the exit IP changes

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Session created
{
"session": {
"id": "b1f2…",
"status": "live",
"createdAt": "2026-07-21T12:00:00Z"
},
"viewerUrl": "/api/browser/sessions/b1f2…/viewer",
"viewerTokenRequired": true,
"viewerTokenUrl": "/api/browser/sessions/b1f2…/viewer-token"
}
200Session created from a profile — pinned says whether THIS session egressed through the profile's pinned exit (false when the pin is sidelined: re-capture or retry shortly)
{
"session": {
"id": "b1f2…",
"status": "live",
"createdAt": "2026-07-21T12:00:00Z"
},
"viewerUrl": "/api/browser/sessions/b1f2…/viewer",
"viewerTokenRequired": true,
"viewerTokenUrl": "/api/browser/sessions/b1f2…/viewer-token",
"pinned": true
}
400Empty or over-long clientRef / projectId, or a body clientRef that disagrees with the X-Client-Ref header
{
"error": "clientRef in the body does not match the X-Client-Ref header"
}
402Insufficient credits, or no billing account for this user
{
"error": "Insufficient credits"
}
404profileId names a profile you cannot see, or one with no captured login yet
{
"error": "Profile not found, or it has no captured login yet"
}
429Per-user or per-key session limit, monthly minute quota, or the browserSession rate limit
{
"error": "Session limit reached (max 10). Close an existing session first."
}
502The browser service answered without a usable session, or the credit deduction failed for a reason other than balance
{
"error": "Invalid response from browser service"
}
503Global capacity (Retry-After: 30), browser service unreachable (BROWSER_SERVICE_UNAVAILABLE), or billing not configured — match on the message
{
"error": "Browser service is temporarily unavailable",
"code": "BROWSER_SERVICE_UNAVAILABLE",
"retryable": true,
"retryAfterSeconds": 15
}
GET/api/browser/sessions
Auth Required

List Sessions

List the caller's live sessions, each with a viewer URL. With X-Client-Ref set, only sessions recorded for that end user are returned. Scope browser:read.

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Live sessions
{
"sessions": [
{
"id": "b1f2…",
"status": "live",
"viewerUrl": "/api/browser/sessions/b1f2…/viewer"
}
]
}
DELETE/api/browser/sessions/{id}
Auth Required

Release Session

Release (close) a session, freeing browser capacity and stopping the runtime meter. Scope browser:write. Idempotent. The session's state is captured back onto its profile first, so cookies rotated during the session are not lost — pass capture=false to skip that. Skip it when the session is not worth storing: a replay the site rejected is sitting on a sign-in page, and capturing that overwrites a good stored login with a signed-out one. The cookie count can even go UP while the authentication goes away, so nothing downstream notices the loss. Scrappy cannot tell an authenticated session from a signed-out one; a caller that has just checked can. Accepted as ?capture=false or as {"capture": false} in the body. Only an explicit false opts out, and skipping the capture still settles billing and releases the session.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id (UUID)

Query Parameters

NameTypeRequiredDescription
capturebooleanOptionalDefault true. false releases without writing this session's state back to its profile.

Example Request

curl -X DELETE "https://scrappy.gg/api/browser/sessions/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Released
{
"released": true
}
POST/api/browser/sessions/{id}/navigate
Auth Required

Navigate

Navigate the session to an http/https URL. Scope browser:write. When the deployment has automatic CAPTCHA solving switched on, a navigation that lands on a solvable challenge clears it before answering and reports that under a `captcha` key (outcome solved/failed/capped/unsupported/budget_exceeded); the key is absent when solving is off or the page was clean, so the response shape is unchanged for everyone else. SSRF-protected (private/internal hosts blocked). Readiness is read from the document's navigation timing entry, not from readyState — Chromium sets readyState to 'interactive' when parsing ends, BEFORE deferred scripts run (measured: interactive at +91ms, the deferred script's global at +984ms). By default this answers as soon as the navigation COMMITS — the new document exists, document.readyState is still 'loading' and no script has run — so a click issued next can land on server-rendered markup whose handlers do not exist yet, and both the click API and the page report success while nothing happens. Pass waitUntil:'domcontentloaded' to return once deferred scripts and end-of-body bundles have executed (what you want before interacting), or 'load' to also wait for images, fonts and CSS. A readiness wait that runs out returns 200 with timedOut:true and the readyState reached — the page did load, it is just still busy. Failures split by whose problem they are, and the split is machine-readable — branch on error.code and error.retryable, never on the message. 422 NAVIGATION_ERROR (retryable:false) is the page failing to load and retrying changes nothing. 504 NAVIGATION_TIMEOUT (retryable:true) is a navigation that did not commit within 15s — the site sent no response headers in time, or a dialog (beforeunload included) holds the tab; when a dialog is the cause the error carries dialogOpen and a recovery hint pointing at the dialog endpoint, so answer it and retry, otherwise retry the same session. 503 UPSTREAM_UNAVAILABLE (retryable:true) is the session's egress route failing to carry the request; nothing is wrong with your call. Scrappy probes the exit before answering and retires it only if it genuinely refuses a tunnel — the same Chromium error is also what a browser-service restart looks like, and retiring a working exit for that would move your end user's IP for no reason. When it IS retired, a retry in a NEW session gets a different route; retrying against the same session keeps failing either way, since a session is bound to its exit at launch. If it came from a profile, create the retry with rotateEgress:true. 503 SESSION_RESUMING (retryable:true) is the OPPOSITE instruction and nothing to do with the proxy: Scrappy replaced the machine the browser was on (a deploy, or a suspend/resume) and is rebuilding the session under the SAME id, so retry that same session after retryAfterSeconds rather than creating a new one — it keeps its id, its cookies and its exit IP. Alert on it as 'the provider is restarting', not as an outage. Both of those apply to EVERY session action, not just navigate — a rebuild breaks a click or a screenshot the same way. 503 BROWSER_SERVICE_UNAVAILABLE (retryable:true) means the browser service could not be reached at all AND Scrappy's own session ledger has no live row for this session under your credentials — so nothing could be established about it, including whether it still exists. Retry it; it is not a statement that the session is gone. A session you created and have not released has a live ledger row, so while the browser service is unreachable it answers SESSION_RESUMING (retryAfterSeconds 30) rather than this, from the very first request: it will be parked and rebuilt under the same id, and the instruction to you is the same as after the rebuild has been recorded. A session you released answers 404 SESSION_ENDED from the ledger even while the service is down.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
urlstringRequiredAbsolute URL (max 8192 chars)
waitUntilstringOptionalcommit (default — returns before any script runs) | domcontentloaded (page globals exist; use this before clicking) | load (also images/fonts/CSS)
timeoutnumberOptionalMax wait for that readiness state in ms (500–60000, default 15000). Ignored when waitUntil is commit
tabstringOptionalTarget tab (CDP target id); defaults to the most recent page

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/navigate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com"
}'

Responses

200Navigated
{
"success": true,
"data": {
"url": "https://example.com"
}
}
200Navigated and settled (waitUntil)
{
"success": true,
"data": {
"url": "https://example.com",
"waitUntil": "domcontentloaded",
"readyState": "interactive",
"waitedMs": 412
}
}
422The page could not be loaded — not retryable
{
"success": false,
"error": {
"code": "NAVIGATION_ERROR",
"message": "net::ERR_NAME_NOT_RESOLVED",
"retryable": false
}
}
504The navigation did not commit within 15s — retry the same session; if a dialog is holding the tab, the error carries dialogOpen
{
"success": false,
"error": {
"code": "NAVIGATION_TIMEOUT",
"message": "CDP command timeout: Page.navigate",
"retryable": true
}
}
503The session's egress route failed — retry in a new session
{
"success": false,
"error": {
"code": "UPSTREAM_UNAVAILABLE",
"message": "net::ERR_TUNNEL_CONNECTION_FAILED",
"retryable": true,
"retryAfterSeconds": 15
}
}
503The session's browser was replaced and is rebuilding — retry the SAME session
{
"success": false,
"error": {
"code": "SESSION_RESUMING",
"message": "This session's browser was just rebuilt and is still settling. Retry shortly; the session id is unchanged.",
"retryable": true,
"retryAfterSeconds": 5
}
}
503The browser service could not be reached and the ledger has no live row for this session under your credentials — nothing could be established about it, including whether it exists
{
"error": "Browser service is temporarily unavailable",
"code": "BROWSER_SERVICE_UNAVAILABLE",
"retryable": true,
"retryAfterSeconds": 15
}
POST/api/browser/sessions/{id}/screenshot
Auth Required

Screenshot

Capture a screenshot as base64. Four framings, at most one per call: the viewport (the default), fullPage, a single element (ref or selector — scrolled into view and captured whole even when it is taller than the window), or an explicit clip region in page coordinates. Cropping to the element is usually what you want when checking one control or reading one card: a fraction of the pixels, and it cannot be misread as a different part of the page. Naming two framings is a 400 rather than a silent choice between them. Scope browser:write; rate-limited (browserScreenshot).

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
formatstringOptionalpng | jpeg | webp (default png)
qualitynumberOptional0-100, ignored for png
fullPagebooleanOptionalCapture the full scrollable page
refstringOptionalCapture just this element — a ref from read-page or find
selectorstringOptionalCapture just the element this CSS selector names
piercebooleanOptionalAlso search open shadow roots when resolving selector
clipobjectOptional{ x, y, width, height, scale? } in CSS pixels from the top of the page
maxEdgenumberOptionalLongest edge of the returned image, 256-8000. Rendered at the matching scale by Chrome, never upscaled; the response then carries width, height and scale (screenshot px / scale = CSS px). Use it when the image goes to a model with image-size limits
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/screenshot" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Screenshot
{
"success": true,
"data": {
"base64": "iVBORw0KGgo…",
"format": "png",
"mimeType": "image/png"
}
}
200Bounded by maxEdge
{
"success": true,
"data": {
"base64": "/9j/4AAQ…",
"format": "jpeg",
"mimeType": "image/jpeg",
"width": 1568,
"height": 811,
"scale": 0.8167
}
}
200Element framing — the response echoes the clip that was captured and the ref when one was given
{
"success": true,
"data": {
"base64": "iVBORw0KGgo…",
"format": "png",
"mimeType": "image/png",
"clip": {
"x": 120,
"y": 340,
"width": 400,
"height": 48
},
"ref": "ref_31"
}
}
400Two framings named at once
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "Choose one framing: fullPage, ref, selector or clip"
}
}
422The element has collapsed to a zero-sized box
{
"success": false,
"error": {
"code": "ELEMENT_NOT_VISIBLE",
"message": "Element for ref_31 has no visible box"
}
}
GET/api/browser/sessions/{id}/screenshot
Auth Required

Screenshot (raw bytes)

The same capture as POST, returned as raw image bytes (Content-Type image/{format}, Cache-Control no-store) for use directly in an <img src> or a file download, rather than base64 inside JSON. Viewport framing only — the element and clip framings are POST-only. Scope browser:read (the read scope, unlike POST); rate-limited (browserScreenshot).

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
formatstringOptionalpng | jpeg | webp (default png)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/screenshot" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Image bytes
{
"contentType": "image/png"
}
POST/api/browser/sessions/{id}/recording
Auth Required

Record Session (video)

Record what the session does, as an MP4. action:'start' begins capturing the active page, 'stop' finalises the file and returns a downloadUrl, 'status' reports progress, 'discard' throws it away. GET this same path for the video itself, once stopped. The capture is a CDP screencast, so the page cannot observe it and nothing is injected into it — an in-page MediaRecorder would need a user gesture and would leave getDisplayMedia fingerprints on a session whose whole point is not to look automated. Encoding happens inside the browser container, next to the frames: the alternative pushes every JPEG across the network to one of several app machines, and then the file lives on whichever machine served the start call rather than the one serving the download. Defaults to 5fps at 1280x720 — a click-by-click record at a fraction of the size of real-time video. It stops on its own at maxDurationMs, at maxBytes, or when the session is released; a recording nobody stopped must not be able to fill the disk of a box every session on that machine shares. One recording per session: starting again while one runs returns the running one rather than competing with it. Download before releasing the session — the file lives with the browser that made it. `record` is also a valid step inside /batch, which is how you wrap a whole flow in one call. Scope browser:write; rate-limited (browserRecording, 30/min).

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
actionstringOptionalstart | stop | status | discard (default status)
fpsnumberOptionalFrames per second, 1-15 (default 5)
qualitynumberOptionalJPEG quality of captured frames, 10-100 (default 60)
maxWidthnumberOptionalCapture width cap in px, 320-2560 (default 1280)
maxHeightnumberOptionalCapture height cap in px, 240-1440 (default 720)
maxDurationMsnumberOptionalStop automatically after this long, 1s-15min (default 2min)
maxBytesnumberOptionalStop automatically at this size, 1MB-512MB (default 100MB)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/recording" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Recording status
{
"success": true,
"data": {
"sessionId": "b1f2…",
"state": "stopped",
"startedAt": "2026-08-26T12:00:00.000Z",
"stoppedAt": "2026-08-26T12:00:42.000Z",
"durationMs": 42000,
"frames": 210,
"fps": 5,
"sizeBytes": 1841203,
"mimeType": "video/mp4",
"filename": "session-b1f2….mp4",
"stopReason": "requested",
"error": null,
"downloadUrl": "/api/browser/sessions/b1f2…/recording"
}
}
404Session is not live, or has no page to record
{
"success": false,
"error": {
"code": "SESSION_NOT_RECORDABLE",
"message": "Session is not live"
}
}
502The browser service failed the request (both POST and GET answer this) — ffmpeg missing from the image surfaces here, at start
{
"success": false,
"error": {
"code": "RECORDING_ERROR",
"message": "ffmpeg exited with code 1"
}
}
POST/api/browser/sessions/{id}/scrape
Auth Required

Scrape

Return page content in the shape you need. Beyond raw html/text, three simplified formats run server-side over the rendered DOM (so they see post-JavaScript state) and are far cheaper to work with: markdown strips boilerplate down to the main content, skeleton collapses repeated components to one exemplar plus the CSS selector matching all of them, and structured returns JSON-LD, microdata, OpenGraph and embedded framework state. Try structured first — when a site publishes its own data, reading it beats inferring it from markup. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
formatstringOptionalhtml (default) | text | markdown | skeleton | structured | clean
selectorstringOptionalCSS selector to scope extraction
maxCharsnumberOptionalCap output size for the simplified formats (default 200000, max 1000000)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/scrape" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Content
{
"success": true,
"data": {
"content": "<html>…",
"url": "https://example.com",
"title": "Example"
}
}
200Simplified content (markdown, skeleton, structured, clean)
{
"success": true,
"data": {
"content": "main#content\n div.results\n [repeat ×47] selector: div.card\n h3.card__title \"Ada Lovelace\"",
"url": "https://example.com",
"title": "Example",
"format": "skeleton",
"stats": {
"inputChars": 184320,
"outputChars": 1620,
"reduction": 113.8
},
"truncated": false
}
}
422Page HTML exceeds the 10MB simplifier limit
{
"success": false,
"error": {
"code": "HTML_TOO_LARGE",
"message": "HTML is 14204KB, over the 10240KB simplifier limit"
}
}
POST/api/browser/sessions/{id}/scrape-mode
Auth Required

Scrape Mode

Render pages for extraction instead of for humans. Blocks the resource categories that cost the most and contribute nothing to extracted data (images, media, fonts, trackers), then runs an in-page pass that removes cookie/consent walls, unsticks fixed headers that swallow clicks, completes the animations that keep waits from settling, promotes lazy-loaded data-src URLs to real src attributes, and pauses media. With persist (the default) it is registered as a document-start script, so every later navigation renders the same way. Pair with the simplified scrape formats for the read side. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
actionstringRequiredon | off
presetstringOptionallight (trackers only) | standard (default: images, media, fonts, trackers) | aggressive (also CSS and JS — static-HTML sites only, it leaves a client-rendered page empty)
blockarrayOptionalOverride the preset: image, media, font, stylesheet, script, tracking
striparrayOptionalOverride the in-page passes: animations, overlays, sticky, lazy, media, scrolllock
blockPatternsarrayOptionalExtra raw URL globs, e.g. "*ads.example.com*" (max 100)
persistbooleanOptionalRe-apply on every later navigation in this tab (default true)
watchbooleanOptionalKeep watching ~15s for consent walls mounted late by async vendor scripts (default true)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/scrape-mode" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "string"
}'

Responses

200Scrape mode enabled
{
"success": true,
"data": {
"enabled": true,
"preset": "standard",
"blockedCount": 28,
"strip": [
"animations",
"overlays",
"sticky",
"lazy",
"media",
"scrolllock"
],
"persisted": true,
"report": {
"overlaysRemoved": 2,
"unstuck": 0,
"lazyHydrated": 14,
"mediaPaused": 1,
"cssInjected": true,
"watching": true
}
}
}
200Scrape mode disabled
{
"success": true,
"data": {
"enabled": false,
"blocked": [],
"styleRemoved": true,
"note": "Blocking cleared and styles removed. Elements already removed from the current document return on reload."
}
}
POST/api/browser/sessions/{id}/keepalive
Auth Required

Keepalive

Mark a session as still in use. Reaping is idle-based, so any action already counts as activity and most integrations never need this. Use it when a user is viewing an embedded browser without anything being driven — reading, filling a form, completing MFA — which otherwise looks identical to an abandoned session. Returns the idle window and a recommended refresh interval — poll on that rather than hard-coding one. Takes no request body: no body, an empty body and {} are all accepted. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/keepalive" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Refreshed
{
"success": true,
"data": {
"alive": true,
"idleTimeoutMs": 1800000,
"recommendedIntervalMs": 600000
}
}
GET/api/browser/profiles
Auth Required

List Profiles

List persistent browser profiles — saved logins (cookies + localStorage) that survive session teardown, letting an agent act on a user's behalf days after they signed in. Pass clientRef to scope to one of your end-users. Stored cookies are never returned. Scope browser:read.

Query Parameters

NameTypeRequiredDescription
clientRefstringOptionalOnly profiles for this end-user reference. A filter, not a boundary — X-Client-Ref is the boundary, and a query value that disagrees with the header is rejected (400)

Example Request

curl -X GET "https://scrappy.gg/api/browser/profiles" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Profiles
{
"success": true,
"data": {
"profiles": [
{
"id": "prf_123",
"name": "acme-facebook",
"clientRef": "user_42",
"cookieCount": 18,
"capturedAt": "2026-07-22T10:00:00Z"
}
]
}
}
POST/api/browser/profiles
Auth Required

Create Profile

Create an empty profile. It holds no login until you capture one from a live session. Pin proxyUrl and userAgent for any profile you plan to reuse — replaying a session cookie from a new IP and fingerprint each run is the most common reason a persisted login gets challenged. Scope browser:write.

Request Body

NameTypeRequiredDescription
namestringRequiredProfile name, unique per account
clientRefstringOptionalYour own end-user reference. Enforced on every later read, capture, delete and profileId replay made with X-Client-Ref. Defaults to the header when omitted; must match it when both are present
proxyUrlstringOptionalPinned egress proxy for a stable exit IP
userAgentstringOptionalPinned user agent for fingerprint consistency

Example Request

curl -X POST "https://scrappy.gg/api/browser/profiles" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Name"
}'

Responses

201Created
{
"success": true,
"data": {
"id": "prf_123",
"name": "acme-facebook",
"cookieCount": 0
}
}
409Name already used
{
"error": "A profile named \"acme-facebook\" already exists"
}
POST/api/browser/profiles/{id}/capture
Auth Required

Capture Login into Profile

Save a live session's cookies and localStorage into the profile. Call this once after your end-user signs in inside the embedded viewer; from then on, sessions created with that profileId start already authenticated. Sessions released with a profileId re-capture automatically. An empty capture is rejected rather than written, so a transient failure cannot wipe a working profile. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile id

Request Body

NameTypeRequiredDescription
sessionIdstringRequiredLive session to capture the login from — must be a UUID (anything else is a 400)

Example Request

curl -X POST "https://scrappy.gg/api/browser/profiles/{id}/capture" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "abc123"
}'

Responses

200Captured
{
"success": true,
"data": {
"captured": true,
"cookieCount": 18,
"originCount": 2
}
}
400Malformed body, or sessionId is not a UUID
{
"error": "Invalid request body"
}
404The session is not one you can see — another key's, another end-user's, another project's, or already released
{
"error": "Session not found"
}
422Nothing to capture
{
"error": "Nothing captured — the profile may not exist, or the session had no cookies or storage to save"
}
PATCH/api/browser/profiles/{id}
Auth Required

Update Profile

Change how a profile is routed — name, pinned proxy, pinned user agent — without touching the login it holds. proxyUrl:null un-pins the profile so its next session re-rolls onto a fresh exit; that is the durable fix when a pinned exit is gone for good, while rotateEgress:true on session creation is the per-run one. A later capture through a working exit re-pins the profile automatically. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile id

Request Body

NameTypeRequiredDescription
namestringOptionalNew profile name
proxyUrlstringOptionalPinned egress proxy, or null to un-pin and let the next session re-roll
userAgentstringOptionalPinned user agent, or null to clear

Example Request

curl -X PATCH "https://scrappy.gg/api/browser/profiles/{id}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Updated
{
"success": true,
"data": {
"id": "prf_123",
"name": "acme-facebook",
"proxyUrl": null
}
}
400Malformed JSON, or a body with none of name / proxyUrl / userAgent — an empty patch is refused rather than answered as a no-op
{
"error": "Invalid input",
"details": []
}
404Profile not found
{
"error": "Profile not found"
}
DELETE/api/browser/profiles/{id}
Auth Required

Delete Profile

Permanently delete a profile and the encrypted login it holds. This is the revocation path — call it when an end-user disconnects their account. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredProfile id

Example Request

curl -X DELETE "https://scrappy.gg/api/browser/profiles/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Deleted
{
"success": true,
"data": {
"deleted": true
}
}
POST/api/browser/sessions/{id}/check
Auth Required

Check / Uncheck

Set the checked state of a checkbox or radio input. Use this rather than clicking one: a click TOGGLES, so re-running a step that clicked an already-checked box turns it off, which makes an idempotent retry destructive. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>) — names the element directly. Use instead of selector
selectorstringOptionalCSS selector of the checkbox or radio (or use ref)
checkedbooleanRequiredDesired state — true checks, false unchecks
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/check" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"checked": true
}'

Responses

200Set
{
"success": true,
"data": {
"checked": true
}
}
POST/api/browser/sessions/{id}/hover
Auth Required

Hover

Move the pointer over an element, by ref, CSS selector or coordinates. Menus and tooltips that only appear on hover need this before their contents exist in the DOM at all — reading the page first and finding nothing is the expected result, not a failure. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>) — names the element directly. Use instead of selector
selectorstringOptionalCSS selector of the element to hover (or use ref, or x/y)
xnumberOptionalX coordinate, with y
ynumberOptionalY coordinate, with x
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/hover" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Hovered
{
"success": true,
"data": {
"hovered": true,
"x": 120,
"y": 340
}
}
POST/api/browser/sessions/{id}/select
Auth Required

Select Option

Choose option(s) in a native <select> by value OR by visible text, addressed by ref or selector. Each wanted string is matched against the option's value exactly, then its label (trimmed, case-insensitive), then its value case-insensitively — so 'Pro plan' and 'p2' both pick <option value=p2>Pro plan</option>, and a dropdown that reads '1' and stores '1.60934400' takes '1'. Sets each option's selected flag and dispatches input and change events, so frameworks listening see it. The answer carries the live option list (value, label, selected, and disabled:true for a disabled option; first 100) so a caller with a fuzzier idea of what it wants can pick from the real names and call again. A live option is preferred over a disabled one with the same value or label; a lone disabled match is still taken and reported with disabled:true in matched. Nothing matching is 422 OPTION_NOT_FOUND with the same list under error.details — never a 200 with an empty selection, which is what it used to answer and which a run read as success while it ran at a vendor's default. Pass values (array) for a multi-select; values: [] clears it (answer carries cleared: true). Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>) — names the element directly. Use instead of selector
selectorstringOptionalCSS selector of the <select> (or use ref)
valuestringOptionalOption to select — its value attribute or its visible text
valuesarrayOptionalOptions for a multi-select, each a value or a visible text
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/select" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Selected
{
"success": true,
"data": {
"selected": [
"ca"
],
"matched": [
{
"value": "ca",
"label": "California"
}
],
"unmatched": [],
"options": [
{
"value": "ca",
"label": "California",
"selected": true
},
{
"value": "wa",
"label": "Washington",
"selected": false
}
],
"optionCount": 2
}
}
422No option matched
{
"success": false,
"error": {
"code": "OPTION_NOT_FOUND",
"message": "No option matches \"5 miles\" by value or label — pick one of the 6 options returned in details.options",
"details": {
"options": [
{
"value": "8.04672",
"label": "5",
"selected": false
}
],
"optionCount": 6,
"unmatched": [
"5 miles"
]
}
}
}
POST/api/browser/sessions/{id}/scroll
Auth Required

Scroll

Scroll the page — to a ref or selector, by pixel offset, to top/bottom, or by dispatching a real mouse-wheel at a point. Scrolling to an element is what makes it clickable by coordinates: an off-screen element reports coordinates outside the viewport, and a mouse event dispatched there hits nothing. The offset and to-element modes move the page's own scroller; `wheel` instead scrolls whatever nested scroller sits under (x, y) — a results list, an inner dashboard panel, a virtualized grid — which is the only way to reach a scrollable region you cannot name with a selector. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>) — names the element directly. Use instead of selector
selectorstringOptionalCSS selector to scroll into view (or use ref)
xnumberOptionalHorizontal pixel offset, with y (integer; default 0)
ynumberOptionalVertical pixel offset, with x (integer)
positionstringOptionaltop | bottom
wheelobjectOptionalDispatch a mouse-wheel at a point: { x, y, deltaX?, deltaY } in CSS px (positive deltaY scrolls down). x, y, deltaX and deltaY are all integers — a fractional value is a 400. Scrolls the nested pane under (x, y) rather than the window
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/scroll" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Scrolled
{
"success": true,
"data": {
"scrolled": true,
"scrollX": 0,
"scrollY": 1840
}
}
200Wheel scrolled a nested pane
{
"success": true,
"data": {
"scrolled": true,
"wheel": {
"x": 640,
"y": 400,
"deltaX": 0,
"deltaY": 600
}
}
}
POST/api/browser/sessions/{id}/press
Auth Required

Press Key

Press a key, a character, or a chord — Enter, Tab, ArrowDown, F5, 'a', 'ctrl+a', 'shift+End', 'cmd+shift+t' — via CDP key events. DOM and xdotool key names are both understood. Modifiers can be named in the chord or passed separately as a CDP bitmask (1 Alt, 2 Ctrl, 4 Meta, 8 Shift), a chord string ('ctrl+shift') or an array of DOM names (['Control', 'Shift']). With ctrl, alt or meta held the key types nothing: ctrl+a selects all, it does not insert 'a'. Letter chords were a silent no-op until 2026-09-22 (the letter's ASCII code was sent where Chrome's editing commands key off the virtual key code). Keys go to whatever currently has focus, which after a click or a navigation is frequently not the element you meant, and that failure is silent because the press itself succeeds — pass ref to focus a specific element first. repeat presses the same key up to 100 times, which is how a long native select is walked with ArrowDown. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
keystringRequiredKey name (Enter, Tab, Escape, ArrowDown, F5…), a single character (any printable one — é or 漢 types itself; a lone ' ' is the space key), or a chord joined with + (ctrl+a, shift+End)
refstringOptionalRef from read-page to focus before pressing
modifiersstringOptionalHeld modifiers, in any of three shapes — number or string or string[]: the CDP bitmask (a number 0–15: 1 Alt, 2 Ctrl, 4 Meta, 8 Shift), a chord string ('ctrl+shift'), or an array of DOM names (['Control', 'Shift'], max 4)
repeatnumberOptionalPress this many times, 1-100 (default 1)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/press" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"key": "string"
}'

Responses

200Pressed
{
"success": true,
"data": {
"pressed": true,
"key": "Enter",
"modifiers": [],
"typed": true,
"repeat": 1
}
}
200Chord pressed
{
"success": true,
"data": {
"pressed": true,
"key": "ctrl+a",
"modifiers": [
"ctrl"
],
"typed": false,
"repeat": 1
}
}
POST/api/browser/sessions/{id}/evaluate
Auth Required

Evaluate JavaScript

Run JavaScript in the page and return its result. The escape hatch for anything the typed endpoints do not cover — reading a framework's state, calling a page function, extracting a shape no selector expresses. Returns the value by value, so DOM nodes and functions do not survive; return a serialisable summary instead. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
expressionstringRequiredJavaScript to evaluate. The completion value is returned
timeoutnumberOptionalMilliseconds before giving up (default 10000)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/evaluate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expression": "string"
}'

Responses

200Result
{
"success": true,
"data": {
"result": {
"rows": 12
}
}
}
GET/api/browser/sessions/{id}/dom
Auth Required

Get DOM

A simplified DOM tree for the page. Useful when you need structure rather than semantics — read-page is the better starting point for deciding what to act on, because it reports roles, names and state as a screen reader would, and hands back refs you can act on directly. Bounded: eight levels deep, fifty children per element, thirty top-level elements of <body> — plus up to ten elements that are in front of the page and showing but the walk did not reach — dialogs, PerimeterX containers, and the fixed-position element under the middle of the viewport — appended with overlay:true, since sites append modals and walls last or nest them deep in an app root. `truncated` says whether anything was left out. It cannot see inside iframes, where captcha widgets and PerimeterX's Press & Hold live; use the captcha endpoint for those. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
tabstringOptionalTarget tab id

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/dom" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200DOM tree
{
"tree": [
{
"tag": "main",
"children": []
}
],
"truncated": false
}
GET/api/browser/sessions/{id}/console
Auth Required

Console Entries

Buffered console output captured from the page (Runtime.consoleAPICalled). Capture ARMS on your first read of a session: the CDP domain that feeds it is the single loudest automation signal a page can read (Google's sign-in acts on it), so it is not enabled on sessions that never ask for console output. History is NOT lost — Chromium replays the messages it buffered before the domain was enabled — but it arrives asynchronously, so the FIRST read is normally empty and the second carries everything, including lines logged before you ever called this. Measured against production 2026-08-29: a marker logged before the first read was absent from that read and present in the next one. Poll twice before concluding a page logged nothing. Filter before you read: a busy page emits hundreds of lines and every one of them costs you. `contains` is a case-insensitive substring, not a regex — a caller-supplied pattern would run in Scrappy's own process, where a catastrophically backtracking one blocks every other request on that machine. Each read advances your cursor past everything drained, filtered out or not, so the response reports drained/matched/dropped and `peek=true` leaves the cursor alone while you poll for one line. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
containsstringOptionalCase-insensitive substring of the message text
levelstringOptionalComma-separated levels to keep, e.g. error,warn
limitnumberOptionalReturn at most this many, newest kept (1-500)
peekbooleanOptionalLeave the read cursor where it is
tabstringOptionalTarget tab id

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/console" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Entries — peeked echoes whether the read cursor was left in place
{
"entries": [
{
"level": "error",
"text": "Uncaught TypeError"
}
],
"drained": 84,
"matched": 1,
"returned": 1,
"dropped": 83,
"truncated": false,
"peeked": false
}
GET/api/browser/sessions/{id}/network
Auth Required

Network Entries

Buffered network activity captured from the page. Shows what the page actually requested and what came back — the difference between 'the form did nothing' and 'the form posted and the server returned 422' is usually only visible here. A single page load is easily 200 requests, so filter: `contains` matches the URL (case-insensitive substring, not a regex), `method` and `type` narrow by verb and resource kind, and statusMin=400 is the fast way to ask what failed (a request still in flight has no status and is not matched). Same cursor rules as the console endpoint — the response reports drained/matched/dropped, and `peek=true` leaves the cursor alone. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
containsstringOptionalCase-insensitive substring of the request URL
methodstringOptionalComma-separated methods to keep, e.g. POST,PUT
typestringOptionalComma-separated resource types, e.g. xhr,fetch
statusMinnumberOptionalOnly responses at or above this status
limitnumberOptionalReturn at most this many, newest kept (1-500)
peekbooleanOptionalLeave the read cursor where it is
tabstringOptionalTarget tab id

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/network" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Entries — peeked echoes whether the read cursor was left in place
{
"entries": [
{
"method": "POST",
"url": "/api/save",
"status": 422
}
],
"drained": 212,
"matched": 1,
"returned": 1,
"dropped": 211,
"truncated": false,
"peeked": false
}
GET/api/browser/sessions/{id}/devtools
Auth Required

Console + Network

Console and network entries in one response. The same data as the two endpoints separately, in one round trip — worth it when diagnosing a failure, where the useful signal is usually a console error and the request that preceded it read together. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
tabstringOptionalTarget tab id

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/devtools" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Both streams
{
"success": true,
"data": {
"console": [],
"network": [],
"bufferInitializedAt": 1758570000000
}
}
POST/api/browser/sessions/{id}/history
Auth Required

History Navigation

Go back, go forward, or reload. Back is not the same as navigating to the previous URL: it replays the entry, preserving form state and scroll position the way a person pressing the button would. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
actionstringRequiredback | forward | reload
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/history" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "string"
}'

Responses

200Navigated — the response carries only the action; read the URL back with /context or /read-page
{
"success": true,
"data": {
"action": "back"
}
}
POST/api/browser/sessions/{id}/recover-tab
Auth Required

Recover a Hung Tab

Replace a tab that has stopped answering with a fresh one in the same session. A hung tab services no page-level command, so reload and navigate cannot rescue it and retrying the failing action will not either — but the browser can still close it, and a new tab in the same context keeps the session's cookies, storage and sign-in. Only the hung document, and anything typed into it, is lost. You do not have to guess when to call this: after three consecutive `CDP command timeout` failures with no page-level success between them, each further failure carries `error.pageUnresponsive: true` and an `error.recovery` object naming this endpoint. Status, code and message are unchanged, so existing error handling keeps working. Dialog suppression and element refs are per-document — re-apply and re-read afterwards. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
tabstringOptionalThe hung tab's id (defaults to the tab commands without a `tab` land on)
restoreUrlbooleanOptionalNavigate the new tab to the old tab's URL (default true)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/recover-tab" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Tab replaced
{
"success": true,
"data": {
"recovered": true,
"tab": "9F3A…",
"closedTab": "0806…",
"url": "https://example.com/settings",
"restored": true
}
}
422The browser could not open or close a tab
{
"success": false,
"error": {
"code": "RECOVER_TAB_ERROR",
"message": "…"
}
}
POST/api/browser/sessions/{id}/claude-tools
Auth Required

Run Claude Browser Toolset Calls

Execute Claude's browser use tool (`browser_toolset_20260801`) against this session. Declare `{ "type": "browser_toolset_20260801" }` in your Messages API request; when the response ends with `stop_reason: "tool_use"`, POST its `content` array here unchanged and send back `data.tool_results` as the next user message's content. Every call carrying `toolset_name: "browser"` is run in order and answered; text blocks and your own tools' calls are ignored, so answer those yourself. The results already follow the toolset contract: `toolset_name` echoed, tab state in `browser_state` blocks (tab ids are the session's tab ids), screenshots scaled to fit the model's image limits with Claude's coordinates scaled back before dispatch, element refs that are the same `ref_<n>` handles read-page returns, and the batch rule applied — after a failing call, the rest are answered with `Not executed: an earlier action in this turn failed.` The toolset has no dialog member, so a native alert/confirm/prompt is answered for Claude per `options.dialogPolicy` and reported in the result text. The four optional members (javascript_exec, read_console, read_network, file_upload) are refused unless listed in `options.enabledOptional` — enable the same ones in your toolset `configs`. file_upload attaches only files the caller staged in `options.documents`, by the `document_ids` Claude names; `paths` are refused. With `options.allowedHosts`, the list is enforced after every call, not only on navigate: a tab that leaves it is cleared and the call fails, and any other tab outside it (a popup) is closed. A turn in which no call reached the page does not count as session activity. If the request itself fails (404 SESSION_ENDED, 503 SESSION_RESUMING), your loop must answer each browser call with an is_error result itself, or the conversation cannot continue. With `options.approval` (member names, or the groups input / navigation / all), the turn stops before any gated call and answers `pending` with `complete: false`; post the same content again with `options.resume = { from, approved | denied }` to continue, and append the new results to the earlier ones. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
contentarrayRequiredThe assistant message's content blocks, as returned by the Messages API (at most 200 blocks)
options.maxImageEdgenumberOptionalLongest screenshot edge in px, 256–2000 (default 1568). Keep it constant for a conversation
options.imageFormatstringOptionaljpeg (default) or png
options.imageQualitynumberOptionalJPEG quality, 10–100 (default 80)
options.dialogPolicystringOptionaldismiss (default) or accept. beforeunload is always accepted
options.enabledOptionalarrayOptionalOptional members to allow: javascript_exec, read_console, read_network, file_upload
options.disabledarrayOptionalMembers you disabled in configs (max 50); answered with an error if Claude names them
options.allowedHostsarrayOptionalWhen set (max 200 hosts), navigate refuses other hosts (subdomains included) and clears a tab redirected off the list

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/claude-tools" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": []
}'

Responses

200Every browser call answered. A failed call is an is_error result, not an HTTP error
{
"success": true,
"data": {
"tool_results": [
{
"type": "tool_result",
"tool_use_id": "toolu_01NRL…",
"toolset_name": "browser",
"content": [
{
"type": "text",
"text": "Navigated to https://example.com/docs."
},
{
"type": "browser_state",
"tabs": [
{
"tab_id": "9F3A…",
"title": "Documentation",
"url": "https://example.com/docs",
"active": true
}
]
}
]
}
],
"failedAt": null,
"elapsedMs": 812
}
}
400Malformed body — more than 200 content blocks, or options outside their bounds. Calls past 50 in one turn are answered with an error result, never refused
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "Invalid input",
"details": []
}
}
POST/api/browser/sessions/{id}/viewport
Auth Required

Set Viewport

Set viewport size and device emulation. Changing this changes what the page renders — responsive layouts move controls, and a selector or ref found at one size can be absent at another, so read the page again after resizing. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
widthnumberRequiredViewport width in px
heightnumberRequiredViewport height in px
deviceScaleFactornumberOptionalDevice pixel ratio (default 1)
mobilebooleanOptionalEmulate a mobile device (default false)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/viewport" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"width": 1,
"height": 1
}'

Responses

200Set
{
"success": true,
"data": {
"width": 1280,
"height": 720
}
}
POST/api/browser/sessions/{id}/iframe
Auth Required

Iframe Interaction

Act inside a same-origin iframe — read its html or text, or click and type within the frame. Cross-origin frames are not reachable this way, by browser design rather than by our choice; for those, the frame's own URL usually has to be driven as its own page. `evaluate` is accepted by the schema but NOT supported: it answers 422 UNSUPPORTED_ACTION — run /evaluate against the top document and reach the frame through its contentWindow instead. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
frameSelectorstringRequiredCSS selector of the iframe element
actionstringRequiredhtml | text | click | type (evaluate is refused with 422 UNSUPPORTED_ACTION)
selectorstringOptionalSelector INSIDE the frame, for click and type
textstringOptionalText, for type
expressionstringOptionalAccepted for evaluate, which is then refused — see action
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/iframe" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"frameSelector": "string",
"action": "string"
}'

Responses

200Done
{
"success": true,
"data": {
"action": "click",
"clicked": true
}
}
422evaluate is not supported inside a frame
{
"success": false,
"error": {
"code": "UNSUPPORTED_ACTION",
"message": "evaluate is not supported for iframes"
}
}
GET/api/browser/sessions/{id}/cookies
Auth Required

Get Cookies

All cookies visible to the session, including httpOnly values. Cookies are credential-grade for a signed-in site — a stored jar grants account access without passing MFA again — so treat a response from here the way you would a password, and GET requires scope browser:write (changed 2026-10-01; reading the login is as much authority as writing it).

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/cookies" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Cookies
{
"success": true,
"data": {
"cookies": [
{
"name": "session",
"domain": ".example.com"
}
]
}
}
POST/api/browser/sessions/{id}/cookies
Auth Required

Set Cookies

Inject cookies into the session — how a stored login is replayed. Note that a faithful replay is not the same as being signed in: a platform that binds its session to the IP that created it rejects the cookies from a different exit, and the browser lands on its sign-in page with every cookie present. If that happens, the login is fine and the egress is not. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
cookiesarrayRequiredCookie objects (1–100): name, value, domain, path, secure, httpOnly, sameSite, expires. Cookies belong to the browser context, so there is no tab parameter

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/cookies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"cookies": []
}'

Responses

200Set — one entry per cookie sent, saying whether the browser took it
{
"success": true,
"data": {
"set": [
{
"name": "session",
"success": true
},
{
"name": "li_at",
"success": true
}
]
}
}
DELETE/api/browser/sessions/{id}/cookies
Auth Required

Delete Cookies

Delete cookies by name and/or domain, or clear them all. name + domain deletes that one cookie; name alone deletes that cookie on EVERY domain it is set for; domain alone deletes EVERY cookie on that domain (domains are compared with or without the leading dot on either side). Each of those answers { deleted: [{ name, domain }] } listing what was actually removed, so an empty array means nothing matched. Only a request with NEITHER field clears the whole jar, answering { cleared: true } — clearing signs the session out of everything, which is occasionally what you want before a fresh login and never what you want mid-run. Cookies belong to the browser context, so there is no tab parameter. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
namestringOptionalCookie name to delete (1–256). Alone, deletes it on every domain it is set for
domainstringOptionalDomain to delete on (1–256). Alone, deletes every cookie on that domain

Example Request

curl -X DELETE "https://scrappy.gg/api/browser/sessions/{id}/cookies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Deleted — what was actually removed
{
"success": true,
"data": {
"deleted": [
{
"name": "li_at",
"domain": ".linkedin.com"
}
]
}
}
200Cleared — neither field was sent
{
"success": true,
"data": {
"cleared": true
}
}
GET/api/browser/sessions/{id}/storage
Auth Required

Read Storage

Read localStorage or sessionStorage. Some sites keep auth state here rather than in cookies, so a login that replays cookies alone can still land signed out — if that happens, this is the place to look before blaming the cookie jar. The values are credentials in the same sense the cookie jar is, so GET requires scope browser:write (changed 2026-10-01).

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
typestringRequiredlocal | session — required, there is no default
keystringOptionalSingle key. Omit for all
tabstringOptionalTarget tab id

Example Request

curl -X GET ?type=string"https://scrappy.gg/api/browser/sessions/{id}/storage" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200One key — value is null when it is not set
{
"success": true,
"data": {
"key": "token",
"value": "…"
}
}
200Every key in the store (no key given)
{
"success": true,
"data": {
"entries": {
"token": "…",
"theme": "dark"
}
}
}
POST/api/browser/sessions/{id}/storage
Auth Required

Write Storage

Write a localStorage or sessionStorage value. Writing here does not make a site treat you as signed in — apps read their storage at load and cache it, so a value injected after the page is up is usually ignored until a reload. Set it before navigating, or reload after. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
typestringRequiredlocal | session — required, there is no default
keystringRequiredStorage key (1–1024)
valuestringRequiredValue to store (max 5MB)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/storage" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "string",
"key": "string",
"value": "string"
}'

Responses

200Stored
{
"success": true,
"data": {
"key": "token",
"stored": true
}
}
DELETE/api/browser/sessions/{id}/storage
Auth Required

Clear Storage

Remove one storage key, or clear the store. For a site that keeps auth state in storage rather than cookies, clearing it signs the session out even though every cookie survives — which is a useful way to force a fresh login, and a surprising way to lose one. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
typestringRequiredlocal | session — required, there is no default
keystringOptionalKey to remove. Omit to clear the store
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X DELETE "https://scrappy.gg/api/browser/sessions/{id}/storage" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "string"
}'

Responses

200One key removed
{
"success": true,
"data": {
"key": "token",
"removed": true
}
}
200Store cleared (no key given)
{
"success": true,
"data": {
"cleared": true
}
}
GET/api/browser/sessions/{id}/context
Auth Required

Session Context

The session record merged with the page's state — URL, title, what the browser reports about itself (platform, os, timezone), cookie and localStorage COUNTS (never the values; use /cookies and /storage for those), memory usage — plus the exit it is going out through and the project it belongs to. proxy is the session's proxy URL reduced to a credential-free display shape, absent when there is none; egressIp is best-effort — resolved by fetching an echo service from inside the browser, falling back to a throwaway blank tab when the page's CSP blocks it, and absent when the egress cannot reach the echo at all. Worth checking when a replayed login is rejected: if the exit IP is not the one that minted the session, the cookies were never the problem. Answers a bare object, not the success envelope. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
tabstringOptionalThe tab whose page state is read (defaults to the active page)

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/context" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Context
{
"id": "b1f2…",
"status": "live",
"createdAt": "2026-07-21T12:00:00Z",
"pageUrl": "https://www.linkedin.com/feed",
"pageTitle": "LinkedIn",
"platform": "Win32",
"os": "Windows",
"timezone": "America/Los_Angeles",
"cookieCount": 42,
"localStorage": 15,
"memoryUsage": {
"usedJSHeapSize": 52428800,
"totalJSHeapSize": 67108864,
"jsHeapSizeLimit": 2147483648
},
"proxy": {
"label": "residential"
},
"egressIp": "76.27.31.230",
"projectId": "project_123"
}
POST/api/browser/sessions/{id}/captcha
Auth Required

CAPTCHA

Detect or solve a CAPTCHA. Detection distinguishes a challenge that is actually RENDERED from a page merely carrying the vendor's script — most sign-in pages load the script unconditionally, so presence of the code proves nothing. Solving requires a solver key (TWO_CAPTCHA_API_KEY, ANTI_CAPTCHA_API_KEY or CAPSOLVER_API_KEY); without one a challenge is reported for a human to clear rather than silently retried. Where the deployment also sets CAPTCHA_AUTO_SOLVE, a navigation that lands on a solvable challenge clears it without this endpoint being called at all — this stays the way to force or inspect one. A PerimeterX (HUMAN) Press & Hold wall reports rendered:true with vendor:'perimeterx' — go by `vendor`, since `type` comes from the page's scripts and an Auth0 page behind the wall still says RECAPTCHA_V2. No solving provider handles Press & Hold, so solve skips it without charge and says a person must hold the button in the live viewer. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
actionstringOptionaldetect (default) | solve
maxAttemptsnumberOptionalSolve attempts before giving up, 1–5
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/captcha" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Result
{
"success": true,
"data": {
"detected": true,
"rendered": true,
"vendor": "perimeterx",
"solved": false
}
}
200solve skipped — the captcha code is present but nothing is rendered, so nothing was attempted or charged
{
"success": true,
"data": {
"detected": true,
"rendered": false,
"solved": false,
"skipped": "No challenge is rendered on the page — the captcha code is present but not asking anything, so no solve was attempted."
}
}
409solve asked with no solver key (TWO_CAPTCHA_API_KEY / ANTI_CAPTCHA_API_KEY / CAPSOLVER_API_KEY) — the challenge is reported for a human to clear
{
"success": false,
"error": {
"code": "SOLVER_NOT_CONFIGURED",
"message": "No CAPTCHA solving provider is configured"
}
}
422The solver ran out of attempts or the page rejected its token
{
"success": false,
"error": {
"code": "CAPTCHA_SOLVE_FAILED",
"message": "CAPTCHA solve failed after 3 attempts"
}
}
POST/api/browser/sessions/{id}/sleep
Auth Required

Sleep Session

Capture the session's state and release its browser, returning a snapshot id. Stops the per-minute meter while keeping the login: wake restores cookies and the first tab's URL into a fresh session. Use it for a session a person will come back to rather than paying for an idle browser. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/sleep" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Slept — tabs and cookies are the counts captured; sleeping lists every snapshot you now hold (a bare object, not the success envelope)
{
"snapshotId": "snp_123",
"tabs": 2,
"cookies": 18,
"sleeping": [
{
"snapshotId": "snp_123",
"tabs": 2,
"createdAt": "2026-07-21T12:00:00Z"
}
]
}
POST/api/browser/sessions/{id}/release
Auth Required

Release (beacon)

Release the session, shaped for navigator.sendBeacon — a page being closed cannot reliably finish a DELETE, and an abandoned session bills per minute until the reaper catches it. Prefer DELETE /sessions/{id} everywhere a normal request can complete. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/release" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Released
{
"released": true
}
GET/api/browser/sessions/{id}/devtools-stream
Auth Required

Console + Network (SSE)

Server-sent event stream of console and network entries as they happen, rather than the buffered snapshot the other endpoints return. Use it to watch a page while it is doing something; use /devtools when diagnosing after the fact. Returns text/event-stream, so it is read with an EventSource rather than a JSON client, and it authenticates by bearer token or session cookie like every other browser route. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/devtools-stream" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Event stream
{
"event": "console",
"data": {
"type": "error",
"text": "Uncaught TypeError"
}
}
POST/api/browser/sessions/{id}/type
Auth Required

Type

Type text into an input, textarea or contenteditable, identified by a ref from read-page or a CSS selector. Prefer ref: it names the element directly, so nothing has to be guessed and it cannot silently target the wrong node after a re-render. The value is set through the element's own setter and an input event is dispatched, so React and other frameworks that ignore a raw value assignment still see the change, and the response reports the value that actually settled rather than the one that was sent — a masked, formatted or rejected field will differ, and that difference is the useful part. clear OMITTED means APPEND: only clear:true replaces the existing value, which is the long-standing behaviour of this endpoint and why fill-form clears by default while type does not. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>) — types into exactly that node. Use instead of selector
selectorstringOptionalCSS selector of the input/textarea (or use ref)
textstringRequiredText to type (max 10000 chars)
clearbooleanOptionaltrue replaces the existing value; omitted or false APPENDS to it
piercebooleanOptionalAlso search inside open shadow roots. Selector path only
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/type" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "string"
}'

Responses

200Typed
{
"success": true,
"data": {
"typed": true,
"ref": "ref_18",
"value": "Jane Doe"
}
}
422Element not found
{
"success": false,
"error": {
"code": "ELEMENT_NOT_FOUND",
"message": "Element for ref_18 is no longer on the page — call read-page again for fresh refs"
}
}
POST/api/browser/sessions/{id}/read-page
Auth Required

Read Page

The page as an agent should see it: Chrome's own accessibility tree, flattened, with a stable handle per node. Every other way to find an element here starts from a CSS selector the caller has to guess, and a guess fails quietly — it matches a different element after a re-render, or matches nothing because the control is a styled div rather than a real input, or expects an id where a name was given. Each node returns role, accessible name, value, state (disabled, checked, expanded, required, invalid, readonly, selected) and depth. Roles and names come from Chrome's own accessibility computation, so aria-label, label[for], alt text, title and content are already accounted for — the same thing a screen reader would say. `ref` is `ref_<backendNodeId>` and can be passed straight to the click endpoint. Backend node ids are browser-wide rather than scoped to the connection that produced them, so a ref stays valid without any server-side registry; it dies when the page re-renders that node, and a click will say so rather than clicking stale coordinates. Scope browser:read. A modal that opened at the end of a big page is not lost to maxNodes: rows inside an open dialog (role dialog/alertdialog, or aria-modal) are kept past the cut, up to 200 more. An overlay that never declares itself a dialog is still cut.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
interactiveOnlybooleanOptionalOnly actionable roles — buttons, links, inputs (default false)
maxNodesnumberOptionalCap on returned nodes, 1-2000 (default 500)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/read-page" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Accessibility tree
{
"success": true,
"data": {
"nodes": [
{
"ref": "ref_18",
"role": "textbox",
"name": "Client",
"depth": 4,
"required": true
},
{
"ref": "ref_23",
"role": "button",
"name": "Save",
"depth": 4,
"disabled": true
}
],
"count": 2,
"truncated": false,
"treeSize": 214
}
}
POST/api/browser/sessions/{id}/find
Auth Required

Find Elements

Locate elements by what they say, and get refs back. Read Page already returns everything actionable, but 'everything' on a real page is hundreds of nodes, and an agent that only wants the Sign in button pays for the whole tree — in tokens, and in the mistakes that come from reading a long list. This asks Chrome the same question and returns only the rows that answer it, ranked: the accessible name first, then value, description and role. `role` is a filter rather than a preference, which is what stops a query from returning the heading above the button you meant. Refs are the same ref_<backendNodeId> handles Read Page hands out, so a match goes straight to click, type, hover, element-info or screenshot. No matches is a success, not a 404 — the page does not say that, and the next move is a full read-page rather than a retry. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
querystringRequiredWhat you would say to point at the element, e.g. 'Sign in'
rolestringOptionalRestrict to one accessibility role: button, link, textbox, checkbox, …
interactiveOnlybooleanOptionalOnly actionable roles (default false)
limitnumberOptionalMax matches, 1-50 (default 10)
maxNodesnumberOptionalHow much of the tree to search, 1-5000 (default 2000)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/find" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "search term"
}'

Responses

200Ranked matches
{
"success": true,
"data": {
"matches": [
{
"ref": "ref_31",
"role": "button",
"name": "Sign in",
"depth": 5,
"score": 108,
"matchedOn": "name"
},
{
"ref": "ref_12",
"role": "link",
"name": "Sign in with Google",
"depth": 4,
"score": 76,
"matchedOn": "name"
}
],
"count": 2,
"searched": 214,
"truncated": false,
"treeSize": 402
}
}
POST/api/browser/sessions/{id}/batch
Auth Required

Batch Actions

Run several session actions in one request, in order. This is the biggest speed-up available over this API: every other endpoint is a full round trip — network, auth, ownership lookup — and a login or checkout flow is a dozen of them, all spent waiting on a browser that is already sitting there. Steps run sequentially and never concurrently, because they act on one page where order is the meaning. Each step is charged to the rate-limit bucket its own endpoint uses, so this is a latency win rather than a quota loophole, and each step runs the *same* handler that endpoint runs — nothing here is a second implementation that could drift. stopOnError defaults to true: typing step 5 after step 4 failed to find its field types into whatever had focus instead, which is how a password ends up in a search box. Steps that never ran come back under `skipped`. Supported actions: navigate, click, type, press, hover, scroll, select, check, drag_drop, paste, wait, wait_for_selector, wait_for_network, wait_for_function, read_page, find, element_info, screenshot, record, scrape, scrape_mode, evaluate, history, viewport, iframe, dialog, intercept, captcha, pdf. `wait` exists only here — { action: 'wait', ms: 400 } is how you pause for a menu without paying for another round trip. Upload, the session lifecycle routes and cookies/storage are deliberately absent. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
stepsarrayRequiredOrdered steps, each { action, ...that action's own parameters } minus the session id (1-25)
stopOnErrorbooleanOptionalStop at the first failing step (default true)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/batch" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"steps": []
}'

Responses

200Per-step results
{
"success": true,
"data": {
"results": [
{
"index": 0,
"action": "navigate",
"ok": true,
"status": 200,
"data": {
"url": "https://example.com/login"
}
},
{
"index": 1,
"action": "type",
"ok": false,
"status": 422,
"error": {
"code": "ELEMENT_NOT_FOUND",
"message": "Element not found: #email"
}
}
],
"skipped": [
{
"index": 2,
"action": "click",
"reason": "step 1 (type) failed"
}
],
"completed": 2,
"failed": 1,
"stoppedAt": 1,
"elapsedMs": 1840
}
}
POST/api/browser/sessions/{id}/fill-form
Auth Required

Fill Form

Set every field of a form — and optionally submit it — in one request. Batch can already express this as a list of type steps and does the same thing underneath; this exists because a form is the most common multi-step interaction there is, and spelling it as fields removes the two mistakes that actually happen: forgetting to clear a field that already has a value, and reaching for type on a checkbox. The control type is inferred from the value — a string types, a boolean checks, kind:'select' picks an option — and each field reports its own result, so a failure names the field rather than a step index. If a field fails the submit is skipped: a half-filled form that gets posted anyway is worse than one that does not, because the site records the partial attempt and some rate-limit the retry. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
fieldsarrayRequiredFields in order (1-50): { ref | selector, value: string | boolean, kind?: text | select | check, clear?, pierce? }
submitobjectOptional{ ref | selector } to click once every field is set
stopOnErrorbooleanOptionalStop at the first failing field and skip the submit (default true)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/fill-form" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": []
}'

Responses

200Per-field results
{
"success": true,
"data": {
"fields": [
{
"index": 0,
"target": "#email",
"action": "type",
"ok": true,
"status": 200,
"data": {
"typed": true,
"value": "a@b.c"
}
},
{
"index": 1,
"target": "#terms",
"action": "check",
"ok": true,
"status": 200,
"data": {
"checked": true
}
}
],
"skipped": [],
"filled": 2,
"failed": 0,
"stoppedAt": null,
"submitted": {
"action": "click",
"ok": true,
"status": 200
},
"submitSkipped": false
}
}
POST/api/browser/sessions/{id}/element-info
Auth Required

Element Info

Inspect one element: tag, text, value, visibility, enabled/checked state, bounding box and attributes. It also answers two questions a selector alone cannot. When the element is disabled, `gating` reports what is holding it — which fields in the surrounding form are empty, which are empty AND required, which fail constraint validation, and whether it is disabled by the `disabled` property or only by aria (different causes, different fixes). A vendor form can gate its submit on a field it never marks required and renders no error for, so `emptyFields` is often the only available clue and `emptyRequiredFields` will be empty. Separately, `contentDigest` fingerprints what the element currently contains, so reading it before and after an action proves the action CHANGED something: an unchanged digest is proof of a no-op, which a presence check reports as success. `rowCount` is the coarser companion that survives cosmetic churn — a ticking timestamp moves the digest but not the count. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>) — inspect exactly that node. Use instead of selector
selectorstringOptionalCSS selector of the element to inspect (or use ref)
piercebooleanOptionalAlso search inside open shadow roots
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/element-info" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Element details
{
"success": true,
"data": {
"tag": "button",
"text": "Save",
"isVisible": true,
"isEnabled": false,
"gating": {
"disabledVia": "property",
"scope": "noteCard",
"emptyFields": [
{
"name": "CLIENT",
"id": "client",
"type": "text",
"label": "Client",
"required": false,
"empty": true
}
],
"emptyRequiredFields": [],
"invalidFields": [],
"formValid": true
},
"contentDigest": "8f3a21c4:412",
"descendantCount": 87,
"rowCount": 12
}
}
200A <select> — options entries carry value, label, selected and disabled
{
"success": true,
"data": {
"tag": "select",
"isSelected": null,
"selectedText": "California",
"options": [
{
"value": "ca",
"label": "California",
"selected": true,
"disabled": false
},
{
"value": "wa",
"label": "Washington",
"selected": false,
"disabled": true
}
],
"optionCount": 2
}
}
400Neither selector nor ref given
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "Provide either selector or ref"
}
}
404A stale ref — the page re-rendered that node since read-page
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Element for ref_18 is no longer on the page — call read-page again for fresh refs"
}
}
422A selector that matches nothing
{
"success": false,
"error": {
"code": "ELEMENT_NOT_FOUND",
"message": "Element not found: #save"
}
}
POST/api/browser/sessions/{id}/click
Auth Required

Click

Click by CSS selector or coordinates. button:'right' opens context menus; clickCount:2 double-clicks, which is how many data grids enter cell-edit mode. pierce:true also searches open shadow roots — document.querySelector does not cross shadow boundaries, so a control inside a web component is invisible to a normal selector even though it is plainly on screen; set pierce when a selector cannot find something you can see. pierce is also accepted by type, element-info and wait-for-selector. holdMs turns the click into a press and hold, which is the only way past a 'Press & Hold to confirm you are a human' widget: those start a timer on pointerdown and pass only if pointerup arrives after their threshold, so an ordinary click — which releases in the same millisecond it presses — always fails. Note that clearing the gesture is not the same as clearing the challenge; these systems also score the session server-side once the hold completes. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>). Names the element directly — preferred over a selector, which can silently match the wrong node
selectorstringOptionalCSS selector (or use ref, or x/y)
xnumberOptionalX coordinate, with y
ynumberOptionalY coordinate, with x
buttonstringOptionalleft (default) | right | middle
clickCountnumberOptional1–3; 2 for a double-click (default 1)
piercebooleanOptionalAlso search inside open shadow roots
holdMsnumberOptionalHold the button down this many ms before releasing (0–30000, default 0). Use ~6000–10000 for a 'Press & Hold' challenge; ignores clickCount
jitternumberOptionalMax px of random pointer drift while held (0–10, default 2). 0 holds perfectly still, which reads as synthetic
modifiersnumberOptionalModifier bitmask held for the click — 1 Alt, 2 Ctrl, 4 Meta, 8 Shift, summed (0–15, default 0). Any non-zero value forces the real mouse-event path, because a plain left click goes through the element's own .click() and cannot carry a modifier. This is how you ctrl/shift-click to extend a selection
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/click" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Clicked by selector — via says whether real pointer input was dispatched or the element's own click() was called; label is the element's accessible text
{
"success": true,
"data": {
"clicked": true,
"tag": "button",
"label": "Save",
"via": "pointer",
"x": 500,
"y": 328,
"button": "left",
"clickCount": 1
}
}
200Held
{
"success": true,
"data": {
"clicked": true,
"tag": "button",
"button": "left",
"holdMs": 8000,
"jitter": 2,
"x": 500,
"y": 328
}
}
404A stale ref — the page re-rendered that node since read-page
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Element for ref_31 is no longer on the page — call read-page again for fresh refs"
}
}
422A selector that matches nothing
{
"success": false,
"error": {
"code": "ELEMENT_NOT_FOUND",
"message": "Element not found: #save (try pierce:true if it is inside a web component)"
}
}
POST/api/browser/sessions/{id}/upload
Auth Required

Upload a File

Attach a file to a file input. Send the bytes one of three ways, and name the target input one of two ways. Files go to the session's file storage and attach via DOM.setFileInputFiles, so bytes never pass through a JS expression — the old approach capped uploads near 1MB. multipart/form-data carries raw bytes (9MB — the same ~10MB body limit as content, but none of it spent on base64; repeat the file field for a multi-select input); sourceUrl lets the browser service fetch the file itself (100MB, SSRF-checked like navigate) and is the path for anything larger; content is base64 and is capped near 7MB — it accepts a plain base64 string or a full data:...;base64,... URL, tolerates the line wrapping Ruby's Base64.encode64 and Python's encodestring produce, and refuses anything outside the base64 alphabet rather than decoding it into a corrupt file (the naive decode never throws: it turns a data URL's prefix into garbage welded onto the front of the real bytes and reports success). Exactly one source per request — two is a 400 rather than a silent choice. After attaching, the input is read back and the response reports what it actually holds: Chrome accepts a path that does not exist (leaving a 0-byte file of that name) and a file list longer than a non-multiple input can take (keeping one), both without complaint, so those come back as ATTACH_INCOMPLETE instead of a false success. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
selectorstringOptionalCSS selector of the file input. Required unless backendNodeId is given
backendNodeIdnumberOptionalCDP backend node id of the input, from an intercepted file chooser. Names the exact input the user clicked, including hidden ones behind custom Browse buttons that no selector would find. Valid across CDP connections
filenamestringOptionalName the page should see. Required unless a file is uploaded, which carries its own
filefileOptionalmultipart only — raw bytes, up to 9MB total. Repeat the field to fill an <input multiple> (max 10)
sourceUrlstringOptionalURL the browser service fetches directly — up to 100MB, enforced upstream while streaming. Prefer a short-lived signed URL
contentstringOptionalBase64 file bytes — practical ceiling ~7MB. A data:<type>;base64,... URL, line-wrapped base64 and base64url are all accepted
mimeTypestringOptionalInferred from the data URL, then the filename extension, before falling back to application/octet-stream — some upload widgets reject an octet-stream PDF client-side
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/upload" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json or multipart/form-data"

Responses

200Attached
{
"success": true,
"data": {
"uploaded": true,
"filename": "signed-psa.pdf",
"bytes": 3088632,
"via": "sourceUrl",
"verified": true,
"attached": [
{
"name": "signed-psa.pdf",
"size": 3088632
}
]
}
}
422Stored, but the page did not take it
{
"success": false,
"error": {
"code": "ATTACH_INCOMPLETE",
"message": "Upload stored but attached as 0 bytes: signed-psa.pdf — the stored file was empty or its path was wrong"
}
}
413Too large — use a bigger-capacity path
{
"success": false,
"error": {
"code": "FILE_TOO_LARGE"
}
}
400Unsafe sourceUrl
{
"success": false,
"error": {
"code": "UNSAFE_URL",
"message": "Navigation to private IP addresses is not allowed"
}
}
GET/api/browser/tabs
Auth Required

List Tabs

List a session's open tabs with the targetId every tab operation needs — the id appears in no other response, so this is the only way to name a tab. Tabs are in creation order: the first entry is the oldest, the last is the popup a click just opened. activeTargetId is the tab an action with no tab parameter lands on: the last tab activated through DELETE /api/browser/tabs while it is still open, otherwise the newest non-blank page. Only the calling session's own tabs are listed; a targetId from another session is rejected everywhere. Scope browser:read.

Query Parameters

NameTypeRequiredDescription
sessionIdstringRequiredSession id. Without it the route answers 200 with an empty list rather than an error, which is also what a session with no tabs looks like — check that you sent it before reading an empty list as 'no tabs'

Example Request

curl -X GET ?sessionId=abc123"https://scrappy.gg/api/browser/tabs" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Tabs in creation order, and the tab untargeted actions land on
{
"tabs": [
{
"targetId": "AE77D31F…",
"title": "Dashboard",
"url": "https://example.com/dashboard"
},
{
"targetId": "9F3A02C4…",
"title": "Sign in with Google",
"url": "https://accounts.google.com/…"
}
],
"activeTargetId": "9F3A02C4…"
}
404Session not found on the browser service
{
"error": "Session not found"
}
POST/api/browser/tabs
Auth Required

Create Tab

Open a new blank tab in the session's own browser context. Useful to keep a long form intact while you look something up, instead of navigating away and losing the filled fields. The new tab is blank, so until it is navigated untargeted actions keep landing on the page you were working on; after that, activate the tab you want with DELETE /api/browser/tabs or pass tab explicitly. Scope browser:write.

Request Body

NameTypeRequiredDescription
sessionIdstringRequiredSession id

Example Request

curl -X POST "https://scrappy.gg/api/browser/tabs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "abc123"
}'

Responses

200The new tab — id is the targetId to pass as tab or to DELETE /api/browser/tabs
{
"tab": {
"id": "AB12CD…",
"title": "about:blank",
"type": "page",
"url": "about:blank"
}
}
400No sessionId
{
"error": "sessionId is required"
}
404Session not found
{
"error": "Session not found"
}
DELETE/api/browser/tabs
Auth Required

Close or Activate a Tab

Close a tab, or make one the active tab with action:'activate'. Web apps open tabs constantly (OAuth consent, payment windows, target=_blank), and without this an agent can create tabs but never dismiss them, so popups accumulate and keep consuming session resources. activate brings the tab to the foreground AND makes it the tab every action without a tab parameter lands on — click, type, evaluate, screenshot — until it is closed; the choice is shared across the API's machines, so it holds on every later request. Closing the active tab (by this route, by the page, or by recover-tab) reverts the default to the newest non-blank page, and recover-tab's replacement becomes the active tab. Scope browser:write.

Request Body

NameTypeRequiredDescription
sessionIdstringRequiredSession id
targetIdstringRequiredCDP target id from GET /api/browser/tabs
actionstringOptionalclose (default) | activate

Example Request

curl -X DELETE "https://scrappy.gg/api/browser/tabs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "abc123",
"targetId": "abc123"
}'

Responses

200Closed
{
"closed": true,
"targetId": "AE77D31F…"
}
200Activated — untargeted actions now land here
{
"activated": true,
"targetId": "AE77D31F…"
}
400Missing or malformed ids
{
"error": "sessionId and targetId are required"
}
POST/api/browser/sessions/{id}/wait-for-selector
Auth Required

Wait for Selector

Block until an element is visible (default), present, hidden, or detached. Essential between steps of a write flow — after clicking Edit wait for the form, after saving wait for the confirmation. Polls server-side, so it costs one request instead of many. Returns found:false rather than erroring on timeout. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
selectorstringRequiredCSS selector to wait for
piercebooleanOptionalAlso search inside open shadow roots (default false)
statestringOptionalpresent | visible | hidden | detached (default visible)
timeoutnumberOptionalMax wait in ms (500–60000, default 10000)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/wait-for-selector" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"selector": "string"
}'

Responses

200Settled or timed out
{
"success": true,
"data": {
"found": true,
"state": "visible",
"waitedMs": 340
}
}
POST/api/browser/sessions/{id}/wait-for-function
Auth Required

Wait for Function

Block until a JavaScript expression evaluates truthy in the page. This is the wait for 'the app is ready', which no selector can express: server-rendered markup is present and visible the moment the document commits, while the bundle that gives it behaviour has not run — so waiting for the button and then clicking it fires a handler that throws a ReferenceError, and every layer still reports success. Wait for what the handler needs instead, e.g. typeof clientModal !== 'undefined'. An expression that THROWS counts as not-yet-true, which is what makes this usable while a page is still booting (an evaluate racing a navigation's context swap fails too); a timeout carries lastError when the final attempt threw — the page's own message, since the expression runs inside a try/catch in the page (CDP alone reports the literal string 'Uncaught' for every failure). Values come back JSON-serialised and capped at 4096 chars, so a predicate cannot ship megabytes per poll. Must be synchronous — evaluation does not await promises, so an async predicate would be truthy on the first poll; that is rejected with 422 INVALID_PREDICATE rather than answered wrongly. Predicate on a boolean (!!document.querySelector('#row')), since values come back JSON-serialised. Polls server-side: one request and one rate-limit charge instead of a dozen evaluate round trips. Returns satisfied:false rather than erroring on timeout. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
expressionstringRequiredJavaScript expression (not a statement body) polled until truthy, max 10000 chars
timeoutnumberOptionalMax wait in ms (500–60000, default 10000)
pollIntervalnumberOptionalMs between evaluations (50–5000, default 200)
requiredbooleanOptionalFail with 422 WAIT_TIMEOUT instead of answering satisfied:false — needed inside /batch, where a soft timeout is a 200 and stopOnError will not stop for it
tabstringOptionalTarget tab (CDP target id); defaults to the most recent page

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/wait-for-function" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expression": "string"
}'

Responses

200Satisfied
{
"success": true,
"data": {
"satisfied": true,
"value": true,
"waitedMs": 480,
"polls": 3
}
}
200Timed out
{
"success": true,
"data": {
"satisfied": false,
"timedOut": true,
"value": null,
"waitedMs": 10000,
"polls": 50,
"lastError": "clientModal is not defined"
}
}
422The expression returned a Promise
{
"success": false,
"error": {
"code": "INVALID_PREDICATE",
"message": "Expression returned a Promise."
}
}
POST/api/browser/sessions/{id}/dialog
Auth Required

JavaScript Dialogs

Suppress, inspect or answer native alert/confirm/prompt/beforeunload dialogs. An open dialog blocks its tab: every page command times out until it is answered, which from outside is indistinguishable from a hung tab. `suppress` (default) replaces the dialog functions with no-ops — call it BEFORE a click that might open one. It is per-document, so re-apply after each navigation, and top-frame only: a dialog opened from an iframe is not suppressed. `status` asks nothing of the browser and reports the dialog this server saw open, with its type, message and the URL of the frame that opened it. `accept` and `dismiss` answer it; the page and any unsaved input survive. Answering one dialog can open another, and `nextDialog` in the response says so. When an action fails with a `CDP command timeout` while a dialog is known to be open, the failure itself carries `error.dialogOpen` and an `error.recovery` pointing here. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
actionstringOptionalsuppress (default) | status | accept | dismiss
confirmReturnsbooleanOptionalsuppress only: what confirm() returns (default true = proceed)
promptTextstringOptionalsuppress: what prompt() returns (default null). accept: the text to submit to a prompt
tabstringOptionalTarget tab id (defaults to the active tab; status with no tab reports any tab's dialog)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/dialog" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Suppressed
{
"success": true,
"data": {
"suppressed": true,
"confirmReturns": true
}
}
200Status — a dialog is open
{
"success": true,
"data": {
"open": true,
"dialog": {
"type": "confirm",
"message": "Discard changes?",
"url": "https://example.com/frame",
"tab": "9F3A…",
"openedAt": 1790026017000
}
}
}
200Answered, and another dialog opened behind it
{
"success": true,
"data": {
"answered": true,
"action": "dismiss",
"dialog": {
"type": "confirm",
"message": "Discard changes?"
},
"nextDialog": {
"type": "alert",
"message": "Nothing was saved."
}
}
}
POST/api/browser/sessions/{id}/drag-drop
Auth Required

Drag and Drop

Drag from one point to another. Each end can be an element (ref or selector) or raw coordinates: use sourceX/sourceY and targetX/targetY for a slider dragged to a position, a resize gutter, or an SVG handle that has a meaningful drop point but no element under it to match. Pass `path` — a list of points — to trace an arbitrary gesture instead (drawing on a canvas, panning a map, swiping a custom control), where the path itself is the input and a straight line between two boxes would be the wrong motion; it overrides source/target. Moves through intermediate points so drag libraries register the motion. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
sourceSelectorstringOptionalCSS selector of the element to drag (or sourceRef / sourceX+sourceY)
sourceRefstringOptionalRef from read-page for the drag source
targetSelectorstringOptionalCSS selector of the drop target (or targetRef / targetX+targetY)
targetRefstringOptionalRef from read-page for the drop target
sourceXnumberOptionalX of the drag start, when the source is not an element (with sourceY)
sourceYnumberOptionalY of the drag start, when the source is not an element (with sourceX)
targetXnumberOptionalX of the drop point, when the target is not an element (with targetY)
targetYnumberOptionalY of the drop point, when the target is not an element (with targetX)
patharrayOptionalPoly-line of { x, y } points (2–100) to drag along: press at the first, glide through the rest, release at the last. Overrides source/target
stepsnumberOptionalIntermediate mouse moves per segment (1–20, default 5)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/drag-drop" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Dragged
{
"success": true,
"data": {
"dragged": true,
"from": {
"x": 120,
"y": 340
},
"to": {
"x": 480,
"y": 340
}
}
}
200Dragged along a path
{
"success": true,
"data": {
"dragged": true,
"from": {
"x": 100,
"y": 100
},
"to": {
"x": 300,
"y": 260
},
"points": 12
}
}
422Element not found
{
"success": false,
"error": {
"code": "ELEMENT_NOT_FOUND",
"message": "Element not found: #handle"
}
}
POST/api/browser/sessions/{id}/paste
Auth Required

Paste

Paste text and/or an image into an element — the round trip a Ctrl+V makes, for the case a file upload cannot cover. A rich editor (a GitHub comment, a chat composer, a doc) takes a pasted screenshot straight off the clipboard with no file input to target, so the upload endpoint has nothing to attach to; this decodes the image and delivers it as a real paste event with a DataTransfer the editor reads, the same shape most rich editors (ProseMirror, Slate, Draft.js) consume. Give imageBase64 (raw base64 or a data: URL), text, or both. Targets a ref, a selector, or — with neither — the currently focused element. The one thing it cannot satisfy is an editor that gates on a genuine OS-level (trusted) paste, since there is no OS clipboard to read headless. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
refstringOptionalRef from read-page (ref_<n>) to paste into
selectorstringOptionalCSS selector of the paste target. Omit ref and selector both to paste into the focused element
textstringOptionalPlain text to paste (max 100000). Provide text, imageBase64, or both
imageBase64stringOptionalImage as base64 or a data:<type>;base64,... URL. Decoded to a File and delivered on the paste event's clipboard data
mimeTypestringOptionalImage MIME type (default image/png; inferred from a data URL)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/paste" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Pasted
{
"success": true,
"data": {
"pasted": true,
"consumed": true,
"hasText": false,
"hasImage": true,
"selector": "div.comment-body"
}
}
400Bad image
{
"success": false,
"error": {
"code": "INVALID_IMAGE",
"message": "imageBase64 is not valid base64"
}
}
422No target
{
"success": false,
"error": {
"code": "ELEMENT_NOT_FOUND",
"message": "No element is focused — pass ref or selector"
}
}
POST/api/browser/sessions/{id}/wait-for-network
Auth Required

Wait for Network Idle

Block until the page stops issuing requests. Essential for single-page apps that render before their data arrives. Polls server-side, so this costs one request instead of dozens of evaluate round-trips. Returns idle:false on timeout rather than erroring. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
idleTimenumberOptionalQuiet period in ms before declaring idle (100–10000, default 500)
timeoutnumberOptionalMax total wait in ms (1000–60000, default 15000)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/wait-for-network" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200Settled or timed out
{
"success": true,
"data": {
"idle": true,
"waitedMs": 1240,
"requestCount": 37
}
}
POST/api/browser/sessions/{id}/intercept
Auth Required

Intercept / Block Requests

Block requests by resource type or URL pattern so pages load faster and scrapes cost less bandwidth. action:'stop' clears all blocking. To observe traffic rather than block it, use the network endpoint. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
actionstringRequiredstart | stop
blockTypesarrayOptionalimage, media, font, stylesheet, script, tracking
blockPatternsarrayOptionalRaw URL globs, e.g. "*ads.example.com*" (max 100)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/intercept" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "string"
}'

Responses

200Blocking started — blocked is the expanded pattern list, count its length
{
"success": true,
"data": {
"intercepting": true,
"blocked": [
"*.png",
"*.jpg",
"*ads.example.com*"
],
"count": 8
}
}
200Blocking stopped — no count on this shape
{
"success": true,
"data": {
"intercepting": false,
"blocked": []
}
}
400Nothing specified to block
{
"success": false,
"error": {
"code": "NOTHING_TO_BLOCK",
"message": "Provide at least one of blockTypes or blockPatterns when starting interception"
}
}
POST/api/browser/sessions/{id}/download
Auth Required

Resolve Download

Resolve the file URL behind a link or button (CSV exports, PDFs, attachments) so you can fetch it directly. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
selectorstringRequiredCSS selector of the download trigger
timeoutnumberOptionalTimeout in ms (1000–30000, default 5000)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/download" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"selector": "string"
}'

Responses

200Download resolved
{
"success": true,
"data": {
"url": "https://example.com/export.csv",
"filename": "export.csv"
}
}
GET/api/browser/sessions/{id}/downloads
Auth Required

List Downloads

The files this session's pages have downloaded, newest first, as Chrome reported them — a click on 'Export CSV' leaves a record instead of nothing. Each carries id, url, filename (the name the site proposed), state (in_progress | completed | canceled), byte counts, the tab that started it (null for a cross-site iframe), and once finished the storage path under which GET …/downloads/{id} serves the bytes. Chrome saves each download into the session's own directory under its download id, so same-named exports never overwrite each other. Pass since (epoch ms, the previous answer's now) to get only what changed; it overlaps by 5s because records are stamped by whichever app machine saw the event, so expect an occasional repeat. Read-only: no browser round trip. Records come from Chrome's own download events on the app's CDP socket, are scoped to the session's frames (an iframe's download is attributed to its tab), and are merged through Redis so either app machine answers the same. What it cannot see is a download that began before the app had a CDP socket to the session (for a session driven only through the viewer, every download until the first API action), or while the session's browser context was still unknown to the app. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
sincenumberOptionalOnly records updated at or after this epoch ms, inclusive (use the previous answer's now)

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/downloads" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Downloads
{
"success": true,
"data": {
"downloads": [
{
"id": "2437d70a-7d21-4ca5-bf9a-0fe8fc9e22fa",
"url": "https://example.com/export.csv",
"filename": "export.csv",
"path": "export.csv",
"state": "completed",
"receivedBytes": 20480,
"totalBytes": 20480,
"tab": "69DED302C73348586798A28FCA80FD93",
"startedAt": 1758570000000,
"updatedAt": 1758570000450
}
],
"count": 1,
"now": 1758570001000
}
}
GET/api/browser/sessions/{id}/downloads/{downloadId}
Auth Required

Fetch a Download

Stream the bytes of a completed download (Content-Disposition: attachment). 409 DOWNLOAD_NOT_READY while it is still in progress or was canceled, 404 for an id this session never reported, 502 DOWNLOAD_GONE when the browser service no longer has the file. Only a file this session's own download events named is ever served: the browser's files directory is shared by every session and the filename never comes from the caller. Fetch before releasing the session — the file lives on the browser machine and goes with it. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id
downloadIdstringRequiredA download id from GET …/downloads

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/downloads/{downloadId}" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200The file
{
"note": "binary body with Content-Type from the browser service, Content-Disposition attachment"
}
400downloadId is not shaped like a download id
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "downloadId is not a download id"
}
}
404No such download on this session
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "No such download on this session — list GET …/downloads for the ids it knows"
}
}
409Not finished
{
"success": false,
"error": {
"code": "DOWNLOAD_NOT_READY",
"message": "Download is in_progress (4096/20480 bytes) — the bytes are served once it completes",
"details": {
"state": "in_progress",
"receivedBytes": 4096,
"totalBytes": 20480
}
}
}
410Canceled (DOWNLOAD_CANCELED), or completed outside this session's storage and not servable (DOWNLOAD_UNAVAILABLE)
{
"success": false,
"error": {
"code": "DOWNLOAD_CANCELED",
"message": "This download was canceled; there are no bytes to serve",
"details": {
"state": "canceled"
}
}
}
502The browser service answered 404/500 (DOWNLOAD_GONE — file removed or machine replaced) or some other failure (DOWNLOAD_FETCH_FAILED)
{
"success": false,
"error": {
"code": "DOWNLOAD_FETCH_FAILED",
"message": "The browser service answered 503 for export.csv — the file may have been removed, or the session's machine replaced"
}
}
503The browser service could not be reached at all
{
"success": false,
"error": {
"code": "BROWSER_SERVICE_UNAVAILABLE",
"message": "Could not reach the browser service: fetch failed"
}
}
POST/api/browser/sessions/{id}/pdf
Auth Required

Render PDF

Render the current page to a PDF via CDP Page.printToPDF and return it base64-encoded inside JSON ({ base64, pageCount }), not as raw bytes. Rate-limited under browserScreenshot, since it encodes a frame like a screenshot does. Scope browser:write.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Request Body

NameTypeRequiredDescription
landscapebooleanOptionalLandscape orientation
printBackgroundbooleanOptionalInclude background graphics
scalenumberOptionalScale factor (0.1–2)
paperWidthnumberOptionalPaper width in inches (0.1–100)
paperHeightnumberOptionalPaper height in inches (0.1–100)
marginTopnumberOptionalTop margin in inches (≥0)
marginBottomnumberOptionalBottom margin in inches (≥0)
marginLeftnumberOptionalLeft margin in inches (≥0)
marginRightnumberOptionalRight margin in inches (≥0)
tabstringOptionalTarget tab id (defaults to the active tab)

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/pdf" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"

Responses

200PDF rendered
{
"success": true,
"data": {
"base64": "JVBERi0xLjQK…",
"pageCount": 3
}
}
GET/api/browser/sessions/{id}/viewer
Auth Required

Session Viewer (HTML)

Returns an HTML page (not JSON) embedding the live browser. Auth by ?token= viewer token (the only option for a cookie-less external iframe), session cookie, or sk_ API key. Scope browser:read.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Query Parameters

NameTypeRequiredDescription
tokenstringOptionalViewer token from the viewer-token endpoint (required for external cross-origin embedding)

Example Request

curl -X GET "https://scrappy.gg/api/browser/sessions/{id}/viewer" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Viewer HTML
{
"contentType": "text/html"
}
POST/api/browser/sessions/{id}/viewer-token
Auth Required

Viewer Token

Mint a short-lived (~5 min) signed token bound to {sessionId, userId} that authenticates a cookie-less iframe to the viewer and its screencast WebSocket. Call from a trusted backend (scope browser:write — changed 2026-10-01 from browser:read, because the viewer's socket carries input back, so the token is interactive control of the session, not read-only access); never expose the API key to a browser. Returns an absolute viewerUrl with the token embedded.

Path Parameters

NameTypeRequiredDescription
idstringRequiredSession id

Example Request

curl -X POST "https://scrappy.gg/api/browser/sessions/{id}/viewer-token" \
-H "Authorization: Bearer YOUR_API_KEY"

Responses

200Token minted
{
"token": "eyJ2…",
"expiresAt": 1753104000000,
"expiresInMs": 300000,
"viewerUrl": "https://scrappy.gg/api/browser/sessions/b1f2…/viewer?token=eyJ2…"
}

Infrastructure

Health checks and monitoring endpoints

2 endpoints
GET/api/health
Public

Health Check

Check database and Redis connectivity, and whether background jobs are running. Wrapped in the standard envelope; status is healthy | degraded | unhealthy; database and redis are connected | disconnected | disabled | unknown, and worker is alive | stale | not_consuming | disabled | unknown. Only unhealthy (database down) answers 503 — degraded (Redis down, or background jobs not running, database fine) stays 200 so a Redis blip does not get the machine restarted. worker: stale means background jobs (webhooks, per-minute billing, scheduled sources) are queueing and not running.

Example Request

curl -X GET "https://scrappy.gg/api/health"

Responses

200Health status
{
"success": true,
"data": {
"status": "healthy",
"timestamp": "2024-01-15T10:30:00Z",
"services": {
"database": "connected",
"redis": "connected",
"worker": "alive"
}
}
}
503Database unreachable
{
"success": false,
"data": {
"status": "unhealthy",
"timestamp": "2024-01-15T10:30:00Z",
"services": {
"database": "disconnected",
"redis": "connected",
"worker": "alive"
}
}
}
POST/api/client-errors
Auth Required

Report a Client Error or Metric

The server half of browser-side error reporting and web vitals. Reports land in AppLog with reason client_error; level 'metric' rows land as client.<message> metrics beside the server's own timings. Accepts a dashboard session cookie or an API key with the profiles:read scope — never anonymous, so a signed-out visitor's report is answered 401 and dropped. Always answers 204 with no body: over the rate limit (30/min), oversized (32KB body, 4KB meta), malformed or unparseable reports are dropped silently rather than answered 4xx, because the reporter must never become something a page retries. Bearer-shaped values (token=, sig=, key=, apiKey=, sk_…) are redacted from url and stack before storage.

Rate Limited: 30 requests per 1m per user

Request Body

NameTypeRequiredDescription
levelstringOptionalwarn | error | metric (default error)Options: warn, error, metricDefault: error
messagestringRequiredWhat happened (1–1000). For a metric, the metric name — bounded to 60 chars and low-cardinality
valuenumberOptionalThe measurement, required when level is metric (ms for timings; CLS is a unitless score)
urlstringOptionalWhere the page was (≤2048)
stackstringOptionalStack trace (≤8000)
countnumberOptionalHow many identical events the client coalesced into this report (1–10000, default 1)
metaobjectOptionalExtra fields; for a metric only rating, path and navigationType become tags

Example Request

curl -X POST "https://scrappy.gg/api/client-errors" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "string"
}'

Responses

204Accepted, or dropped — the two are indistinguishable by design
{}

Need help? Check out our User Guides or contact support.