POST /v1/memories adds text, a document or a conversation to the library, and POST /v1/search and POST /v1/answer get it back. Each add takes exactly one of text, file or messages. Search and answers find a memory once its status is ready.
Conversation snapshots#
The messages field preserves speakers, turn order and optional timestamps. Each turn has a role of user or assistant, a nonblank content, and an optional created_at timestamp including its time zone. Array order is authoritative. The limit is 200 turns and 30,000 rendered characters, including the turn headings; larger inputs return 422 before storage.
Request
curl -X POST https://api.fryri.com/v1/memories \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "The review is Tuesday."},
{"role": "assistant", "content": "Noted."},
{"role": "user", "content": "It has moved to Wednesday."}
],
"title": "Review planning",
"source_ref": "thread:planning",
"end_user_id": "user_42"
}'The returned type is conversation. Send the complete updated snapshot with the same source_ref when more turns arrive. This replaces the previous snapshot; it does not append. Poll its status before asking a question. Reading the memory returns the original turns under messages on the first page, plus readable transcript text and line coordinates. The downloadable original is canonical JSON; sha256 hashes those original bytes.
Answers read relevant conversation history for corrections and cite the supporting turn. Conflicting sources without an explicit correction remain a conflict, and assistant suggestions are not confirmed user facts. This interpretation uses the answering model; ready means the source is processed and searchable, not that every possible factual conflict has been resolved.
Answer citations#
In an answer, [1] links to the source whose citation is 1. Its cited field is true only when that marker resolves to source lines delivered during the answer and the source revision is still current. location contains inclusive, 1-based line_start and line_end, with turn_start and turn_end for conversation sources. text contains up to 300 characters from those lines, and text_truncated reports whether more remains in the cited range. Open the memory to read it in full.
A source with cited: false is retrieved context, not proof that the answer used it. Such sources have no numbered citation. The final streaming done event has the same citation fields as a non-streaming response; replace the accumulated answer with its final answer.
Citation checks verify the source and quoted lines. Whether those lines support the answer still depends on the answering model.
If identical conversation JSON was previously stored in an incompatible format, the add returns 409 with memory_format_conflict. The existing source remains intact; remove it explicitly before importing those bytes as a conversation.
Three ways to add#
Text. Send text you already have, such as a note, a message or a transcript.
Request
curl -X POST https://api.fryri.com/v1/memories \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Project Atlas uses Python and FastAPI. The launch review is on 14 October.",
"title": "Project Atlas notes",
"source_ref": "crm:note:881",
"end_user_id": "user_42"
}'Response
{
"id": "file_NkolfLrElMmM2fCn",
"type": "text",
"title": "Project Atlas notes",
"status": "processing",
"status_reason": null,
"created_at": "2026-09-25T09:02:54.915644Z",
"source_ref": "crm:note:881",
"sha256": "886d5886b41216cf0dc8e2234f4ffb24cb5666425200e5e2c5ee33d841b91be4",
"duplicate": false,
"replaced": []
}title names the memory. source_ref is your own id for the original, such as a file path, a URL or a record id. Fryri returns it on every read and search result, so each memory maps back to your record. Adding again with the same source_ref replaces the earlier memory, and replaced lists the ids it removed.
A document. Upload the file as multipart/form-data, up to 50 MB. Fryri reads the text out of the document, including scanned pages.
Request
curl -X POST https://api.fryri.com/v1/memories \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-F "file=@atlas-budget.txt" \
-F "title=Atlas budget" \
-F "end_user_id=user_42"Response
{
"id": "file_khWTQ9FQRD0vNdAx",
"type": "text",
"title": "Atlas budget",
"status": "processing",
"status_reason": null,
"created_at": "2026-09-25T09:02:58.286297Z",
"source_ref": null,
"sha256": "c6a6c1b533130aa2c02bb580edb9263885ac229f1ebd7016e1a341885a908ee4",
"duplicate": false,
"replaced": []
}A document inline. A JSON body can carry the file instead of a multipart upload. file.name is the file name with its extension, file.data is the bytes, base64-encoded, up to about 8 MB, and file.type is an optional MIME type.
Fryri reads PDF, Word, PowerPoint, Excel and OpenDocument files, Markdown, plain text, CSV, JSON, HTML, RTF, email (.eml and .msg), EPUB, Jupyter notebooks and source code. An add of a photo, a video or an audio file fails with 422 unsupported_format and stores nothing.
To remember a web page, fetch it in your own code and add its text, with the address of the page as source_ref. An add of the same address later replaces the earlier copy.
Duplicates and retries#
Adding identical content again returns the existing memory with duplicate set to true, and nothing new is added.
Response
{
"id": "file_NkolfLrElMmM2fCn",
"type": "text",
"title": "Project Atlas notes",
"status": "processing",
"status_reason": null,
"created_at": "2026-09-25T09:02:54.915644Z",
"source_ref": "crm:note:881",
"sha256": "886d5886b41216cf0dc8e2234f4ffb24cb5666425200e5e2c5ee33d841b91be4",
"duplicate": true,
"replaced": []
}An Idempotency-Key header makes a retry safe. The same key and body replay the first response for 24 hours. The same key with a different body fails with 409 idempotency_key_reused.
Getting it back#
POST /v1/search returns the memories that match a query, best match first. It matches by meaning and by exact words, and each result carries the most relevant passage of its memory in text.
Request
curl -X POST https://api.fryri.com/v1/search \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "When is the Atlas launch review?", "end_user_id": "user_42", "limit": 3}'Response
{
"results": [
{
"id": "file_NkolfLrElMmM2fCn",
"type": "text",
"title": "Project Atlas notes",
"text": "Project Atlas uses Python and FastAPI. The launch review is on 14 October.",
"score": 0.526897227015439,
"score_basis": "cosine",
"location": null,
"created_at": "2026-09-25T09:02:54.915644Z",
"source_ref": "crm:note:881"
},
{
"id": "file_khWTQ9FQRD0vNdAx",
"type": "text",
"title": "Atlas budget",
"text": "Atlas budget\nDesign: 12,000 NZD\nBuild: 30,000 NZD",
"score": 0.234705295047091,
"score_basis": "cosine",
"location": null,
"created_at": "2026-09-25T09:02:58.286297Z",
"source_ref": null
}
],
"next_cursor": null,
"cost_usd": 0.0025
}query: what to find, in natural language or exact words. Length[1, 8000].limit: results on this page. Default value is10. Range[1, 50].cursor: thenext_cursorfrom the previous page of the same query.
Response fields:
score: relevance in[0, 1]. Higher is a better match.score_basis: howscorewas measured.relevanceis the calibrated mixed score,cosinemeasures how close the meanings are, andlexicalis a keyword-only match, which stays below0.5.location: wheretextsits in the memory, asline_startandline_endfor a document, andnullwhen the match covers the whole memory.GET /v1/memories/{memory_id}counts the same lines.cost_usd: what the call cost. Each search costs $0.0025.
Answers#
POST /v1/answer answers a question from the library and distinguishes cited evidence from retrieved context. An answer never adds or changes a memory.
Request
curl -X POST https://api.fryri.com/v1/answer \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "When is the Atlas launch review, and what does the project use?", "end_user_id": "user_42"}'Recorded staging response before numbered citations
{
"answer": "The **launch review is on 14 October**, and Project Atlas uses **Python and FastAPI**.\n\nThat's from your \"Project Atlas notes.txt\". Given today's date, that review is about three weeks away. The notes also don't say which year, but since you uploaded the file today, 14 October 2026 is the natural reading.",
"sources": [
{
"type": "text",
"id": "file_NkolfLrElMmM2fCn",
"title": "Project Atlas notes",
"text": "Project Atlas uses Python and FastAPI. The launch review is on 14 October."
}
],
"cost_usd": 0.02
}sources: cited evidence first, followed by retrieved context. A numbered citation includescited: true, itscitationnumber, exact source lines inlocation, and an excerpt intext. See Answer citations. Areferencesource is a quoted passage of a reference work.cost_usd: $0.02 per answer. A run past 32,000 input tokens or 4,000 output tokens bills further $0.02 steps.
Streaming#
With "stream": true, the answer arrives as server-sent events while it is written. Each event is one data: line with a type: status, delta, reset, done or error.
Request
curl -N -X POST https://api.fryri.com/v1/answer \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "When is the Atlas launch review?", "end_user_id": "user_42", "stream": true}'Recorded staging stream before numbered citations
data: {"type": "delta", "text": "The Atlas launch review is on **14 October** (per your Project Atlas notes, so 14 October 2026, just under three weeks away)."}
data: {"type": "done", "answer": "The Atlas launch review is on **14 October** (per your Project Atlas notes, so 14 October 2026, just under three weeks away).", "sources": [{"type": "text", "id": "file_NkolfLrElMmM2fCn", "title": "Project Atlas notes", "text": "Project Atlas uses Python and FastAPI. The launch review is on 14 October."}], "cost_usd": 0.02}The stream ends with done or error. done carries the same body as a response without stream, and its answer replaces the joined delta text. A reset event discards the text so far.
Add a local folder#
A folder of files becomes a library with a short loop that uploads each file with its path as source_ref. A rerun of the loop is safe. An unchanged file comes back with duplicate set to true, and a changed file replaces its earlier memory. A photo, a video or an audio file in the folder is skipped, because its add fails with unsupported_format. Keep a codebase in sync also removes the memories of deleted files and skips unchanged files without sending them.
Complete example
import hashlib
import os
import time
from pathlib import Path
import requests
API = "https://api.fryri.com/v1/memories"
HEADERS = {"Authorization": f"Bearer {os.environ['FRYRI_API_KEY']}"}
for path in sorted(Path("./contracts").rglob("*")):
if not path.is_file():
continue
data = path.read_bytes()
# The same file with the same content always sends the same key.
key = hashlib.sha256(str(path).encode() + data).hexdigest()
while True:
# Send request
response = requests.post(
API,
headers={**HEADERS, "Idempotency-Key": key},
files={"file": (path.name, data)},
data={"source_ref": str(path), "end_user_id": "user_42"},
)
if response.status_code != 429:
break
time.sleep(int(response.headers.get("Retry-After", "5")))
if response.status_code == 422 and response.json()["error"]["code"] == "unsupported_format":
print("skipped", path) # a photo, a video or an audio file
continue
response.raise_for_status()
# Read response
memory = response.json()
print(memory["id"], memory["status"], memory["duplicate"], path)Best Practices#
Adding
- Send
source_refwith every add, so each memory maps back to your record and a changed original replaces its earlier memory. - Send an
Idempotency-Keywith every add, so a retry after a dropped connection adds nothing twice. - Wait for
readybefore a search or an answer needs a new memory.
Recalling
- Call
POST /v1/searchwhen your code works with the passages, andPOST /v1/answerwhen a person reads the result. - Show
sourcesnext to an answer, so a reader can check where it came from. - Send the same
end_user_idon every call for one user, so each user only ever reaches their own library.
Tip: A
lexicalresult matched keywords only, so itsscorestays below0.5.
Warning: A new memory is
processinguntil Fryri has read it: usually seconds for a short note, and a few minutes for a long document or a large batch. Search and answers do not find it before it isready.