HTTP Request - Batch API Calls [$4/1K] avatar

HTTP Request - Batch API Calls [$4/1K]

Pricing

from $4.00 / 1,000 http requests

Go to Apify Store
HTTP Request - Batch API Calls [$4/1K]

HTTP Request - Batch API Calls [$4/1K]

Send HTTP requests to REST APIs from a Clay table, n8n or Make: one call per Clay row through Clay's Apify integration, or batches of up to 1,000 with encrypted secret headers, retries and lookup keys. $4 per 1,000 answered requests; blocks, timeouts and retries are never charged.

Pricing

from $4.00 / 1,000 http requests

Rating

0.0

(0)

Developer

Vadim Bezrukov

Vadim Bezrukov

Maintained by Community

Actor stats

0

Bookmarked

4

Total users

3

Monthly active users

2 days ago

Last modified

Share

Send a list of API requests and get one result per request, including the HTTP status and response body. Use lookupKey to match each response back to its Clay row, n8n item or Make step. Put shared credentials in encrypted secretHeaders. From a Clay table, run it once per row with Clay's Apify integration (recipe). Already have the rows in an Apify Dataset? Make one API call per Dataset row from a template.

You pay only for HTTP responses that actually arrived. Security blocks, timeouts, transport failures, retries, redirects and callbacks are never charged, and the bill is predictable before the run: $0.004 × responses plus a $0.00005 start.

Latest update, 19 October 2026: point the Actor at an Apify Dataset and it makes one API call per row from a request template. Full history is in the changelog.

Try one request

{
"requests": [
{
"lookupKey": "demo-post-1",
"method": "GET",
"url": "https://jsonplaceholder.typicode.com/posts/1",
"headers": { "Accept": "application/json" }
}
]
}

Expected output: one Dataset row with status, httpStatus, parsed JSON body, durationMs and bytesReceived. Typical first useful result under normal conditions is under 10 seconds and about $0.004 (one http-request plus the $0.00005 start).

Use from Clay

Clay's own Apify integration can run this Actor for every row of a table. Clay lists that integration on all of its plans, while its HTTP API enrichment and webhooks are listed on Growth and Enterprise only (Clay pricing page, read on 23 October 2026). You need an Apify account: Apify bills this Actor there, and Clay counts the enrichment toward your Clay plan's Actions.

  1. Connect Apify in Clay: Settings > Connections > Add Connection > Apify, then paste your API token from the Integrations page in Apify Console.
  2. In the table, click Add enrichment, search for "Apify", choose Run Apify Actor, and pick your Apify account and this Actor.
  3. Paste this into Input Data, then replace /Request URL and /Domain with your own column tokens. As Clay's guide says, keep each key in quotes and leave the token itself unquoted.
{
"requests": [
{
"lookupKey": /Domain,
"method": "GET",
"url": /Request URL,
"headers": { "Accept": "application/json" }
}
],
"secretHeaders": { "Authorization": "Bearer YOUR_API_KEY" }
}
  1. Run it on one row first. Open the enrichment cell and add httpStatus, ok and the body fields you need as new columns.
  2. In the run settings, a conditional formula can skip rows without a URL. Autoupdate, on by default, enriches new rows as they arrive.

/Request URL is a column holding the full URL for that row, for example a formula column that appends the domain to your API's address. For a POST, set "method": "POST" and add a "body" object whose values are column tokens. The API key in secretHeaders is encrypted by Apify and never written to the results or logs.

What one row costs: $0.00405 on Apify, which is one answered request ($0.004) plus the $0.00005 start. A 404 or 500 from your API is still an answer (ok is false). If the API does not answer at all (timeout or network error), only the $0.00005 start is charged and the run ends as FAILED. A private or blocked address gets an uncharged SECURITY_BLOCKED row.

Rows that are already in Apify, for example a scraper's output: skip the per-row runs. Set datasetId and a requestTemplate (details) to make up to 1,000 calls in one run, then in a Clay workbook click + Add, search for Apify and import that run's results as a source. 1,000 calls cost $4.00005 this way, against $4.05 as 1,000 single-row runs.

If your Clay plan includes webhooks, callbackUrl can also POST each final result row, with its lookupKey, to a Clay webhook table.

Use from AI agents with MCP

Direct tool URL (authenticated OAuth or API token required to run): https://mcp.apify.com?tools=automa-flow/http-request-batch

Ask your agent something like: Call GET https://jsonplaceholder.typicode.com/posts/1 with lookupKey demo-post-1 through automa-flow/http-request-batch, keep maxRequests at 10 or lower, set maxTotalChargeUsd to 0.05, then report RUN_SUMMARY status, billedRequests and nextAction before reading the Dataset.

