cloro WebsiteAPI Get an API key
API Reference › Async Tasks › Webhooks

Webhooks

Have cloro push the finished result straight to your server the moment an async task settles, instead of repeatedly polling for it.

Overview

Attach a webhook.url to any async task and cloro will deliver an HTTP POST to that address as soon as the task reaches a terminal state — either COMPLETED or FAILED. The request body carries the same result you would otherwise fetch by hand, so a webhook consumer never has to call the status endpoint at all. If no URL is supplied, the task falls back to ordinary polling.

The opt-in lives entirely in the create-task call. Add the webhook object alongside your normal payload:

POST https://api.cloro.cloud/v1/async/task
{
  "taskType": "CHATGPT",
  "webhook": {
    "url": "https://your-app.example.com/hooks/cloro"
  },
  "payload": {
    "prompt": "Summarize today's market open",
    "country": "US"
  }
}

Nothing else is required. When the task finishes, the URL receives the full record described below.

Delivery payload

Each delivery is a single JSON document with three top-level sections: identifying metadata for the task, the credit accounting for the run, and the provider's response.

FieldTypeDescription
task.idstringStable UUID for the task; use it to deduplicate repeated deliveries.
task.taskTypestringThe provider the task ran against, e.g. CHATGPT.
task.statusstringTerminal outcome — COMPLETED or FAILED.
task.priorityintegerQueue priority the task was submitted with.
task.createdAtstringISO 8601 timestamp of when the task was accepted.
task.idempotencyKeystringThe client-supplied key echoed back, when one was provided.
credits.creditsToChargeintegerCredits the run was expected to cost.
credits.creditsChargedintegerCredits actually deducted from your balance.
response.modelstringUnderlying model that produced the answer.
response.textstringPlain-text answer from the provider.
response.htmlstringURL to the captured HTML rendering of the result.
response.markdownstringMarkdown version of the answer.
response.sourcesarrayCited sources referenced in the answer.
response.searchQueriesarraySearch queries the provider issued while answering.

Response example

{
  "task": {
    "id": "9f3c0d84-51ab-4e0f-bc72-6d2a1f9e4c10",
    "taskType": "CHATGPT",
    "status": "COMPLETED",
    "priority": 5,
    "createdAt": "2026-08-14T09:15:00.000Z",
    "idempotencyKey": "order-4821"
  },
  "credits": {
    "creditsToCharge": 10,
    "creditsCharged": 10
  },
  "response": {
    "model": "gpt-5-3-mini",
    "text": "Markets opened higher on strong earnings...",
    "html": "https://storage.cloro.cloud/results/9f3c0d84-51ab-4e0f-bc72-6d2a1f9e4c10/page-1.html",
    "markdown": "Markets opened higher on strong earnings...",
    "sources": [],
    "shoppingCards": [],
    "entities": [],
    "searchQueries": ["market open today"]
  }
}

Acknowledging a delivery

Return any 2xx status — 200 OK is the convention — to tell cloro the delivery was accepted. Reply as soon as you have persisted the body and run the heavier work afterward. Any non-2xx response, TLS failure, or timeout is treated as a failed delivery and will be retried.

Retries and deduplication

Failed deliveries are retried up to five times with an exponential backoff, roughly doubling the wait between attempts (about 2, 4, 8, and 16 minutes after the first try). Because of this, the same task can legitimately reach your endpoint more than once. To process each result exactly once, key your handler on task.id. Signed deliveries additionally carry a per-attempt X-Cloro-Webhook-Id header shaped as <task-id>-<attempt>.

Verifying deliveries

Anyone who learns your endpoint URL could forge a request that looks like a genuine delivery, so enable webhook signing whenever your handler performs sensitive actions. Signing is turned on per organization from the dashboard; when you enable it, cloro shows a secret prefixed with whsec_ exactly once. Store it in your secret manager — it is all your endpoint needs to check signatures.

Every signed delivery adds three headers on top of the standard Content-Type: application/json:

FieldTypeDescription
X-Cloro-TimestampstringUnix time in seconds when cloro signed the delivery.
X-Cloro-SignaturestringHMAC-SHA256 digest, hex-encoded and prefixed with a scheme version, e.g. v1=....
X-Cloro-Webhook-IdstringUnique per-attempt delivery id, <task-id>-<attempt>.

The signed string is the timestamp, a literal dot, and the exact raw request body concatenated together: signed = timestamp + "." + rawBody. Compute HMAC-SHA256(secret, signed), hex-encode it, and compare it against the value after v1= in the signature header.

Notes

Verify against the raw request bytes, not a re-serialized copy. Frameworks that parse and re-encode JSON (Express's express.json(), Flask's request.json, some serverless wrappers) can shift whitespace, key order, or number formatting, which will make an otherwise-correct signature fail. Capture the raw body first and parse it only after verification passes.

Always compare signatures with a constant-time function — crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python, or hmac.Equal in Go — and reject any delivery whose timestamp is more than a few minutes old to defeat replay attempts. If a signing secret is ever exposed, rotate it from the dashboard immediately. For help, reach us at [email protected].