semust logo

API Platform

Tasks & Long-Running Requests

Requests that take longer than a minute return 202 with a task id. Collect the result later, retry safely, and never pay twice.

Some requests take longer than an HTTP connection should stay open. On the endpoints below, every request runs as a task. If it finishes within 60 seconds, you get the result exactly as before. If it does not, you get 202 Accepted with a task_id, and you collect the result from GET /v1/tasks/{task_id} when it is ready. The work is not lost and you are not charged twice.

Which endpoints run as tasks

Endpointservice
POST /v1/serpserp
POST /v1/keyword-datakeyword_data
POST /v1/domain-researchdomain_research
POST /v1/youtubeyoutube
POST /v1/trendstrends
POST /v1/on-pageon_page
POST /v1/business-databusiness_data

Every other endpoint, including Backlinks, Google Images and Technical SEO, still answers synchronously, as before. More endpoints will follow.

How it works

  1. Send the request as usual

    Same URL, same headers, same body. We store the task before any work starts, then hold the connection open for up to wait seconds (60 by default).

  2. Finished in time: you get the result

    The status code and body are the same as they have always been. The only addition is an X-Task-Id header that holds the task id.

  3. Still running: you get 202

    The response has a Location header pointing to the task and a Retry-After header saying how many seconds to wait before asking again.

  4. Collect the result

    Call GET /v1/tasks/{task_id} after Retry-After seconds, and repeat until the status is no longer 202. The final response is exactly what the POST would have returned.

Integrations written before tasks keep working for every request that finishes within a minute. Every client should still handle 202, because any request can take longer than wait, and during a deploy a waiting request can receive its 202 early. The helper functions in Examples handle it for you.

Where to find the task id

Every task has an id, a UUID like 4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39. It comes with every answer, so you can always go back to a result:

What you receivedWhere the task id is
202 Acceptedtask_id in the body, the X-Task-Id header, and the end of Location and poll_url
A result (200) or an error, straight from the POSTThe X-Task-Id header. Domain Research, YouTube, Trends, On-Page and Business Data also put it in the body as request_id
An answer from GET /v1/tasks/{task_id}The X-Task-Id header
Nothing: your process crashed or lost the responseGET /v1/tasks lists your recent tasks with their task_id (see Listing tasks)

task_id, request_id, X-Task-Id and X-Request-Id are always the same value. X-Request-Id stays for clients written before tasks existed. Log the task id with every request: it is how you fetch a result again, and how our support traces a request end to end.

Free cache hits have no task id

A SERP answered from the free cache is returned at once without creating a task, so it has no X-Task-Id. Every request that does work or is charged has one.

The 202 response

When a task is still running after wait seconds, the POST returns 202 Accepted:

HeaderValue
Location/v1/tasks/{task_id}, the path to poll
Retry-AfterSeconds to wait before polling
X-Task-IdThe task id
X-Request-IdThe task id (same value, kept for older clients)
X-Task-Statuspending
HTTP/1.1 202 Accepted
Location: /v1/tasks/4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39
Retry-After: 5
X-Task-Id: 4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39
X-Request-Id: 4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39
X-Task-Status: pending
Content-Type: application/json
{
  "success": true,
  "status": "pending",
  "task_id": "4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39",
  "request_id": "4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39",
  "service": "serp",
  "poll_url": "https://data.semust.com/v1/tasks/4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39",
  "retry_after": 5,
  "created_at": "2026-11-06T13:15:26Z",
  "elapsed_seconds": 60,
  "deadline_at": "2026-11-06T13:25:26Z",
  "deadline_in_seconds": 540
}
FieldTypeDescription
successbooleanAlways true: the request was accepted
statusstringpending
task_idstringThe task's UUID
request_idstringSame as task_id
servicestringserp, keyword_data, domain_research, youtube or trends
poll_urlstringFull URL of GET /v1/tasks/{task_id}
retry_afterintegerSame as the Retry-After header
created_atstringWhen the task was created, RFC 3339 in UTC
elapsed_secondsintegerSeconds since created_at
deadline_atstringWhen an unfinished task is given up: it then fails with 504 TIMEOUT and is refunded. RFC 3339 in UTC
deadline_in_secondsintegerSeconds left until deadline_at. Counts down on every poll, so you know how long polling can last

success: true does not mean the result is here

