Skip to main content
A webhook is a URL of yours that we call when an event happens, so you don’t have to poll. Every call is signed with HMAC-SHA256.

Events

Subscribe with job.* to get every job event. job.created, job.started and job.progress are accepted when you subscribe, but are not sent today.

Add a webhook

For every job of your organization: create it once. The answer includes signing_secret in full, this one time; afterwards only its first 8 characters are shown.

Per-job webhooks

Add webhooks to a job when only that job should call a URL. Each one gets its own signing_secret, returned in full in the create answer under ad_hoc_webhooks, and masked afterwards.
The SDK takes Apify’s event names and translates them: ACTOR.RUN.SUCCEEDED is job.completed, ACTOR.RUN.FAILED is job.failed, ACTOR.RUN.TIMED_OUT is job.timed_out and ACTOR.RUN.ABORTED is job.cancelled. A webhook URL must use HTTPS, reach a public address (private and internal addresses are refused), be at most 2,048 characters, and not end in .php, .asp, .aspx, .jsp or .cgi.

What we send

A POST with a JSON body and these headers: A real job.completed delivery from a test run on a local stack, headers and body. usageTotalUsd is a number; its value is shown as "…" because the docs leave amounts out.
The job events’ body is Apify-compatible: account.low_balance sends organization_id, balance, threshold, currency (USD), level (low, or zero when the balance is used up) and timestamp. Each level is sent at most once a day.

Verify the signature

Compute the HMAC-SHA256 of the raw request body, before any JSON parsing, with your signing secret, and compare it with the header in constant time. Parsing and re-serialising the body changes the bytes and breaks the check.
The SDK has the same check:
In a web app, read the raw body and the header, then reject what fails:

Answer fast, retry safely

  • Answer with any 2xx within 10 seconds; the connection must open within 5. Do the work afterwards, for example from a queue.
  • Redirects are not followed: a 3xx counts as a failure.
  • A failed delivery is retried up to 5 times, after 2, 4, 8, 16 and 32 minutes. A TLS certificate error is not retried.
  • After 5 events in a row fail every retry, the webhook is turned off: is_active becomes false and disabled_reason says why. Turn it back on with PATCH /api/v1/webhooks/{id}/ and {"is_active": true}.
  • The same event can arrive twice. Use X-Delivery-ID, or the job id and event, to process it once.

Test and inspect

The test sends the event test with the body {"event": "test", "timestamp": ..., "data": {"message": ...}}, signed like any other.
Each delivery shows its event_type, status (pending, delivered or failed), response_status_code and attempts. POST /api/v1/webhooks/deliveries/{id}/retry/ sends one again.