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 4 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 4 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 4 hours from creation to start, and five minutes from its first start to finish. 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" |
Engine that should handle this task. |
payload * |
object |
Request body specific to the engine. 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: the key stays bound while its task is kept (4 to 5 days), and a failed task frees it 24 hours after failing. |
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": 0.0006,
"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. A queue too long to clear in one call answers 202 with clearing: true:
the rest is deleted without further calls, and GET /v1/async/status shows the queue shrink.
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 same as a task. 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
waits in the queue instead of failing, and can be batched. Use synchronous calls for interactive work and tasks for anything scheduled or large.