# HTTP Request — Bulk API Calls for Clay, n8n & Make (`actor-forge/http-request-runner`) Actor

Send HTTP requests to any API (GET, POST, PUT, PATCH, DELETE) from Clay, n8n, Make, Zapier or the Apify API: one call or thousands in a batch, with {{variable}} templating, retries, redirects, JSON field extraction and webhook callbacks. A Clay HTTP API alternative at $2 per 1,000 requests.

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

## Pricing

from $2.00 / 1,000 request sents

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 — Bulk API Calls for Clay, n8n & Make

Send HTTP requests to any API (GET, POST, PUT, PATCH, DELETE) from Clay, n8n, Make, Zapier or the Apify API: one call or thousands in a batch, with {{variable}} templating, retries, redirects, JSON field extraction and webhook callbacks. A Clay HTTP API alternative at $2 per 1,000 requests.

**Call any REST API or webhook from Clay, n8n, Make, Zapier or your own code, with no HTTP plan upgrade and no server to run.** Send one request or a batch of thousands (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) with custom **headers**, **query parameters** and **JSON, form or raw bodies**. Get back one clean row per request: **status code**, **response headers**, the **body** (parsed JSON, text, or base64 for binary), **extracted fields** pulled out of the JSON by path (e.g. `data.items[0].email`), duration, attempts and the full redirect chain. It works as a **Clay HTTP API alternative**: Clay only offers its HTTP API integration on higher plans, but any Clay table can run an Apify actor per row. It also does **bulk API requests** for n8n and Make (one run, many calls, results as items), and you can **forward every response to a webhook** (a Clay table webhook, n8n or Make) with an optional `lookupKey` to match rows.

**Batch and templating.** Put `{{field}}` placeholders in the URL, headers, query or body and pass a `variables` list: one request per row, e.g. enrich 5,000 domains with one input. URL values are percent-encoded, JSON bodies are escaped, and a lone `"{{count}}"` keeps its number type. **Reliable by default:** retries with exponential backoff on network errors, timeouts, 429 and 5xx (honours `Retry-After`; POST is never blindly re-sent after a 500), per-host concurrency limits for rate-limited APIs, redirects (credentials are dropped on cross-domain hops), gzip/brotli decoding, and a size cap on bodies.

**Secure.** Your API keys never appear in the run log, and the secret **Authorization header** field is stored encrypted. Requests to private networks, localhost and cloud-metadata addresses are blocked at connect time, including after redirects and DNS rebinding.

**Drop-in compatible** with `riceman/send-http-requests` inputs: `url`, `method`, `headers`, `params`, `data`, `responseTimeout`, `retryOnFailure`, `maxRetries`, `callbackUrl`, `callbackHeaders` and `lookupKey` all work unchanged, and rows keep the `data`, `status`, `statusText` and `headers` fields, **at $2 per 1,000 requests instead of $5**.

### How to use it from Clay, n8n, Make and Zapier

Your actor ID is `actor-forge~http-request-runner`. Find your Apify API token in Apify Console → Settings → API & Integrations.

**Clay (one API call per table row)**

1. In your Clay table click **Add enrichment**, search for **Apify** and choose the action that runs an actor. Connect your Apify account with your API token.
2. Pick the actor `actor-forge/http-request-runner`.
3. In the input, insert Clay columns where values go, e.g. `{"url": "https://api.example.com/v1/people?email=<Email column>", "method": "GET", "extract": ["person.title", "company=person.company.name"]}`. Put an API key in **authorizationHeader** (`Bearer …` or `X-Api-Key: …`).
4. Run the column, then map `status`, `extracted.title`, `extracted.company` or `data.…` to new columns.
5. Large responses: set `callbackUrl` to a Clay table webhook and `lookupKey` to the row ID, and the full row is posted there.

**n8n**

1. Add an **HTTP Request** node: method `POST`, URL `https://api.apify.com/v2/acts/actor-forge~http-request-runner/run-sync-get-dataset-items`, header `Authorization: Bearer YOUR_APIFY_TOKEN`, body type JSON.
2. Body = the actor input. n8n uses `{{ }}` for its own expressions, so map values with n8n expressions instead of our placeholders, e.g. `{"requests": {{ JSON.stringify($json.urls) }}, "extract": ["id", "name"]}`. Each returned item is one response row.
3. Or use the Apify node (Run actor, wait for finish, then get dataset items).

**Make**

1. Add **HTTP → Make a request**: method `POST`, URL `https://api.apify.com/v2/acts/actor-forge~http-request-runner/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN`, body type Raw, content type JSON, and **Parse response** on.
2. Request content = the actor input JSON, with Make's mapped values inserted directly (Make also uses `{{ }}`, so map fields rather than using our placeholders). The output is an array with one bundle per request.
3. Or use the Apify app: **Run an actor** (synchronous), then **Get dataset items**.