Anonymous discovery (no token) only supports Store search tools: https://mcp.apify.com?tools=search-actors,fetch-actor-details. Direct Actor execution always needs the caller's own Apify account.

Estimate the charge before running:

SizeInputTotal charge (platform usage included)
Small1 request$0.00405
Small batch10 requests$0.04005
CapmaxRequests (default 100, hard max 1000)$0.00005 + N × $0.004

Each billable Dataset write and http-request event are atomic. At maxTotalChargeUsd, the executor stops scheduling new target calls; any in-flight response refused by the charge limit becomes an uncharged CANCELLED row with no response payload. Read RUN_SUMMARY.nextAction first.

  • Walk every page of one API: Paginated REST API Fetcher follows a configured page, offset, cursor or next-link pattern. Choose it when later requests depend on the previous response; use this HTTP batch Actor for an explicit list of independent calls.
  • Store files from response URLs: Parallel Bulk File Downloader takes a list of public file URLs and returns stored files plus a manifest. Extract the permitted URLs from your API results and map them into its input.
  • Extract website text: Website Content Crawler turns authorized public HTML into Markdown and optional chunks. Supply website URLs and the required rights declaration; an API response is not a crawl configuration.
  • Flatten and rename response rows: Dataset Cleaner & LLM Processor moves nested response fields such as body.id to top-level columns, casts and filters the rows of this run's Dataset.

Each tool runs separately with its own input and pricing. Your workflow must select successful results, map the next input and start the next run.

Batch example

{
"requests": [
{
"lookupKey": "customer-1",
"method": "GET",
"url": "https://api.example.com/customers/1"
},
{
"lookupKey": "customer-2",
"method": "POST",
"url": "https://api.example.com/customers",
"headers": { "Content-Type": "application/json" },
"body": { "name": "Ada" }
}
],
"secretHeaders": {
"Authorization": "Bearer YOUR_TOKEN"
},
"maxConcurrency": 20,
"maxRetries": 2,
"maxRequests": 100
}

Call an API for every row of a Dataset

If the rows are already in an Apify Dataset, for example a scraper's output, you do not need to build the requests list yourself. Set datasetId and describe one call in requestTemplate. The Actor sends one request per row and writes one result row for each.

{
"datasetId": "YOUR_DATASET_ID",
"requestTemplate": {
"method": "POST",
"url": "https://api.example.com/companies/{{ domain }}/enrich",
"headers": { "Content-Type": "application/json" },
"body": { "name": "{{ name }}", "employees": "{{ size.employees }}" }
},
"secretHeaders": { "Authorization": "Bearer YOUR_TOKEN" },
"lookupKeyField": "domain",
"includeFields": ["name", "domain"],
"maxRequests": 500
}

How the template is filled:

  • {{ field }} takes a value from the row. Dot paths reach nested fields and list items: {{ user.id }}, {{ items.0.sku }}. Spaces inside the braces are optional, but keep them in Apify integrations (see below).
  • Placeholders work in the URL path, query values, header values and the body. The scheme and host stay literal, so the data can never choose which server is called. A path value is encoded as one path segment.
  • A body value that is exactly one placeholder keeps the row's JSON type: a number stays a number, an object stays an object. Inside a longer string it becomes text.
  • lookupKeyField names the row field that becomes lookupKey; by default it is the row's position in the Dataset. Every result also has datasetRowIndex, and includeFields copies up to 20 row fields into sourceFields, so you can join responses back to your data.
  • secretHeaders apply to every request, and rows cannot supply or override them. Put credentials there, not in the template.

A row whose placeholder field is missing or null gets an uncharged INVALID_INPUT row that names the field, and the other rows still run. Billing is the same as for requests: $0.004 per answered response.

Large Datasets. One run reads up to maxRequests rows (default 100, at most 1,000) from datasetOffset. When rows remain, RUN_SUMMARY.nextDatasetOffset and nextAction tell you where the next run starts. An empty range finishes with status NO_ROWS and no charge.

After a scraper finishes (Actor-to-Actor integration). On the scraper or its Task, open Integrations, connect this Actor, and use input like:

{
"datasetId": "{{resource.defaultDatasetId}}",
"requestTemplate": { "url": "https://api.example.com/lookup?domain={{ domain }}" },
"lookupKeyField": "url",
"maxRequests": 1000
}

Write the row placeholders with spaces, {{ domain }}. The integration fills its own variables such as {{resource.defaultDatasetId}} and replaces any other name written without spaces with null, so {{domain}} would never reach this Actor. If that happens, the run stops before sending anything and says to keep the spaces. Put the Dataset ID in datasetId itself: an ID that appears only inside a webhook payload is not read, and the run stops with a message saying so.

