Asynchronous work
Lookups and batches run in the background - the 202 answer, the states of a lookup and of a batch, how to poll, and webhooks as the alternative.
Classification does not happen inside the request that asks for it. The request stores the work and answers at once; the result is read afterwards, by polling or through a webhook.
POST /api/classify answers 202 Accepted with the lookup in the state queued. The same holds for a rerun
(POST /api/lookups/{lookup_id}/rerun), which answers 202 with a new lookup.
status |
Meaning | Final |
|---|---|---|
queued |
Stored, waiting for a worker | No |
processing |
Being classified. progress names the stage |
No |
proposed |
A code is proposed with enough support | Yes |
needs_review |
A code is proposed, and a person should check it. review_flags say why |
Yes |
needs_info |
The description does not decide between candidates. missing_info and result.questions say what to add; a rerun with the answers continues it |
Yes |
error |
The classification failed. error holds the message; the lookup is not charged |
Yes |
A lookup is finished when its status is neither queued nor processing. After that the status does not change
by itself. A reviewer's decision is recorded next to it, in decision (approved, overridden, rejected) and
final_code; it does not replace status.
While a lookup is processing, progress describes the run: the current stage, its index out of total,
and each stage with its state (done, running, skipped, pending). It is null before the run starts and
after it ends.
Polling a lookup
Read GET /api/lookups/{lookup_id} until the status is final:
import json, os, time, urllib.request
BASE = "https://htspilot.com/api"
HEADERS = {"X-API-Key": os.environ["HTS_PILOT_API_KEY"], "Content-Type": "application/json"}
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
request = urllib.request.Request(BASE + path, data=data, headers=HEADERS, method=method)
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
lookup = call("POST", "/classify", {"description": "Cotton terry bath towel, 70 x 140 cm", "market": "US"})
delay = 1.0
while lookup["status"] in ("queued", "processing"):
time.sleep(delay)
delay = min(delay * 1.5, 10)
lookup = call("GET", f"/lookups/{lookup['id']}")
print(lookup["status"], lookup["recommended_code"])
- Start at about one second and let the pause grow to ten.
- Stop after a few minutes and treat the lookup as still pending: read it again later, or rely on the webhook. Do not create it a second time: that is a second lookup.
- Do not poll many lookups in a tight loop. For volume, use a batch or a webhook.
A batch is a file of products. It is uploaded, checked, started, and then processed row by row in the background (Batches).
status |
Meaning | Final |
|---|---|---|
uploaded |
The file is stored and its rows were read. Nothing is classified yet | No |
queued |
Started, waiting for a worker | No |
running |
Rows are being classified | No |
completed |
Every row is done | Yes |
completed_with_errors |
Finished, and at least one row failed. POST /api/batches/{batch_id}/retry runs the failed rows again |
Yes |
error |
The batch itself failed. error holds the message |
Yes |
Polling a batch
Poll GET /api/batches/{batch_id}/progress, which is small:
{
"id": "9058ee5650844af9b9d6f473bdf3c544",
"status": "completed",
"updated_at": "2026-10-03T16:07:52.103466",
"total_rows": 3,
"processed_rows": 3,
"failed_rows": 0,
"error": null,
"counts": {
"done": 3
},
"version": "f3bb25533d548f36"
}
version is a token that changes whenever the batch or one of its rows changed. Keep the last one you saw and
read the rows again (GET /api/batches/{batch_id}/rows) only when it differs. The token is opaque: compare it,
do not parse it. A pause of two to five seconds between polls is enough.
Register a webhook and the API calls you when the work is done (Webhooks):
| Event | Sent when |
|---|---|
lookup.completed |
A lookup created by POST /api/classify or a rerun finished with a result: proposed, needs_review or needs_info. A lookup that ends in error sends no event, and neither do the rows of a batch |
batch.completed |
A batch reached completed or completed_with_errors |
lookup.decided |
A reviewer approved, overrode or rejected a lookup |
The event carries the ids and the outcome, not the whole object: read the lookup or the batch for the details. Because a failed lookup sends no event and a delivery can fail for good after its retries, keep a slow poll as a safety net for work that has been pending for longer than expected.