Download any dataset with one HTTP call
Ask for a dataset and get it as CSV or JSONL, refreshed first if it is older than you allow. When you need your own targets, every scraper is also a callable with a typed input. Both are paid in credits, by the row.
The base URL is https://api.pandorlabs.com/v1. Requests and responses are JSON. Nothing on these pages is written by hand: the scraper list, each scraper's parameters and every endpoint are read from the same registry the API validates against, so what you read here is what the API accepts.
Download a dataset
Three calls: create a download, poll it, fetch the file. The examples use the CryptoPunks Leaderboard dataset; swap in any id from the scraper directory (a scraper and its dataset share one id). Export your key as PANDORLABS_KEY first; it needs the datasets:export scope.
- 1
Create a download
Both body fields are optional.
maxAgeHours(default 24) is how old the data may be. If the dataset was refreshed within that window, you are charged the cached rate immediately and the download isready. If it is older, the scraper is re-run with its configured settings, the download isrefreshing, and you are charged the live rate once that run completes. The API answers202with the download, whoseidyou keep.Terminalcurl -X POST "https://api.pandorlabs.com/v1/datasets/cryptopunks-leaderboard/downloads" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{"maxAgeHours":24,"format":"csv"}' - 2
Poll the download
Fetch the download until
statusisready. A cached download is ready on the first response; a refresh takes as long as the scraper run. If the refresh fails, the status isfailedand nothing is charged.Terminalcurl "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY" - 3
Fetch the file
The file is streamed as CSV or JSONL, whichever
formatyou asked for. Once paid, it can be fetched any number of times untilexpiresAt, 24 hours after the download became ready.Terminalcurl "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID/file" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -o dataset.csv
The whole flow in one script
DOWNLOAD_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/datasets/cryptopunks-leaderboard/downloads" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{"maxAgeHours":24,"format":"csv"}' | jq -r '.id')while true; do STATUS=$(curl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY" | jq -r '.status') [ "$STATUS" != "refreshing" ] && break sleep 5donecurl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID/file" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -o dataset.csvRun with your own parameters
A download gives you the dataset as the scraper is configured. To choose the targets yourself, start a run with your own parameters. The key needs the scrapers:run scope, and the account needs a positive credit balance to start.
- 1
Start a run
Send the scraper's parameters as
input. Every parameter is optional; anything you leave out uses its documented default. The API answers202straight away with the run, whoseidyou keep.curl -X POST "https://api.pandorlabs.com/v1/scrapers/cryptopunks-leaderboard/runs" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "maxHolders": 100, "enrichWithOpenSea": false, "maxEnriched": 100, "enrichmentDelayMs": 500 } }' - 2
Poll the run
Runs are asynchronous and take from seconds to minutes. Fetch the run until
statusiscompleted,failedorcancelled. At that point it is billed at the live rate for the rows it collected, andbilling.statusbecomespaid.Terminalcurl "https://api.pandorlabs.com/v1/runs/$RUN_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY" - 3
Read the rows
Rows are readable once the run has finished and is paid; before that the endpoint answers
409(still queued or running) or402(payment required). They are paginated: keep requesting the next page whilehasNextis true.Terminalcurl "https://api.pandorlabs.com/v1/runs/$RUN_ID/rows?page=1&limit=100" \ -H "Authorization: Bearer $PANDORLABS_KEY"
The whole flow in one script
RUN_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/scrapers/cryptopunks-leaderboard/runs" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "maxHolders": 100, "enrichWithOpenSea": false, "maxEnriched": 100, "enrichmentDelayMs": 500 } }' | jq -r '.id')while true; do RUN=$(curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY") STATUS=$(echo "$RUN" | jq -r '.status') BILLING=$(echo "$RUN" | jq -r '.billing.status') case "$STATUS" in queued|running) ;; *) [ "$BILLING" != "pending" ] && break ;; esac sleep 5doneecho "$RUN" | jq '{status, rowCount, billing}'curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID/rows?page=1&limit=100" \ -H "Authorization: Bearer $PANDORLABS_KEY"Pricing
You pay in credits, by the row, in blocks of 100 rows; a partly filled block counts as a whole one. Buy credits in the dashboard under Credits.
| Rate | Per 100 rows | Applies to |
|---|---|---|
| cached | 1 credit | A download served from data that is already fresh enough for your maxAgeHours. |
| live | 2 credits | A download that had to re-run the scraper first, and every run you start with your own parameters. |
The price is ceil(rows / 100) × credits per block. For example, 2,450 rows is 25 blocks: 25 credits cached, or 50 credits live.
A download is priced on the rows in the dataset; its credits field carries the amount. A run is priced on the rows it collected, reported in billing.credits; a run that collects nothing costs nothing. A refresh that fails is never charged.
While a download is refreshing, its credits are an estimate based on the rows the dataset held before the re-run. The final price uses the rows it holds afterwards; if that is more than your balance the download waits as payment_required until you top up.
Starting a download needs enough credits to cover its quote, and starting a run needs a positive balance. Otherwise the API answers 402 with the amount required and your balance.
Authentication
Every request carries an API key as a bearer token. Create keys in the dashboard under API keys; a key is shown once, at creation.
Authorization: Bearer $PANDORLABS_KEYKeys are scoped: each one only reaches the endpoints its scopes allow. Downloading datasets needs datasets:export (export datasets); starting, following and cancelling runs needs scrapers:run (run scrapers). The scope each endpoint requires is listed in the API reference. Give each script its own key so one can be revoked without breaking the rest.
Statuses and billing
Downloads and runs are both asynchronous. Poll every few seconds; there is no need to go faster.
Downloads
- refreshing
The dataset was older than maxAgeHours, so its scraper is being re-run. runId points at that run. Nothing has been charged yet.
- ready
Paid for and downloadable. Fetch the file as often as you like until expiresAt.
- payment_required
The refresh finished but the balance could not cover it. Top up and request the file again; the charge is retried.
- failedterminal
The refresh run failed and nothing was charged. Start a new download with a larger maxAgeHours to accept the data that already exists.
- expiredterminal
Past expiresAt. Start a new download to get the file again.
Runs
- queued
Accepted and waiting for a worker. Nothing has been fetched yet.
- running
Collecting. rowCount climbs as rows are parsed; the rows themselves are not readable yet.
- completedterminal
Finished normally. Billed for the rows it collected.
- failedterminal
Stopped on an error, reported in error.message. Billed for the rows collected before the failure.
- cancelledterminal
Stopped by a cancel request. Billed for the rows collected before the cancel.
To stop a run early, send POST /v1/runs/{runId}/cancel. GET /v1/runs lists the runs your account started, and GET /v1/downloads your downloads.
Run billing
Every run carries a billing object next to its status. The rows are readable only when the run has finished and its billing is paid.
- pending
The run has not been billed yet.
- paid
Charged billing.credits. The rows are readable. A run that collected no rows is paid at zero credits.
- payment_required
The balance could not cover the run. Top up, then fetch the run or its rows again; the charge is retried on that request.
Errors and rate limits
Errors use standard HTTP status codes with a JSON body. The message field is a string, or a list of strings when several things are wrong at once.
- 400Bad request
An unknown parameter, or a value that fails the scraper’s input schema. The message field lists every problem.
- 401Unauthorized
The key is missing, malformed, expired or revoked.
- 402Payment required
Not enough credits: to start a download or a run, or to read a run’s rows or a download’s file while its billing is payment_required. Top up and send the same request again.
- 403Forbidden
The key is valid but lacks the scope the endpoint needs. The message names the missing scope.
- 404Not found
No dataset or scraper with that id, or no run or download with that id on your account.
- 409Conflict
Not ready yet: the rows of a run that is still queued or running, or the file of a download that is still refreshing. Keep polling.
- 410Gone
The download has expired. Start a new one to get the file again.
- 429Too many requests
The key went over its per-minute rate limit, or the account already has 5 runs in flight.
{ "statusCode": 400, "message": [ "maxItems must be a number", "unknown parameter: colour" ], "error": "Bad Request"}{ "statusCode": 402, "message": "Not enough credits", "required": 50, "balance": 0}Rate limits
Each key has its own per-minute request limit, shown next to the key in the dashboard. Every response reports where you stand in x-ratelimit-limit and x-ratelimit-remaining. Separately, an account can have at most 5 runs queued or running at once; starting another returns 429 until one finishes or is cancelled.
OpenAPI spec
The whole API is published as an OpenAPI 3.1 document, generated from the same registry as these pages. It needs no key, and has one typed operation per scraper, so a generated client knows each scraper’s parameters.
curl "https://api.pandorlabs.com/v1/openapi.json"MCP and agents
Each scraper is described the way a tool is: an id, a description and a JSON Schema for its input.
To give an agent the scrapers as tools, build the tool list from GET /v1/scrapers: use each entry's inputSchema as the tool's input schema and forward the call to POST /v1/scrapers/{scraperId}/runs. Runs are billed, so let the agent see billing in the result. The OpenAPI document carries the same schemas for frameworks that import a spec directly.
PandorLabs also has an MCP server, but there is no hosted MCP endpoint to connect to. It is a local stdio process that ships with the platform and authenticates with the platform's internal service token rather than an API key, so it is used on deployments we operate, not from outside. With an API key, the HTTP API above is the supported integration.
