Sync calls, async tasks or webhooks: how to call an answer API
An AI assistant’s answer takes far longer than a typical API call: the engine runs the prompt, searches and writes. The API offers three ways to deal with that. All return the same result JSON, so the choice is about cost, failure modes and plumbing.
The examples read two environment variables: API_URL, your API base URL, and API_KEY.
Synchronous: one call, one answer
curl -X POST "$API_URL/v1/monitor/chatgpt" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Best CRM for small agencies", "country": "US"}'
The connection stays open until the answer is ready, and the body is {"success": true, "result": {...}}. The simplicity has costs:
- A surcharge. Synchronous calls cost a few credits more than the same request as a task (pricing).
- A free concurrency slot. The call runs now or not at all: with every slot busy you get
429 CONCURRENT_LIMIT_EXCEEDED. - Long timeouts. A synchronous request may take up to five minutes, so your HTTP client must wait longer than that.
- Careful retries. If the connection drops mid-call, the request may still finish and be charged. Retry only when it never reached the server; a
500or502is final because the server has already retried.
Use it for interactive tools, one-off checks and agents calling through the MCP server.
Async with polling
curl -X POST "$API_URL/v1/async/task" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"taskType": "CHATGPT", "payload": {"prompt": "Best CRM for small agencies", "country": "US"}, "idempotencyKey": "crm-agencies-us-2026-09-15"}'
This answers at once with task.id and status QUEUED. The task waits for a slot instead of failing and costs the base price. Read it with GET /v1/async/task/{id} until the status is COMPLETED or FAILED:
import os, random, time
import httpx
api = httpx.Client(base_url=os.environ["API_URL"], headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}, timeout=30)
def wait(task_id: str, limit_s: float = 600) -> dict:
deadline, cap = time.monotonic() + limit_s, 0.5
while time.monotonic() < deadline:
res = api.get(f"/v1/async/task/{task_id}")
if res.status_code == 200 and res.json()["task"]["status"] in ("COMPLETED", "FAILED"):
return res.json()
if 400 <= res.status_code < 500 and res.status_code != 429:
res.raise_for_status()
time.sleep(random.uniform(0, cap))
cap = min(cap * 2, 10)
raise TimeoutError(task_id)
The SDKs ship this loop as waitForTask and wait_for_task. Polling suits a script watching a handful of tasks; with thousands, polls eat into your rate limit and add delay between a task finishing and you reading it.
Async with webhooks
Add a webhook URL to the task and the finished task is POSTed to you:
{
"taskType": "CHATGPT",
"payload": { "prompt": "Best CRM for small agencies", "country": "US" },
"idempotencyKey": "crm-agencies-us-2026-09-15",
"webhook": { "url": "https://your-app.com/hooks/answers" }
}
The body is what GET /v1/async/task/{id} returns. Three rules keep a receiver correct:
- Verify the signature on the raw body before parsing; the webhooks guide shows how.
- Answer 2xx quickly, then work. A delivery without a 2xx within 15 seconds is retried with backoff.
- Deduplicate by
task.id. Deliveries are concurrent and retried independently, so they can repeat and arrive out of order.
import json
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/hooks/answers")
def answers():
raw = request.get_data()
if not signature_valid(raw, request.headers.get("Webhook-Signature")): # your check, from the guide
abort(400)
delivery = json.loads(raw)
if not delivery.get("test"):
jobs.put(delivery) # your queue; store by delivery["task"]["id"], skipping ids already stored
return "", 204
Choosing
| Synchronous | Async + polling | Async + webhook | |
|---|---|---|---|
| Price | Base + surcharge | Base | Base |
| Slots all busy | 429, retry later | Waits in queue | Waits in queue |
| Tasks per request | 1 | Up to 500 in a batch | Up to 500 in a batch |
| You run | A long HTTP call | A polling loop | A public HTTPS endpoint |
| Suits | Interactive use, agents | Scripts, small jobs | Pipelines, schedules |
Whichever you pick, give each task an idempotencyKey derived from what it means (prompt, market, day). Retrying creation after a timeout is then safe: a duplicate is refused with 409 RESOURCE_CONFLICT instead of running twice.