A 202 body has "success": true because the request was accepted, not because it finished. Branch on the HTTP status code. A client that checks only success and then reads items or organic will find them missing.

Polling a task

GET /v1/tasks/{task_id} with the same SEMUST-API-USER and SEMUST-API-PASSWORD headers you used for the POST.

Task stateStatus codeBodyHeaders
Running202The pending body above; elapsed_seconds keeps growingRetry-After, Location, X-Task-Id, X-Task-Status: pending
CompletedWhat the POST would have returned, normally 200The endpoint's normal response, byte for byteX-Task-Status: completed, X-Task-Expires-At, X-Task-Expires-In, X-Task-Id
FailedThe endpoint's normal error status, e.g. 502 or 404The endpoint's normal error body, e.g. REQUEST_FAILED, SERP_FAILED, VIDEO_NOT_FOUNDX-Task-Status: failed, X-Task-Expires-At, X-Task-Expires-In, X-Task-Id
Result deleted410TASK_EXPIRED
Unknown id, or another API user's task404TASK_NOT_FOUND
Not a UUID400INVALID_TASK_ID

A finished task answers with the same status and the same body bytes the POST would have returned, so the response parser you already have handles it unchanged. Only the X-Task-* headers tell the two apart:

  • X-Task-Status: completed means the task succeeded and was charged. failed means it was not charged.
  • X-Task-Expires-At is the time (RFC 3339) after which the result is deleted, and X-Task-Expires-In the seconds left until then.

These headers come with every finished answer, including a result returned directly on the POST, so you always know whether you were charged and how long you can fetch the result again:

HTTP/1.1 200 OK
X-Task-Id: 4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39
X-Task-Status: completed
X-Task-Expires-At: 2026-11-09T13:16:41Z
X-Task-Expires-In: 259200

Other rules:

  • Fetch as often as you like. A finished result can be fetched any number of times until it expires. Fetching is free.
  • Only the creator can read it. A task can be read only by the API user that created it. Any other API user, including another API user on the same account, gets 404 TASK_NOT_FOUND.
  • Synchronous answers are tasks too. Every request that does real work is stored as a task, including the ones answered within wait. Any response with an X-Task-Id header can be fetched again by that id within the retention window.
  • Polling does not wait. GET /v1/tasks/{task_id} answers immediately. Honour Retry-After between calls: 3 seconds during a task's first 30 seconds, 5 seconds until 2 minutes, then 10 seconds.

The wait parameter

?wait= sets how many seconds the POST holds the connection open before answering 202.

waitBehaviour
Not sent60 seconds
1–80Wait up to that many seconds for the result
0Answer 202 immediately, without waiting
Anything else400 INVALID_PARAMETER with "field": "wait"

The maximum is 80 because the edge in front of the API cuts connections at 100 seconds. Set your HTTP client's read timeout above wait, for example wait + 30 seconds.

wait=0 is fire-and-collect: submit a batch of requests without holding a connection open for each, then collect the results by polling or by listing. A request that finishes instantly, such as a cache hit, is still answered with its result straight away.

POST https://data.semust.com/v1/domain-research?wait=0

Listing tasks

GET /v1/tasks lists your recent tasks whose results can still be fetched, newest first. It uses the same headers and is free.

Query parameterValuesDefault
statuspending, completed, failedall
serviceserp, keyword_data, domain_research, youtube, trendsall
fetchedtrue or falseboth
page1 or more1
limit1–10050

An invalid status, fetched, page or limit returns 400 INVALID_PARAMETER, and field names the parameter.

