Async results and polling
Three Miso endpoints do their work in the background. You submit a request, get an id, and poll until the job is over.
| API | Submit | Poll | You read |
|---|---|---|---|
| Answer | POST /v1/ask/questions |
GET /v1/ask/questions/{question_id}/answer |
answer, sources |
| Summary | POST /v1/ask/summary |
GET /v1/ask/summaries/{question_id}/summary |
answer, sources |
| Content Tagging | POST /v1/ask/tagging/{tag_set} |
GET /v1/ask/tagging/{tag_set}/{tagging_id} |
tags |
They report progress the same way, so one polling client serves all three.
finished and finish_reason
finished says the job is over. finish_reason says whether it worked.
Do not branch on the progress label.
answer_stageon Answer and Summary, andstageon Tagging, are written for a person reading the response. They do not mark the end of the job. A successful tagging leavesstageat"Done", and only a failure sets"Failed". New stage names can appear at any time.
Is the job over?
finished |
What it means | What to do |
|---|---|---|
false |
The job is still running. | Poll again. |
true |
The job is over. | Read finish_reason. |
A 404 on an early poll is not a failure. A new id takes a moment to become
queryable. Wait 2 to 4 seconds before your first poll. Treat a 404 in the
first 10 seconds as "not ready yet".
After that window, a 404 means Miso lost the job. Submit the original request
again rather than giving up: the id came from your own submit, so it was valid
when you got it.
Did it succeed?
| Value | Meaning | Retry |
|---|---|---|
not_finished |
The job is still running. | Poll again. |
success |
The result is complete. | Not needed. |
error |
The pipeline failed. | Yes. Bounded retry, same payload. |
forbidden |
Miso withheld the result. blocked_reason says why. |
No. |
forbidden is not a fault on your side or on Miso's. It is the rate limit, the
query check, or metering turning the request down, and blocked_reason names
which one. Show the reader the text in answer.
Treat any value you do not know as a failure.
An empty result is not a failure signal
Read finish_reason before you read the result.
finish_reason |
Result | What it means |
|---|---|---|
success |
Filled | The normal case. |
success |
Empty | Miso found nothing to return. This is an answer, not an error. Accept it. |
| A failure value | Anything | A processing failure. The result is absent or partial. Discard it. |
An empty result on success does not improve if you ask again.
If the job never finishes
If finished is still false after 3 minutes, treat the job as lost and
submit the original request again.
Set your own limit above the slowest request you see in practice. A summary with
rules, or one over a large article set, runs longer than a plain one.
HTTP status
| Status | Meaning | Retry |
|---|---|---|
200 |
The poll worked. Read finished and finish_reason. |
— |
401 |
The API key is missing or wrong. | No. |
404 |
Not ready yet inside the grace window. A lost job after it. | Yes. Submit the request again. |
422 |
The request body is invalid. Read detail. |
No. Fix the body. |
429 |
Over the rate limit. | Yes. Back off first. |
500 |
A fault on Miso's side. | Yes. Bounded retry. |
See Errors & Rate Limits for the error body itself.
A polling loop
One function submits, polls, and submits again when the job is lost. It either returns the result or gives up.
repeat 3 times:
id = submit the request
wait 3 seconds
start = now
loop:
GET the poll endpoint
404, under 10 seconds wait 2 seconds, poll again # not ready yet
404, after that leave the loop, submit again # the job is gone
429 or 5xx wait, poll again # Miso is busy
not finished, under 3 min wait 2 seconds, poll again
not finished, over 3 min leave the loop, submit again
finish_reason "success" return the result, even when empty
finish_reason "error" leave the loop, submit again
finish_reason "forbidden" give up, read blocked_reason
give up
Only forbidden ends it on the spot. Everything else is worth another submit,
bounded by the attempt count.
Each version takes a submit callback and the poll URL.
def get_result(submit, poll_url, api_key, attempts=3):
"""Submit, poll, and submit again when the job is lost.
Returns the finished `data`, or None when it is not worth asking again.
"""
session = requests.Session()
session.headers["X-Api-Key"] = api_key
for _ in range(attempts):
job_id = submit()
time.sleep(3) # a new id is not queryable at once
start = time.monotonic()
while True:
response = session.get(poll_url(job_id), timeout=30)
waited = time.monotonic() - start
if response.status_code == 404:
if waited < 10:
time.sleep(2) # not ready yet, not lost
continue
break # the job is gone, submit it again
if response.status_code == 429 or response.status_code >= 500:
time.sleep(5) # Miso is busy
continue
data = response.json()["data"]
if not data["finished"]:
if waited > 180:
break # lost, submit it again
time.sleep(2)
continue
if data["finish_reason"] == "success":
return data # accept it, even when empty
if data["finish_reason"] == "error":
break # a fault, submit it again
return None # forbidden, read blocked_reason
return None # out of attempts
// Submit, poll, and submit again when the job is lost.
// Returns the finished `data`, or null when it is not worth asking again.
async function getResult(submit, pollUrl, apiKey, attempts = 3) {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
for (let attempt = 0; attempt < attempts; attempt++) {
const jobId = await submit();
await wait(3000); // a new id is not queryable at once
const start = Date.now();
let lost = false;
while (!lost) {
const response = await fetch(pollUrl(jobId), {
headers: { "X-Api-Key": apiKey },
});
const waited = Date.now() - start;
if (response.status === 404) {
if (waited < 10_000) { await wait(2000); continue; } // not ready yet
lost = true; continue; // the job is gone, submit it again
}
if (response.status === 429 || response.status >= 500) {
await wait(5000); // Miso is busy
continue;
}
const { data } = await response.json();
if (!data.finished) {
if (waited > 180_000) { lost = true; continue; } // submit again
await wait(2000);
continue;
}
if (data.finish_reason === "success") return data; // even when empty
if (data.finish_reason === "error") { lost = true; continue; }
return null; // forbidden, read data.blocked_reason
}
}
return null; // out of attempts
}
# Submit, poll, and submit again when the job is lost.
# Prints the finished `data`. Exit 1 means it is not worth asking again.
get_result() { # get_result <submit-cmd> <poll-url-prefix>
local attempt job_id waited start status out=$(mktemp)
for attempt in 1 2 3; do
job_id=$($1) # your submit command prints the new id
sleep 3 # a new id is not queryable at once
start=$SECONDS
while :; do
status=$(curl -sS -o "$out" -w '%{http_code}' -H "X-Api-Key: $KEY" "$2/$job_id")
waited=$(( SECONDS - start ))
case $status in
404) (( waited < 10 )) && { sleep 2; continue; } # not ready yet
break ;; # gone, submit again
429|5??) sleep 5; continue ;; # Miso is busy
esac
if [ "$(jq -r .data.finished "$out")" != true ]; then
(( waited > 180 )) && break # lost, submit again
sleep 2; continue
fi
case $(jq -r .data.finish_reason "$out") in
success) jq .data "$out"; return 0 ;; # even when empty
error) break ;; # submit again
*) return 1 ;; # forbidden
esac
done
done
return 1 # out of attempts
}
The grace window keeps an early 404 from ending the job. The stall guard
keeps a lost job from hanging your worker. Backing off on a 429 keeps a busy
minute from turning into a failed document.
How long to wait between polls
| API | Typical run | Poll every |
|---|---|---|
| Answer | About 12 seconds | 1 to 2 seconds |
| Summary | About 16 seconds for 3 bullets over 10 articles | 1 to 2 seconds |
| Content Tagging | 30 to 50 seconds for one news article | 2 to 3 seconds |
Answer and Summary fill the body as the model writes, so a short interval lets you render the text while it arrives. Tagging returns nothing until it is done, so a short interval only costs you requests.
Get the result in one call
Every one of these endpoints takes ?wait_for_answer=true on the POST. The
request then blocks and returns the finished result.
Use it in a script or a test. In production, poll. The connection can time out after about 60 seconds, and the slowest requests run longer than that.
Next
- Answer API — the fields an answer carries.
- Summary API —
rules, and what they do to the result. - Content Tagging — the shape of a tag result.
- Errors & Rate Limits — status codes across every endpoint.
