# Introduction (https://pullwire.dev/docs)
> What Pullwire is, how it works, and where to start.
Pullwire gives your AI agent or app live data from the web. You get **one API key** and **one credit balance**, and every data tool works the same way: send an input, get clean JSON back, pay only for the results you receive.
## How it works [#how-it-works]
1. **Pick a tool.** Each tool reads one kind of public web data, for example YouTube transcripts, job listings or ads from the Facebook Ad Library. The full list is in the [tool reference](/docs/tools).
2. **Start a run.** Send the tool's input as JSON to `POST /api/v1/tools/{toolId}/runs` with your API key.
3. **Get results.** Short runs answer right away with the results. Longer runs can run in the background; you fetch the results when they are done, as JSON or CSV.
4. **Pay per result.** Each result costs the credits listed for its tool. Before a run starts, credits are set aside for the most results it can return; afterwards you are charged only for the results you actually got and the rest goes back to your balance.
## Where to go next [#where-to-go-next]
## Connect [#connect]
## For AI agents [#for-ai-agents]
These docs are also available as plain text, so an agent can read them without rendering HTML:
- [`/llms.txt`](/llms.txt): a short index of the docs and all tools.
- [`/llms-full.txt`](/llms-full.txt): every docs page and the full tool reference in one file.
- [`/api/v1/openapi.json`](/api/v1/openapi.json): the OpenAPI 3 description of the API.
- [`/api/v1/tools`](/api/v1/tools): the live tool catalog as JSON, including each tool's input schema.
# Quickstart (https://pullwire.dev/docs/quickstart)
> Create an account, get an API key and make your first call.
This guide takes you from zero to your first result. It uses the YouTube transcripts tool because it needs nothing but a video link.
## 1. Create an account [#1-create-an-account]
Sign in at [/login](/login) with your email address. You get a sign-in link by email, no password needed. New accounts start with **2,000 free credits**.
## 2. Get an API key [#2-get-an-api-key]
API keys start with `af_live_` and are shown to you **once**, right after you create them. We only store a hash of the key, so we cannot show it again; if you lose it, create a new one and revoke the old one.
The dashboard page for creating and revoking keys is still being built. Until it ships, keys for early
access are issued manually.
Keep the key out of your code and your repository. Put it in an environment variable instead:
```bash
export PULLWIRE_API_KEY="af_live_..."
```
## 3. Make your first call [#3-make-your-first-call]
Start a run of the `youtube-transcripts` tool. The request body is the tool's input; the [tool reference](/docs/tools#youtube-transcripts) lists every field.
```bash
curl -X POST "https://pullwire.dev/api/v1/tools/youtube-transcripts/runs" \
-H "x-api-key: $PULLWIRE_API_KEY" \
-H "content-type: application/json" \
-d '{"videos": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"], "includeSegments": false, "maxResults": 1}'
```
```js
const res = await fetch('https://pullwire.dev/api/v1/tools/youtube-transcripts/runs', {
method: 'POST',
headers: { 'x-api-key': process.env.PULLWIRE_API_KEY, 'content-type': 'application/json' },
body: JSON.stringify({
videos: ['https://www.youtube.com/watch?v=jNQXAC9IVRw'],
includeSegments: false,
maxResults: 1,
}),
});
const { run, results } = await res.json();
console.log(run.status, run.creditsUsed, results[0]?.title);
```
```python
import os
import requests
res = requests.post(
"https://pullwire.dev/api/v1/tools/youtube-transcripts/runs",
headers={"x-api-key": os.environ["PULLWIRE_API_KEY"]},
json={
"videos": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
"includeSegments": False,
"maxResults": 1,
},
timeout=60,
)
data = res.json()
print(data["run"]["status"], data["run"]["creditsUsed"])
```
A run that finishes within 25 seconds answers with the run and its results. The response looks like this:
```json
{
"run": {
"id": "6f1c2a9e-0000-4000-8000-000000000000",
"toolId": "youtube-transcripts",
"status": "succeeded",
"source": "api",
"resultsCount": 1,
"creditsUsed": 1,
"error": null
},
"results": [
{
"videoId": "jNQXAC9IVRw",
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"title": "Me at the zoo",
"text": "…"
}
]
}
```
Illustration of the response shape, shortened. Real responses contain more fields (see the [tool
reference](/docs/tools#youtube-transcripts)), real IDs and the real transcript.
## 4. Check your balance [#4-check-your-balance]
```bash
curl "https://pullwire.dev/api/v1/me" -H "x-api-key: $PULLWIRE_API_KEY"
```
The answer contains your `orgId` and your current `balance` in credits.
## Next steps [#next-steps]
- Long runs and background jobs: [Runs](/docs/runs)
- What a run costs: [Credits](/docs/credits)
- What can go wrong and what to do: [Errors](/docs/errors)
# MCP: Claude, ChatGPT and Cursor (https://pullwire.dev/docs/mcp)
> Connect Pullwire to Claude, ChatGPT, Cursor, Claude Code and n8n. One server, every tool.
The remote server is `https://pullwire.dev/api/mcp` (Streamable HTTP). Its name is `pullwire`. Listing the tools is free and needs no key. Each tool call is charged to your credits, the same as the API.
[Sign up](/login) with your email. New accounts get 2,000 free credits, no card. Then either
sign in with OAuth (Claude and ChatGPT) or create a key at [API keys](/app/keys) (it starts
with `af_live_`) for Claude Code, Cursor and n8n.
## Claude (claude.ai, Desktop, Cowork) [#claude-claudeai-desktop-cowork]
Claude’s connector directory and custom connectors sign in with OAuth. You do not paste an API key.
1. In Claude, open **Customize → Connectors → Add → Custom → Web**.
2. Name it `Pullwire`. Server URL: `https://pullwire.dev/api/mcp`.
3. Authentication: **Sign in when needed** (or Sign in now). OAuth client: **Register automatically**, or **Use Claude’s published identity**.
4. Approve the Pullwire consent screen. That connects the tools to your credit balance.
The same URL works for a directory submission. Tool calls without a session return `401` with the OAuth discovery header, which is what Claude uses to start sign-in. Listing tools stays open, so a directory can read the catalog first.
A fixed `x-api-key` header is a beta option on some Claude Team and Enterprise plans (Request headers when you add the connector). The header name is `x-api-key`. Prefer OAuth when each person should use their own account.
## Claude Code [#claude-code]
Run this once in your terminal. Replace `YOUR_API_KEY` with your key.
```bash title="Terminal"
claude mcp add --transport http pullwire \
https://pullwire.dev/api/mcp \
--header "x-api-key: YOUR_API_KEY"
```
Claude Code can also sign in with OAuth (a localhost callback) if you add the server without a header. The API-key header is the path that does not open a browser.
## Claude Desktop (local config) [#claude-desktop-local-config]
Settings → Developer → Edit Config (claude_desktop_config.json). Needs Node.js. The small `mcp-remote` helper connects the desktop app to our server.
```json title="claude_desktop_config.json"
{
"mcpServers": {
"pullwire": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://pullwire.dev/api/mcp",
"--header",
"x-api-key:${PULLWIRE_API_KEY}"
],
"env": {
"PULLWIRE_API_KEY": "YOUR_API_KEY"
}
}
}
}
```
## ChatGPT [#chatgpt]
Custom MCP servers are available on ChatGPT Business and Enterprise. You do not need Developer Mode.
1. Open **Plugins** (or Apps) and choose **Add custom MCP server**.
2. Name: `Pullwire`. URL: `https://pullwire.dev/api/mcp`.
3. Authentication: OAuth. ChatGPT reads `/.well-known/oauth-authorization-server`, sends you to the Pullwire consent screen, and refreshes the token (`offline_access` is advertised).
4. Review the tools and save.
Sign up first if you do not have an account yet. The consent screen uses the same login as the dashboard.
## Cursor [#cursor]
Add to ~/.cursor/mcp.json (or .cursor/mcp.json in a project). Restart Cursor so it picks up the new server. A `.mcp.json` at the project root is the file cursor.directory reads. Put your key in the `PULLWIRE_API_KEY` environment variable.
```json title="mcp.json"
{
"mcpServers": {
"pullwire": {
"url": "https://pullwire.dev/api/mcp",
"headers": {
"x-api-key": "YOUR_API_KEY"
}
}
}
}
```
## n8n and other HTTP clients [#n8n-and-other-http-clients]
n8n’s HTTP Request node calls the REST API. The [n8n guide](/docs/n8n) has the cURL import. For an MCP client node, use the same server URL and send the header `x-api-key`.
Any other client that speaks Streamable HTTP can do one of two things:
- Send `x-api-key: YOUR_API_KEY` or `Authorization: Bearer YOUR_API_KEY` on every request.
- Follow the OAuth 2.1 discovery on a `401` (`/.well-known/oauth-protected-resource`, then the authorization server). PKCE S256 is required. Public clients can register at `POST /oauth/register`.
## Ask your agent [#ask-your-agent]
Try: “Get the transcript of https://www.youtube.com/watch?v=jNQXAC9IVRw and summarise it.” The agent picks the `youtube-transcripts` tool, and the answer costs 1 credit.
## Good to know [#good-to-know]
- Server URL: `https://pullwire.dev/api/mcp`. Protocol name: `pullwire`.
- Listing the tools is free. Each tool call is charged per result that comes back. Failed results are free.
- Most tools cost 1 credit per result. A page that needs a browser or a residential proxy costs more; the tool’s description says the price.
- Revoke a connected client from **API keys** in the dashboard (the key is named `MCP · …`).
- Logo for directory forms: [512×512 mark](/pullwire-mark-512.png). Legal pages: [Imprint](/imprint), [Privacy](/privacy), [Terms](/terms).
# TypeScript and Python SDKs (https://pullwire.dev/docs/sdks)
> Typed clients generated from the OpenAPI document: run a tool, wait for a run, fetch results.
Both SDKs are generated from our OpenAPI document, so every tool input is typed. Same calls in TypeScript and Python.
You need an API key (it starts with `af_live_`). [Sign up](/login) with your email and you get
2,000 free credits.
## 1. Install [#1-install]
The packages go on npm and PyPI soon. Until then you can call the HTTP API directly (last step) or use MCP.
## 2. Run a tool [#2-run-a-tool]
Set `PULLWIRE_API_KEY` in your environment, or pass the key in code. `run` waits for the results and returns them with the credits used.
```ts
import { createPullwire } from '@af/sdk';
const pw = createPullwire(); // reads PULLWIRE_API_KEY
const { run, results } = await pw.run('youtube-transcripts', {"videos":["https://www.youtube.com/watch?v=jNQXAC9IVRw"],"maxResults":3});
console.log(run.creditsUsed, results[0]);
```
```python
from pullwire import Pullwire
pw = Pullwire() # reads PULLWIRE_API_KEY
out = pw.run("youtube-transcripts", {"videos":["https://www.youtube.com/watch?v=jNQXAC9IVRw"],"maxResults":3})
print(out.run.credits_used, out.results[0])
```
## 3. Bigger jobs [#3-bigger-jobs]
`start` returns right away; `waitForRun` / `wait_for_run` checks every 2 seconds until the run is done.
```ts
const queued = await pw.start('youtube-transcripts', {"videos":["https://www.youtube.com/watch?v=jNQXAC9IVRw"],"maxResults":3});
const done = await pw.waitForRun(queued.id);
const rows = await pw.results(done.id); // or pw.resultsCsv(done.id)
```
```python
queued = pw.start("youtube-transcripts", {"videos":["https://www.youtube.com/watch?v=jNQXAC9IVRw"],"maxResults":3})
done = pw.wait_for_run(str(queued.id))
rows = pw.results(str(done.id)) # or pw.results_csv(...)
```
## 4. Errors [#4-errors]
Every error comes with a stable code: `INSUFFICIENT_CREDITS` (402), `INVALID_INPUT` (400), `RATE_LIMITED` (429), `UNAUTHORIZED` (401). The SDKs throw `PullwireError` with `status`, `code` and `message`.
```ts
import { PullwireError } from '@af/sdk';
try {
await pw.run('youtube-transcripts', { videos: [] });
} catch (err) {
if (err instanceof PullwireError) console.log(err.status, err.code, err.message);
}
```
```python
from pullwire import PullwireError
try:
pw.run("youtube-transcripts", {"videos": []})
except PullwireError as err:
print(err.status, err.code, err.message)
```
## 5. Any other language [#5-any-other-language]
The API is plain HTTPS and JSON. The full OpenAPI 3.1 document lives at `/api/v1/openapi.json`, so you can generate a client for Go, Ruby or anything else.
```bash title="cURL"
curl -X POST https://pullwire.dev/api/v1/tools/youtube-transcripts/runs \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{"videos":["https://www.youtube.com/watch?v=jNQXAC9IVRw"],"maxResults":3}'
```
The call waits up to 25 seconds for the results. For bigger jobs add ?async=1 to the URL: you get a run id back right away and fetch the results a bit later.
# n8n (https://pullwire.dev/docs/n8n)
> Call any tool from the built-in HTTP Request node and split the results into items.
No custom node needed. The built-in HTTP Request node calls any tool, and Split Out turns the results into items.
You need an API key (it starts with `af_live_`). [Sign up](/login) with your email and you get
2,000 free credits.
## 1. Import the request [#1-import-the-request]
Add an HTTP Request node, click Import cURL, paste this command and click Import. n8n fills in the method, URL, header and body.
n8n imports every body value as text, so a number like `maxResults` would arrive as "3". Step 3 fixes that by switching the body to JSON.
```bash title="cURL"
curl -X POST https://pullwire.dev/api/v1/tools/youtube-transcripts/runs \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{"videos":["https://www.youtube.com/watch?v=jNQXAC9IVRw"],"maxResults":3}'
```
## 2. Keep your key in a credential [#2-keep-your-key-in-a-credential]
Under Authentication pick Generic Credential Type → Header Auth and create a credential with Name `x-api-key` and your key as Value. Then remove the `x-api-key` row from Send Headers, so the key is not stored in the workflow.
## 3. Map your own input [#3-map-your-own-input]
With Send Body on, set Body Content Type to JSON and Specify Body to Using JSON, then paste the body below. Drag fields from earlier nodes into it, for example a video URL from a sheet.
```json title="JSON body"
{
"videos": [
"{{ $json.videoUrl }}"
],
"maxResults": 3
}
```
## 4. One item per result [#4-one-item-per-result]
The answer has a `run` (status, credits used) and a `results` list. Add a Split Out node with Fields To Split Out set to `results` to get one item per result.
```json title="Answer (SAMPLE)"
{
"run": {
"id": "…",
"status": "succeeded",
"resultsCount": 1,
"creditsUsed": 1,
"error": null
},
"results": [
{
"…": "one object per result, fields depend on the tool"
}
]
}
```
The values above are a SAMPLE: real answers carry real IDs and all fields of the tool.
## 5. Bigger jobs [#5-bigger-jobs]
Add `?async=1` to the URL. Then a Wait node, then a second HTTP Request node (GET) to `/api/v1/runs/{{ $json.run.id }}/results` with the same credential.
## Good to know [#good-to-know]
- The call waits up to 25 seconds for the results. For bigger jobs add `?async=1` to the URL: you get a run id back right away and fetch the results a bit later.
- Errors come back with a code, e.g. `INSUFFICIENT_CREDITS` (402) or `INVALID_INPUT` (400).
# Make (https://pullwire.dev/docs/make)
> Call any tool from the HTTP “Make a request” module and iterate over the results.
The built-in HTTP app calls any tool. Parse response turns the answer into fields you can map.
You need an API key (it starts with `af_live_`). [Sign up](/login) with your email and you get
2,000 free credits.
## 1. Add the request [#1-add-the-request]
Add the HTTP app → Make a request. Set Method to POST and URL to:
```text title="URL"
https://pullwire.dev/api/v1/tools/youtube-transcripts/runs
```
## 2. Add your key [#2-add-your-key]
Under Headers add one item: Name `x-api-key`, Value your API key.
## 3. Send the input [#3-send-the-input]
Set Body content type to application/JSON and Body input method to JSON string, then paste the input. Map values from earlier modules into it.
```json title="Request content"
{
"videos": [
"https://www.youtube.com/watch?v=jNQXAC9IVRw"
],
"maxResults": 3
}
```
## 4. Read the results [#4-read-the-results]
Set Parse response to Yes and run the module once so Make learns the fields. Then add Flow Control → Iterator on `results` to handle one result per bundle.
```json title="Answer (SAMPLE)"
{
"run": {
"id": "…",
"status": "succeeded",
"resultsCount": 1,
"creditsUsed": 1,
"error": null
},
"results": [
{
"…": "one object per result, fields depend on the tool"
}
]
}
```
The values above are a SAMPLE: real answers carry real IDs and all fields of the tool.
## 5. Bigger jobs [#5-bigger-jobs]
Add `?async=1` to the URL, then Tools → Sleep, then a second Make a request (GET) to `/api/v1/runs/{run.id}/results` with the same header.
## Good to know [#good-to-know]
- The call waits up to 25 seconds for the results. For bigger jobs add `?async=1` to the URL: you get a run id back right away and fetch the results a bit later.
- Errors come back with a code, e.g. `INSUFFICIENT_CREDITS` (402) or `INVALID_INPUT` (400).
# Zapier (https://pullwire.dev/docs/zapier)
> Call any tool with Webhooks by Zapier (Custom Request) and use the results in later steps.
Webhooks by Zapier sends the request. Zapier lists it as a premium app, so it needs a paid Zapier plan.
You need an API key (it starts with `af_live_`). [Sign up](/login) with your email and you get
2,000 free credits.
## 1. Add the action [#1-add-the-action]
Add an action step, pick Webhooks by Zapier and choose the Custom Request event.
## 2. Fill in the request [#2-fill-in-the-request]
Method: POST. URL:
```text title="URL"
https://pullwire.dev/api/v1/tools/youtube-transcripts/runs
```
## 3. Send the input [#3-send-the-input]
Paste the input into Data as raw JSON. Zapier sends it exactly as typed, so keep it valid JSON. Map fields from earlier steps into it.
```json title="Data"
{
"videos": [
"https://www.youtube.com/watch?v=jNQXAC9IVRw"
],
"maxResults": 3
}
```
## 4. Add the headers [#4-add-the-headers]
Under Headers add `x-api-key` with your key and `Content-Type` with `application/json`. Without the content type the request is not read as JSON.
## 5. Test and use the results [#5-test-and-use-the-results]
Test the step. The answer shows the run and its `results`; map them into later steps, or add Looping by Zapier to handle each result on its own.
```json title="Answer (SAMPLE)"
{
"run": {
"id": "…",
"status": "succeeded",
"resultsCount": 1,
"creditsUsed": 1,
"error": null
},
"results": [
{
"…": "one object per result, fields depend on the tool"
}
]
}
```
The values above are a SAMPLE: real answers carry real IDs and all fields of the tool.
## 6. Bigger jobs [#6-bigger-jobs]
Add `?async=1` to the URL, then Delay by Zapier, then a Webhooks GET step to `/api/v1/runs/{run id}/results` with the same `x-api-key` header.
## Good to know [#good-to-know]
- The call waits up to 25 seconds for the results. For bigger jobs add `?async=1` to the URL: you get a run id back right away and fetch the results a bit later.
- Errors come back with a code, e.g. `INSUFFICIENT_CREDITS` (402) or `INVALID_INPUT` (400).
# Authentication (https://pullwire.dev/docs/authentication)
> Send your API key in the x-api-key header or as a Bearer token. Keys, storage and rate limits.
Every request that reads or spends your credits needs an API key. Send it in the `x-api-key` header:
```bash
curl "https://pullwire.dev/api/v1/me" -H "x-api-key: $PULLWIRE_API_KEY"
```
`Authorization: Bearer` works too, so clients that only know Bearer tokens need no extra setup:
```bash
curl "https://pullwire.dev/api/v1/me" -H "Authorization: Bearer $PULLWIRE_API_KEY"
```
If a request sends both headers, `x-api-key` is used.
Two endpoints are public and need no key: the tool catalog (`GET /api/v1/tools`) and the OpenAPI description (`GET /api/v1/openapi.json`).
## API keys [#api-keys]
- Keys look like `af_live_` followed by 56 hex characters. The first 8 characters after `af_live_` are the visible prefix, so you can tell your keys apart without seeing the secret.
- The full key is shown **once**, when you create it. We store only a SHA-256 hash of the key, never the key itself, so nobody can read it back from our database.
- A revoked key stops working immediately and answers with `401 UNAUTHORIZED`.
- Each key belongs to one organization. Runs started with the key spend that organization's credits, and the key can only see that organization's runs.
## Keep your key safe [#keep-your-key-safe]
- Store it in an environment variable or a secret manager, not in your code or repository.
- Call the API from your server or your agent's backend, never from a browser or a mobile app, where anyone can read the key.
- Use one key per app or agent, so you can revoke one without breaking the others.
## Rate limit [#rate-limit]
Each key can send **a burst of 120 requests, then 60 per minute**. A new or rested key can fire 120 calls at once, for example a batch of short runs. After that it gets one more request every second, so steady use of 60 per minute never hits the limit. When the key is out of requests you get `429 RATE_LIMITED` with a `Retry-After` header that says how many seconds to wait. The limit counts requests, not results: one run that returns 200 results is one request.
A 429 also carries `X-RateLimit-Limit` (the burst size, 120), `X-RateLimit-Remaining` (requests left, 0 on a 429) and `X-RateLimit-Reset` (seconds until the next request is allowed, same as `Retry-After`). The same values are sent as `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`.
For bigger jobs, put more items into one run instead of sending many small runs, and add `?async=1` so a long run is not cut off after 25 seconds. See [Runs](/docs/runs).
## Errors [#errors]
| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `UNAUTHORIZED` | No key was sent (neither `x-api-key` nor `Authorization: Bearer`), the key is unknown or it was revoked. |
| 429 | `RATE_LIMITED` | This key used up its burst of 120 requests and its 60 per minute. |
All error codes are listed on the [Errors](/docs/errors) page.
# Runs (https://pullwire.dev/docs/runs)
> Start a run, wait for it or let it run in the background, and fetch the results as JSON or CSV.
A run is one execution of one tool with one input. You start it with `POST /api/v1/tools/{toolId}/runs`; the request body is the tool's input as JSON. The input fields of every tool are in the [tool reference](/docs/tools) and in the live catalog at `GET /api/v1/tools`.
## Sync runs (default) [#sync-runs-default]
Without extra parameters the request waits for the run and answers `200` with the run and its results:
```bash
curl -X POST "https://pullwire.dev/api/v1/tools/youtube-transcripts/runs" \
-H "x-api-key: $PULLWIRE_API_KEY" -H "content-type: application/json" \
-d '{"videos": ["jNQXAC9IVRw"]}'
```
A sync run gets **25 seconds**. If the tool is not done by then, it stops: you get the results it collected so far, they are charged and saved, and `run.error` says that the run stopped at the deadline. For bigger jobs use an async run.
## Async runs [#async-runs]
Add `?async=1` to start the run in the background. The request answers right away with `202` and the run:
```bash
curl -X POST "https://pullwire.dev/api/v1/tools/youtube-transcripts/runs?async=1" \
-H "x-api-key: $PULLWIRE_API_KEY" -H "content-type: application/json" \
-d '{"videos": ["jNQXAC9IVRw", "dQw4w9WgXcQ"]}'
```
An async run gets up to **50 seconds**. Check its status with the run ID until it is `succeeded` or `failed`:
```bash
curl "https://pullwire.dev/api/v1/runs/$RUN_ID" -H "x-api-key: $PULLWIRE_API_KEY"
```
## Run status [#run-status]
| Status | Meaning |
| --- | --- |
| `queued` | The run was created and is waiting to start (async runs). |
| `running` | Credits are reserved and the tool is working. |
| `succeeded` | The run finished. `resultsCount` can be 0 if nothing matched your input. If `error` is set, the results are partial: the run stopped early (for example at the deadline) or some items could not be fetched (for example `BLOCKED`). Everything in the results was charged and saved. |
| `failed` | The run could not start (for example not enough credits), crashed, or ended without a single result because the target site could not be reached (for example every request was `BLOCKED`). `error` says why. A run that simply found nothing is `succeeded` with 0 results, not `failed`. Results produced before a crash were charged and saved. |
Other useful fields on a run: `resultsCount`, `creditsUsed`, `durationMs`, `createdAt` and `updatedAt`.
## How many results [#how-many-results]
Most tools accept a `maxResults` field. If you leave it out, a run returns **at most 20 results**. You can ask for up to **200 results per run**; for more, start several runs. Credits are reserved for `maxResults` results before the run starts, so a high value needs enough balance even if fewer results come back (see [Credits](/docs/credits)).
## Fetch results [#fetch-results]
Results of a finished run stay available. Fetch them as JSON:
```bash
curl "https://pullwire.dev/api/v1/runs/$RUN_ID/results" -H "x-api-key: $PULLWIRE_API_KEY"
```
or as CSV, for a spreadsheet:
```bash
curl "https://pullwire.dev/api/v1/runs/$RUN_ID/results?format=csv" \
-H "x-api-key: $PULLWIRE_API_KEY" -o results.csv
```
For an Excel file, use `?format=xlsx` instead (same headers, `-o results.xlsx`).
Large runs are also saved as ready-made files you can download for 30 days, see [Datasets](/docs/datasets).
You can only read runs of your own organization; any other run ID answers `404 NOT_FOUND`.
## When a sync run crashes [#when-a-sync-run-crashes]
If a sync run fails after it already produced results, the API answers `500 INTERNAL_ERROR` and includes the `runId` in the error body. The results produced before the crash were charged, and you can still fetch them with `GET /api/v1/runs/{runId}/results`.
# Datasets (https://pullwire.dev/docs/datasets)
> Large runs are saved as CSV, Excel and JSON files for 30 days. List them and get private download links.
When a run returns **50 or more results**, its results are also saved as a dataset: one CSV file, one Excel file and one JSON file. Smaller runs that still produce a lot of data (512 KB of JSON or more) are saved too. You find every dataset in the dashboard under **Datasets**, and through the API.
Datasets are kept for **30 days**. After that the files are deleted for good. The run itself and its results stay in [Runs](/docs/runs), so you can still fetch them with `GET /api/v1/runs/{id}/results`.
Saving a dataset never costs extra credits and never slows a run down by more than a few seconds. If saving fails, the run is not affected.
## List your datasets [#list-your-datasets]
```bash
curl "https://actor-factory-web.vercel.app/api/v1/datasets" -H "x-api-key: $AF_API_KEY"
```
Add `?runId=` to get only the dataset of one run. The answer lists each dataset with its run, tool, number of results, size and the time it expires.
## Download a dataset [#download-a-dataset]
Ask for one dataset to get a download link per format:
```bash
curl "https://actor-factory-web.vercel.app/api/v1/datasets/$DATASET_ID" -H "x-api-key: $AF_API_KEY"
```
SAMPLE response (shortened):
```json
{
"dataset": {
"id": "6f1c2a9e-0000-4000-8000-000000000000",
"runId": "1a2b3c4d-0000-4000-8000-000000000000",
"toolId": "maps-leads",
"status": "ready",
"items": 120,
"formats": ["csv", "xlsx", "json"],
"expiresAt": "2026-11-20T10:00:00.000Z"
},
"downloads": {
"csv": { "url": "https://…", "filename": "maps-leads-1a2b3c4d.csv", "expiresAt": "…" }
}
}
```
Each link is private and works for **15 minutes**, without an API key, so you can hand it to a script or a browser. Ask again for a fresh link.
```bash
curl -L "$CSV_URL" -o leads.csv
```
## Who can see a dataset [#who-can-see-a-dataset]
Only your organization. A dataset ID of another organization answers `404 NOT_FOUND`, exactly like an ID that does not exist. The files sit in a private store; the only way to read them is a link you got from the API or the dashboard.
# Credits (https://pullwire.dev/docs/credits)
> One balance for every tool. How credits are reserved before a run, charged per result and released afterwards.
All tools draw from **one credit balance** per organization. You pay per result: each tool lists how many credits one result costs (the `credits` field in `GET /api/v1/tools` and in the [tool reference](/docs/tools)).
## Free credits [#free-credits]
New accounts start with **2,000 free credits** for a 7-day trial, no card needed. Trial credits that are left after 7 days expire.
## Buying credits [#buying-credits]
On the [Billing](/app/billing) page you can pick a monthly plan or top up once:
| | Price | Credits |
|---|---|---|
| Starter plan | $19 / month | 8,000 every month |
| Growth plan | $49 / month | 22,000 every month |
| Pro plan | $99 / month | 46,000 every month |
| Top-up | $10 / $25 / $100 | 4,000 / 10,000 / 40,000 |
Credits you pay for stay valid for **3 months**. We always use the credits that expire first, and expired credits leave your balance in a daily sweep (so for up to a day after the date they can still show up). Invoices come from Stripe by email and are listed under "Manage billing", where you can also change or cancel a plan (a plan change starts with your next billing date).
## What a run costs [#what-a-run-costs]
```
cost = number of results you receive × credits per result
```
Results that fail are not charged. A run that returns nothing costs nothing.
## Reserve, charge, release [#reserve-charge-release]
Credits move in three steps, so parallel runs can never spend more than you have:
1. **Reserve.** Before the run starts, credits for the most results it can return are set aside: `maxResults × credits per result` (20 results if you do not set `maxResults`, at most 200). If your balance is too low, the run does not start and you get `402 INSUFFICIENT_CREDITS`.
2. **Charge.** When the run ends, you are charged for the results you actually received.
3. **Release.** The rest of the reservation goes back to your balance in the same step.
This also happens when a run stops early or crashes: you pay for what you got, the rest is released, and no credits stay locked.
**Example.** A tool costs 1 credit per result. You start a run with `"maxResults": 50`, so 50 credits are reserved. The run returns 12 results: you are charged 12 credits and 38 go back to your balance.
## Check your balance [#check-your-balance]
```bash
curl "https://pullwire.dev/api/v1/me" -H "x-api-key: $PULLWIRE_API_KEY"
```
Every run also reports its own `creditsUsed`.
## Too few credits [#too-few-credits]
If your balance cannot cover the reservation, lower `maxResults` so the reservation fits. A request with invalid input is rejected before anything is reserved, so it never costs credits.
# Errors (https://pullwire.dev/docs/errors)
> Every error has the same JSON shape, an HTTP status and a stable code.
When a request fails, the API answers with an HTTP error status and this body:
```json
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Not enough credits for this run."
}
}
```
Use `code` in your program; it stays stable. `message` is meant for people and can change.
## Error codes [#error-codes]
| Status | Code | What happened | What to do |
| --- | --- | --- | --- |
| 400 | `INVALID_INPUT` | The body is not valid JSON, or it does not match the tool's input fields. The message names the field. | Fix the input. Check the fields in the [tool reference](/docs/tools). Nothing was charged. |
| 401 | `UNAUTHORIZED` | The `x-api-key` header is missing, or the key is unknown or revoked. | Send a valid key. See [Authentication](/docs/authentication). |
| 402 | `INSUFFICIENT_CREDITS` | Your balance cannot cover the credits this run reserves. | Lower `maxResults` so the reservation fits your balance. See [Credits](/docs/credits). |
| 404 | `NOT_FOUND` | The tool ID does not exist, or the run does not exist in your organization. | Check the ID. `GET /api/v1/tools` lists all tool IDs. |
| 429 | `RATE_LIMITED` | This key used up its burst of 120 requests and its 60 per minute. | Wait the seconds in the `Retry-After` header (also `retryAfterSeconds` in the body), then retry. See [Authentication](/docs/authentication#rate-limit). |
| 500 | `INTERNAL_ERROR` | Something broke on our side. If a run had already started, the body also contains `runId`. | Retry later. With a `runId`, fetch the partial results that were charged: `GET /api/v1/runs/{runId}/results`. |
| 503 | `SERVICE_UNAVAILABLE` | A part of the service is temporarily not available. | Retry later. |
| 503 | `TOOL_UNAVAILABLE` | Our latest automatic checks of this tool failed, so runs are paused. Nothing was reserved or charged. | Retry later. The [status page](/status) shows when the tool works again. |
## Runs that stop early [#runs-that-stop-early]
A run that stops early is not an HTTP error. A sync run that hits its deadline still answers `200`: the results it collected are in the body, and `run.error` explains why it stopped, for example:
```json
{ "status": "succeeded", "resultsCount": 7, "error": "Stopped at the 25 s deadline. Partial results were charged and saved." }
```
If the target website blocks a run, `run.error` names the reason code. When nothing could be fetched at all, the run is `failed` (not an empty success) and nothing is charged:
```json
{ "status": "failed", "resultsCount": 0, "error": "No results: 3 items could not be fetched (BLOCKED). The target site blocked our requests, so this is not an empty result. Nothing was charged." }
```
When only some items were blocked, the run is `succeeded`, the results you got are charged, and `run.error` says how many items could not be fetched. See [Runs](/docs/runs) for all statuses.
# Tool reference (https://pullwire.dev/docs/tools)
> Every tool with its ID, price per result and input fields. Generated from the live tool catalog.
Each tool has an ID (used in `POST /api/v1/tools/{toolId}/runs`), a price in credits per result and a set of input fields. Send the input fields as the JSON body of the run. Fields marked required must be set; all others are optional or have the default shown.
This page is generated from the same tool catalog the API uses, so it always matches `GET /api/v1/tools`.
## Web Scraper (`scrape`)
Turn any public web page into clean, LLM-ready Markdown, raw HTML or JSON fields picked with CSS selectors. Optional JavaScript rendering, screenshots and an automatic residential-proxy fallback when a site blocks.
- Endpoint: `POST /api/v1/tools/scrape/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `url` | string | no | | URL: The page to scrape. |
| `urls` | string[] | no | | URLs: Several pages in one run (up to 20); one result per page. |
| `format` | "markdown" \| "html" \| "json" | no | `"markdown"` | Output format: markdown: clean, LLM-ready text of the page. html: the page HTML. json: the fields from `extract`. |
| `onlyMainContent` | boolean | no | `true` | Main content only: Markdown: drop navigation, cookie banners, sidebars and footers. |
| `extract` | object | no | | Extraction rules: For format json: field name -> CSS selector, or {selector, attribute, all}. attribute reads e.g. href/src/content ("html" = inner HTML); all returns every match. |
| `renderJs` | boolean | no | `false` | Render JavaScript: Load the page in a headless browser first (for sites that build their content with JS). |
| `waitFor` | string | no | | Wait for: With renderJs: a CSS selector to wait for, or a delay in milliseconds (e.g. 2000, max 15000). |
| `proxy` | "auto" \| "none" \| "residential" | no | `"auto"` | Proxy: auto: direct first, residential proxy only if the site blocks us. none: never a proxy. residential: always. |
| `country` | string | no | | Country: Exit country for the residential proxy (two-letter code). |
| `timeoutMs` | integer | no | `20000` | Timeout (ms): Per page, for loading (and rendering). |
| `screenshot` | boolean | no | `false` | Screenshot: With renderJs: a PNG of the viewport, returned as a signed download URL (valid 7 days). |
## YouTube Transcripts (`youtube-transcripts`)
Transcripts of YouTube videos with timestamps, chapters, language choice and video metadata.
- Endpoint: `POST /api/v1/tools/youtube-transcripts/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `videos` | string[] | yes | | Videos: YouTube video URLs (watch, youtu.be, shorts, embed, live) or 11-character video IDs. |
| `languages` | string[] | no | `[]` | Preferred languages: Language codes in order of preference, e.g. de, en, pt-BR. Empty: the original transcript. If none is available, the default transcript is returned with languageMatched = false. |
| `preferManual` | boolean | no | `true` | Prefer human-made captions: Use uploaded captions over auto-generated ones when both exist for a language. |
| `includeSegments` | boolean | no | `true` | Include timed segments: Adds the transcript split into segments with start and end time in seconds. |
| `includeMetadata` | boolean | no | `true` | Include video metadata: Adds title and channel. Views, duration, publish date and the language list are added when YouTube provides them. |
| `maxResults` | integer | no | | Max results: Stop after this many transcripts. Empty: all videos. |
## Facebook Ads Library Scraper (`facebook-ads-library`)
Search the Facebook Ad Library by keyword or advertiser Page ID: ad text, images, videos, CTA, run dates and platforms. No login, no browser.
- Endpoint: `POST /api/v1/tools/facebook-ads-library/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | object[] | yes | | Searches: One entry per keyword or advertiser. Each returns the first page of matching ads (see totalMatchingAds/hasMoreResults). |
| `maxResults` | integer | no | | Max results: Stop after this many ads in total. Empty: all ads from all searches. |
## LinkedIn Jobs Scraper (`linkedin-jobs`)
Search LinkedIn job postings by keyword, location, date posted, work type, experience level and job type: title, company, location, full description, seniority, employment type and applicant count. No login, no browser.
- Endpoint: `POST /api/v1/tools/linkedin-jobs/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | object[] | yes | | Searches: One entry per keyword/location combination. |
| `includeDescription` | boolean | no | `true` | Include full description: Fetch each job’s detail page for the full description, seniority, employment type, industries and applicant count. One extra request per job. |
| `maxResults` | integer | no | | Max results: Stop after this many jobs in total across all searches. Empty: no extra cap. |
## Indeed Jobs Scraper (`indeed-jobs`)
Search Indeed job postings by keyword, job title, company, location and posted date: title, company, rating, salary, job type, benefits, the full description and the employer’s website and size. No login, no browser.
> Coming soon: the source site blocks requests from our servers, so runs are not reliable until proxy access is switched on.
- Endpoint: `POST /api/v1/tools/indeed-jobs/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | object[] | yes | | Searches: One entry per search: keywords, job title, company, location and filters. |
| `includeDescription` | boolean | no | `true` | Include full description: Add the full job description text. Fetched in batches of 10 jobs per extra request, same price per job. |
| `includeCompanyDetails` | boolean | no | `true` | Include company website and size: Add the employer’s website, domain, industry, employee range, revenue range and headquarters from its Indeed company page (one extra request per distinct company, same price per job). |
| `maxResults` | integer | no | | Max results: Stop after this many jobs in total across all searches. Empty: no extra cap. |
## Amazon Product Search (`amazon-products`)
Search amazon.com by keyword and get product listings: title, price, list price, rating, review count, image, badges (Amazon’s Choice / Best Seller) and URL. No login, no browser.
> Runs through residential network access and is switched on only while our automatic checks pass. Live state: /status. A paused tool answers 503 TOOL_UNAVAILABLE and charges nothing.
- Endpoint: `POST /api/v1/tools/amazon-products/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | object[] | yes | | Searches: One entry per keyword search. |
| `maxResults` | integer | no | | Max results: Stop after this many products in total across all searches. Empty: no extra cap. |
## Amazon Product Reviews (`amazon-reviews`)
Get the reviews embedded on an amazon.com product page by ASIN or URL: rating, title, date, verified purchase, helpful votes and full text. No login, no browser.
> Runs through residential network access and is switched on only while our automatic checks pass. Live state: /status. A paused tool answers 503 TOOL_UNAVAILABLE and charges nothing.
- Endpoint: `POST /api/v1/tools/amazon-reviews/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `products` | object[] | yes | | Products: One entry per product (ASIN or amazon.com product URL). |
| `maxResults` | integer | no | | Max results: Stop after this many reviews in total across all products. Empty: no extra cap. |
## Reddit Posts, Comments & Search (`reddit`)
Read subreddit listings, run searches or fetch one post with its comments: title, author, text/body, link, created date and permalink. No login, no browser.
> Runs through residential network access and is switched on only while our automatic checks pass. Live state: /status. A paused tool answers 503 TOOL_UNAVAILABLE and charges nothing.
- Endpoint: `POST /api/v1/tools/reddit/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `requests` | object[] | yes | | Requests: One entry per subreddit listing, search or single post. |
| `includeComments` | boolean | no | `false` | Include comments for subreddit/search posts: Fetch each found post’s top comments too (one extra request per post, capped by that request’s "Max comments"). kind=post always includes comments regardless of this flag. |
| `maxResults` | integer | no | | Max results: Stop after this many posts+comments in total across all requests. Empty: no extra cap. |
## Zillow Listings Scraper (`zillow`)
Search Zillow for-sale listings by city/location: price, beds, baths, square footage, Zestimate, home type and status. No login, no browser.
> Runs through residential network access and is switched on only while our automatic checks pass. Live state: /status. A paused tool answers 503 TOOL_UNAVAILABLE and charges nothing.
- Endpoint: `POST /api/v1/tools/zillow/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `locations` | object[] | yes | | Locations: One entry per city/location to search. |
| `maxResults` | integer | no | | Max results: Stop after this many listings in total across all locations. Empty: no extra cap. |
## Google Maps Leads Scraper (`maps-leads`)
Find businesses on Google Maps by type and location (optionally within a radius): name, address, phone, website, rating, reviews, categories and opening hours, plus emails (with the page they were found on), phone numbers and social profiles from each business website. One row per company. No login, no browser.
- Endpoint: `POST /api/v1/tools/maps-leads/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | object[] | yes | | Searches: One entry per business type + location combination. |
| `enrichContacts` | boolean | no | `true` | Find emails and social profiles: Visit each business website (homepage plus contact, imprint or privacy pages when needed) to collect email addresses, phone numbers and Facebook, Instagram, LinkedIn, X, YouTube, TikTok and Pinterest profiles. |
| `maxContactPages` | integer | no | `3` | Website pages per business: Homepage plus up to this many minus one contact, imprint or privacy pages, only while no address of the business itself has been found (an agency or platform address alone does not count). |
| `language` | string | no | `"en"` | Language of categories, opening hours and descriptions, e.g. "en", "de", "fr". |
| `countryCode` | string | no | | Country bias: Two-letter country code that biases ambiguous place names, e.g. "us" or "de". Empty: none. |
| `dedupe` | boolean | no | `true` | Skip duplicates across searches: Return each business only once even when several searches find it. |
| `onePerCompany` | boolean | no | `true` | One row per company: Skip a business whose website domain or phone number was already returned (branches of one company, double listings). Shared hosts like Facebook pages or booking platforms never count as the same company. |
| `maxResults` | integer | no | | Max results: Stop after this many businesses in total across all searches. Empty: no extra cap. |
## Bing & DuckDuckGo SERP API (`serp`)
Search results from Bing and DuckDuckGo as structured JSON: organic results with position, title, URL and snippet, Bing ads and related searches, result count, pagination, country and language. No browser, no Google.
- Endpoint: `POST /api/v1/tools/serp/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `queries` | string[] | yes | | Search queries: One search per entry, exactly as you would type it into the search box. Operators like site: work. |
| `engine` | "bing" \| "duckduckgo" | no | `"bing"` | Search engine: Bing returns organic results, ads, related searches and the result count. DuckDuckGo returns organic results; its related searches come from DuckDuckGo’s own query suggestions and it shows no ads on this route. |
| `country` | string | no | | Country: Two-letter country code for the result market, e.g. "us", "gb", "de". Empty: the engine picks the market (Bing by location, DuckDuckGo "All regions"). |
| `language` | string | no | | Language: Interface and result language, e.g. "en", "de", "fr". For DuckDuckGo it picks between a country’s regions (e.g. Canada + fr). |
| `maxPagesPerQuery` | integer | no | `1` | Pages per query: How many result pages to fetch for each query (about 10 organic results per page). |
| `includeRelatedSearches` | boolean | no | `true` | Related searches: Include related searches. Free on Bing (same page); one extra request per query on DuckDuckGo. |
| `maxResults` | integer | no | | Max result pages: Stop after this many result pages in total across all queries. Empty: no extra cap. |
## Glassdoor Jobs Scraper (`glassdoor-jobs`)
Search Glassdoor job listings by keyword and location: title, company, company rating, location, salary range, posted age, Easy Apply flag, description snippet and skills. No login, no browser.
> Coming soon: the source site blocks requests from our servers, so runs are not reliable until proxy access is switched on.
- Endpoint: `POST /api/v1/tools/glassdoor-jobs/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | object[] | yes | | Searches: One entry per keyword/location combination, each with its own filters. |
| `maxResults` | integer | no | | Max results: Stop after this many jobs in total across all searches. Empty: no extra cap. |
## Yelp Business & Reviews Scraper (`yelp`)
Search Yelp businesses by keyword and location or scrape business URLs: name, rating, review count, categories, price, phone, website, address, coordinates, opening hours and optional full review texts. No login, no browser.
> Coming soon: the source site blocks requests from our servers, so runs are not reliable until proxy access is switched on.
- Endpoint: `POST /api/v1/tools/yelp/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | object[] | no | `[]` | Searches: One entry per search: a search term and the location to search in. |
| `businessUrls` | string[] | no | `[]` | Business URLs: Yelp business pages to scrape directly, e.g. https://www.yelp.com/biz/lou-malnatis-pizzeria-chicago (the part after /biz/ also works on its own). |
| `maxBusinessesPerSearch` | integer | no | `10` | Max businesses per search: How many businesses to return per search, in Yelp's own ranking (sponsored results excluded). Yelp returns at most 25 per search, so use more specific terms or locations for more. |
| `maxReviewsPerBusiness` | integer | no | `0` | Max reviews per business: Full review texts to include with every business (0 = none, up to 100). Reviews are loaded 20 at a time. |
| `reviewsSort` | "relevance" \| "newest" \| "oldest" \| "highest_rated" \| "lowest_rated" \| "elites" | no | `"relevance"` | Review order: Order of the reviews: Yelp's recommended order, newest, oldest, highest or lowest rated, or Elite reviewers first. |
| `reviewsLanguage` | string | no | `"en"` | Review language: Two-letter language code of the reviews to load, e.g. "en", "es", "fr". |
| `maxResults` | integer | no | | Max businesses: Stop after this many businesses in total. Empty: no extra cap. |
## Idealista Scraper (`idealista`)
Scrape idealista.com property listings for any Spanish city, district or search URL: price, price drops, rooms, size, floor, lift, parking, agency, coordinates, photos. Homes, offices, premises, garages, land and rooms, for sale or rent.
> Coming soon: the source site blocks requests from our servers, so runs are not reliable until proxy access is switched on.
- Endpoint: `POST /api/v1/tools/idealista/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searches` | string[] | yes | | Locations or search URLs: One entry per search. Either a location as it appears in idealista.com URLs ("madrid-madrid", "barcelona-barcelona", "madrid-provincia", "madrid/centro"), combined with the operation and property type below, or a full idealista.com search URL copied from your browser with all filters applied, e.g. https://www.idealista.com/alquiler-viviendas/barcelona-barcelona/con-precio-hasta_1500/. A URL keeps its own operation, type, filters and sort order. |
| `operation` | "sale" \| "rent" | no | `"sale"` | Operation: For location entries only: properties for sale or for rent. |
| `propertyType` | "homes" \| "new-homes" \| "offices" \| "premises" \| "garages" \| "land" \| "storage-rooms" \| "buildings" \| "rooms" | no | `"homes"` | Property type: For location entries only: homes (flats and houses), new-homes, offices, premises, garages, land, storage-rooms, buildings, or rooms (rent only). |
| `maxResultsPerSearch` | integer | no | `90` | Max listings per search: Stops paginating a search after this many listings. idealista shows 30 listings per page; the maximum here is 1800 (60 pages). |
| `maxResults` | integer | no | | Max listings in total: Stop after this many listings across all searches. Empty: no extra cap. |
## Threads Scraper (`threads`)
Scrape public Threads posts by keyword search, profile or post URL: text, author, likes, replies, reposts, images, videos, links, mentions, profile details and the first page of replies. No login, no browser.
- Endpoint: `POST /api/v1/tools/threads/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `searchQueries` | string[] | no | `[]` | Search keywords: Keywords or #hashtags to search on Threads. Each search returns the top ~20 matching threads (what Threads shows without an account). |
| `usernames` | string[] | no | `[]` | Profiles: Usernames or profile URLs. Returns the profile details and its latest public threads (Threads shows ~5 without an account). |
| `postUrls` | string[] | no | `[]` | Post URLs: Links to single Threads posts. Returns the post and, if enabled, its replies. |
| `includeReplies` | boolean | no | `true` | Include replies: For post URLs: also return the author’s follow-up posts and the first page of replies (~10, what Threads shows without an account). |
| `maxResults` | integer | no | | Max results: Stop after this many posts in total. Empty: no extra cap. |
## Website Contact Scraper (`website-contacts`)
Get email addresses, phone numbers and Facebook, Instagram, LinkedIn, X, YouTube, TikTok and Pinterest profiles from a list of company websites: homepage plus contact and imprint pages. No login, no browser.
- Endpoint: `POST /api/v1/tools/website-contacts/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `websites` | string[] | yes | | Websites: Company domains or website URLs, one per line, e.g. "kaercher.com" or "www.example-bakery.de/kontakt". One result per website; duplicates are skipped. |
| `maxPagesPerWebsite` | integer | no | `3` | Pages per website: Homepage plus up to this many minus one contact, imprint or about pages. Extra pages are only visited while an email or a phone number is still missing. |
| `maxResults` | integer | no | | Max results: Stop after this many websites. Empty: all of them. |
## LinkedIn Company Scraper (`linkedin-company`)
Get public LinkedIn company data from a company URL or website domain: industry, size, employees on LinkedIn, headquarters and address, all locations, website, type, founding year, specialties, followers, similar companies and recent posts. No login.
> Runs through residential network access and is switched on only while our automatic checks pass. Live state: /status. A paused tool answers 503 TOOL_UNAVAILABLE and charges nothing.
- Endpoint: `POST /api/v1/tools/linkedin-company/runs`
- Price: 1 credit per result
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `companies` | string[] | yes | | Companies: LinkedIn company or showcase page URLs (linkedin.com/company/) or company websites/domains (e.g. "hubspot.com"), one per line. A domain is matched to its LinkedIn page through the link on its homepage. One result per company; duplicates are skipped. |
| `includePosts` | boolean | no | `true` | Include recent posts: Add the latest posts shown on the public page (up to about 10): text, date, likes and link. |
| `maxResults` | integer | no | | Max results: Stop after this many companies. Empty: all of them. |