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.FAILEDorACTOR.BUILD.SUCCEEDEDevents? - 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, defaultX-Webhook-Secret). Look it up case-insensitively. APIFY_WEBHOOK_SECRETis the exact, whole header value, including anyBearerprefix you typed into the template. Don't strip or parse a prefix.- Return
401on a missing header or mismatch,400for a body that isn't a JSON object,200once 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:
| Event | Sent When |
|---|---|
ACTOR.RUN.CREATED | A new Actor run is created |
ACTOR.RUN.SUCCEEDED | An Actor run finishes with status SUCCEEDED |
ACTOR.RUN.FAILED | An Actor run finishes with status FAILED |
ACTOR.RUN.ABORTED | An Actor run finishes with status ABORTED |
ACTOR.RUN.TIMED_OUT | An Actor run finishes with status TIMED-OUT |
ACTOR.RUN.RESURRECTED | An Actor run is resurrected |
ACTOR.BUILD.CREATED | A new Actor build is created |
ACTOR.BUILD.SUCCEEDED | An Actor build finishes with status SUCCEEDED |
ACTOR.BUILD.FAILED | An Actor build finishes with status FAILED |
ACTOR.BUILD.ABORTED | An Actor build finishes with status ABORTED |
ACTOR.BUILD.TIMED_OUT | An 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
- references/overview.md - What Apify webhooks are, events and payload shape
- references/setup.md - Create the webhook and add the secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests