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#

FieldTypeRequiredDescription
endpointstringYesWhat to run (see the table below)
requestobjectNoThe parameters for that endpoint. Must include q (ai/… tasks also accept prompt), except for goto
webhookobjectNo{ "url": "https://…", "secret": "…" }. url is required inside it; secret (max 256 chars) signs each delivery
prioritynumberNo1–100 (default: 50). Higher runs first
idempotencyKeystringNoMax 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.

endpointCreditsRuns
serp3 per page; from 2026-11-01, +5 when the AI Overview had to be completed by rendering the pageFull 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/serp2 per pageGET /search/news/serp, with the same location rule
rankings/check3 per 10 results of depthRank Checks. Send keyword and domain as usual and repeat the keyword as q, which the queue requires
ai/chatgpt, ai/copilot8AI Answers
ai/perplexity2AI Answers
ai/gemini4AI Answers
web1Web search. request goes to the search service as is: use num (1–10), start, country and language, not limit, page, gl and hl
maps1Maps search. Use num for the result count; gl, hl, ll, lat, lng and zoom work as on /search/maps
shopping1Shopping search. Use num for the result count, plus gl and hl
goto0Resolves 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:

JSON
{  "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.

  • result appears once the task is COMPLETED. For serp, news/serp and ai/… it is the body the synchronous route returns; for rankings/check it is the check's result object with success: true.
  • error (code, message, retryable) appears when the task is FAILED.
  • A retryable failure (capacity, an unusable upstream page) is queued again after a delay, the engine's Retry-After when it gave one, up to a configurable number of attempts. After that, or on a non-retryable error, the task is FAILED and 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; expiresAt says 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:

JSON
{  "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:

JSON
{  "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, and X-Searlo-Event (task.completed or task.failed)
  • X-Searlo-Timestamp: milliseconds since the epoch
  • X-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.