**Zapier**
Use the Apify app's **Run Actor** action with this actor and the input JSON, or **Webhooks by Zapier → POST** to the `run-sync-get-dataset-items` URL above.

**Any code / curl**
`curl -X POST "https://api.apify.com/v2/acts/actor-forge~http-request-runner/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" -H "Content-Type: application/json" -d '{"url": "https://httpbin.org/get"}'` returns the response rows as a JSON array. For a single request, `.../run-sync` returns the row itself (the run's `OUTPUT` record). Synchronous calls wait up to 300 seconds; for bigger batches start the run asynchronously and read the dataset, or use `callbackUrl`.

### What data does it return?

Every run outputs clean, schema-validated rows like these:

| index | lookupKey | request.method | request.url | status | ok | extracted | durationMs | error |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | row-17 | GET | https://jsonplaceholder.typicode.com/users/1 | 200 | true | {"email":"Sincere@april.biz","company.name":"Romaguera-Crona","address.city":"Gwenborough"} | 153 |  |
| 1 | row-18 | POST | https://postman-echo.com/post | 200 | true | {"json.name":"Acme Ltd"} | 182 |  |
| 2 | row-19 | GET | https://httpbin.org/status/404 | 404 | false | {} | 97 |  |
| 3 | row-20 | GET | http://169.254.169.254/latest/meta-data/ |  | false | {} | 0 | blocked: internal address 169.254.169.254 is not allowed |

### How much does it cost?

Transparent pay-per-event pricing — you only pay for what you get, and the
actor **always respects your run budget**: set a maximum total charge and it
stops cleanly at the cap instead of overrunning.

| Event | Price | Charged | Worked example |
| --- | --- | --- | --- |
| `request-sent` | $0.002 | per request that got an HTTP response (any status code, after retries and redirects; retries are not charged again) | 1,000 requests = $2.00; a 10,000-row Clay table = $20 |
| `apify-actor-start` | $0.00005 | per run per GB of memory (platform start fee) | One run at up to 1 GB = $0.00005 (charged as one event) |

### Input options

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `url` | string | no | — | URL for a single request (http or https). Supports {{field}} placeholders filled from Variables. Leave empty when you use Requests (batch). |
| `method` | string | no | — | HTTP method. If empty: GET, or POST when a body is given. |
| `headers` | object | no | — | Request headers as a JSON object, e.g. {"Content-Type": "application/json"}. Values support {{field}} placeholders. Header values are never written to the run log. Put API keys in the secret Authorization header field below instead, so they are stored encrypted. |
| `authorizationHeader` | string | no | — | Optional, stored encrypted. A value like "Bearer sk_123" or "Basic dXNlcjpwYXNz" is sent as the Authorization header; "X-Api-Key: abc123" (name: value) sends that header instead. Added to every request and overrides the same header in Headers. |
| `queryParams` | object | no | — | Query string parameters as a JSON object, URL-encoded and appended to the URL. Arrays repeat the key (tags=a\&tags=b). Alias: params. |
| `body` | string | no | — | Raw request body, e.g. a JSON string or form data. A body starting with { or \[ is sent as application/json unless you set Content-Type. Placeholders in JSON bodies are JSON-escaped automatically. |
| `data` | object | no | — | Request body as a JSON object (sent as application/json, or form-encoded when Content-Type is application/x-www-form-urlencoded). A string that is exactly one placeholder, like "{{count}}", keeps the variable's type. Used when Body (raw) is empty. |
| `contentType` | string | no | — | Shortcut for the Content-Type header, e.g. application/json; charset=utf-8. |
| `requests` | array | no | — | List of requests: URL strings or objects {url, method, headers, queryParams, body, lookupKey}. Each item inherits the top-level settings and can override them. |
| `variables` | array | no | — | List of objects, e.g. \[{"domain": "stripe.com"}, {"domain": "notion.so"}]. Every request is sent once per row with {{domain}} replaced. Dot paths work ({{company.name}}, {{tags\[0]}}); {{{field}}} inserts raw. A row's lookupKey field is copied to its result. |
| `lookupKey` | string | no | — | Optional identifier copied to the result row and the callback, e.g. the Clay row ID, so you can match responses to rows. |
| `extract` | array | no | — | Paths to pull out of JSON responses into the extracted column, e.g. data.items\[0].email, results\[\*].id or email=data.items\[0].email (alias=path names the key). Absent paths give null. |
| `responseFormat` | string | no | `"auto"` | How the response body is returned. |
| `maxResponseBytes` | integer | no | `2097152` | Bodies larger than this are cut off and flagged bodyTruncated. Default 2 MB, maximum 9 MB (the dataset item limit). |
| `includeHeaders` | boolean | no | `true` | Return response headers (names lowercased). |
| `includeBody` | boolean | no | `true` | Return the response body. Turn off when you only need status and extracted fields (smaller rows). |
| `timeoutSecs` | integer | no | — | Maximum time for one attempt (connect, headers and body). Default 30. (alias: responseTimeout in milliseconds) |
| `retries` | integer | no | — | Retries on network errors, timeouts, 429 and 5xx, with exponential backoff and Retry-After. POST and PATCH are only retried when the server certainly did not process them (429, 503, connection refused). Default 2. (alias: retryOnFailure + maxRetries) |
| `followRedirects` | boolean | no | `true` | Follow 3xx redirects (up to Max redirects). Off: the 3xx response itself is returned. |
| `maxRedirects` | integer | no | `10` | Maximum redirect hops per request. |
| `concurrency` | integer | no | `5` | Requests in flight at once across all hosts. |
| `maxConcurrencyPerHost` | integer | no | `5` | Requests in flight at once to the same host, to stay under API rate limits. |
| `failOnHttpError` | boolean | no | `false` | Mark the run as FAILED if any request got a non-2xx status, a network error or was invalid (rows are still saved). Default off: the status is recorded and the run succeeds. |
| `callbackUrl` | string | no | — | Optional: each result row is also POSTed as JSON to this URL (e.g. a Clay table webhook, n8n or Make webhook). Supports {{field}} placeholders. |
| `callbackHeaders` | object | no | — | Headers for the callback request, e.g. {"x-clay-webhook-auth": "..."}. |

### Limits & honest notes

- You pay only for requests that got a response from the server, whatever the status code (a 404 or 500 is still a response). Network failures, timeouts, invalid URLs, template errors and blocked targets are free.
- Fast to start: a synchronous single-request call (run-sync-get-dataset-items) typically returns within a few seconds end to end, including the actor start, at 256 MB memory. Batches run up to 20 requests in parallel.
- Up to 25,000 requests per run (requests × variables rows), 20 in flight at once. Synchronous API calls (run-sync) wait at most 300 seconds, so keep sync batches to a few hundred quick requests or use the dataset/callback for more.
- Response bodies are capped at 2 MB by default and 9 MB at most (the Apify dataset item limit). Larger bodies are cut off and flagged with bodyTruncated. Truncated JSON is returned as text. The data field (riceman-compatible copy of body) is filled for bodies up to 1 MB.
- Only public http and https targets. Localhost, private networks (10.x, 172.16-31.x, 192.168.x), link-local and cloud-metadata addresses (169.254.169.254) and .internal/.local hostnames are blocked, and the check is repeated on every redirect and at connect time, so DNS rebinding is blocked too. Host header overrides are rejected, and URLs with user:password@ are refused (send credentials as a header).
- POST and PATCH are retried only when the server certainly did not process the request (429, 503 or connection refused), to avoid duplicate side effects. GET, PUT, DELETE, HEAD and OPTIONS are also retried on 500, 502 and 504. Retry-After waits are capped at 20 seconds.
- Requests go out from Apify's shared datacenter IPs without a proxy. Sites that block datacenter traffic or need a browser (JavaScript, CAPTCHAs) are out of scope; this is a tool for APIs and webhooks.
- Respects your run budget: set a maximum total charge and the run stops cleanly at the cap, with every delivered row charged exactly once.

### Related ActorForge actors

- [Website to Markdown](https://apify.com/actor-forge/website-to-markdown)
- [Domain WHOIS & DNS Lookup](https://apify.com/actor-forge/domain-lookup)
- [Tech Stack Detector](https://apify.com/actor-forge/tech-stack-detector)

### Changelog

- **2026-10-19** — Initial release: single and batch HTTP requests, {{variable}} templating, retries with backoff and Retry-After, redirects, per-host concurrency, JSON field extraction, webhook callbacks, secret Authorization header, SSRF protection, riceman/send-http-requests input compatibility.

***

*Built and maintained by **ActorForge**. Reliability first: every actor in the
portfolio is monitored by an automated nightly self-test, and breakages are
typically fixed the same day.*

# Actor input Schema

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

URL for a single request (http or https). Supports {{field}} placeholders filled from Variables. Leave empty when you use Requests (batch).

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

HTTP method. If empty: GET, or POST when a body is given.

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

Request headers as a JSON object, e.g. {"Content-Type": "application/json"}. Values support {{field}} placeholders. Header values are never written to the run log. Put API keys in the secret Authorization header field below instead, so they are stored encrypted.

## `authorizationHeader` (type: `string`):

Optional, stored encrypted. A value like "Bearer sk_123" or "Basic dXNlcjpwYXNz" is sent as the Authorization header; "X-Api-Key: abc123" (name: value) sends that header instead. Added to every request and overrides the same header in Headers.

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

Query string parameters as a JSON object, URL-encoded and appended to the URL. Arrays repeat the key (tags=a\&tags=b). Alias: params.

## `body` (type: `string`):

Raw request body, e.g. a JSON string or form data. A body starting with { or \[ is sent as application/json unless you set Content-Type. Placeholders in JSON bodies are JSON-escaped automatically.

## `data` (type: `object`):

Request body as a JSON object (sent as application/json, or form-encoded when Content-Type is application/x-www-form-urlencoded). A string that is exactly one placeholder, like "{{count}}", keeps the variable's type. Used when Body (raw) is empty.

## `contentType` (type: `string`):

Shortcut for the Content-Type header, e.g. application/json; charset=utf-8.

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

List of requests: URL strings or objects {url, method, headers, queryParams, body, lookupKey}. Each item inherits the top-level settings and can override them.

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

List of objects, e.g. \[{"domain": "stripe.com"}, {"domain": "notion.so"}]. Every request is sent once per row with {{domain}} replaced. Dot paths work ({{company.name}}, {{tags\[0]}}); {{{field}}} inserts raw. A row's lookupKey field is copied to its result.

## `lookupKey` (type: `string`):

Optional identifier copied to the result row and the callback, e.g. the Clay row ID, so you can match responses to rows.

## `extract` (type: `array`):

Paths to pull out of JSON responses into the extracted column, e.g. data.items\[0].email, results\[\*].id or email=data.items\[0].email (alias=path names the key). Absent paths give null.

## `responseFormat` (type: `string`):

How the response body is returned.

## `maxResponseBytes` (type: `integer`):

Bodies larger than this are cut off and flagged bodyTruncated. Default 2 MB, maximum 9 MB (the dataset item limit).

## `includeHeaders` (type: `boolean`):

Return response headers (names lowercased).

## `includeBody` (type: `boolean`):

Return the response body. Turn off when you only need status and extracted fields (smaller rows).

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

Maximum time for one attempt (connect, headers and body). Default 30. (alias: responseTimeout in milliseconds)

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

Retries on network errors, timeouts, 429 and 5xx, with exponential backoff and Retry-After. POST and PATCH are only retried when the server certainly did not process them (429, 503, connection refused). Default 2. (alias: retryOnFailure + maxRetries)

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

Follow 3xx redirects (up to Max redirects). Off: the 3xx response itself is returned.

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

Maximum redirect hops per request.

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

Requests in flight at once across all hosts.

## `maxConcurrencyPerHost` (type: `integer`):

Requests in flight at once to the same host, to stay under API rate limits.

## `failOnHttpError` (type: `boolean`):

Mark the run as FAILED if any request got a non-2xx status, a network error or was invalid (rows are still saved). Default off: the status is recorded and the run succeeds.

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

Optional: each result row is also POSTed as JSON to this URL (e.g. a Clay table webhook, n8n or Make webhook). Supports {{field}} placeholders.

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

Headers for the callback request, e.g. {"x-clay-webhook-auth": "..."}.

## Actor input object example

```json
{
  "url": "https://jsonplaceholder.typicode.com/users/1",
  "method": "GET",
  "extract": [
    "email",
    "company.name",
    "address.city"
  ],
  "responseFormat": "auto",
  "maxResponseBytes": 2097152,
  "includeHeaders": true,
  "includeBody": true,
  "followRedirects": true,
  "maxRedirects": 10,
  "concurrency": 5,
  "maxConcurrencyPerHost": 5,
  "failOnHttpError": false
}
```

# Actor output Schema

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

One row per request with status, headers, body, extracted fields, timing and error.

## `summary` (type: `string`):

Single request: the response row itself. Batch: counts of responses, HTTP errors, network failures and invalid requests (only responses are charged).

# 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://jsonplaceholder.typicode.com/users/1",
    "method": "GET",
    "extract": [
        "email",
        "company.name",
        "address.city"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("actor-forge/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 = {
    "url": "https://jsonplaceholder.typicode.com/users/1",
    "method": "GET",
    "extract": [
        "email",
        "company.name",
        "address.city",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("actor-forge/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 '{
  "url": "https://jsonplaceholder.typicode.com/users/1",
  "method": "GET",
  "extract": [
    "email",
    "company.name",
    "address.city"
  ]
}' |
apify call actor-forge/http-request-runner --silent --output-dataset

```

## MCP server setup

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