Skip to main content
See Making requests for an overview of both request modes. You submit a task, we return a taskId, and the request runs in the background — like a tracking number rather than waiting at the counter. Submit tasks one at a time or in batches of up to 500.

Step 1: make an API request

payload takes the same parameters as the provider’s sync request. Include a webhook.url and we notify you when the job is done; omit it and you poll for the result.
Identifying your requests with idempotencyKeyYou can optionally include an idempotencyKey in your request. This is a unique string you create that allows you to easily identify and reference your requests in your own system.The idempotencyKey must be a unique string across your entire account. If you submit a request with an idempotency key that has already been used, the API returns 409:
Generate unique keys using UUIDs, timestamps, or a combination of user ID + timestamp to ensure no duplicates.Key retention: a key stays bound to its task until the task record is deleted, whether it ended COMPLETED or FAILED. To retry a failed task before then, submit it with a new key.Serverless crash recovery: GET /v1/async/task/{taskId} finds a task by its taskId only, not by its idempotencyKey. Store the taskId as soon as you receive the submit response. If your serverless function (Vercel, Lambda) crashes before it saves the taskId, a resubmit with the same idempotencyKey returns 409 and does not create a second task, so you are not charged twice. Set a webhook.url when you submit: the webhook payload carries task.id and task.idempotencyKey, so you still receive the result.
Understanding task states
  • QUEUED: received and waiting its turn, in priority order. A task waiting to retry a failed attempt is back in QUEUED.
  • PROCESSING: a worker is running the task.
  • COMPLETED: cloro got a result, returned in response. That result can be the provider’s own error or refusal text (details).
  • FAILED: no attempt returned a result, or the scheduler rejected the task when it reached it (invalid payload or insufficient credits). response carries the error.
The submit response always shows QUEUED. Poll the task status endpoint to follow the transitions; a webhook fires only once the task is COMPLETED or FAILED.Finished tasks are deleted about 24 hours after you create them, and HTML URLs in a result expire after 24 hours on their own clock.

Step 2: receive the results

Two ways to retrieve a result. Either way, a finished task carries task.latencyMs — the milliseconds from first pickup to the final outcome, excluding the initial queue wait. It is null while the task is QUEUED or PROCESSING, and stays null on a task that failed before processing ever started. On a retried task it covers every attempt, including the backoff between them, so it can be far longer than one provider call.
Don’t read task latency from the X-Latency-Ms header on a status call. That header times the status call itself — a few milliseconds — not the task. Task latency is only ever task.latencyMs.
If you provided a webhook.url in your request, we will send an HTTP POST to that URL containing the full result as soon as the task reaches a terminal state. See Webhooks for the payload shape, retry behavior, signature verification, and troubleshooting.

Option B: polling

Without a webhook URL, GET the task status endpoint with your taskId. Once the task completes, the response carries the full result.

Understanding limits

Task submission limits

Your concurrency limit never blocks submission. Besides the per-second rate limit every endpoint has, two limits can:
  • Credit limit: your balance must cover the task’s creditsToCharge — see what happens when credits run out.
  • Queue limit: your organization can hold up to 100,000 QUEUED tasks, counting tasks waiting to retry but not PROCESSING ones. Past that, a submit returns 429 QUEUE_LIMIT_EXCEEDED, and a batch that would overflow the queue is rejected whole. Contact support if you need it raised.

Task processing limits

The scheduler picks up queued tasks in priority order, and this is where your plan’s concurrency limit applies: if your plan allows 10 concurrent requests, up to 10 of your tasks run in parallel. Sync requests draw on the same slots, so sync traffic in flight leaves fewer for your tasks.

Request prioritization

Set priority from 1 to 10 when you submit. Higher numbers run first, and tasks with the same priority run in the order they were received. Omitting it leaves the task at 1, so existing integrations are unaffected. Monitor how your queue is distributed across levels via the async status endpoint.
For practical concurrency patterns and examples, see our concurrency documentation.

Common questions

How do I cancel pending async tasks?

Individual queued tasks cannot be canceled by taskId — once a task enters the QUEUED state, it stays there until it is either processed (COMPLETED / FAILED) or removed by a queue-wide clear. To wipe every pending task in one call, use the DELETE /v1/async/queue endpoint. It removes every QUEUED task for your organization (tasks already PROCESSING keep running) and returns the number cleared:
cURL

How long do async tasks stay in the queue?

Tasks stay in the queue until they are processed, resulting in either COMPLETED or FAILED status. There is no fixed time-to-result. How quickly a task completes depends on:
  • Queue depth: how many of your tasks are ahead of it in priority order.
  • Concurrency limit: your plan’s limit caps how many of your tasks process in parallel, so a deep queue drains at most that many tasks at a time. See Rate & concurrency limits.
  • Provider processing time: once a task is picked up, the time to the final outcome is reported in task.latencyMs. Queue wait is excluded from that number.
Check your current queue depth and concurrency usage with the async status endpoint.

How do I track the credits consumed by each task?

Both the polling response and the webhook payload include a credits object:
  • creditsToCharge — the estimated cost, computed at submission; nothing is reserved
  • creditsCharged — null until the task finishes, then the amount actually billed (0 for a FAILED task)
Use creditsCharged from the terminal state to attribute cost per job. How billing works covers what is charged.

What happens to async tasks when my credits run out?

Credits are checked twice, and the balance is never reserved in advance:
  • At submission. POST /v1/async/task rejects with 403 INSUFFICIENT_CREDITS when your balance does not cover that task’s cost. POST /v1/async/task/batch instead returns 200 with a per-task INSUFFICIENT_CREDITS error for each task that doesn’t fit, so a partially affordable batch is partially accepted.
  • At scheduling. Tasks already sitting in QUEUED are re-checked when the scheduler picks them up. A task that no longer fits the balance moves to FAILED with creditsCharged: 0 and an INSUFFICIENT_CREDITS error — it is not held, retried, or resumed when you top up. Re-submit those tasks after topping up.
Both checks are skipped if your organization is on a subscription with overages enabled. Because credits are only deducted when a task completes, the balance from GET /v1/credits reflects completed charges only — it does not net out creditsToCharge for work still queued or processing. To decide whether a submission will fit, subtract your own outstanding creditsToCharge from remaining rather than trusting remaining alone.

A task came back COMPLETED but the result looks like an upstream error. What happened?

COMPLETED means cloro got a result from the provider, and that result can be the provider’s own error or refusal text; it is charged like any other result. Errors cloro recognizes are retried instead, and end in FAILED only if every attempt fails. See empty responses from AI providers to detect and retry the rest.

Does async cost more credits than sync?

No — async is 2 credits cheaper per request. The base cost and feature add-ons are identical whether you use synchronous or async delivery, but sync monitor requests (/v1/monitor/*) carry a +2 credit surcharge that async and batch requests (/v1/async/*) do not. The creditsCharged field in the terminal task state shows the actual amount billed for each task.

The async endpoint feels slow today. What should I check?

Call GET /v1/async/status to see your account’s queue depth and concurrency usage. If the queue is deep and concurrency is saturated, throughput is plan-bound — see concurrency for how to raise it.