The real problem
Why this is harder than it looks
PhantomBuster is not a typical OAuth integration. It authenticates with a single API key that grants full access to an organization — launching agents, reading lead data, managing billing, and everything else. That simplicity is deceptive, because in a multi-tenant product it creates a storage and isolation problem most developers don't anticipate until they're in production.
Every user who connects PhantomBuster to your product has their own API key. You need a place to store those keys that is securely isolated per user — one customer's key must never be accessible when you're executing on behalf of another. Unlike OAuth, there's no token exchange or expiry lifecycle to manage, but the storage and retrieval problem is real: you need a vault that maps your internal user identifiers to PhantomBuster credentials, and that mapping has to be consistent across every tool call your agent makes.
Beyond storage, there are operational patterns specific to PhantomBuster that are easy to get wrong. Agents are asynchronous — phantombuster_agent_launch returns a container ID, not a result. You need polling logic that reads incremental output using fromOutputPos offsets, detects terminal states, and then fetches the structured result separately. Skip the quota check before launching and you'll hit a 429 mid-run with no partial result to recover from. PhantomBuster also has a plan-tiered API — AI completions require credits, branch management requires Team+, and CRM contact saving requires an active HubSpot integration — so your agent needs to reason about capability availability, not just call tools blindly.
Scalekit handles API key storage and per-user isolation, injects credentials on every request, and gives your agent structured tools for the full PhantomBuster API surface. Your code names a tool and passes parameters. The credential plumbing is not your problem.
Capabilities
What your agent can do with PhantomBuster
Once connected, your agent has 38 pre-built tools covering the full PhantomBuster platform:
- Launch and monitor automation agents: start runs immediately or on a delay, stream incremental console output, stop running containers, and fetch structured results when execution completes
- Manage the full agent lifecycle: create, configure, schedule, and delete agents; unschedule all at once for emergency stops; audit deleted agents for recovery planning
- Save and query leads at scale: bulk-save up to 1,000 profiles per call to named lead lists, fetch with pagination, delete by ID — the full lead management surface
- Check and respect resource limits: fetch org-level execution time, AI credit, and storage balances before launching to avoid mid-run quota failures
- Run AI enrichment on scraped data: call PhantomBuster's AI completion service with structured response schemas to parse job titles, extract skills, or enrich raw profile output
Setup context
What we're building
This guide connects a lead generation agent to PhantomBuster — launching scraping automations, streaming results, and saving enriched profiles to lead lists on behalf of each user.
🤖
Example agent
Lead generation assistant launching PhantomBuster scrapers, monitoring runs, and saving profiles to lead lists on behalf of each user
🔑
Auth model
API Key auth — each user connects their own PhantomBuster account. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no PhantomBuster secrets, no user API keys, nothing belonging to your customers.
pip install scalekit-sdk-python
npm install @scalekit-sdk/node
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
import { ScalekitClient } from '@scalekit-sdk/node';
import 'dotenv/config';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENV_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const actions = scalekit.actions;
Already have credentials?
Connected Accounts
2 Per-User Auth: Registering connected accounts
PhantomBuster uses API key authentication — there is no OAuth redirect flow. Each user provides their PhantomBuster API key once, and you register it with Scalekit. From that point on, Scalekit injects the key automatically on every tool call for that user.
scalekit.actions.upsert_connected_account(
connection_name="phantombuster",
identifier="user_pb_123", # your internal user ID
credentials={"api_key": "your-users-phantombuster-api-key"}
)
# Call this when a user connects their PhantomBuster account in your product's settings page
await actions.upsertConnectedAccount({
connectionName: "phantombuster",
identifier: "user_pb_123", // your internal user ID
credentials: { api_key: "your-users-phantombuster-api-key" }
});
// Call this when a user connects their PhantomBuster account in your product's settings page
This call is idempotent — safe to call on key rotation or reconnection. Scalekit stores the API key encrypted in its vault and returns it on every subsequent tool call for this identifier.
No OAuth flow needed
PhantomBuster uses API key auth — there is no authorization link, redirect URI, or consent screen. Once you call upsertConnectedAccount, the user's account is immediately active. Scalekit injects the API key as the X-Phantombuster-Key header on every request. You never handle it in application code.
Get the API key from users at setup
Users find their PhantomBuster API key at phantombuster.com → Settings → API. Add a settings page in your product where users paste this key. Your backend calls upsertConnectedAccount to register it. The key grants full org access — communicate this clearly in your UI so users understand the permission scope before connecting.
Calling PhantomBuster
3 Calling PhantomBuster: What your agent writes
With the connected account registered, your agent calls PhantomBuster tools using execute_tool. Name the tool, pass parameters. Scalekit handles credential retrieval and request construction.
Launch an agent and stream output
The most common PhantomBuster workflow: launch an automation, poll for incremental output using fromOutputPos, then fetch the structured result when execution completes.
import time
# Step 1: Launch the agent
launch = actions.execute_tool(
identifier="user_pb_123",
tool_name="phantombuster_agent_launch",
tool_input={
"id": "AGENT_ID",
"output": "result-object"
}
)
container_id = launch["containerId"]
# Step 2: Stream output incrementally until finished
output_pos = 0
while True:
output = actions.execute_tool(
identifier="user_pb_123",
tool_name="phantombuster_container_fetch_output",
tool_input={"id": container_id, "fromOutputPos": output_pos}
)
print(output.get("output", ""), end="", flush=True)
output_pos = output.get("nextOutputPos", output_pos)
if output.get("status") in ("finished", "error"):
break
time.sleep(3)
# Step 3: Fetch the structured result
result = actions.execute_tool(
identifier="user_pb_123",
tool_name="phantombuster_container_fetch_result",
tool_input={"id": container_id}
)
# Returns scraped profiles, leads, or whatever the script produces
// Step 1: Launch the agent
const launch = await actions.executeTool({
identifier: "user_pb_123",
toolName: "phantombuster_agent_launch",
toolInput: {
id: "AGENT_ID",
output: "result-object"
}
});
const containerId = launch.containerId;
// Step 2: Stream output incrementally until finished
let outputPos = 0;
while (true) {
const output = await actions.executeTool({
identifier: "user_pb_123",
toolName: "phantombuster_container_fetch_output",
toolInput: { id: containerId, fromOutputPos: outputPos }
});
process.stdout.write(output.output || "");
outputPos = output.nextOutputPos || outputPos;
if (["finished", "error"].includes(output.status)) break;
await new Promise(r => setTimeout(r, 3000));
}
// Step 3: Fetch the structured result
const result = await actions.executeTool({
identifier: "user_pb_123",
toolName: "phantombuster_container_fetch_result",
toolInput: { id: containerId }
});
// Returns scraped profiles, leads, or whatever the script produces
Check resource quota before launching
Avoid mid-run failures by verifying execution time and credit balances before starting a scraping job. PhantomBuster enforces plan-level quotas — a 429 mid-run produces no partial result.
resources = actions.execute_tool(
identifier="user_pb_123",
tool_name="phantombuster_org_fetch_resources",
tool_input={}
)
exec_time = resources.get("executionTime", {})
if exec_time.get("remaining", 0) < 30:
raise RuntimeError(
f"Insufficient execution time: {exec_time.get('remaining')} min remaining."
)
print(f"Execution time: {exec_time['remaining']} min remaining")
const resources = await actions.executeTool({
identifier: "user_pb_123",
toolName: "phantombuster_org_fetch_resources",
toolInput: {}
});
const execTime = resources.executionTime || {};
if ((execTime.remaining || 0) < 30) {
throw new Error(`Insufficient execution time: ${execTime.remaining} min remaining.`);
}
console.log(`Execution time: ${execTime.remaining} min remaining`);
Save scraped profiles as leads
After a run completes, bulk-save extracted profiles to a named lead list. Up to 1,000 leads per call — more efficient than looping phantombuster_leads_save.
# Fetch available lead lists
lists = actions.execute_tool(
identifier="user_pb_123",
tool_name="phantombuster_lists_fetch_all",
tool_input={}
)
list_id = lists[0]["id"] # or filter by name
# Bulk-save profiles from a completed run
actions.execute_tool(
identifier="user_pb_123",
tool_name="phantombuster_leads_save_many",
tool_input={
"listId": list_id,
"leads": [
{
"firstName": p.get("firstName"),
"lastName": p.get("lastName"),
"email": p.get("email"),
"linkedinUrl": p.get("linkedinUrl"),
"company": p.get("company"),
"jobTitle": p.get("title"),
"additionalFields": {"source": "phantombuster", "agentId": "AGENT_ID"}
}
for p in result
]
}
)
print(f"{len(result)} leads saved to list {list_id}")
// Fetch available lead lists
const lists = await actions.executeTool({
identifier: "user_pb_123",
toolName: "phantombuster_lists_fetch_all",
toolInput: {}
});
const listId = lists[0].id; // or filter by name
// Bulk-save profiles from a completed run
await actions.executeTool({
identifier: "user_pb_123",
toolName: "phantombuster_leads_save_many",
toolInput: {
listId,
leads: result.map(p => ({
firstName: p.firstName,
lastName: p.lastName,
email: p.email,
linkedinUrl: p.linkedinUrl,
company: p.company,
jobTitle: p.title,
additionalFields: { source: "phantombuster", agentId: "AGENT_ID" }
}))
}
});
console.log(`${result.length} leads saved to list ${listId}`);
Enrich leads with AI completions
Use PhantomBuster's AI service to extract structured fields from raw scraper output — seniority levels, primary skills, or any other structured extraction. Requires AI credits (Pro+).
completion = actions.execute_tool(
identifier="user_pb_123",
tool_name="phantombuster_ai_completions",
tool_input={
"model": "gpt-4o",
"messages": [
{
"role": "system",
"content": "Extract seniority and primary skill from this LinkedIn headline. Return JSON only."
},
{
"role": "user",
"content": "Senior Software Engineer at Acme Corp | React, TypeScript, GraphQL"
}
],
"responseSchema": {
"type": "object",
"properties": {
"seniority": {"type": "string", "enum": ["junior", "mid", "senior", "lead", "exec"]},
"primarySkill": {"type": "string"}
},
"required": ["seniority", "primarySkill"]
}
}
)
# → {"seniority": "senior", "primarySkill": "React"}
const completion = await actions.executeTool({
identifier: "user_pb_123",
toolName: "phantombuster_ai_completions",
toolInput: {
model: "gpt-4o",
messages: [
{
role: "system",
content: "Extract seniority and primary skill from this LinkedIn headline. Return JSON only."
},
{
role: "user",
content: "Senior Software Engineer at Acme Corp | React, TypeScript, GraphQL"
}
],
responseSchema: {
type: "object",
properties: {
seniority: { type: "string", enum: ["junior", "mid", "senior", "lead", "exec"] },
primarySkill: { type: "string" }
},
required: ["seniority", "primarySkill"]
}
}
});
// → { seniority: "senior", primarySkill: "React" }
Framework wiring
4 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides which tools to call; Scalekit handles credential injection on every invocation. No API key plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
pb_tools = get_tools(
connection_name="phantombuster",
identifier="user_pb_123"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a lead generation assistant. Use the available tools to launch PhantomBuster agents, monitor runs, and manage lead lists."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), pb_tools, prompt)
result = AgentExecutor(agent=agent, tools=pb_tools).invoke({
"input": "Check my execution time balance, launch the LinkedIn scraper agent, and save any new profiles to the Outbound Q2 lead list"
})
import { ChatAnthropic } from "@langchain/anthropic";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { getTools } from "@scalekit-sdk/langchain";
const pbTools = getTools({
connectionName: "phantombuster",
identifier: "user_pb_123"
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a lead generation assistant. Use the available tools to launch PhantomBuster agents, monitor runs, and manage lead lists."],
new MessagesPlaceholder("chat_history", true),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createToolCallingAgent({
llm: new ChatAnthropic({ model: "claude-sonnet-4-6" }),
tools: pbTools,
prompt
});
const result = await AgentExecutor.fromAgentAndTools({
agent,
tools: pbTools
}).invoke({
input: "Check my execution time balance, launch the LinkedIn scraper agent, and save any new profiles to the Outbound Q2 lead list"
});
Other frameworks supported
Tool reference
All 38 PhantomBuster tools
Grouped by capability. Your agent calls tools by name — no API wrappers to write.
phantombuster_agents_fetch_all
Retrieve all automation agents in the org — IDs, names, scripts, schedules, and current status
phantombuster_agent_fetch
Get full details of a specific agent by ID — script, schedule, launch type, arguments, and status
Create or update an agent. Omit id to create; include id to update. Supports script, schedule, proxy, notifications, and execution limits
phantombuster_agent_delete
Permanently delete an agent and all its execution history. Irreversible
phantombuster_agent_launch
Launch an agent immediately. Returns a containerId for tracking progress via container tools
phantombuster_agent_launch_soon
Schedule an agent to launch in N minutes without setting up a full recurring schedule
Stop a running agent. Saves partial results collected up to that point
phantombuster_agents_fetch_deleted
Retrieve all deleted agents with deletion timestamps and actor — useful for audit trails
phantombuster_agents_unschedule_all
Disable automatic launch for every agent in the org. Use with caution — affects all scheduled agents at once
phantombuster_containers_fetch_all
List all execution runs for a specific agent — IDs, status, exit codes, and timestamps. Paginated
phantombuster_container_fetch
Get a single container by ID. Optionally include output, result object, and runtime events in one call
phantombuster_container_fetch_output
Retrieve console output from a container. Use fromOutputPos to poll incrementally without re-reading prior output
phantombuster_container_fetch_result
Fetch the structured result object from a completed container — scraped profiles, extracted data, or exported records
phantombuster_container_attach
Attach to a running container and stream live console output in real-time
phantombuster_agent_fetch_output
Get the most recent container's output for an agent. Use fromOutputPos for incremental reads
phantombuster_org_fetch_running_containers
List all containers currently executing across the org — useful for monitoring live runs and detecting runaway agents
Save a single lead to a list. Updates if a lead with the same identifier already exists
phantombuster_leads_save_many
Bulk-save up to 1,000 leads per call. More efficient than looping phantombuster_leads_save for post-run ingestion
phantombuster_leads_delete_many
Permanently delete up to 1,000 leads by ID. Irreversible
phantombuster_leads_fetch_by_list
Fetch leads from a list with pagination. Up to 1,000 per page
phantombuster_lists_fetch_all
Retrieve all lead lists — IDs, names, lead counts, and creation timestamps
Retrieve a specific lead list by ID
phantombuster_list_delete
Permanently delete a lead list and all leads within it. Irreversible
Retrieve org details — plan, billing info, timezone, proxy config, and connected CRM integrations
phantombuster_org_fetch_resources
Get current resource usage and limits — execution time, AI credits, SERP credits, storage, and agent count
phantombuster_org_fetch_agent_groups
Retrieve agent groups and their ordering for dashboard organisation
phantombuster_org_save_agent_groups
Update agent groups and their ordering. Order is preserved exactly as provided
phantombuster_org_export_agent_usage
Export a CSV of agent usage metrics for up to 180 days. Pro+ for full range
phantombuster_org_export_container_usage
Export a CSV of container run metrics. Optionally filter to a specific agent
phantombuster_org_save_crm_contact
Save a contact to the connected HubSpot CRM. Requires HubSpot integration configured in PhantomBuster settings (Pro+)
phantombuster_scripts_fetch_all
Retrieve all scripts for the current user — IDs, names, slugs, branches, and manifests
phantombuster_script_fetch
Get a specific script by ID including its argument schema. Source code available with Team+
phantombuster_branches_fetch_all
Retrieve all script branches in the org — for managing staging vs. production script versions
phantombuster_branch_create
Create a new branch for developing and testing script changes without affecting production agents
phantombuster_branch_delete
Permanently delete a branch and all associated scripts. Irreversible
phantombuster_branch_release
Promote specified scripts from a branch to production
phantombuster_ai_completions
Run AI completions via PhantomBuster's service. Supports GPT-4o and GPT-4.1-mini with structured JSON response schemas. Requires AI credits (Pro+)
phantombuster_location_ip
Geolocate an IPv4 or IPv6 address — useful for validating proxy locations or enriching lead data
Connector notes
PhantomBuster-specific behavior
Agent execution is asynchronous — always poll for results
phantombuster_agent_launch returns a containerId, not a result. The agent runs asynchronously. Use phantombuster_container_fetch_output with fromOutputPos to stream output incrementally, checking the status field for "finished" or "error" to know when to stop. Never assume a launch call means the result is ready.
Plan tier gates specific tools
Not all tools are available on all plans. AI completions require AI credits (Pro+). Branch management requires Team+. CRM contact saving requires a HubSpot integration configured in PhantomBuster settings (Pro+). Calling a tool your plan doesn't support returns a permission error — check phantombuster.com/pricing before building workflows that depend on tier-gated tools.
API key grants full org access
A PhantomBuster API key gives complete access to the user's organization — launching agents, reading leads, managing billing. Communicate this clearly in your product's settings UI when asking users to connect. Users can find and regenerate their key at phantombuster.com → Settings → API.
Infrastructure decision
Why not build this yourself
The PhantomBuster API is documented. Storing an API key per user isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Per-user API key storage that is securely isolated across tenants — one customer's PhantomBuster key must never be retrieved when executing on behalf of another
PROBLEM 02
Asynchronous execution patterns — launch, incremental output polling with byte offsets, terminal state detection, and structured result retrieval as a reliable multi-step workflow
PROBLEM 03
Plan-tiered API surface where tool availability varies by subscription — AI credits, branch access, and CRM tools each have different requirements that must be checked before invocation
PROBLEM 04
Quota management across execution time, AI credits, and storage — exceeded limits return 429s mid-run with no partial result, requiring proactive balance checks before every launch
That's one connector. Your agent product will eventually need Salesforce, GitHub, Gmail, Slack, and whatever else your customers ask for. Each has its own auth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.