User History
Save a reader's questions, and let them reopen the thread.
User History saves every question a signed-in reader asks. The reader can reopen past conversations. Miso can also tell the reader when it has a fresh answer to an earlier question. Below we build a history page, then a fresh-answer indicator.
Two capabilities, one API:
- User History — Miso saves each question a signed-in user asks and groups the follow-ups into conversation threads. You can list, reopen, rename, and delete them.
- Answer Updates — a user can subscribe to a thread. Miso then watches that topic. When Miso finds important new content, it appends a fresh follow-up answer and raises an unread indicator.
Availability. Miso enables User History and Answer Updates per app. To turn them on for your app, contact your Miso representative.
All endpoints use POST. Pass the API key as ?api_key=YOUR_KEY or in the
X-Api-Key header. Base URL: https://api.askmiso.com.
When the feature is off
User history is enabled per app. Until Miso turns it on, every endpoint on this
page returns 403, and no history is recorded:
{
"message": "user history is not enabled for this app"
}
Ask your Miso representative to enable it. A 403 here is a setting, not your
key: the same key reaches the rest of the API.
How records are created
You do not write history yourself. Miso does it automatically. When a signed-in
user asks a question through the Answer API
(POST /v1/ask/questions), Miso records it in the background.
- Only the first question of a conversation becomes a history row (a thread). Miso attaches the follow-ups to that thread and moves it to the top of the list.
- Miso records only questions from an authenticated user. It never saves an anonymous question to history.
Exclude a question from history
To keep a question out of history, pass log_user_history: false on
POST /v1/ask/questions. Miso then does not save that question. Use this for a
per-question "private mode" toggle. The default is true.
{
"user_id": "alice",
"question": "How do interest rate cuts affect mortgages?",
"log_user_history": false
}
This controls only the saved history for the user. Your usage analytics do not change.
Authentication
Every User History endpoint needs your API key and an authenticated user. You authenticate the user in one of two ways. The method depends on which key you use.
| Key type | Where the user comes from | JWT |
|---|---|---|
| Publishable key (browser) | the JWT | Required |
| Secret key (server) | user_id in the request body |
Optional |
-
Secret key (server-side). The key authenticates the caller. You pass
user_idin the body, and Miso trusts it. -
Publishable key (browser). This key is safe to put in the browser. It must
carry a signed JWT that proves who the user is. Pass the JWT as
Authorization: Bearer <token>(preferred) or in ajwt_tokenbody field. Sign the JWT with your app's secret key and the HS256 algorithm. Itsuser_id(orsub) claim is the authenticated user.
If a request supplies user_id in both the JWT and the body, the two values
must match. If they do not match, Miso returns 401.
Create a browser JWT (server-side)
Sign a short-lived token on your server with your secret key. Then send the token to the browser.
import hmac, hashlib, base64, json, time
def b64(d): return base64.urlsafe_b64encode(d).rstrip(b"=").decode()
secret = "YOUR_SECRET_API_KEY" # sign with your app's SECRET key
header = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
claims = {"user_id": "alice", "exp": int(time.time()) + 3600}
payload = b64(json.dumps(claims).encode())
signing = f"{header}.{payload}".encode()
sig = b64(hmac.new(secret.encode(), signing, hashlib.sha256).digest())
jwt = f"{header}.{payload}.{sig}"
Read and manage history
A thread is a root question plus the follow-ups that hang off it. The root
question id is the thread_id. Reading a thread is two calls: …/thread
for the ids, then /v1/ask/answers for the answers in one round trip.
List a user's threads
POST /v1/ask/user_history/list
| Field | Type | Default | Notes |
|---|---|---|---|
user_id |
string | required | The signed-in user. |
rows |
integer | 100 |
Page size, 1–200. |
start |
integer | 0 |
Pagination offset. |
order |
string | desc |
desc = newest thread first. asc = oldest first. |
before |
datetime | — | Return only threads before this time (cursor paging). |
The response is an object, and it tells you whether another page exists:
{
"message": "success",
"data": {
"threads": [
{
"time": "2026-06-12T10:15:00Z",
"thread_id": "0e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"title": "How do interest rate cuts affect mortgages?",
"subscribed": true,
"has_new": false
}
],
"has_more": true,
"start": 0,
"rows": 20
}
}
curl -X POST "https://api.askmiso.com/v1/ask/user_history/list" \
-H "X-Api-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "alice",
"rows": 20
}'
const res = await fetch("https://api.askmiso.com/v1/ask/user_history/list", {
method: "POST",
headers: {
"X-Api-Key": KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: "alice",
rows: 20
}),
});
const data = (await res.json()).data;
import requests
res = requests.post(
"https://api.askmiso.com/v1/ask/user_history/list",
headers={"X-Api-Key": KEY},
json={
"user_id": "alice",
"rows": 20
},
)
data = res.json()["data"]
| Field | What it is |
|---|---|
thread_id |
The thread. Pass it to every other endpoint on this page. |
title |
The thread's display name. Render it as the row label. Miso writes it from the first question, and rename replaces it. |
time |
The last activity. The list shows the most recent first. |
subscribed |
Whether this thread subscribes to Answer Updates. |
has_new |
The thread-level unread indicator. An unseen update is ready. |
Read a thread
POST /v1/ask/user_history/thread
Returns the question_id values in a thread, in order. The root question comes
first, then the follow-ups.
| Field | Type | Default | Notes |
|---|---|---|---|
thread_id |
UUID | required | The thread's root question id. |
user_id |
string | required | Miso confirms that this user owns the thread. |
rows |
integer | 30 |
Page size (max 100). |
after |
UUID | — | Cursor — return the questions after this id. |
order |
string | asc |
asc = root → newest. desc = newest → root. |
Send
user_idon every call. Miso checks that the thread belongs to that reader, and answers404 {"message": "Thread not found"}when it does not. A thread that does not exist answers the same404, on purpose: a separate status confirms which thread ids are real.
To open a long thread at the newest answer, send order: "desc". The first
page then holds the most recent questions, and after pages backwards
toward the root. This is what a chat view wants: render the newest turn at
once, and fetch the older ones as the reader scrolls up.
{
"message": "success",
"data": {
"question_ids": [
"0e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
],
"has_more": false
}
}
Paging. To get the next page, pass the last question_id of the current
page as after. Repeat until has_more is false.
Fetch answers in bulk
POST /v1/ask/answers
After you have the question_id values of a thread, fetch all their answers in
one request. This is faster than one call per question. This endpoint reads
existing answers only. It does not re-run the answer pipeline.
| Field | Type | Notes |
|---|---|---|
question_ids |
UUID[] | The ids to fetch (1–50). |
curl -X POST "https://api.askmiso.com/v1/ask/answers" \
-H "X-Api-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"question_ids": [
"0e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
]
}'
const res = await fetch("https://api.askmiso.com/v1/ask/answers", {
method: "POST",
headers: {
"X-Api-Key": KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
question_ids: [
"0e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
]
}),
});
const data = (await res.json()).data;
import requests
res = requests.post(
"https://api.askmiso.com/v1/ask/answers",
headers={"X-Api-Key": KEY},
json={
"question_ids": [
"0e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
]
},
)
data = res.json()["data"]
{
"message": "success",
"data": [
{
"question_id": "0e1f2a3b-…",
"question": "…",
"answer": "…",
"sources": []
},
null
]
}
-
The
dataarray matches your input.data[i]corresponds toquestion_ids[i]. -
An id that does not exist, or does not belong to your app, returns
null. As a result, answers never leak across apps. -
Each entry matches the
dataofGET /v1/ask/questions/{question_id}/answer(see Answer response fields). - To render a whole thread, use this endpoint instead of many single fetches. It makes one round trip, not many.
Rename a thread
POST /v1/ask/user_history/thread/rename
| Field | Type | Notes |
|---|---|---|
thread_id |
UUID | The thread's root question id. |
user_id |
string | Required. |
title |
string | The new display title (max 1000 chars). |
Miso echoes the new name:
{
"message": "success",
"data": {
"thread_id": "0e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"title": "Rocky Linux — renamed"
}
}
The list endpoint then returns it as each row's title. If the thread does
not exist, the call returns 404.
Miso titles a thread from its first question. If the question is longer than about 60 characters, Miso generates a short title.
Delete specific threads
POST /v1/ask/user_history/delete
| Field | Type | Notes |
|---|---|---|
thread_ids |
UUID[] | The threads to delete. |
user_id |
string | Required. |
{
"user_id": "alice",
"thread_ids": ["2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"]
}
thread_ids holds the same value as /thread, /thread/rename and the update
endpoints, so one identifier covers the whole API.
Deleting works per thread, not per question. A thread is one row of
history, and only the root question is written there. A follow-up's question id
matches nothing, and the call returns 200 with deleted_count: 0. Send the
thread_id the list endpoint gives you.
Read deleted_count and compare it with the number of ids you sent:
{
"message": "success",
"data": {
"deleted_count": 2
}
}
An empty list means no history
A read that fails now answers 500. It no longer returns an empty list, and a
delete that fails no longer answers deleted_count: 0.
That matters for what you render. An empty threads array means the reader has
no history, so draw the empty state. Before, a storage fault looked exactly the
same, so a reader with a full sidebar watched it come back blank.
Treat 500 as "try again", and keep whatever you already had on screen.
Delete all history
POST /v1/ask/user_history/delete_all
| Field | Type | Notes |
|---|---|---|
user_id |
string | Required. |
Every delete is a soft delete. Miso can reverse it. But the record disappears from every read immediately.
Putting it together
A typical front-end flow:
-
On load — call
…/user_history/listfor the sidebar. Read each thread'shas_newfield for its own indicator, andhas_moreto page. -
Open a thread — call
…/threadwith itsthread_id, then…/answersto fetch every answer in one call. - Let users manage history —
…/rename,…/delete, and…/delete_all.
Next
Answer Updates is the next step. It covers how a reader subscribes to a thread, how Miso appends a fresh answer when the story moves on, and how the indicators clear.
