webhook.url on a task and we POST the result there when the task reaches a
terminal state — COMPLETED or FAILED.
Delivery payload
We POSTContent-Type: application/json with this body:
object
required
The same task metadata you got at submit time, now with a terminal
status and your
idempotencyKey echoed back.object
required
A nominal per-engine figure mirroring Cloro’s pricing table —
3 for Perplexity, 7 for
ChatGPT, and so on. Nothing is billed. creditsCharged is 0 when the task failed.
Unlike the submit and poll responses, which always report zero, this field is non-zero.object
required
The engine-specific result. See Engines. On a failed task this is
{ "error": "<reason>" } instead of the engine result.Retries
Return any2xx to acknowledge. Anything else — including a timeout — is a failure and
we retry with exponential backoff:
After the fifth attempt the delivery is abandoned and logged. There is no dead-letter
queue you can read from, so the task result is only recoverable by polling within the
24-hour retention window.
Each attempt has a 30 second request timeout.
Writing a safe handler
1
Acknowledge fast
Return
200 as soon as you have durably enqueued the payload. Do your parsing and
database writes after. A slow handler burns the 30-second timeout and earns a retry
you did not want.2
Deduplicate on idempotencyKey
Retries mean your handler can legitimately see the same task more than once. Match on
task.idempotencyKey (or task.id) and make the write idempotent.3
Branch on status, not on presence
Check
task.status === "FAILED" explicitly. A failed task still delivers a webhook.Webhook requests carry no signature or shared secret. If your endpoint is public, treat
the payload as untrusted and use an unguessable URL path, or place the handler behind
network-level restrictions.