Apify run-completion callback
Receives Apify's notification that a research run reached a terminal state. Apify cannot present the caller's bearer token, so the callback is authorised by the `sig` query parameter: an HMAC-SHA256 over `<jobId>.<ownerId>` keyed with the server's own `RESEARCH_WEBHOOK_SECRET`. A bad or missing signature is a 401 and changes nothing. The signature also binds the job id, so a signature minted for one job cannot be replayed against another — a `job` the server did not sign is a 401, not a lookup. Polling (`GET /research/jobs/{id}`) is the fallback whenever the secret is not configured.
Receives Apify's notification that a research run reached a terminal state. Apify cannot present the caller's bearer token, so the callback is authorised by the sig query parameter: an HMAC-SHA256 over <jobId>.<ownerId> keyed with the server's own RESEARCH_WEBHOOK_SECRET. A bad or missing signature is a 401 and changes nothing. The signature also binds the job id, so a signature minted for one job cannot be replayed against another — a job the server did not sign is a 401, not a lookup. Polling (GET /research/jobs/{id}) is the fallback whenever the secret is not configured.
Query Parameters
The job id Putty signed into this callback URL. The handler recomputes the signature from the job's owner and rejects the callback when it does not match.
1 <= lengthThe HMAC-SHA256 signature over <jobId>.<ownerId>, keyed with the server's own RESEARCH_WEBHOOK_SECRET. It is not a user key and never leaves the server.
1 <= lengthResponse Body
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/research/webhooks/apify?job=string&sig=string"{ "accepted": true, "jobId": "string"}Read a research job GET
Returns one research job, refreshed against Apify first, so a job started before an api restart is resumable by id. A finished job's full dataset is stored as JSON in Garage under `research/<ownerId>/<jobId>.json` and reported as `fullDataUrl`; the `content` field is a truncated summary wrapped in an `<untrusted-data>` fence, because scraped text is data and never an instruction. The stored dataset is deleted 30 days after the job was created. A job owned by another user answers with the same 404 as a job that does not exist.
Zernio post status deliveries POST
Where Zernio POSTs `post.scheduled`, `post.published`, `post.failed`, `post.partial` and `post.cancelled` for a user's own account. **This route is public** — there is no session on a delivery from Zernio's servers — so the per-user signing secret is the only credential. It looks the caller's `ZERNIO_WEBHOOK_SECRET` up by `ownerWebhookId`, verifies the lowercase-hex HMAC-SHA256 of the raw body against the `X-Zernio-Signature` header, and only then updates the matching `PublishRequest`. A delivery that fails verification answers 401 and changes nothing. Zernio retries a non-2xx up to seven times on an exponential backoff, so an unparseable or untracked event answers 200 and is dropped rather than burning the retry ladder.