curl "https://data.semust.com/v1/tasks?status=completed&service=domain_research&limit=20" \
  -H "SEMUST-API-USER: your_username" \
  -H "SEMUST-API-PASSWORD: your_password"
{
  "success": true,
  "total": 2,
  "page": 1,
  "limit": 50,
  "items": [
    {
      "task_id": "9d3a6c1e-27b4-4f0a-8e5d-3c1b7a9f2e60",
      "service": "serp",
      "status": "pending",
      "http_status": null,
      "cost": null,
      "fetched": false,
      "poll_url": "https://data.semust.com/v1/tasks/9d3a6c1e-27b4-4f0a-8e5d-3c1b7a9f2e60",
      "created_at": "2026-11-06T13:17:02Z",
      "completed_at": null,
      "expires_at": null,
      "expires_in_seconds": null,
      "deadline_at": "2026-11-06T13:27:02Z"
    },
    {
      "task_id": "4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39",
      "service": "domain_research",
      "status": "completed",
      "http_status": 200,
      "cost": 0.0264,
      "fetched": false,
      "poll_url": "https://data.semust.com/v1/tasks/4b7e2f1c-9a3d-4e8b-b6c2-0d5f8a1e7c39",
      "created_at": "2026-11-06T13:15:26Z",
      "completed_at": "2026-11-06T13:16:41Z",
      "expires_at": "2026-11-09T13:16:41Z",
      "expires_in_seconds": 259080,
      "deadline_at": null
    }
  ]
}
FieldTypeDescription
task_idstringThe task's UUID
servicestringWhich endpoint it belongs to
statusstringpending, completed or failed
http_statusinteger | nullStatus code of the result; null while pending
costnumber | nullWhat was charged, in USD; null unless completed
fetchedbooleanWhether the finished result has reached you — on the POST itself, a replay, or GET /v1/tasks/{task_id}
poll_urlstringWhere to fetch the result
created_atstringWhen the task was created
completed_atstring | nullWhen it finished; null while pending
expires_atstring | nullWhen the result will be deleted; null while pending
expires_in_secondsinteger | nullSeconds left until expires_at; null while pending
deadline_atstring | nullWhile pending: when the task is given up and refunded; null once finished

total is the number of matching tasks; page and limit are the paging applied.

Recovering lost work

If your process crashed, lost a 202, or never waited for one, GET /v1/tasks?fetched=false lists the finished results that have not reached you yet. Fetch each poll_url and the task is marked as fetched.

A result that was returned to you directly on the POST already counts as fetched, so fetched=false works as an inbox: it holds only results nobody has received.

Idempotency

Send an Idempotency-Key header to make a POST safe to repeat. Generate one key, such as a UUID, for each logical request, and send the same key again when you retry after a network error or a client timeout.

  • Up to 128 characters. A longer key returns 400 INVALID_PARAMETER with "field": "Idempotency-Key".
  • Works on every task endpoint. Domain Research, YouTube and Trends also keep their idempotency_key body field. If you send both, the body field wins.
  • The same key from the same API user to the same endpoint always refers to the same task:
The original task isThe repeat gets
Still runningIts 202
FinishedIts stored result, with the same status and body

The task is charged once, however many times you send the key. The key stays reserved as long as the result is stored, which is 72 hours after the task finishes. After that, the same key starts a new task.

The body of a repeated request is not compared with the original. A request with different parameters needs a new key.

curl -X POST https://data.semust.com/v1/serp \
  -H "SEMUST-API-USER: your_username" \
  -H "SEMUST-API-PASSWORD: your_password" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0c5d2e8a-61f4-4b39-a7de-58c90f2b4e17" \
  -d '{"keyword": "seo tools", "language": "en", "country": "US"}'

Retention

Rule
How long a result is kept72 hours after the task finishes
Exact expiryX-Task-Expires-At and X-Task-Expires-In (seconds left) headers on every finished answer; expires_at and expires_in_seconds in the list
After expiryThe result is deleted; GET /v1/tasks/{task_id} returns 410 TASK_EXPIRED
Results larger than 5 MB compressedNot stored; fetching returns 410 TASK_EXPIRED. This practically never happens on these endpoints

Store anything you need longer on your side. Eventually the task record itself is removed, and its id then returns 404 TASK_NOT_FOUND.

Billing

  • A task is charged when it completes successfully, whether or not you ever poll it.
  • A failed task is refunded in full, including one that passed its deadline.
  • Polling and listing are free.
  • Cache hits work as before. They are answered immediately and billed as each endpoint's page describes.
  • Each endpoint's pricing is unchanged. The balance is checked and held when you POST, so 402 INSUFFICIENT_CREDITS comes back on the POST, never on a poll. Whatever the task does not use is refunded when it finishes.

Limits

LimitValueWhen exceeded
POST requestsYour account's per-minute and per-day limits, unchanged429 RATE_LIMIT_EXCEEDED
GET /v1/tasks and GET /v1/tasks/{task_id}600 per minute per API user, in their own bucket429 RATE_LIMIT_EXCEEDED
Unfinished tasks100 per API user429 TOO_MANY_PENDING_TASKS, Retry-After: 10

Polling does not count against the per-minute or daily limits of your POST requests, so collecting results never stops you from submitting new ones.

