Agent skill

Apify Webhooks Skill

Receive and authenticate Apify webhooks. Use when setting up an Apify webhook endpoint, adding a secret header with the Headers template, debugging 401s, or handling Actor run and build events like ACTOR.RUN.SUCCEEDED, ACTOR.RUN.FAILED and ACTOR.BUILD.SUCCEEDED.

Install this skill

npx skills add hookdeck/webhook-skills --skill apify-webhooks


Apify (a web scraping and automation platform) sends a JSON webhook when an Actor run or build changes state: a run succeeds, fails, times out or is aborted, a build finishes. Webhooks are not signed. To authenticate them, add a secret header with the webhook's Headers template (for example {"X-Webhook-Secret": "..."}) and have your endpoint compare that header with the value you stored.

Apify's docs suggest putting a secret token in the webhook URL. This skill uses a header instead: it keeps the secret out of URLs and access logs, and it is what Hookdeck's Apify source checks. X-Apify-Webhook and X-Apify-Webhook-Dispatch-Id are system headers Apify always sets, not secrets.

When to Use This Skill

  • How do I receive Apify webhooks?
  • How do I authenticate an Apify webhook? (No signature: use a secret header)
  • How do I use the Apify Headers template to send a secret?
  • How do I handle ACTOR.RUN.SUCCEEDED, ACTOR.RUN.FAILED or ACTOR.BUILD.SUCCEEDED events?
  • Why does my Apify webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

function safeEqual(a, b) {
  const x = Buffer.from(String(a));
  const y = Buffer.from(String(b));
  return x.length === y.length && crypto.timingSafeEqual(x, y); // lengths first: it throws otherwise
}

function verifyApifyRequest(headers, secret = process.env.APIFY_WEBHOOK_SECRET) {
  if (!secret) return false;                                     // never accept when unconfigured
  const name = (process.env.APIFY_WEBHOOK_HEADER || 'X-Webhook-Secret').toLowerCase();
  const received = headers[name];                                // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);       // whole value, no 'Bearer ' parsing
}
  • The header name is whatever you chose in the Headers template (APIFY_WEBHOOK_HEADER, default X-Webhook-Secret). Look it up case-insensitively.
  • APIFY_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed into the template. Don't strip or parse a prefix.
  • Return 401 on a missing header or mismatch, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) and check the header before parsing it.

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Anyone who captures one request can replay it. Use HTTPS only and rotate the secret if it leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The default payload is {userId, createdAt, eventType, eventData, resource}. eventType is one of:

EventSent When
ACTOR.RUN.CREATEDA new Actor run is created
ACTOR.RUN.SUCCEEDEDAn Actor run finishes with status SUCCEEDED
ACTOR.RUN.FAILEDAn Actor run finishes with status FAILED
ACTOR.RUN.ABORTEDAn Actor run finishes with status ABORTED
ACTOR.RUN.TIMED_OUTAn Actor run finishes with status TIMED-OUT
ACTOR.RUN.RESURRECTEDAn Actor run is resurrected
ACTOR.BUILD.CREATEDA new Actor build is created
ACTOR.BUILD.SUCCEEDEDAn Actor build finishes with status SUCCEEDED
ACTOR.BUILD.FAILEDAn Actor build finishes with status FAILED
ACTOR.BUILD.ABORTEDAn Actor build finishes with status ABORTED
ACTOR.BUILD.TIMED_OUTAn Actor build finishes with status TIMED-OUT

Run events carry eventData.actorId, actorRunId (and actorTaskId for task runs); build events carry actorId and actorBuildId. resource is the run or build object. If you customize the payload template these fields may differ.

Environment Variables

APIFY_WEBHOOK_HEADER=X-Webhook-Secret    # header name from your Headers template (optional)
APIFY_WEBHOOK_SECRET=your-secret-value   # exact whole value of that header

Local Development

Apify can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 apify --path /webhooks/apify

Use the printed URL as the webhook's request URL in Apify Console.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →