# Dataset to REST API (Sync Rows to Any API) (`nerolabs/dataset-to-rest-api`) Actor

Sends every row of an Apify dataset, file or Google Sheet URL, or JSON array to any REST API as its own request. Templated URL, body and headers with {{field}} placeholders, auth presets, rate limits, retries. One result row per request. Charged per answered request. Agent-ready via x402 and MCP.

- **URL**: https://apify.com/nerolabs/dataset-to-rest-api.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** Developer tools, Integrations, Agents
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Dataset to REST API (Sync Rows to Any API)

You have the rows. Your CRM, your app, your warehouse or your client's system wants them, one record at a time, over its own API. Right now that means writing a script: loop the rows, build the JSON, add the auth header, handle the rate limit, retry the 429s, and keep a note of which rows the API actually accepted.

This Actor is that script, as a form you fill in.

Point it at an Apify dataset, a CSV or Excel file, a Google Sheet or a JSON array. Write your endpoint once, with `{{placeholders}}` where the row's values go. Every row is sent as its own request, and you get one result row back per request telling you exactly what the API said.

### What it does

- **Reads from anywhere**: an Apify dataset (any scraper's output), a public CSV, TSV, Excel, JSON or JSON Lines file by URL, a Google Sheet link, or an inline JSON array.
- **Templates every part of the request**: `{{field}}` works in the URL path, the query parameters, the headers and the body. Nested fields work too (`{{address.city}}`), and values going into the URL are encoded for you.
- **Keeps your data types**: a body template value of exactly `{{mrr}}` sends the number `49`, not the string `"49"`. That matters to APIs that validate their input.
- **Handles auth for you**: Bearer token, Basic, an API key in a header, or an API key in the query string. The secret is stored encrypted by Apify, never logged, and never echoed back into the output.
- **Respects the API**: requests are spaced to your requests-per-second setting, retried with doubling backoff on 429, 408 and 5xx, and a `Retry-After` header from the API is obeyed. A 400 or 401 is a real answer, so it is never retried.
- **Tells you what happened, row by row**: status, HTTP code, attempts, how long it took, the start of the response body, the parsed JSON, and any of your own row fields you asked to carry across so results join back to your data.
- **Dry run first**: see the exact requests, resolved, before a single one is sent. Nothing is sent and nothing is charged.

### Example

Input rows (from a dataset, a sheet, or pasted JSON):

| id | email | name | mrr |
|---|---|---|---|
| 1001 | ana@example.com | Ana Silva | 49 |
| 1002 | ben@example.com | Ben Okafor | 19 |

Settings:

- **Endpoint URL**: `https://api.example.com/contacts/{{id}}`
- **HTTP method**: `PUT`
- **Request body**: use a body template
- **Body template**: `{"fields": {"Email": "{{email}}", "Name": "{{name}}", "Value": "{{mrr}}"}}`
- **Authentication**: Bearer token, secret pasted in

Row 1001 goes out as `PUT https://api.example.com/contacts/1001` with an `Authorization: Bearer ...` header and this body:

```json
{ "fields": { "Email": "ana@example.com", "Name": "Ana Silva", "Value": 49 } }
```

And you get back:

| rowIndex | email | status | httpStatus | attempts | responseSnippet |
|---|---|---|---|---|---|
| 0 | ana@example.com | success | 200 | 1 | `{"id":"rec123","created":true}` |
| 1 | ben@example.com | failed | 422 | 1 | `{"error":"Value must be a number"}` |

One glance tells you which rows landed and which need fixing.

### What it is good for

- Push a scraper's results straight into your CRM, Airtable, Notion, HubSpot, Baserow or Supabase REST API on a schedule.
- Fire one webhook per row into Zapier, Make, n8n or your own app.
- Enrich rows through a third-party API and keep the response next to the row that produced it.
- Bulk-create or bulk-update records in an internal tool that has an API but no import screen.
- Give an AI agent one call that sends a whole dataset to an API, instead of a loop it has to write and pay for.

### Pricing

Pay per event, no subscription.

| Event | Price |
|---|---|
| Request | $0.005 per request that got a reply |
| Webhook delivery | $0.02, only when your endpoint confirms receipt |
| Actor start | $0.00005 per run |

Store discounts apply automatically: Bronze 10% off, Silver 20% off, Gold 30% off every event.

**What that costs in practice.** A nightly sync of 200 new rows is $1.00 a night, about $30 a month. A one-off import of 1,000 rows is $5.00. Sending 50 rows an hour, around the clock, is about $180 a month. If your API accepts arrays, raising **Rows per request** to 50 turns that same 1,000-row import into 20 requests, or 10 cents.

**What is not charged.** A dry run costs nothing but the run start. A request that never got a reply at all (a timeout, a DNS failure, a refused connection) costs nothing, because nothing was delivered. Retries of the same row are one request, not one per attempt. Rows skipped because an earlier one failed, or because a placeholder was empty, cost nothing. A failed webhook delivery costs nothing.

A request that came back 400, 401 or 500 **is** charged, because your API was reached and it answered. That answer is usually the most useful thing in the run.

### Notes worth knowing before your first real run

- **Start with Dry run on.** It shows the first three requests exactly as they would go out, including the resolved URL and body, with header names only so your token is never printed. When it looks right, turn it off.
- **Misspelt placeholders are caught up front.** If no row has a field you used in a placeholder, the run warns you by name rather than quietly sending blanks.
- **Rows per request is the cost lever.** At 1, each row is its own request, which is what most APIs want. If yours accepts an array of records, raising it packs that many rows into one charged request. Use the **Batch wrapper field** if the API expects `{"records": [...]}` rather than a bare array.
- **Requests per second defaults to 5.** Lower it if the API pushes back. Retries and `Retry-After` are handled either way.
- **Order is preserved and results are joinable.** Use **Row fields to copy into the results** to carry an id or an email onto its result row.

### FAQ

**What happens to rows the API rejects?**
Nothing stops, by default. Every row is attempted and the rejects come back as `failed` with the API's own status code and response body, so you can fix and re-run just those. Turn on **Stop on first error** if you would rather the run halt at the first problem, for example on a careful first run against a live system.

**Can it send to an API that needs a signed or custom header?**
Yes. Anything you put in **Headers (JSON)** is sent as-is and supports `{{placeholders}}`, so a per-row token or signature that already exists in your data works. That field is stored encrypted. What it cannot do is compute a signature (an HMAC, for example) at send time.

**Is my API key safe?**
It is stored encrypted by Apify, sent only to the URL you set, never written to the run log, and never written into the output. A dry run deliberately reports header names only, not values.

**Will it duplicate records if I run it twice?**
That depends on your API, not on this Actor. Use `PUT` or `PATCH` against a per-row URL such as `/contacts/{{id}}` if your API treats that as an upsert. This Actor sends what you tell it to send, once per row, per run.

**Can I stop response bodies being stored?**
Yes. Set **Response characters to keep** to 0. Nothing from the response body is written to the dataset, which is the right setting when responses carry personal data.

**Does it work with GET or DELETE?**
Yes, and no body is sent for those. Placeholders in the URL and query still work, so `GET https://api.example.com/lookup?email={{email}}` sends one lookup per row and stores each answer.

**How large an input can it handle?**
Up to 100,000 rows per run, and up to 100 MB for a file URL. Use **Maximum input rows** as a cost guard while testing.

If this saved you writing and babysitting a sync script, a review on the Store page genuinely helps.

### The rest of the toolkit

- [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter): dedupe (exact, normalized or fuzzy), flatten nested JSON, clean emails, phones and URLs, then export CSV or Excel.
- [Dataset Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform): keep the rows you want and reshape the fields (dates, replace, split, hash, 25 ops), sort, dedupe, limit.
- [Dataset Join & Merge](https://apify.com/nerolabs/dataset-join-merge): VLOOKUP-style joins and unions across two datasets, files or Google Sheets on a key field.
- [Dataset Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot): counts, sums, averages and pivot tables per group.
- [Dataset Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector): what was added, removed or changed since last time.
- [Dataset AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich): add LLM-generated columns (classify, extract, summarise) to every row, no API key needed.
- [Dataset Charts & Report](https://apify.com/nerolabs/dataset-charts-report): chart images (PNG, SVG) and a PDF or HTML report from any data.
- [Dataset to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database): write the rows straight into a database table, creating it if needed.
- **Dataset to REST API** (this one): send every row to any API as its own request, with templating and auth presets.
- [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner): chain several of these together in one run, each step fed the previous step's dataset.

A common pipeline: a scraper, then Cleaner, then Filter & Transform, then Join to enrich from a sheet, then Aggregate for the weekly summary, with Diff watching what changed and Charts & Report turning the numbers into the Monday PDF. Pipeline Runner runs that whole chain in one call.

### For AI agents

Pay per event, agent-payable through x402 and MCP, limited permissions, no standby. Inputs: `datasetId`, `fileUrl` or `data` for the rows; `url`, `method`, `bodyMode`, `bodyTemplate`, `queryParams` and `headers` for the request, all accepting `{{field}}` placeholders resolved per row; `authType` plus `authSecret` for Bearer, Basic or API-key auth; `batchSize`, `rateLimitPerSecond`, `retries` and `stopOnFirstError` for pacing. Set `dryRun` to true to get the resolved requests back without sending them. Returns one dataset item per request with `status`, `httpStatus`, `attempts`, `responseSnippet`, `responseJson` and `error`, plus a `SYNC_SUMMARY` key-value record. Charged per request that received an HTTP response, including non-2xx; requests that got no response at all are not charged.

# Actor input Schema

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

Pick an existing Apify dataset (for example the output of any scraper run). Use this OR 'File URL' OR 'Data (inline)' below. Declaring it this way is what lets this Actor run with limited permissions: it may read the dataset you point at, and nothing else on your account.

## `fileUrl` (type: `string`):

Instead of a dataset, download the rows from a public link: a CSV or TSV file, an Excel .xlsx file (first sheet, header row), a JSON array or JSON Lines file, or a Google Sheet (paste the normal sheet link, sharing set to 'Anyone with the link can view'). The format is detected automatically. Up to 100 MB per run. The download is never charged; only requests that get a reply are.

## `data` (type: `array`):

A JSON array of records to send, for ad-hoc data instead of a dataset ID or file URL.

## `fileFormat` (type: `string`):

Only needed if automatic detection gets the file URL's format wrong.

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

The endpoint each row is sent to, for example https://api.example.com/contacts. Put a placeholder in the path to hit a per-row URL: https://api.example.com/contacts/{{id}}. Values in the URL are URL-encoded automatically.

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

POST to create, PUT or PATCH to update, GET or DELETE for requests with no body.

## `bodyMode` (type: `string`):

'Send the whole row' posts the row exactly as it is, which is what most APIs that accept JSON want. 'Use a body template' sends only the shape you define below, so you can rename and nest fields to match the API. 'No body' sends nothing (automatic for GET and DELETE).

## `bodyTemplate` (type: `object`):

Used when 'Request body' is set to the template option. Any JSON shape, with {{field}} placeholders. A value that is exactly one placeholder keeps the row's real type, so {"mrr": "{{mrr}}"} sends the number 49, not the text "49". Nesting and arrays are supported.

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

Optional. Added to the URL as ?key=value, with {{field}} placeholders supported and values URL-encoded for you. Anything already in the URL is kept.

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

Optional extra headers, for example {"X-Source": "apify"}. Content-Type is set to application/json automatically when a body is sent. {{field}} placeholders work here too. Use the Authentication section below for tokens rather than writing an Authorization header by hand, so the secret is stored encrypted.

## `authType` (type: `string`):

'Bearer token' sends Authorization: Bearer <secret>, which covers most modern APIs (Airtable, Notion, HubSpot, Stripe). 'Basic' sends a base64 user:password. 'API key header' sends the secret in a header you name. 'API key in query' puts it in the query string instead.

## `authSecret` (type: `string`):

The bearer token, API key, or the password for Basic auth. Stored encrypted by Apify.

## `authUsername` (type: `string`):

Only used when Authentication is set to Basic.

## `authName` (type: `string`):

The name the API expects for the key, for example X-API-Key, apikey or api\_key. Defaults to X-API-Key for a header and api\_key for the query string.

## `batchSize` (type: `integer`):

1 sends one request per row, which is what most APIs expect. A higher number packs that many rows into one request as a JSON array, for endpoints that accept bulk writes. You are charged per request, so batching is also how you cut the cost of a large sync.

## `batchWrapperField` (type: `string`):

Only used when 'Rows per request' is above 1. Leave empty to send a bare JSON array. Set it to wrap the array in a field the API expects, for example 'records' produces {"records": \[...]}.

## `rateLimitPerSecond` (type: `integer`):

Requests are spaced out so the endpoint is never hit faster than this. Lower it if the API rate limits you (many allow 2 to 10 per second). 0 removes the spacing entirely.

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

Extra attempts after the first one, for HTTP 429, 408, 5xx and connection failures only. A 4xx answer such as 400 or 401 is a real answer from the API and is never retried. Backoff doubles each time, and a Retry-After header from the API is obeyed.

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

How long to wait for each response before giving up on that attempt.

## `stopOnFirstError` (type: `boolean`):

On: the run stops as soon as one request fails, and the remaining rows are reported as skipped. Useful for a first careful run against a live API. Off: every row is attempted and the failures are listed in the output.

## `skipRowsMissingFields` (type: `boolean`):

On: a row with an empty value for any {{field}} used in the URL, query, headers or body template is not sent, and is reported as skipped with the field named. Off: it is sent with that placeholder left blank.

## `includeFields` (type: `array`):

Field names copied from each source row into its result row, so results can be joined back to the data you sent (for example 'email' or 'id'). If a name clashes with an output field such as 'url' or 'status', the output field wins.

## `responseSnippetChars` (type: `integer`):

How much of each response body to store in the result row. 0 stores none, which is the right setting when responses contain personal data you do not want copied into a dataset. JSON responses within this budget are also parsed into a 'responseJson' field.

## `dryRun` (type: `boolean`):

On: nothing is sent and nothing is charged. The output shows the first three requests exactly as they would go out, including the resolved URL and body, with header names only so a secret is never echoed back. Turn off to actually send.

## `maxItems` (type: `integer`):

Stop loading after this many rows, a cost guard for large inputs. There is a hard safety ceiling of 100,000 rows per run regardless.

## `webhookUrl` (type: `string`):

Optional. If set, the run summary (how many requests were sent, how many succeeded, the status codes; never the rows or the response bodies) is POSTed here as JSON the moment the sync finishes, so a scheduled job can tell Slack, Zapier, Make, n8n or your own API that the sync ran. Only charged when the endpoint confirms receipt (HTTP 2xx); a failed delivery is reported as a warning and costs nothing.

## Actor input object example

```json
{
  "data": [
    {
      "id": "1001",
      "email": "ana@example.com",
      "name": "Ana Silva",
      "company": "Silva Ltd",
      "plan": "Pro",
      "mrr": 49
    },
    {
      "id": "1002",
      "email": "ben@example.com",
      "name": "Ben Okafor",
      "company": "Okafor & Co",
      "plan": "Starter",
      "mrr": 19
    },
    {
      "id": "1003",
      "email": "cara@example.com",
      "name": "Cara Lind",
      "company": "Lind Studio",
      "plan": "Free",
      "mrr": 0
    }
  ],
  "fileFormat": "auto",
  "url": "https://httpbin.org/post",
  "method": "POST",
  "bodyMode": "row",
  "bodyTemplate": {
    "properties": {
      "Email": "{{email}}",
      "Name": "{{name}}",
      "Monthly value": "{{mrr}}"
    },
    "source": "apify"
  },
  "authType": "none",
  "batchSize": 1,
  "rateLimitPerSecond": 5,
  "retries": 3,
  "timeoutSecs": 60,
  "stopOnFirstError": false,
  "skipRowsMissingFields": false,
  "includeFields": [
    "email"
  ],
  "responseSnippetChars": 500,
  "dryRun": true
}
```

# Actor output Schema

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

One row per request: status, HTTP status code, attempts, response snippet, error, and any row fields you asked to copy across.

## `syncSummary` (type: `string`):

Requests sent, succeeded, failed and skipped, the status codes returned, and any warnings from this run.

# 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 = {
    "data": [
        {
            "id": "1001",
            "email": "ana@example.com",
            "name": "Ana Silva",
            "company": "Silva Ltd",
            "plan": "Pro",
            "mrr": 49
        },
        {
            "id": "1002",
            "email": "ben@example.com",
            "name": "Ben Okafor",
            "company": "Okafor & Co",
            "plan": "Starter",
            "mrr": 19
        },
        {
            "id": "1003",
            "email": "cara@example.com",
            "name": "Cara Lind",
            "company": "Lind Studio",
            "plan": "Free",
            "mrr": 0
        }
    ],
    "url": "https://httpbin.org/post",
    "bodyTemplate": {
        "properties": {
            "Email": "{{email}}",
            "Name": "{{name}}",
            "Monthly value": "{{mrr}}"
        },
        "source": "apify"
    },
    "includeFields": [
        "email"
    ],
    "dryRun": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/dataset-to-rest-api").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 = {
    "data": [
        {
            "id": "1001",
            "email": "ana@example.com",
            "name": "Ana Silva",
            "company": "Silva Ltd",
            "plan": "Pro",
            "mrr": 49,
        },
        {
            "id": "1002",
            "email": "ben@example.com",
            "name": "Ben Okafor",
            "company": "Okafor & Co",
            "plan": "Starter",
            "mrr": 19,
        },
        {
            "id": "1003",
            "email": "cara@example.com",
            "name": "Cara Lind",
            "company": "Lind Studio",
            "plan": "Free",
            "mrr": 0,
        },
    ],
    "url": "https://httpbin.org/post",
    "bodyTemplate": {
        "properties": {
            "Email": "{{email}}",
            "Name": "{{name}}",
            "Monthly value": "{{mrr}}",
        },
        "source": "apify",
    },
    "includeFields": ["email"],
    "dryRun": True,
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/dataset-to-rest-api").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 '{
  "data": [
    {
      "id": "1001",
      "email": "ana@example.com",
      "name": "Ana Silva",
      "company": "Silva Ltd",
      "plan": "Pro",
      "mrr": 49
    },
    {
      "id": "1002",
      "email": "ben@example.com",
      "name": "Ben Okafor",
      "company": "Okafor & Co",
      "plan": "Starter",
      "mrr": 19
    },
    {
      "id": "1003",
      "email": "cara@example.com",
      "name": "Cara Lind",
      "company": "Lind Studio",
      "plan": "Free",
      "mrr": 0
    }
  ],
  "url": "https://httpbin.org/post",
  "bodyTemplate": {
    "properties": {
      "Email": "{{email}}",
      "Name": "{{name}}",
      "Monthly value": "{{mrr}}"
    },
    "source": "apify"
  },
  "includeFields": [
    "email"
  ],
  "dryRun": true
}' |
apify call nerolabs/dataset-to-rest-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nerolabs/dataset-to-rest-api"
        }
    }
}

```

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/5a4D1jjIN6tbAY0ag/builds/P0vYKAJ1zAICv7Rhz/openapi.json
