logoPandorLabs
Developer docs

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.

770 scrapers · 27 endpointsBearer API keyFrom 1 credit per 100 rowsOpenAPI 3.1

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. 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 is ready. If it is older, the scraper is re-run with its configured settings, the download is refreshing, and you are charged the live rate once that run completes. The API answers 202 with the download, whose id you keep.

    Terminal
    curl -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. 2

    Poll the download

    Fetch the download until status is ready. A cached download is ready on the first response; a refresh takes as long as the scraper run. If the refresh fails, the status is failed and nothing is charged.

    Terminal
    curl "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID" \  -H "Authorization: Bearer $PANDORLABS_KEY"
  3. 3

    Fetch the file

    The file is streamed as CSV or JSONL, whichever format you asked for. Once paid, it can be fetched any number of times until expiresAt, 24 hours after the download became ready.

    Terminal
    curl "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.csv

Run 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. 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 answers 202 straight away with the run, whose id you 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. 2

    Poll the run

    Runs are asynchronous and take from seconds to minutes. Fetch the run until status is completed, failed or cancelled. At that point it is billed at the live rate for the rows it collected, and billing.status becomes paid.

    Terminal
    curl "https://api.pandorlabs.com/v1/runs/$RUN_ID" \  -H "Authorization: Bearer $PANDORLABS_KEY"
  3. 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) or 402 (payment required). They are paginated: keep requesting the next page while hasNext is true.

    Terminal
    curl "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.

RatePer 100 rowsApplies to
cached1 creditA download served from data that is already fresh enough for your maxAgeHours.
live2 creditsA 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.

Header
Authorization: Bearer $PANDORLABS_KEY

Keys 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.

Example 400 response
{  "statusCode": 400,  "message": [    "maxItems must be a number",    "unknown parameter: colour"  ],  "error": "Bad Request"}
Example 402 response
{  "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.

Terminal
curl "https://api.pandorlabs.com/v1/openapi.json"
Open 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.

© 2026 PandorLabs LLC. All rights reserved.