# Bulk HTTP Requests — API Calls from CSV, JSON or Dataset (`saliga/bulk-http-requests`) Actor

Send bulk HTTP requests: call any API once for every row of a CSV, JSON list or Apify dataset. Write the request once with {{column}} placeholders. Up to 200 parallel requests, per-host rate limit, free retries, JSON field extraction, webhooks for Clay, n8n and Make. $1 per 1,000 requests.

- **URL**: https://apify.com/saliga/bulk-http-requests.md
- **Developed by:** [Salih Küçükkayış](https://apify.com/saliga) (community)
- **Stats:** 4 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 http requests

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Send bulk HTTP requests to any API: one call for every row of a CSV, JSON list or Apify dataset. Write the request once as a template, and the Actor fills in each row, sends the requests in parallel, retries what fails, and gives you one clean result row per request.

**$1 per 1,000 requests** ($0.90, $0.80 and $0.70 on the Starter, Scale and Business plans). Retries are free. No subscription, no platform usage fees. (Plus a tiny $0.00005 start fee per run.)

### Quick start

Press **Start** with the example input to see it work. It calls a public test API three times, once per row:

```json
{
  "url": "https://httpbin.org/anything/{{id}}",
  "method": "GET",
  "csv": "id\n1\n2\n3"
}
```

Each request becomes one dataset row (trimmed here):

```json
{
  "index": 0,
  "method": "GET",
  "url": "https://httpbin.org/anything/1",
  "statusCode": 200,
  "ok": true,
  "json": { "method": "GET", "url": "https://httpbin.org/anything/1" },
  "durationMs": 43,
  "waitMs": 3,
  "attempts": 1,
  "error": null,
  "row": { "id": "1" }
}
```

A POST with a JSON body built from each row:

```json
{
  "url": "https://api.example.com/contacts",
  "method": "POST",
  "headers": { "Authorization": "Bearer YOUR_TOKEN" },
  "body": { "email": "{{email}}", "company": "{{company}}" },
  "csv": "email,company\nana@example.com,Acme\nbo@example.com,Globex",
  "extract": { "contactId": "data.id" }
}
```

### Typical uses

- Enrich a lead list: call your enrichment API for every email in a CSV.
- Push rows into a CRM or webhook: one POST per row with a JSON body built from the row.
- Check thousands of URLs or IDs against an API and pull out one or two fields.
- Feed the output of another Actor into an API (use its dataset ID as input).
- Give an AI agent a safe "call this API for all of these" tool through the Apify MCP server.

### How it works

1. **Write the request once.** Put `{{column}}` wherever a value from the row should go: in the URL, query parameters, headers or JSON body.
   - `https://api.example.com/users/{{id}}`
   - Header `Authorization: Bearer {{token}}`
   - Body `{"email": "{{email}}", "score": "{{score}}"}` (a value that is exactly `{{column}}` keeps its type, so numbers stay numbers)
   - Add `|url` to URL-encode: `?q={{name|url}}`
2. **Give it rows.** Paste CSV, pass JSON, link a CSV/JSON file (for example a published Google Sheet), or point to an Apify dataset.
3. **Run.** You get one result per request with status, timing, attempts, the parsed JSON (or text), any fields you asked to extract, and the original row.

### Built to be fast and polite

- **Parallel**: up to 200 requests at a time.
- **Rate limit per host**: stays under API limits (default 10 requests/second per host).
- **Retries with backoff**: timeouts, network errors, 408, 425, 429 and 5xx. Honors `Retry-After`.
- **Field extraction**: `{"userId": "data.id", "firstTag": "tags[0]"}` adds those columns to the output, so you don't have to dig through JSON.
- **Webhook**: optionally POST each result to Clay, Make, n8n or your own endpoint as it finishes.
- **Spending limit aware**: stops cleanly when your run's spending limit is reached.

### Output

| Field | Meaning |
|---|---|
| `index` | Row number (0-based) |
| `method`, `url` | The request that was sent |
| `statusCode`, `ok` | HTTP status and whether it was 2xx |
| `json` / `body` | Parsed JSON response, or the text body |
| `bodyBase64`, `contentType` | For binary responses (images, PDFs, files): the body as base64 |
| your extracted fields | Columns from `extract` |
| `responseHeaders` | Response headers |
| `durationMs`, `attempts` | Time from sending to final answer (retries included), and how many tries |
| `waitMs` | Time spent waiting for the per-host rate limit before sending |
| `truncated` | `true` if the body was larger than the size limit |
| `error` | Why it failed, if it did |
| `row` | The input row, so results stay joined to your data |

### Good to know

- Requests to private and internal addresses (localhost, 10.x, 192.168.x, cloud metadata) are blocked. This also applies to every redirect hop and is checked again when the connection is made. A row whose redirect points to a private address fails with a "Blocked redirect" error and is not retried.
- You are responsible for calling APIs you have the right to use and for respecting their terms and rate limits.
- Pricing: one charge per request in your input, however many retries it needed, plus $0.00005 when a run starts. Platform usage is included.

### What it costs

| Run | Price (no discount) |
|---|---|
| 100 rows | $0.10 |
| 10,000 rows | $10.00 |
| 100,000 rows | $100.00 |

Starter, Scale and Business plans pay 10 %, 20 % and 30 % less per request. Set **Maximum cost per run** and the run stops cleanly when it is reached.

### Why not a script or a no-code HTTP step?

- **No code to keep running.** Parallel requests, a per-host rate limit, retries with backoff and `Retry-After` are built in.
- **One row in, one row out.** Every result keeps its input row, so results join back to your list without guesswork.
- **Many rows in one step.** One run handles a whole CSV or dataset instead of one automation step per row.

### FAQ

**Can I send POST, PUT or PATCH with a JSON body?** Yes. Put `{{column}}` placeholders in the body; a value that is exactly `{{column}}` keeps its type, so numbers stay numbers.

**Am I charged for failed requests?** Each request in your input is charged once, whatever the outcome. Retries are not charged again.

**How do I keep under an API's rate limit?** Lower `perHostPerSecond` (default 10) and `concurrency` (default 20).

**Can I run it from Make, Zapier, n8n or my own code?** Yes, like any Apify Actor: through Apify's integrations or the Apify API. Results can also be POSTed to your webhook as they finish (`callbackUrl`).

**Can an AI agent use it?** Yes, through the Apify MCP server. The agent passes a URL template and rows, and gets one result per row.

### More from this developer

- [Website Screenshot + Change Detection](https://apify.com/saliga/website-screenshot-change-detection): screenshots of many URLs on desktop and mobile, with visual change detection between runs.
- [Keyword Search Volume](https://apify.com/saliga/keyword-search-volume): Google search volume, CPC and trends for a keyword list, in up to 20 countries in one run.
- [Document to Markdown](https://apify.com/saliga/document-to-markdown): PDF, Word, PowerPoint, Excel and scanned images to clean Markdown with tables and RAG chunks, $2 per 1,000 pages.
- [Audio to Text](https://apify.com/saliga/audio-to-text): audio, video and podcast feeds to timestamped text, SRT/VTT subtitles and RAG chunks with Whisper, $5 per 1,000 audio minutes.
- [Video to Scenes](https://apify.com/saliga/video-to-scenes): scene cuts, one keyframe per scene, a contact sheet and the transcript of each scene, $2 per 1,000 frames.
- [Career Site Jobs](https://apify.com/saliga/career-site-jobs): open jobs from Greenhouse, Lever, Ashby, Workable, Recruitee and Workday career sites, with a new-jobs monitor and a hiring summary per company, $1 per 1,000 jobs.

Questions or a missing feature: open an issue on the Actor page.

# Actor input Schema

## `url` (type: `string`):

Request URL. Use {{column}} to insert values from each row, e.g. https://api.example.com/users/{{id}}. Add |url to URL-encode: {{name|url}}.

## `method` (type: `string`):

HTTP method used for every request.

## `headers` (type: `object`):

Headers for every request. Templates work here too: {"Authorization": "Bearer {{token}}"}.

## `queryParams` (type: `object`):

Added to the URL. Templates work.

## `body` (type: `object`):

Sent as JSON for POST, PUT and PATCH. A value that is exactly "{{column}}" keeps its type (numbers stay numbers).

## `csv` (type: `string`):

Paste CSV with a header row. One request is sent per row.

## `rows` (type: `array`):

Array of objects, one request per object.

## `rowsUrl` (type: `string`):

Public URL of a CSV or JSON file (for example a published Google Sheet CSV link).

## `datasetId` (type: `string`):

Use the output of another Actor as input.

## `maxRows` (type: `integer`):

0 means all rows.

## `requests` (type: `array`):

Array of {url, method, headers, queryParams, body}. Fields you leave out come from the settings above.

## `extract` (type: `object`):

Output column → path in the response, e.g. {"userId": "data.id", "firstTag": "tags\[0]"}.

## `concurrency` (type: `integer`):

How many requests run at the same time (1-200).

## `perHostPerSecond` (type: `integer`):

Keeps you under API rate limits. 0 means no limit.

## `maxRetries` (type: `integer`):

Retries timeouts, network errors, 408, 425, 429 and 5xx. Honors Retry-After.

## `timeoutSecs` (type: `integer`):

Give up on a single attempt after this many seconds.

## `keepBody` (type: `boolean`):

Save the body when the response is not JSON: as text, or as base64 for binary files. JSON responses are always saved as parsed JSON.

## `maxBodyKb` (type: `integer`):

Responses larger than this are cut off and marked truncated.

## `callbackUrl` (type: `string`):

Each result is also POSTed here as JSON when it finishes (e.g. a Clay, Make or n8n webhook).

## `callbackHeaders` (type: `object`):

Extra headers sent with each webhook POST, for example an auth token for your endpoint.

## Actor input object example

```json
{
  "url": "https://httpbin.org/anything/{{id}}",
  "method": "GET",
  "headers": {},
  "queryParams": {},
  "csv": "id\n1\n2\n3",
  "maxRows": 0,
  "concurrency": 20,
  "perHostPerSecond": 10,
  "maxRetries": 2,
  "timeoutSecs": 30,
  "keepBody": true,
  "maxBodyKb": 1000
}
```

# Actor output Schema

## `results` (type: `string`):

One row per request: status, timing, attempts and error.

## `fullResults` (type: `string`):

Every field: parsed JSON or body, response headers, extracted fields and the original input row.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "url": "https://httpbin.org/anything/{{id}}",
    "headers": {},
    "queryParams": {},
    "csv": `id
1
2
3`
};

// Run the Actor and wait for it to finish
const run = await client.actor("saliga/bulk-http-requests").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "url": "https://httpbin.org/anything/{{id}}",
    "headers": {},
    "queryParams": {},
    "csv": """id
1
2
3""",
}

# Run the Actor and wait for it to finish
run = client.actor("saliga/bulk-http-requests").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "url": "https://httpbin.org/anything/{{id}}",
  "headers": {},
  "queryParams": {},
  "csv": "id\\n1\\n2\\n3"
}' |
apify call saliga/bulk-http-requests --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,saliga/bulk-http-requests"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/SOQmgkOByk6dtr5Or/builds/umOBN0iQVThNS33Y7/openapi.json
