Web data
Async Tasks
Queue a request, get a task id back at once, and collect the result later by polling or by webhook. The task runs the same service the synchronous route uses.
Endpoints#
- POST
/async/task - POST
/async/task/batch - GET
/async/task/{taskId} - GET
/async/status - DELETE
/async/queue
Credits: the endpoint's price, deducted when the task is queued. Kept when the task completes; refunded when it fails or is cancelled. Reading tasks and status is free.
Request#
| Field | Type | Required | Description |
|---|---|---|---|
endpoint | string | Yes | What to run (see the table below) |
request | object | No | The parameters for that endpoint. Must include q (ai/… tasks also accept prompt), except for goto |
webhook | object | No | { "url": "https://…", "secret": "…" }. url is required inside it; secret (max 256 chars) signs each delivery |
priority | number | No | 1–100 (default: 50). Higher runs first |
idempotencyKey | string | No | Max 128 chars. Sending the same key again returns the existing task (duplicate: true, HTTP 200) without a new charge |
The field is endpoint, not type, and the webhook is webhook: { url, secret }, not webhookUrl. Unknown top-level fields are stripped without an error: a request that sends webhookUrl is queued with no webhook, and one that sends type instead of endpoint is rejected with 400.
endpoint | Credits | Runs |
|---|---|---|
serp | 3 per page; from 2026-11-01, +5 when the AI Overview had to be completed by rendering the page | Full SERP. request takes the GET /serp fields. A country location (or its uule) is applied as gl; a city or state fails the task at once with CITY_LOCATION_UNAVAILABLE, refunded |
news/serp | 2 per page | GET /search/news/serp, with the same location rule |
rankings/check | 3 per 10 results of depth | Rank Checks. Send keyword and domain as usual and repeat the keyword as q, which the queue requires |
ai/chatgpt, ai/copilot | 8 | AI Answers |
ai/perplexity | 2 | AI Answers |
ai/gemini | 4 | AI Answers |
web | 1 | Web search. request goes to the search service as is: use num (1–10), start, country and language, not limit, page, gl and hl |
maps | 1 | Maps search. Use num for the result count; gl, hl, ll, lat, lng and zoom work as on /search/maps |
shopping | 1 | Shopping search. Use num for the result count, plus gl and hl |
goto | 0 | Resolves Google /goto redirect links: url, or urls (up to 50) |
The validator also accepts keywords/…, domain/…, ai/aimode and ai/grok; those endpoints are switched off in production, so do not queue them.
Response#
POST /async/task answers 202 with the queued task:
{ "success": true, "duplicate": false, "task": { "taskId": "5f0c2b1e-7a3d-4c1b-9d0e-2a6f8b9c1d3e", "endpoint": "serp", "status": "QUEUED", "priority": 50, "attempts": 0, "creditsHeld": 6, "creditsUsed": 0, "creditsRefunded": false, "request": { "q": "best running shoes", "gl": "us", "pages": 2 }, "webhook": { "url": "https://example.com/hooks/searlo", "attempts": 0, "lastStatus": null, "deliveredAt": null }, "queuedAt": "2026-09-29T10:00:00.000Z", "startedAt": null, "completedAt": null, "durationMs": null, "expiresAt": "2026-10-06T10:00:00.000Z" }}GET /async/task/{taskId} returns { "success": true, "task": { … } } in the same shape, or 404 for an unknown id. status is QUEUED, PROCESSING, COMPLETED, FAILED or CANCELLED.
resultappears once the task isCOMPLETED. Forserp,news/serpandai/…it is the body the synchronous route returns; forrankings/checkit is the check'sresultobject withsuccess: true.error(code,message,retryable) appears when the task isFAILED.- A retryable failure (capacity, an unusable upstream page) is queued again after a delay, the engine's
Retry-Afterwhen it gave one, up to a configurable number of attempts. After that, or on a non-retryable error, the task isFAILEDand its credits are refunded (creditsRefunded: true). Errors in the request itself (VALIDATION_ERROR,CITY_LOCATION_UNAVAILABLE,UNKNOWN_LOCATION,INVALID_DOMAIN, a query shape Google refuses on sight) fail at once, without retries. - A result larger than a configurable size has its bulkiest top-level fields removed, listed in
result._trimmed. - Tasks are deleted after a configurable retention period;
expiresAtsays when.
Batch#
POST /async/task/batch takes { "tasks": [ … ] } with 1–500 task objects, each shaped like a single request. Each task is charged when it is queued. The response is 202 when at least one task was accepted, 400 otherwise:
{ "success": true, "accepted": 2, "total": 3, "tasks": [ { "accepted": true, "duplicate": false, "taskId": "5f0c2b1e-7a3d-4c1b-9d0e-2a6f8b9c1d3e", "status": "QUEUED", "creditsHeld": 8 }, { "accepted": true, "duplicate": false, "taskId": "9b1d7e0a-2c4f-4e8b-a6d3-1f2e3d4c5b6a", "status": "QUEUED", "creditsHeld": 2 }, { "accepted": false, "error": "request.q is required", "endpoint": "ai/copilot" } ]}If your credits run out part-way, the batch stops there: the remaining tasks are not queued and do not appear in tasks, so compare tasks.length with total. A task that fails validation (an unknown endpoint, say) rejects the whole batch with 400.
Queue Status#
GET /async/status returns your queue: queued, processing, priorityBreakdown ([{ "priority": 50, "count": 12 }]), last24h (completed, failed) and worker (the answering process's inflight and concurrency per lane). DELETE /async/queue cancels every task still QUEUED and refunds it: { "success": true, "cleared": 12, "creditsRefunded": 36 }.
Webhooks#
When a task ends as COMPLETED or FAILED (not when it is queued again for a retry), Searlo POSTs to webhook.url:
{ "taskId": "5f0c2b1e-7a3d-4c1b-9d0e-2a6f8b9c1d3e", "endpoint": "serp", "status": "COMPLETED", "result": { "success": true, "organic": [], "peopleAlsoAsk": [] }, "creditsUsed": 6, "completedAt": "2026-09-29T10:01:12.000Z"}A failed task sends error instead of result. Headers:
X-Searlo-Task-Id, andX-Searlo-Event(task.completedortask.failed)X-Searlo-Timestamp: milliseconds since the epochX-Searlo-Signature(when you set a secret):v1=followed by the hex HMAC-SHA256, keyed with your secret, of<timestamp>.<raw body>
Any 2xx counts as delivered; redirects are not followed. A failed delivery (another status, or no answer within a configurable timeout) is retried after 5 minutes, 30 minutes, 2 hours and 12 hours, then given up. The URL must be HTTPS and resolve to a public address; it is checked when you queue the task and again before each delivery.
Examples
# Queue a two-page SERP with a signed webhook (6 credits held)curl -X POST "https://api.searlo.tech/api/v1/async/task" \ -H "x-api-key: sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "serp", "request": { "q": "best running shoes", "gl": "us", "pages": 2 }, "webhook": { "url": "https://example.com/hooks/searlo", "secret": "whsec_change_me" }, "idempotencyKey": "serp-best-running-shoes-2026-09-29" }' # Poll itcurl "https://api.searlo.tech/api/v1/async/task/5f0c2b1e-7a3d-4c1b-9d0e-2a6f8b9c1d3e" \ -H "x-api-key: sk_your_api_key"import crypto from "node:crypto"; // Queue one AI answer per engine in a single callconst res = await fetch("https://api.searlo.tech/api/v1/async/task/batch", { method: "POST", headers: { "x-api-key": process.env.SEARLO_API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ tasks: ["chatgpt", "perplexity", "copilot"].map(engine => ({ endpoint: `ai/${engine}`, request: { q: "best crm for startups", gl: "us" }, webhook: { url: "https://example.com/hooks/searlo", secret: process.env.SEARLO_WEBHOOK_SECRET }, })), }),});const { accepted, total, tasks } = await res.json();console.log(`${accepted}/${total} queued`, tasks.map(t => t.taskId)); // In your webhook handler: verify the signature against the RAW request bodyfunction verifySearloSignature(rawBody, headers, secret) { const timestamp = headers["x-searlo-timestamp"]; const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"); const received = String(headers["x-searlo-signature"] || ""); return ( received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)) );}import osimport timeimport requests API = "https://api.searlo.tech/api/v1"HEADERS = {"x-api-key": os.environ["SEARLO_API_KEY"]} r = requests.post( f"{API}/async/task", json={ "endpoint": "rankings/check", # rankings/check reads keyword and domain; the queue also requires q "request": { "q": "project management software", "keyword": "project management software", "domain": "example.com", "depth": 20, }, }, headers=HEADERS,)r.raise_for_status()task_id = r.json()["task"]["taskId"] while True: task = requests.get(f"{API}/async/task/{task_id}", headers=HEADERS).json()["task"] if task["status"] in ("COMPLETED", "FAILED", "CANCELLED"): break time.sleep(5) print(task["status"], task.get("result") or task.get("error"), task["creditsUsed"])Stuck? Write to [email protected], or check live status.