> ## Documentation Index
> Fetch the complete documentation index at: https://cloro.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Asynchronous requests

> Submit async tasks to the cloro API: two-step submit-and-fetch flow, webhooks vs polling, task states, prioritization, queue limits, and concurrency.

See [Making requests](/guides/making-requests) for an overview of both request modes.

Think of an asynchronous (async) request like sending a package with a tracking number.

Instead of waiting at the post office until the package is delivered (a synchronous process), you hand it over, get a `taskId` (your tracking number), and are free to do other things. You can then either receive a delivery notification (a webhook) or check the tracking status online yourself (polling).

Our async API works the same way. You submit a task, we give you a `taskId`, and we process your request in the background. You can submit tasks one at a time or [in batches of up to 500](/api-reference/endpoint/create-batch-tasks).

## When to use async requests

This approach works well when:

* Your application, especially if it's running in a **serverless environment**, has short execution time limits.
* You want to submit a large number of requests quickly without waiting for each one to complete. The [batch endpoint](/api-reference/endpoint/create-batch-tasks) lets you submit up to 500 tasks in a single request.
* You need to build a more resilient system that isn't dependent on a single, long-running connection.

## How it works: a two-step process

The process involves two steps: making a request and then fetching the result.

### Step 1: make an API request

First, you send an API request. If you include a `webhook.url`, we'll notify you when the job is done. If you omit it, you will need to poll for the result.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.cloro.dev/v1/async/task" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "taskType": "CHATGPT",
      "priority": 5,
      "idempotencyKey": "your-custom-identifier-123",
      "webhook": {
        "url": "https://your-app.com/webhook-handler"
      },
      "payload": {
        "prompt": "What is the weather in New York?",
        "country": "US"
      }
    }'
  ```

  ```javascript Node.js (axios) theme={null}
  import axios from 'axios';

  const apiKey = 'YOUR_API_KEY';
  const url = 'https://api.cloro.dev/v1/async/task';

  const data = {
  taskType: 'CHATGPT',
  priority: 5,
  idempotencyKey: 'your-custom-identifier-123',
  webhook: {
  url: 'https://your-app.com/webhook-handler',
  },
  payload: {
  prompt: 'What is the weather in New York?',
  country: 'US',
  },
  };

  try {
  const response = await axios.post(url, data, {
  headers: {
  'Authorization': `Bearer ${apiKey}`,
  'Content-Type': 'application/json',
  },
  });
  console.log(response.data);
  } catch (error) {
  console.error(error);
  }
  ```

  ```python Python (requests) theme={null}
  import requests

  api_key = 'YOUR_API_KEY'
  url = 'https://api.cloro.dev/v1/async/task'

  payload = {
    "taskType": "CHATGPT",
    "priority": 5,
    "idempotencyKey": "your-custom-identifier-123",
    "webhook": {
      "url": "https://your-app.com/webhook-handler"
    },
    "payload": {
      "prompt": "What is the weather in New York?",
      "country": "US"
    }
  }

  headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
  }

  response = requests.post(url, json=payload, headers=headers)
  print(response.json())
  ```
</CodeGroup>

<Info>
  **Identifying your requests with `idempotencyKey`**

  You 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 will return an error:

  ```json theme={null}
  {
    "success": false,
    "error": {
      "code": "RESOURCE_ALREADY_EXISTS",
      "message": "Task already exists",
      "details": {
        "field": "idempotencyKey",
        "value": "abc12312233123441234451112322212222223"
      },
      "timestamp": "2025-12-10T11:41:40.243Z"
    }
  }
  ```

  Generate unique keys using UUIDs, timestamps, or a combination of user ID + timestamp to ensure no duplicates.

  **Failed task key retention**: When a task returns `FAILED`, the `idempotencyKey` is not released — it stays bound for 24 hours, then auto-releases. To retry a failed task within that window, submit the same request with a new key.

  **Serverless crash recovery**: If your serverless function (Vercel, Lambda) crashes before you can save the `taskId`, you can poll `GET /v1/async/task/{taskId}` using the original `idempotencyKey` to retrieve the result — no resubmission and no double charge. Store the `taskId` as early as possible after receiving the submit response.
</Info>

We'll acknowledge your request and provide a `taskId`.

<CodeGroup>
  ```json Initial Response theme={null}
  {
    "success": true,
    "task": {
      "id": "b27a21e1-7c39-4aa2-a347-23e828c426f9",
      "taskType": "CHATGPT",
      "status": "QUEUED",
      "priority": 5,
      "createdAt": "2025-11-10T15:00:00.000Z",
      "idempotencyKey": "your-custom-identifier-123"
    },
    "credits": {
      "creditsToCharge": 10,
      "creditsCharged": 0
    }
  }
  ```
</CodeGroup>

Now, you can store this `taskId` and wait for the results.

<Info>
  **Understanding task states**

  Every async task goes through four possible states:

  * `QUEUED`: The request is received and waiting its turn to be processed. Tasks are processed by [priority](#request-prioritization) first, then in FIFO (first-in, first-out) order within the same priority level.
  * `PROCESSING`: The request is actively being processed by our system. The AI provider is generating your response.
  * `COMPLETED`: The request finished successfully. The final response is included in the same payload.
  * `FAILED`: The request failed to complete. This can happen because of rate limits, provider errors, or invalid input.

  The initial response always shows status as `QUEUED`. You can track state transitions by polling the task status endpoint or receiving webhook updates.

  `COMPLETED` and `FAILED` tasks are stored in our system for **24 hours** after completion. During this time, you can retrieve the full results using the task ID. After 24 hours, the task record and its associated response data are permanently deleted from our system.

  HTML URLs included in responses expire after **24 hours** from generation, regardless of the task's retention status.
</Info>

### Step 2: receive the results

You have two options for retrieving the results of your task.

#### Option A: webhooks (recommended)

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](/guides/webhooks) for the payload shape, retry behavior, signature verification, and troubleshooting.

#### Option B: polling

If you don't provide a webhook URL, you can periodically check for the result yourself. This is called "polling."

You can make a `GET` request to our [Get task status](/api-reference/endpoint/get-task-status) endpoint using the `taskId` you received in Step 1. Once the task is complete, the response will contain the full result.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.cloro.dev/v1/async/task/YOUR_TASK_ID" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript Node.js (axios) theme={null}
  import axios from 'axios';

  const apiKey = 'YOUR_API_KEY';
  const taskId = 'YOUR_TASK_ID';
  const url = `https://api.cloro.dev/v1/async/task/${taskId}`;

  try {
  const response = await axios.get(url, {
  headers: {
  'Authorization': `Bearer ${apiKey}`,
  },
  });
  console.log(response.data);
  } catch (error) {
  console.error(error);
  }
  ```

  ```python Python (requests) theme={null}
  import requests

  api_key = 'YOUR_API_KEY'
  task_id = 'YOUR_TASK_ID'
  url = f'https://api.cloro.dev/v1/async/task/{task_id}'

  headers = {
    "Authorization": f"Bearer {api_key}"
  }

  response = requests.get(url, headers=headers)
  print(response.json())
  ```
</CodeGroup>

## Understanding limits

The asynchronous system handles limits in two steps.

### Task submission limits

When you submit a task, we only check two things:

* **Credit Limit**: We verify you have enough credits for the task.
* **Queue Limit**: Your organization can have a maximum of **100,000 tasks** waiting in the queue. If you exceed this, you will receive a `429 Too Many Requests` error. Please contact our team if you need this limit increased.

### Task processing limits

Once your task is in the queue, our scheduler picks it up for processing. This is where your subscription's **concurrency limit** is enforced. For example, if your plan allows 10 concurrent requests, our scheduler will process up to 10 of your tasks in parallel. Tasks are processed by [priority](#request-prioritization) first, then in the order they were received within the same priority level.

### Request prioritization

You can assign a priority to async tasks using the `priority` field when creating a task. Priority is an integer from **1** (lowest, default) to **10** (highest).

| Parameter  | Type    | Required | Default | Description                                                     |
| ---------- | ------- | -------- | ------- | --------------------------------------------------------------- |
| `priority` | integer | No       | 1       | Task priority level (1-10). Higher numbers are processed first. |

Higher-priority tasks are processed before lower-priority ones within your organization's queue. Tasks at the same priority level are processed in FIFO (first-in, first-out) order.

If you don't specify a priority, tasks default to 1, the same behavior as before this feature was added. Existing integrations are unaffected.

You can monitor how your queue is distributed across priority levels using the [async status endpoint](/api-reference/endpoint/get-async-status).

<Info>
  For practical concurrency patterns and examples, see our
  [concurrency](/guides/concurrency) documentation.
</Info>

## Common questions

### How do I cancel pending async tasks?

There is no API endpoint to cancel or purge queued tasks. Once a task enters the `QUEUED` state it will remain until processed (`COMPLETED` or `FAILED`). If you need a large batch cleared urgently, contact [support@cloro.dev](mailto:support@cloro.dev) — we can clear the queue on the backend.

Best practices to avoid unwanted tasks:

* Test with small batches first
* Use unique `idempotencyKey` values to prevent duplicate submissions
* Implement safeguards in your submission logic
* Monitor your queue depth via the [async status endpoint](/api-reference/endpoint/get-async-status)

### What's the maximum queue depth?

Queue depth is limited to **100,000 tasks** per organization. If you exceed this limit, you'll receive a `429 Too Many Requests` error when trying to submit additional tasks.

If you need a larger queue for your use case, please contact our team.

### 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.

Check your current queue status using the [async status endpoint](/api-reference/endpoint/get-async-status).

### 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 shown while the task is `QUEUED` or `PROCESSING`
* `creditsCharged` — the actual amount billed once the task reaches `COMPLETED` or `FAILED`

Use `creditsCharged` from the terminal state to attribute cost per job. Failed tasks may still incur credits depending on how far processing got. Sync requests [canceled by the client are also charged for work already done](/guides/error-handling#client-cancellation-policy); async tasks [cannot be canceled once queued](#how-do-i-cancel-pending-async-tasks).

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

`COMPLETED` means cloro finished its work and returned what the upstream provider gave us. If the provider returned an error page, a captcha, or a truncated answer, that detail lives inside the `response` payload. Inspect the response body — `FAILED` is reserved for cases where cloro could not produce a result at all (network errors, internal exceptions, repeated upstream timeouts).

### Does async cost more credits than sync?

No. The credit cost per request is identical whether you use synchronous or async delivery. The `creditsCharged` field in the terminal task state shows the same cost you would see from a sync call for the same endpoint and parameters.

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

Call [`GET /v1/async/status`](/api-reference/endpoint/get-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](/guides/concurrency#can-i-increase-my-concurrency-limit) for how to raise it.