From n8n. n8n evaluates every {{ }} in an expression field, so keep the template out of it. Save a Task of this Actor with requestTemplate, then run that Task from n8n with only the Dataset ID mapped:

{ "datasetId": "{{ $json.defaultDatasetId }}" }

Fields you leave out keep the Task's saved values.

Not supported yet: CSV, Google Sheets or file URLs as input, several rows in one request, and writing responses back into the source Dataset.

n8n / Make / API usage

  • n8n / Make: call the Apify Actor node / HTTP module with the JSON input above.
  • API: POST https://api.apify.com/v2/acts/automa-flow~http-request-batch/runs with the same input. Read the default Dataset and RUN_SUMMARY when the run succeeds; read CALLBACK_REPORT when callbacks are enabled.

Schedule recurring batches whenever your automation has a new set of rows/events. To push results into n8n or Make as soon as a run finishes, add a run webhook (POST https://api.apify.com/v2/webhooks):

{
"eventTypes": ["ACTOR.RUN.SUCCEEDED", "ACTOR.RUN.FAILED"],
"condition": { "actorId": "<this Actor's ID>" },
"requestUrl": "https://your-n8n-or-make-webhook.example.com/hook",
"payloadTemplate": "{\"runId\": {{resource.id}}, \"datasetId\": {{resource.defaultDatasetId}}, \"status\": {{resource.status}}}"
}

Then read RUN_SUMMARY (status, billedRequests, nextAction) and the Dataset by datasetId in the receiving workflow.

Auth / secrets

Put credentials in secretHeaders (and callbackSecretHeaders for callbacks). These fields are marked isSecret: true so Apify encrypts them at rest.

  • Secret values are never written to Dataset rows or logs.
  • Redirects never forward secretHeaders, Authorization, cookies or API-key headers to a different origin. Reflected secret values are redacted from body and response-header output.
  • Prefer headers over query-string secrets (token, api_key, …). Every query value is redacted in logs, Dataset URLs and callback payload URLs.
  • Per-request distinct secrets are not supported in this version - use shared run-level secretHeaders.

Output example

The results table includes body and bodyText beside the status and HTTP code. JSON responses are in body. Text responses, and JSON that cannot be parsed, are in bodyText. Open a row when you need headers or attempt detail.

{
"inputIndex": 0,
"lookupKey": "demo-post-1",
"method": "GET",
"url": "https://jsonplaceholder.typicode.com/posts/1",
"status": "SUCCESS",
"httpStatus": 200,
"ok": true,
"contentType": "application/json; charset=utf-8",
"responseHeaders": { "content-type": "application/json; charset=utf-8" },
"body": { "userId": 1, "id": 1, "title": "…", "body": "…" },
"bodyText": null,
"truncated": false,
"bytesReceived": 292,
"attempts": 1,
"durationMs": 180,
"errorType": null,
"errorMessage": null,
"retried": false,
"billable": true,
"callbackStatus": "SKIPPED",
"callbackError": null,
"source": "http-request-batch",
"scrapedAt": "2026-09-17T00:00:00Z",
"schemaVersion": 1
}

Retries / failure semantics

StatusMeaning
SUCCESSTarget returned an HTTP response. ok is true for 2xx. With treatNon2xxAsError=false (default), non-2xx stays SUCCESS with ok=false.
INVALID_INPUTThis request item, or the request built from this Dataset row, was invalid. Other valid items still run.
HTTP_ERRORNon-2xx response when treatNon2xxAsError=true.
TRANSPORT_ERRORNetwork/protocol failure - never presented as an empty success.
TIMEOUTAttempt timed out.
RESPONSE_TOO_LARGEStreaming byte cap hit (default 256 KiB, max 1 MiB).
SECURITY_BLOCKEDURL failed SSRF checks (private/loopback/metadata/credentials/…).
CANCELLEDNot sent or unpaid: beyond maxRequests (errorType=MAX_REQUESTS_EXCEEDED), or stopped by maxTotalChargeUsd / billing. Response body is cleared.

Retries (not billed): connection resets, timeouts, HTTP 408/429/500/502/503/504 - GET/HEAD/OPTIONS only by default. Set retryNonIdempotent=true to retry POST/PUT/PATCH/DELETE. Ordinary 4xx such as 400/401/403/404 are never retried. Redirects are revalidated at every hop (max 5). HTTPS-to-HTTP downgrades and cross-origin redirects that would preserve a request body are blocked. POST may become GET on 301/302; 303 becomes GET except for HEAD; same-origin 301/302 methods and 307/308 preserve method and body. Dataset method reports the method of the final attempt.

One bad request never kills the batch: N inputs → N Dataset rows. A batch longer than maxRequests sends the first maxRequests items; the rest get uncharged CANCELLED rows, so you can resend them by inputIndex or lookupKey.

A run fails only when no request could be answered (every request timed out or hit a network error), the input or budget is invalid, or an unexpected error stops it. For a failure you cannot fix from your input, the run's status message links to this Actor's Issues tab. Paste the message and the run ID there so the failure can be traced.

timeoutSecs covers one complete attempt, including DNS, redirects and body streaming. Retries each receive a fresh deadline. Retry waits are at most 30 seconds: if Retry-After requests a longer wait, the final response is returned without retrying early. DNS failures are retryable transport errors, distinct from private-address security blocks.

Both compressed wire bytes and decoded bytes are capped by maxResponseBytes. Gzip, deflate and identity are supported. Other encodings, concatenated gzip members or invalid compressed data return an uncharged TRANSPORT_ERROR with errorType=RESPONSE_DECODING_ERROR. Invalid JSON, JSON deeper than 64 levels and unsupported numbers are preserved in bodyText. Headers must use valid HTTP names and ASCII values without control characters. Spaces and line breaks around a header value are trimmed. Methods are case-insensitive (post works like POST).

Unexpected system or storage failures can prevent all rows from being saved. The failure summary preserves confirmed progress and billing details; unresolvedRequests identifies unfinished items and progressKnown=false flags uncertain settlement. Inspect the Dataset and charges before replaying requests with side effects.

The primary Dataset row is atomically stored and charged before any callback is sent. Callback retries within a run reuse an Idempotency-Key. A callback-enabled Dataset row has callbackStatus=PENDING; final per-item delivery status is in CALLBACK_REPORT. Callback failure keeps the primary result and makes RUN_SUMMARY.status PARTIAL.

Pricing

Pay-per-event:

EventPrice
apify-actor-start$0.00005 / run (Apify's default synthetic start; one event per GB of run memory)
http-request$0.004 / billable HTTP response ($4 / 1,000)

Billable means the target server produced a real HTTP response (including 4xx/5xx and RESPONSE_TOO_LARGE). Not billed: SSRF blocks, transport failures, timeouts, retries, redirects, callbacks, validation. Platform usage (compute, storage, transfer) is included in the event price; there is no separate usage charge.

Examples (total charge):

  • 1 request → $0.00005 + $0.004 = $0.00405
  • 100 requests → $0.00005 + $0.40 = $0.40005
  • 1,000 requests → $0.00005 + $4.00 = $4.00005

Minimum maxTotalChargeUsd per run: $0.005 (one start plus one response).

Limits

For POST/PUT/PATCH/DELETE use one of body (JSON object), bodyArray (JSON array), or bodyText (raw text). They are mutually exclusive.

LimitDefaultHard max
Requests / run (maxRequests)1001,000
Dataset rows / run (maxRequests from datasetOffset)1001,000
includeFields per rownone20 fields, 64 KB
Concurrency2050
Timeout30 s120 s
Retries25
Response size256 KiB1 MiB

Security / SSRF policy

Only http / https URLs. Rejected:

  • file:, ftp:, data:, javascript:, malformed URLs
  • embedded credentials in the URL
  • loopback, RFC1918 private, link-local, multicast/reserved IPv4
  • IPv6 loopback / link-local / unique-local
  • cloud metadata (169.254.169.254, metadata hostnames)
  • hostnames that resolve to blocked addresses (DNS rebinding)
  • redirect hops to blocked destinations

Callback URLs use the same policy. Use only APIs and data you are authorized to access; do not use this Actor to bypass authentication, paywalls or access controls. API responses may contain personal, regulated or confidential data and are retained in the Apify Dataset until you delete them. Minimize requested data, configure Dataset retention for your obligations, and remember that callbackUrl discloses the full stored response row to that destination.

Server Set-Cookie values never establish a session for later requests, redirects or callbacks. An explicit Cookie in secretHeaders is still sent to each requested target, and is stripped on a cross-origin redirect. Supported secret header values and Bearer, Basic and cookie credentials are redacted when reflected verbatim. Encoded or transformed echoes cannot always be recognized; request only the response data you need.

FAQ

Does this include Apify Proxy?
Not in this version. Plain outbound HTTPS only.

Are retries charged?
No.

What about binary responses?
Metadata is returned; arbitrary binary is not inlined into the Dataset by default.

Can I use distinct secrets per request?
Not in this version - use shared secretHeaders for the run.

Example workflows

  1. Call a REST API from Clay - single GET + lookupKey
  2. POST JSON to a webhook/API - demo POST; secrets via secretHeaders
  3. Send 100 API requests in one batch - concurrency + per-row Dataset identity