Async Results & Polling
One polling contract for Answer, Summary and Content Tagging.
Three Miso endpoints do their work in the background. You submit a request, get an id, and poll until the job is over. Below we show what to read, what to retry, and the loop that does both.
| 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.
What an empty result means
finish_reason tells you how to read the result, so read it first.
finish_reason |
Result | What it means |
|---|---|---|
success |
Filled | The normal case. |
success |
Empty | The job worked, and Miso found nothing to return. |
| A failure value | Anything | The job failed. The result is absent or partial. |
An empty result on success is complete. The same request returns the same
thing, so there is nothing to gain from asking 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.
