# HTTP Request Runner: bulk API calls and webhooks (`examsens.ai/http-request-runner`) Actor

Sends many HTTP requests in one run (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) with your own headers, JSON or form bodies and secret tokens, and returns each response's status, headers, body, parsed JSON and timing.

- **URL**: https://apify.com/examsens.ai/http-request-runner.md
- **Developed by:** [Ed Rayyis](https://apify.com/examsens.ai) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.50 / 1,000 request completeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 and webhooks

Send one HTTP request or a few thousand from a single run and get every response back as a row: status, headers, body, parsed JSON and timing. It handles GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS, with your own headers and a JSON, text or form body on each request.

People use it to call an API on a schedule, fire webhooks from an Apify task, test endpoints after a deploy, or pull data from a JSON API into a dataset without writing code.

### Input

Each request is a JSON object in the Requests list:

```json
[
  {"label": "list users", "url": "https://api.example.com/users?page=1",
   "headers": {"Accept": "application/json"}},
  {"label": "create order", "method": "POST", "url": "https://api.example.com/orders",
   "headers": {"X-Api-Key": "{{secrets.SHOP_KEY}}"}, "json": {"sku": "A-100", "qty": 2}}
]
```

Only `url` is required. A request can also carry `method`, `headers`, one body (`json`, `body` for raw text, or `form` for a URL encoded form), a `label` of your own and its own `timeoutSecs`. An address without `http://` or `https://` gets `https://`.

For plain GET calls you can paste URLs, one per line, in the URLs field instead. They use the default method and default headers.

Other settings:

- Default method (GET) and default headers sent with every request. A request's own headers win where the names match.
- Follow redirects (on) and the maximum number of redirects (10). A 301, 302 or 303 after a POST is repeated as a GET without the body, as a browser does. A 307 or 308 keeps the method and body.
- Maximum body characters (100,000). Longer bodies are cut and marked `bodyTruncated`.
- Include response headers (on)
- Timeout per request in seconds (30). It covers the whole request, body included.
- Retries (2)
- Parallel requests in total (10) and per host (4), and a delay between requests to one host (0 ms)

### Tokens and keys

Two fields keep credentials out of the request list. Both are stored encrypted by Apify.

Secrets takes `NAME=value` lines. Write `{{secrets.NAME}}` in a URL, header, body, JSON or form value and the run puts the real value in just before sending. That works for keys that have to go in a query string too.

Secret headers takes `Name: value` lines, such as `Authorization: Bearer ...`, and adds them to every request. Fill in the hosts list if they should go to some hosts only.

Secret values are never written to the log. Wherever one comes back in a result (an API that echoes your headers, a query string in an error), it shows as `(secret)`. Request rows keep the URL as you wrote it, placeholders and all. When a redirect leads to another host, the secret headers, `Authorization` and `Cookie` are dropped before the next hop. Each request runs on its own, and cookies from one response are never sent with another request.

### Output

One dataset row per request, in the order the requests finish, with `index` giving its place in your input. A row with an answer has:

- `status`, `statusText` and `ok` (true for 2xx)
- `body` as text, and `json` when the answer is JSON and fits whole within the character limit
- `bodyBase64` in place of `body` for images, PDFs and other binary answers
- `contentType`, `bodyBytes`, `bodyTruncated`, `httpVersion` and `responseHeaders`
- `timeMs`, `attempts`, `sentAt`, plus `finalUrl`, `redirectCount` and `redirectChain` when it was redirected

A 404 or 500 is still an answer: you get the row with its status and body.

A request with no answer gets a row with `responded: false`, an `error` and an `errorType`, and the run moves on to the next request. The types are `dnsError`, `timeout`, `tlsError`, `connectionError`, `tooManyRedirects`, `blockedAddress` and `invalidInput` (a bad URL or method, two bodies on one request, or a placeholder with no matching secret).

The `OUTPUT` record in the key value store holds the totals for the run.

### Rate limits and retries

A request that gets 429 is sent again after the wait the server asks for in `Retry-After`, up to 60 seconds, or after a short backoff when there is no header. A 502, 503 or 504 is retried the same way for GET, HEAD, OPTIONS, PUT and DELETE only. POST and PATCH are never repeated after the server answered, so a webhook fires once. A connection that times out before it opens is retried for every method, since nothing was sent.

For an API that allows one call per second, set the delay to 1000 ms. In one test, three calls to one host with a 1500 ms delay went out 1.5 seconds apart.

### Blocked addresses

Hosts that resolve to private, loopback, link local or reserved addresses are refused, and so are `localhost` names. Each redirect hop is checked the same way, so a public URL that redirects to `127.0.0.1` stops with a `blockedAddress` row.

### Pricing

Pay per event: one event for each request that gets an answer from the server, whatever the status code. Requests that fail to connect, time out or are refused cost nothing.

Before each request goes out, the run checks that the event can still be paid for within the maximum charge you set. Once it cannot, no more requests are sent, the run finishes cleanly, and `OUTPUT` shows `stoppedAtChargeLimit: true` with the number of requests not sent.

### Scope and conduct

It sends only the requests you list, through no proxy. Call only APIs and endpoints you are allowed to use, within their terms and rate limits.

### Support

Questions and problems: the Issues tab on this page.

# Actor input Schema

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

One object per request. Keys: url (required), method, headers (object), json (any JSON value, sent as application/json), body (raw text), form (object, sent as a URL encoded form), label (your own name for the row) and timeoutSecs. Write {{secrets.NAME}} anywhere in url, headers, body, json or form to insert a value from Secrets.

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

Optional. Plain URLs, one per line, sent with the default method and default headers. Added after the requests above.

## `defaultMethod` (type: `string`):

Used for URLs and for any request object without a method.

## `defaultHeaders` (type: `object`):

Headers sent with every request, as a JSON object. A request's own headers replace these where the names match.

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

Tokens and keys, one per line as NAME=value. Insert them with {{secrets.NAME}}. Stored encrypted, never written to the log, and replaced by (secret) wherever a value shows up in the results.

## `secretHeaders` (type: `string`):

Headers sent with every request, one per line as Name: value, for example Authorization: Bearer abc123. Stored encrypted and never written to the log. Their values show as (secret) in the results.

## `secretHosts` (type: `array`):

Optional. Host names such as api.example.com. When set, secret headers go only to requests for these hosts. Secret headers, Authorization and Cookie are always dropped when a redirect leads to another host.

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

Follow 3xx answers up to the maximum below. A 301, 302 or 303 after a POST is followed with GET and no body, as browsers do; 307 and 308 repeat the same method and body.

## `maxRedirects` (type: `integer`):

Stop following after this many hops and report an error.

## `maxBodyChars` (type: `integer`):

Response bodies longer than this are cut and marked bodyTruncated. JSON is parsed only when the whole body fits.

## `includeResponseHeaders` (type: `boolean`):

Add every response header to each row.

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

How long to wait for each request. A request object can set its own timeoutSecs.

## `retries` (type: `integer`):

How many times to repeat a request that answers 429, or 502, 503 or 504 for GET, HEAD, OPTIONS, PUT and DELETE, or that times out before connecting. The wait follows the server's Retry-After header, up to 60 seconds.

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

How many requests run at the same time across all hosts.

## `maxPerHost` (type: `integer`):

How many requests one host gets at the same time. Lower it for APIs with tight rate limits.

## `delayMs` (type: `integer`):

Minimum gap between the start of two requests to the same host. 1000 gives at most one request per second per host.

## Actor input object example

```json
{
  "requests": [
    {
      "label": "get example",
      "url": "https://httpbin.org/get?page=1"
    },
    {
      "label": "post json",
      "method": "POST",
      "url": "https://httpbin.org/post",
      "headers": {
        "X-Request-Source": "runner"
      },
      "json": {
        "name": "sample",
        "items": [
          1,
          2,
          3
        ]
      }
    },
    {
      "label": "not found",
      "url": "https://httpbin.org/status/404"
    }
  ],
  "defaultMethod": "GET",
  "followRedirects": true,
  "maxRedirects": 10,
  "maxBodyChars": 100000,
  "includeResponseHeaders": true,
  "timeoutSecs": 30,
  "retries": 2,
  "maxConcurrency": 10,
  "maxPerHost": 4,
  "delayMs": 0
}
```

# Actor output Schema

## `results` (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": "get example",
            "url": "https://httpbin.org/get?page=1"
        },
        {
            "label": "post json",
            "method": "POST",
            "url": "https://httpbin.org/post",
            "headers": {
                "X-Request-Source": "runner"
            },
            "json": {
                "name": "sample",
                "items": [
                    1,
                    2,
                    3
                ]
            }
        },
        {
            "label": "not found",
            "url": "https://httpbin.org/status/404"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("examsens.ai/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": "get example",
            "url": "https://httpbin.org/get?page=1",
        },
        {
            "label": "post json",
            "method": "POST",
            "url": "https://httpbin.org/post",
            "headers": { "X-Request-Source": "runner" },
            "json": {
                "name": "sample",
                "items": [
                    1,
                    2,
                    3,
                ],
            },
        },
        {
            "label": "not found",
            "url": "https://httpbin.org/status/404",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("examsens.ai/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": "get example",
      "url": "https://httpbin.org/get?page=1"
    },
    {
      "label": "post json",
      "method": "POST",
      "url": "https://httpbin.org/post",
      "headers": {
        "X-Request-Source": "runner"
      },
      "json": {
        "name": "sample",
        "items": [
          1,
          2,
          3
        ]
      }
    },
    {
      "label": "not found",
      "url": "https://httpbin.org/status/404"
    }
  ]
}' |
apify call examsens.ai/http-request-runner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,examsens.ai/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/gloe62wR23gchUCge/builds/SxhjLwSstiglugAJf/openapi.json
