# HTTP Request Runner: Bulk API Calls & Checks (`maliksaid/http-request-runner`) Actor

Send a batch of HTTP requests and API calls (GET, POST, PUT, PATCH, DELETE) with headers, JSON body and query. Fill URLs from variables or CSV. Get status, response time, headers, parsed JSON and redirects, with retries, checks and hidden secrets.

- **URL**: https://apify.com/maliksaid/http-request-runner.md
- **Developed by:** [Malik Said](https://apify.com/maliksaid) (community)
- **Categories:** Developer tools, AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 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

## HTTP Request Runner: Bulk API Calls & Checks

HTTP Request Runner sends a batch of HTTP requests and API calls that you define. Each request can use any common method, headers, a query and a JSON or text body. The Actor saves one record per request with the status, response time, headers, the body (parsed when it is JSON) and every redirect. Use it from Clay, n8n, Make, Zapier, an AI agent or the Apify API.

### What HTTP Request Runner does

- Sends GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS requests.
- Fills URLs, headers, query and body from rows of variables. Rows can come from a JSON list, CSV text or another Apify dataset.
- Retries network errors, timeouts and busy answers (408, 425, 429, 500, 502, 503, 504) up to 2 times.
- Parses JSON answers and picks values with simple paths such as `data.items[0].id`.
- Checks each answer: status, text in the body, JSON values, headers and response time.
- Keeps API keys and tokens in an encrypted Secrets field. Their values never appear in the results or the log.
- Follows up to 10 redirects and lists each hop.
- Respects robots.txt on every host and every redirect. Disallowed paths are not sent and cost nothing.

### HTTP request output: what you get

One record per request. Main fields:

| Field | Meaning |
|---|---|
| `index`, `label` | Order of the request and your own label |
| `method`, `url` | What was sent, with variables filled |
| `ok` | True for a 2xx answer with no error |
| `status`, `statusText` | Status of the final answer, or `robots-disallowed` |
| `timeMs`, `firstByteMs` | Response time and time to first byte, in milliseconds |
| `attempts` | Tries made, including retries |
| `redirects`, `finalUrl` | Each redirect hop and the final URL |
| `responseHeaders`, `contentType` | Headers of the final answer |
| `json` | Parsed body when the answer is JSON |
| `body` | Body as text when it is not JSON |
| `bodyType`, `bodyBytes`, `bodyTruncated` | json, text, binary or empty; size kept; whether it was cut |
| `extracted` | Values picked with **Values to pick** |
| `assertionsPassed`, `assertionFailures` | Result of your checks |
| `error` | Why a request failed or was refused |
| `callbackStatus` | Status your callback URL answered for this record. Null when no callback is set |

Binary answers such as images and PDFs are not saved. Their size and headers are. A body longer than **Maximum body size** is cut and marked `bodyTruncated`. A cut body is not parsed as JSON.

Sample record from a test run (the POST example in the input form; some response headers left out):

```json
{
  "id": "request-00001",
  "index": 1,
  "label": "create",
  "variables": null,
  "method": "POST",
  "url": "https://jsonplaceholder.typicode.com/posts",
  "requestHeaders": {
    "Content-Type": "application/json",
    "User-Agent": "Mozilla/5.0 (compatible; http-request-runner/0.1; Apify Actor)",
    "Accept": "application/json, text/plain, */*",
    "Accept-Encoding": "gzip, deflate, br"
  },
  "requestBodyBytes": 47,
  "ok": true,
  "status": 201,
  "statusText": "Created",
  "finalUrl": "https://jsonplaceholder.typicode.com/posts",
  "redirects": [],
  "attempts": 1,
  "timeMs": 227,
  "firstByteMs": 227,
  "responseHeaders": {
    "content-type": "application/json; charset=utf-8",
    "content-length": "73",
    "location": "https://jsonplaceholder.typicode.com/posts/101",
    "server": "cloudflare"
  },
  "contentType": "application/json; charset=utf-8",
  "bodyBytes": 73,
  "bodyTruncated": false,
  "bodyType": "json",
  "body": null,
  "json": { "title": "Hello", "body": "Test post", "userId": 1, "id": 101 },
  "extracted": null,
  "assertionsPassed": true,
  "assertionFailures": [],
  "error": null,
  "requestedAt": "2026-10-22T08:08:10.203Z",
  "callbackStatus": null,
  "scrapedAt": "2026-10-22T08:08:10.432Z"
}
```

### How to send bulk HTTP requests

1. Open the input form. Add your calls in **Requests**, or paste plain URLs in **URLs**.
2. Put tokens and API keys in **Secrets**, one per line, as `NAME=value`.
3. Use `{{secret.NAME}}` where the key goes, for example in a header: `"Authorization": "Bearer {{secret.API_TOKEN}}"`.
4. To call one endpoint for many records, write `{{name}}` in the URL or body. Then add rows in **Variables**, **Variables as CSV** or **Variables from a dataset**.
5. Add **Checks for every response** if you want a pass or fail per request.
6. Run the Actor. Read the results in the Overview table, or as JSON, CSV or Excel.

Example request list:

```json
[
  {
    "label": "post",
    "method": "GET",
    "url": "https://jsonplaceholder.typicode.com/posts/{{postId}}",
    "extract": { "title": "title", "userId": "userId" }
  },
  {
    "label": "create",
    "method": "POST",
    "url": "https://jsonplaceholder.typicode.com/posts",
    "headers": { "Content-Type": "application/json" },
    "body": { "title": "Hello", "body": "Test post", "userId": 1 },
    "expect": { "status": "201", "json": { "title": "Hello" } }
  }
]
```

With Variables `[{"postId": 1}, {"postId": 2}]`, the first request runs twice and the second runs once.

#### Same body for a list of URLs

You do not need the full Requests list to send one body to many URLs. Paste the URLs in **URLs**, set **Method** to POST, and put the JSON in **Body**. Add shared **Query parameters** the same way.

```json
{
  "urls": ["https://jsonplaceholder.typicode.com/posts", "https://jsonplaceholder.typicode.com/comments"],
  "method": "POST",
  "query": { "source": "apify" },
  "body": { "title": "Hello", "token": "{{secret.API_TOKEN}}" }
}
```

Each URL gets one request with the same query and body. Secrets in the body are filled when sent and hidden in the results. GET and HEAD requests send no body; the log says so.

#### Template rules

- `{{name}}` in a URL is URL-encoded. `{{{name}}}` is inserted as written, for example a full base URL.
- In a JSON body, a value that is only `"{{id}}"` keeps the variable's type, so a number stays a number.
- `{{user.name}}` reads a nested field from the row.
- CSV values are always text.
- A request whose variable is missing in a row is refused and not charged.

#### Checks and picked values

`expect` keys: `status` (`"2xx"`, `"200"`, `"200,201"` or `"200-299"`), `bodyContains`, `bodyNotContains`, `maxTimeMs`, `json` and `headerContains`. In `json`, map a path to the value you expect, or to `"*"` to require only that the path exists.

`extract` maps a name to a path: `data.id`, `items[0].name`, `items[-1].name` (last item), `items[*].id` (all items) or `header:etag`.

### Input

| Option | Default | Meaning |
|---|---|---|
| Requests | Example | Full request list (method, url, headers, query, body, label, expect, extract) |
| URLs | None | Quick list, sent with Method and the shared Headers, Query parameters and Body |
| Method | GET | Method for the URLs list |
| Headers | None | Headers added to every request |
| Query parameters | None | Query parameters added to every URL in the URLs list |
| Body | None | JSON body sent with every URL in the URLs list. GET and HEAD send no body |
| Secrets | None | `NAME=value` lines, encrypted, used as `{{secret.NAME}}` |
| Variables | Example | Rows that fill `{{placeholders}}` |
| Variables as CSV | None | Same, as CSV text |
| Variables from a dataset | None | Same, from an Apify dataset |
| Checks for every response | None | Checks applied to every answer |
| Values to pick | None | Paths picked from every JSON answer |
| Maximum requests | 100 | Stop after this many. 0 means the limit of 5,000 |
| Parallel requests | 5 | Up to 10 at once across hosts |
| Delay per host (ms) | 250 | Gap between two requests to one host. At least 200 |
| Timeout per request | 30 s | Up to 60 s |
| Retries | 2 | Up to 2 |
| Follow redirects | On | Up to 10 hops |
| Maximum body size | 256 KB | Up to 1,024 KB |
| Callback URL | None | POST each result record as JSON to your webhook |
| Mark the run failed when a request fails | Off | The run ends as failed after saving every result, so a run-failed alert fires |

An empty input runs the example from the input form.

### robots.txt rule

The Actor reads robots.txt for every host, and again for every redirect hop. It follows the rules for all crawlers (`User-agent: *`). The check always runs. You cannot turn it off.

- A path that robots.txt disallows is not sent. Its row has `status` set to `robots-disallowed`, and it costs nothing.
- The rule applies to every method, because robots.txt has no method rules.
- A site with no robots.txt (404) is allowed.
- A robots.txt that cannot be read (server error or no connection) blocks the host, as RFC 9309 requires.
- Some APIs disallow all crawlers in robots.txt. This Actor cannot call them.

### Limits and safety

The Actor is for API calls and checks. It is not a load testing tool. These limits are fixed:

- At most 5,000 requests per run and 2,000 per host.
- At most 2 requests at a time to one host, and at least 200 ms between them.
- The default run timeout is 3,600 s (1 hour). The run stops sending about 45 s before the timeout, so every saved row is kept.
- Request bodies up to 1 MB. Response bodies kept up to 1 MB.
- Private, local and cloud metadata addresses are refused, also after DNS lookup and after redirects.
- Ports below 1024 are refused, except 80 and 443.
- Usernames and passwords inside URLs are refused. Use Secrets.
- Headers that hold a secret, plus `Authorization` and `Cookie`, are dropped when a redirect goes to another host.

You choose the endpoints. Follow each API's terms and rate limits. The Actor uses no proxy and does not get around logins, CAPTCHAs or bot protection.

### Pricing

You pay a small start fee per run and a fee per request sent. Requests refused before sending (bad URL, private address, missing variable, robots.txt disallow) are saved with the reason and cost nothing. Retries of one request count as one request.

| Event | Free plan | GOLD plan and higher |
|---|---|---|
| Run start | $0.001 | $0.001 |
| HTTP request | $0.001 ($1.00 per 1,000) | $0.0008 ($0.80 per 1,000) |

| HTTP requests in one run | Free plan | GOLD plan and higher |
|---|---|---|
| 100 | $0.101 | $0.081 |
| 1,000 | $1.001 | $0.801 |

Costs include the run start fee.

### FAQ

**Can I call an API that needs a key?** Yes. Put the key in Secrets and use `{{secret.NAME}}` in a header, the query or the URL.

**Can I chain it after another Actor?** Yes. Pick that Actor's dataset in **Variables from a dataset**. Each item becomes one row of variables.

**Does it parse XML or HTML?** It returns them as text in `body`. Only JSON is parsed.

**Why was my request refused?** The `error` field says why. A `status` of `robots-disallowed` means the site's robots.txt does not allow that path.

**How do I get notified when a check fails?** Turn on **Mark the run failed when a request fails** and add Apify's run-failed alert or webhook. Every result is still saved first. To get each result as it arrives, set **Callback URL** to your n8n, Make or Zapier webhook.

### Other Actors by the same developer

- [Technical SEO Audit & Broken Link Checker](https://apify.com/maliksaid/technical-seo-audit)

### Support

Report a problem in the Issues tab. Include the run link. Never paste a secret in an issue.

# Actor input Schema

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

List the requests to send. Each one needs a url. Optional fields: method (GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS), headers, query, body (text, or JSON sent as application/json), label, expect (checks) and extract (values to pick). Use {{name}} to fill a value from Variables, and {{secret.NAME}} to fill a value from Secrets.

## `urls` (type: `array`):

A quick list of URLs. Each one is sent with the Method below and the shared Headers, Query parameters and Body. Use Requests for full control.

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

Set the method for the URLs list.

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

Add headers to every request. A header set on one request replaces the shared one. Put tokens and API keys in Secrets, then write {{secret.NAME}} here.

## `query` (type: `object`):

Add these query parameters to every URL in the URLs list. Values can use {{placeholders}} and {{secret.NAME}}.

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

Send this JSON body with every URL in the URLs list, as application/json. Used with POST, PUT, PATCH, DELETE and OPTIONS only; GET and HEAD send no body. Values can use {{placeholders}} and {{secret.NAME}}.

## `secrets` (type: `string`):

Store tokens and API keys here, one per line, as NAME=value. Use them as {{secret.NAME}} in a URL, header, query or body. Secrets are encrypted by Apify, never logged, and replaced with \[secret] in the results. Headers that hold a secret are dropped when a redirect goes to another host.

## `variables` (type: `array`):

Give one object per row, for example {"postId": 1}. Each request with a {{placeholder}} runs once per row. In URLs, {{name}} is URL-encoded and {{{name}}} is inserted as written. In a JSON body, a value that is only "{{name}}" keeps the variable's type.

## `variablesCsv` (type: `string`):

Paste CSV text. The first line names the columns. Each later line is one row of variables.

## `variablesDatasetId` (type: `string`):

Pick an Apify dataset. Each item is one row of variables, so you can chain this Actor after another one.

## `expect` (type: `object`):

Check each response. Keys: status ("2xx", "200", "200,201" or "200-299"), bodyContains, bodyNotContains, maxTimeMs, json (path to expected value, or "\*" for any value) and headerContains. A request's own expect adds to these checks.

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

Pick values from each JSON response into the extracted field. Map a name to a path such as "data.items\[0].id", "items\[\*].name" or "header:etag".

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

Optional. After each request, POST its result record as JSON to this URL, for example an n8n, Make or Zapier webhook. Secrets stay replaced with \[secret]. Private and local addresses are refused, and the callback host's robots.txt must allow it. One retry, 10 s timeout. A failed callback is logged and never stops the run. Callbacks cost nothing extra.

## `failRunOnError` (type: `boolean`):

Mark the run as failed when any sent request fails, answers with an error status, or fails a check. All results are still saved and charged as usual. Use it with Apify's run-failed alert to watch an API on a schedule.

## `maxRequests` (type: `integer`):

Stop after this many requests. Set 0 for the limit of 5,000 requests per run.

## `maxConcurrency` (type: `integer`):

Set how many requests run at the same time, across all hosts. One host never gets more than 2 at a time.

## `minDelayMsPerHost` (type: `integer`):

Wait at least this long between two requests to the same host. The lowest allowed value is 200 ms.

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

Stop waiting for one answer after this time, including the body.

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

Retry after a network error, a timeout, or status 408, 425, 429, 500, 502, 503 or 504. Waits 1 s, then 2 s, or the time in the Retry-After header up to 30 s.

## `followRedirects` (type: `boolean`):

Follow up to 10 redirects. Every hop is listed in the redirects field. Turn off to get the 3xx answer itself.

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

Keep at most this much of each response body, up to 1,024 KB. A longer body is cut and marked bodyTruncated.

## Actor input object example

```json
{
  "requests": [
    {
      "label": "post",
      "method": "GET",
      "url": "https://jsonplaceholder.typicode.com/posts/{{postId}}",
      "extract": {
        "title": "title",
        "userId": "userId"
      }
    },
    {
      "label": "create",
      "method": "POST",
      "url": "https://jsonplaceholder.typicode.com/posts",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": {
        "title": "Hello",
        "body": "Test post",
        "userId": 1
      },
      "expect": {
        "status": "201",
        "json": {
          "title": "Hello"
        }
      }
    }
  ],
  "urls": [],
  "method": "GET",
  "headers": {},
  "query": {},
  "variables": [
    {
      "postId": 1
    },
    {
      "postId": 2
    }
  ],
  "expect": {},
  "extract": {},
  "failRunOnError": false,
  "maxRequests": 100,
  "maxConcurrency": 5,
  "minDelayMsPerHost": 250,
  "timeoutSecs": 30,
  "maxRetries": 2,
  "followRedirects": true,
  "maxBodyKb": 256
}
```

# Actor output Schema

## `responses` (type: `string`):

No description

# 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 = {
    "requests": [
        {
            "label": "post",
            "method": "GET",
            "url": "https://jsonplaceholder.typicode.com/posts/{{postId}}",
            "extract": {
                "title": "title",
                "userId": "userId"
            }
        },
        {
            "label": "create",
            "method": "POST",
            "url": "https://jsonplaceholder.typicode.com/posts",
            "headers": {
                "Content-Type": "application/json"
            },
            "body": {
                "title": "Hello",
                "body": "Test post",
                "userId": 1
            },
            "expect": {
                "status": "201",
                "json": {
                    "title": "Hello"
                }
            }
        }
    ],
    "variables": [
        {
            "postId": 1
        },
        {
            "postId": 2
        }
    ],
    "maxRequests": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("maliksaid/http-request-runner").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 = {
    "requests": [
        {
            "label": "post",
            "method": "GET",
            "url": "https://jsonplaceholder.typicode.com/posts/{{postId}}",
            "extract": {
                "title": "title",
                "userId": "userId",
            },
        },
        {
            "label": "create",
            "method": "POST",
            "url": "https://jsonplaceholder.typicode.com/posts",
            "headers": { "Content-Type": "application/json" },
            "body": {
                "title": "Hello",
                "body": "Test post",
                "userId": 1,
            },
            "expect": {
                "status": "201",
                "json": { "title": "Hello" },
            },
        },
    ],
    "variables": [
        { "postId": 1 },
        { "postId": 2 },
    ],
    "maxRequests": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("maliksaid/http-request-runner").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 '{
  "requests": [
    {
      "label": "post",
      "method": "GET",
      "url": "https://jsonplaceholder.typicode.com/posts/{{postId}}",
      "extract": {
        "title": "title",
        "userId": "userId"
      }
    },
    {
      "label": "create",
      "method": "POST",
      "url": "https://jsonplaceholder.typicode.com/posts",
      "headers": {
        "Content-Type": "application/json"
      },
      "body": {
        "title": "Hello",
        "body": "Test post",
        "userId": 1
      },
      "expect": {
        "status": "201",
        "json": {
          "title": "Hello"
        }
      }
    }
  ],
  "variables": [
    {
      "postId": 1
    },
    {
      "postId": 2
    }
  ],
  "maxRequests": 100
}' |
apify call maliksaid/http-request-runner --silent --output-dataset

```

## MCP server setup

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

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/ePRgAAd8OjazpG5PU/builds/qORrvzejzbTaJKzg5/openapi.json
