Async tasks and batches
Task lifecycle
| Status | Meaning |
|---|---|
QUEUED | Waiting to start, for the engine's turn. Not charged; can be cleared. A task that has not started within 72 hours of creation fails, uncharged. |
PROCESSING | Running. Transient failures are retried automatically within five minutes of the first start. |
COMPLETED | response holds the engine's result. Charged. |
FAILED | response holds the error. Not charged. This includes a task that never started within 72 hours of creation. |
task.latencyMs runs from the first start to the final outcome, excluding the time queued.
Every task finishes within bounds: it has 72 hours from creation to start, and five minutes from its first start to finish. 72 hours is enough for a queue of
100,000 tasks to drain at engine rate limits. A task that waits out the first bound — behind an engine that is unavailable or saturated — moves
from QUEUED straight to FAILED with 502 EXTERNAL_SERVICE_ERROR
and details.cause QUEUE_WAIT_EXCEEDED, is not charged, is not retried, and has a null latencyMs. Resubmit tasks that
failed this way. A task that started but did not finish within its five minutes fails with Maximum retries exceeded, also uncharged.
A start that was interrupted before any work began is taken back: the task returns to QUEUED and starts again as if it had not started, so a poller can
see PROCESSING followed by QUEUED. The task keeps the deadline it had before that start. Treat only COMPLETED and
FAILED as final.
Creating a task
POST /v1/async/task takes the engine's request as payload and answers at once with the task. Invalid bodies get 422.
priority orders your own queue: higher runs first, equal priorities in submission order, and other accounts are unaffected.
| Field | Type | Description |
|---|---|---|
taskType * |
"AIMODE" | "GOOGLE" | "GOOGLE_NEWS" | "GEMINI" | "CHATGPT" | "COPILOT" | "PERPLEXITY" | "GROK" |
AI provider that should handle this task. |
payload * |
object |
Request body specific to the provider. Needs at least `prompt` (`query` for Google Search). |
priority |
integer |
Priority of the task, from 1 to 10; tasks with higher numbers run first. Default: 1. |
idempotencyKey |
string |
Distinct string that stops the same task from being created twice. No two tasks in your account may share it. |
webhook |
object |
Settings for the webhook that reports a finished task. |
webhook.url * |
string |
Endpoint that gets the webhook POST request once the task finishes. |
Batches
POST /v1/async/task/batch takes 1 to 500 tasks. Each is validated and admitted on its own, and results answers each by its
index in input order. Only an overflowing queue rejects the whole batch (429 QUEUE_LIMIT_EXCEEDED). The SDKs refuse a batch outside that
size before sending it.
curl -X POST "https://api.answerline.dev/v1/async/task/batch" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '[{"taskType":"CHATGPT","payload":{"prompt":"iPhone 17 or Galaxy S26?","country":"US"},"idempotencyKey":"run-0-US-2026-09-15"},{"taskType":"CHATGPT","payload":{"prompt":"iPhone 17 or Galaxy S26?","country":"GB"},"priority":5,"idempotencyKey":"run-0-GB-2026-09-15"}]'
const prompts = ["iPhone 17 or Galaxy S26?", "Best wireless earbuds under $150"];
const tasks = prompts.flatMap((prompt, i) =>
["US", "GB"].map((country) => ({
taskType: "CHATGPT" as const,
payload: { prompt, country },
idempotencyKey: `run-${i}-${country}-2026-09-15`,
})),
);
const batch = await client.createBatch(tasks);
for (const item of batch.results) {
if (!item.success) console.warn(tasks[item.index].idempotencyKey, item.error.code);
}
prompts = ["iPhone 17 or Galaxy S26?", "Best wireless earbuds under $150"]
tasks = [
{"taskType": "CHATGPT", "payload": {"prompt": p, "country": c}, "idempotencyKey": f"run-{i}-{c}-2026-09-15"}
for i, p in enumerate(prompts)
for c in ("US", "GB")
]
batch = client.create_batch(tasks)
failed = [r for r in batch["results"] if not r["success"]]
{
"success": true,
"summary": {
"total": 2,
"succeeded": 1,
"failed": 1
},
"results": [
{
"success": true,
"index": 0,
"task": {
"id": "b27a21e1-7c39-4aa2-a347-23e828c426f9",
"taskType": "CHATGPT",
"status": "QUEUED",
"priority": 1,
"createdAt": "2026-09-15T09:00:00.000Z",
"latencyMs": null,
"idempotencyKey": "run-0-US-2026-09-15"
},
"credits": {
"creditsToCharge": 6.2,
"creditsCharged": null
}
},
{
"success": false,
"index": 1,
"error": {
"code": "RESOURCE_ALREADY_EXISTS",
"message": "…",
"timestamp": "2026-09-15T09:00:00.000Z"
}
}
]
}
Item errors are VALIDATION_ERROR, RESOURCE_ALREADY_EXISTS and INSUFFICIENT_CREDITS.
Getting results
GET /v1/async/task/{id} returns task, credits and, once the task has finished, response.
waitForTask / wait_for_task poll it with jittered backoff from 500 ms up to 10 seconds between polls (intervalMs /
interval) for up to 11 minutes (timeoutMs / timeout). For many tasks, add a webhook instead and let results come to you; see
webhooks.
A finished task stays readable for at least four days after it was created. Store what you need from a result rather than treating the API as your archive.
GET /v1/async/tasks lists your tasks newest first, a page of summaries at a time: status filters by
QUEUED, PROCESSING, COMPLETED or FAILED, limit sets the page size (50, at most 100), and a
full page's nextBefore is the next page's before. The SDKs do not wrap it yet — call it directly.
Idempotency keys
A task that ends FAILED keeps its key for 24 hours after it failed, then releases it: resubmitting the same key before that answers
409, and after it creates a new task under the key. The key of a task that succeeded, or is still queued or running, stays bound while the task is
readable.
An idempotencyKey is unique across your account. Creating a task with a key in use answers 409 RESOURCE_CONFLICT, or
RESOURCE_ALREADY_EXISTS for a batch item, and creates nothing. Derive keys from what the task means, such as prompt, market and day, never at random
per attempt: then retrying a submission after a timeout can never run a task twice. A request whose every task is refused, for example for
INSUFFICIENT_CREDITS, creates nothing and claims no key, neither its tasks' idempotencyKey nor an Idempotency-Key
header, so the same key can be sent again, with any body, once you have topped up.
Managing the queue
GET /v1/async/status reports queuedTasks, processingTasks, queued counts per priority and concurrency in use.
DELETE /v1/async/queue deletes every task still QUEUED, which were never charged, leaves running and finished tasks alone, and returns how
many it removed; it is safe to repeat.
curl -X GET "https://api.answerline.dev/v1/async/status" \ -H "Authorization: Bearer $API_KEY" curl -X DELETE "https://api.answerline.dev/v1/async/queue" \ -H "Authorization: Bearer $API_KEY"
const status = await client.asyncStatus();
const { cleared } = await client.clearQueue();
status = client.async_status() cleared = client.clear_queue()["cleared"]
Sync or async
A synchronous call is one request that waits up to five minutes and costs the synchronous surcharge. Set the HTTP client's read
timeout above that. If the wait runs out first, the error's details.taskId names the submitted task — fetch its outcome with
GET /v1/async/task/{id} instead of submitting again and paying twice. A task costs the base price,
waits in the queue instead of failing, and can be batched. Use synchronous calls for interactive work and tasks for anything scheduled or large.