With many tasks in flight, call GET /v1/tasks?fetched=false every few seconds and fetch only the finished ones, rather than polling each task separately.

Per-endpoint concurrency still applies

Domain Research and Trends run at most 5 and YouTube at most 10 requests per API user at the same time. Tasks you are not waiting on still count until they finish, so with wait=0 a larger batch gets 429 TOO_MANY_CONCURRENT_REQUESTS with Retry-After. Submit the next request when one finishes.

Reliability

You can count on the following:

  • The task is stored before work starts. If you get a task_id back, the task exists.
  • Results travel through a durable queue. A deploy or restart of our API does not lose a task. It finishes, and you can fetch it.
  • Your connection is not the task's lifeline. If you disconnect, time out or crash while waiting, the task carries on. Fetch it by id, or find it with GET /v1/tasks?fetched=false.
  • Every task has a 10-minute deadline. If no result has arrived by then, the task is marked failed with 504 TIMEOUT and refunded, within a few minutes of the deadline. Keep polling until you get a final answer rather than giving up at exactly 10 minutes.
  • Money moves exactly once per task. One hold, then one charge or one refund, however many times you poll or repeat the same Idempotency-Key.

Errors

Errors have the shape {"error", "code", "hint", "retryable", ...}. Send Accept-Language: tr to get error and hint in Turkish; code never changes.

StatusCodeMeaningWhat to do
400INVALID_TASK_IDThe task id is not a UUIDUse the task_id from a 202 or from GET /v1/tasks
400INVALID_PARAMETERwait, Idempotency-Key or a list filter is invalid; field names itSee the rules above
404TASK_NOT_FOUNDNo such task, or it belongs to another API userRead it with the API user that created it
410TASK_EXPIREDThe result is no longer storedResults are kept for 72 hours after a task finishes; send the request again
429TOO_MANY_PENDING_TASKS100 tasks are still running for this API userRetry after Retry-After (10) seconds
429RATE_LIMIT_EXCEEDEDMore than 600 polls in a minute, or your POST limits are reachedSlow down
504TIMEOUTThe task did not finish in time, e.g. it passed its 10-minute deadline. Not chargedSend the request again

TIMEOUT no longer means a slow HTTP request. A slow request becomes a 202, and 504 TIMEOUT is the final answer of a task that failed to finish. See Error Codes for the full list.

{
  "error": "Task not found",
  "code": "TASK_NOT_FOUND",
  "hint": "A task can only be read with the API user that created it",
  "retryable": false
}

Examples

A call(path, body) helper that sends the POST, polls while it gets 202 (honouring Retry-After), and returns the final response. Every request gets an Idempotency-Key, so a POST that fails with a network error is sent again safely. You handle the returned response exactly as you did before tasks existed.

import time
import uuid

import requests

BASE_URL = "https://data.semust.com"
AUTH = {
    "SEMUST-API-USER": "your_username",
    "SEMUST-API-PASSWORD": "your_password",
}


def call(path, body, wait=60, give_up_after=900):
    """POST to a Semust Data API endpoint and return the final response."""
    headers = {**AUTH, "Idempotency-Key": str(uuid.uuid4())}

    # A retry with the same Idempotency-Key joins the same task and is charged once.
    for attempt in range(3):
        try:
            response = requests.post(
                BASE_URL + path,
                params={"wait": wait},
                headers=headers,
                json=body,
                timeout=wait + 30,
            )
            break
        except (requests.ConnectionError, requests.Timeout):
            if attempt == 2:
                raise
            time.sleep(2 ** attempt)

    deadline = time.monotonic() + give_up_after
    while response.status_code == 202:
        task = response.json()
        if time.monotonic() > deadline:
            raise TimeoutError(f"task {task['task_id']} is still running")
        time.sleep(int(response.headers.get("Retry-After", task["retry_after"])))
        response = requests.get(task["poll_url"], headers=AUTH, timeout=30)

    return response


response = call(
    "/v1/domain-research",
    {"type": "keywords", "domain": "example.com.tr", "limit": 100},
)
data = response.json()
if response.status_code != 200:
    raise SystemExit(f"{data['code']}: {data['error']} (task {response.headers.get('X-Task-Id')})")

print(f"{data['count']} keywords, cost ${data['cost']:.6f}")
Last updated: 2026-11-06