Skip to navigation

Apify

Run Apify actors and tasks through one gateway route, as tracked runs priced from their reported cost
View as Markdown

Fronts actor and task execution for Apify on https://api.apify.com. Start a run, wait for it or poll it, and read the dataset it produced. Whatever follows /apify/ is relayed to Apify, provided it names one of the admitted actions:

GET|POST /apify/<api path> → https://api.apify.com/<api path>

Admission is default-deny against a fixed allowlist: only the actions listed below get through, and every other path draws a 404. Mounted methods: GET, POST.

Supported scope

PathPurpose
POST /apify/v2/acts/{actorId}/runsStart an actor run
POST /apify/v2/actor-tasks/{actorTaskId}/runsStart a saved task’s run
GET /apify/v2/actor-runs/{runId}Read a run’s status and usage
GET /apify/v2/datasets/{datasetId}/itemsRead the items a run produced
POST /apify/v2/actor-runs/{runId}/abortStop a run

Give {actorId} / {actorTaskId} in tilde form as one path segment (e.g. apify~web-scraper); the slash-separated username/name form is not accepted.

Add ?waitForFinish=60 to the create and a run that finishes within 60 seconds comes back finished, in the same response, so most short jobs take one call to start and one to read the dataset. A longer run comes back still running: poll GET /apify/v2/actor-runs/{runId}?waitForFinish=60 until its status is terminal. Page the dataset with limit and offset, and trim each item with fields.

The synchronous run-sync and run-sync-get-dataset-items actions, for both actors and saved tasks, were retired on 2026-09-25 and now answer 404. Their responses carried no cost, so they could only be billed a flat rate that fell short of what most runs cost. Use POST .../runs?waitForFinish=60 and read the run’s dataset instead.

Credential handling

Apify will take its token through two channels — a token query parameter or an Authorization: Bearer header — and the gateway closes off both:

  • Authorization is overwritten with Apify’s secret token (Apify reads the very header that carries your gateway token). The gateway consumes your token itself; Apify never receives it.
  • A token supplied by the client in the query is discarded (any encoding), so nothing can displace the injected token.

Ownership

A run you start asynchronously — and the default dataset it writes to — is registered to your account before the create response reaches you. That registration is what makes the run id and dataset id usable: polling, aborting, and reading dataset items all check it first.

An id belonging to another account answers exactly what an id that never existed answers: a 404, with no upstream call made on its behalf. There’s no separate “forbidden” status, so nothing about anyone else’s runs is disclosed — including whether they exist.

A run of your own that Apify has since expired also ends in a 404, but by a different route: the read is authorized, so it reaches Apify, and what comes back is Apify’s own 404. Handle it the same way — the run is gone — without expecting it to look identical to the ownership refusal above.

The account-wide GET /v2/actor-runs and GET /v2/datasets listings carry no id to check, so they aren’t mounted. Enumerate from your own side: keep the id and defaultDatasetId each create hands back.

If a create succeeds upstream but the gateway can’t record the run as yours, it answers 500 and discards the run rather than returning an id nothing tracks — nothing is charged for it. Retry the request.

Billing

Every run reports its own cost, and it’s billed from that report:

  • Starting a run carries no flat charge of its own, and polling and reading the dataset cost nothing.
  • Once a run reaches a terminal state — SUCCEEDED, FAILED, TIMED-OUT, or ABORTED — it is billed once, priced from its own usageTotalUsd as Apify reports it. That figure already sums the run’s compute, proxy traffic, storage operations, dataset writes and the actor’s own per-result charges.
  • The figure is read about 15 seconds after the run finishes. Apify’s first read of a finished run can still carry preliminary costs, so the gateway reads the run again once they’ve settled. You don’t have to poll for this to happen.
  • The charge is taken once per run, no matter how many times you poll it, and an abort followed by a poll still charges once.
  • A terminal run that genuinely cost nothing is charged nothing.
  • The charge lands on the account that created the run — never on whoever happens to poll it.

usageTotalUsd is a running total while a run is live, which is why nothing is billed until the run is finished: charging mid-run would bill for compute the run hasn’t spent yet.

Examples

curl -X POST "$GATEWAY/apify/v2/acts/apify~website-content-crawler/runs?waitForFinish=60" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"startUrls": [{"url": "https://example.com"}], "maxCrawlPages": 20}'

A create answers with the run object. Keep both ids from it:

{
"data": {
"id": "HG7ML7M8z78YcAPEB",
"actId": "HDSasDasz78YcAPEB",
"status": "READY",
"startedAt": "2026-08-08T10:14:02.123Z",
"finishedAt": null,
"defaultDatasetId": "9RaXfDsvMLpvKrYnJ",
"usageTotalUsd": 0
}
}

data.id is what you poll and abort; data.defaultDatasetId is where the run’s items land. Polling returns the same shape with an updated status, finishedAt, and usageTotalUsd.

Responses arrive as Apify’s JSON, untouched. Field-level request and response detail lives in Apify’s API reference; the gateway modifies neither.