Skip to content

Fryri documentation

Store and recall

Updated 2026-11-07

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.

If the original request is running on another worker, 409 idempotency_request_pending is retryable. Wait for Retry-After, then resend the same key and body. An interrupted request can resume after its five-minute execution lease expires. Its key remains bound to the original body for 24 hours, and longer while an active attempt holds its lease. If its receipt is temporarily unavailable, 503 idempotency_unavailable also tells you to retry with that same key and body.

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.000001
}
  • query: what to find, in natural language or exact words. Length [1, 8000].
  • limit: results on this page. Default value is 10. Range [1, 50].
  • cursor: the next_cursor from the previous page of the same query.

Response fields:

  • score: relevance in [0, 1]. Higher is a better match.
  • score_basis: how score was measured. relevance is the calibrated mixed score, cosine measures how close the meanings are, and lexical is a keyword-only match, which stays below 0.5.
  • location: where text sits in the memory, as line_start and line_end for a document, and null when the match covers the whole memory. GET /v1/memories/{memory_id} counts the same lines.
  • cost_usd: what the call cost: the model cost of reading the query and ranking the matches.

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"}'

Response

{
  "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.005918
}
  • sources: cited evidence first, followed by retrieved context. A numbered citation includes cited: true, its citation number, exact source lines in location, and an excerpt in text. See Answer citations. A reference source is a quoted passage of a reference work.
  • cost_usd: what the answer charged: the model use it needed, billed the same as asking in the Fryri app.

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.

Recover an answer after disconnecting#

Answers are not retained by default. To retain one for 48 hours from acceptance, send "resumable": true and a client-generated UUID in request_id. Keep that UUID before sending the request. The library stays read-only. Fryri retains the completed answer and its sources, not the question or incremental transcript, and bills a completed retained answer even if you disconnect.

{"question":"When is the Atlas launch review?","end_user_id":"user_42","resumable":true,"request_id":"58f89938-63dc-4c6c-a2e5-233f5e430ddb"}

A completed POST returns the usual {answer, sources, cost_usd} body. Streaming uses the existing events and authoritative done body. Repeat the POST with the same API key and question, request_id and end_user_id to join the running answer or retrieve its saved result. Changing stream is allowed; changing the question returns 409 answer_request_conflict. A request running on another server returns 202 with its status and Retry-After: 5.

GET /v1/answer/{request_id}?end_user_id=user_42 returns {id, status, expires_at, result, error}. result contains the usual answer body once status is completed. The other statuses are queued, running, failed and interrupted. Retrieval needs no additional credit and performs no new inference.

A server interruption after model work begins but before the result is saved becomes interrupted after its five-minute lease expires. Fryri never silently repeats uncertain model work. A repeated POST then returns 409 answer_interrupted; a new UUID explicitly starts a separate answer. A result saved before a server restart stays retrievable.

DELETE /v1/answer/{request_id}?end_user_id=user_42 erases the retained answer and returns {"id": "<request_id>", "deleted": true}, like the other deletes. A content-free deleted receipt remains until the original expiry so another POST with that UUID returns 410 answer_deleted, even if its question changed. Use a new UUID to explicitly start a separate answer. Deleting an end user or account removes their receipts too. Deletion does not guarantee cancellation of model work already in progress. After deletion or expiry, retrieval returns 404. The original API key and end-user scope are required for reads and deletes; omit end_user_id only for answers from your own library. There is no retained-answer list endpoint.

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)

Shared library#

scope: "shared" adds a memory to your shared library, which every end user reads beside their own library. It suits what is the same for everyone, such as policies, a menu or a product manual. Searches, answers and runs of every end user include it, and those results carry library: "shared". Only your own key, without end_user_id, adds to it, and nothing an end user does changes it.

Request

curl -X POST https://api.fryri.com/v1/memories \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Members may bring one guest per calendar month to any class.",
    "title": "Guest policy",
    "source_ref": "policies/guests.md",
    "scope": "shared"
  }'

GET /v1/memories?scope=shared lists the shared library, and DELETE /v1/memories/{memory_id}?scope=shared deletes one of its memories. A shared add replaces by source_ref only within the shared library. scope with end_user_id fails with 422 invalid_request.

Best Practices#

Adding

  • Send source_ref with every add, so each memory maps back to your record and a changed original replaces its earlier memory.
  • Send an Idempotency-Key with every add, so a retry after a dropped connection adds nothing twice.
  • Wait for ready before a search or an answer needs a new memory.

Recalling

  • Call POST /v1/search when your code works with the passages, and POST /v1/answer when a person reads the result.
  • Show sources next to an answer, so a reader can check where it came from.
  • Send the same end_user_id on every call for one user, so each user only ever reaches their own library.

Tip: A lexical result matched keywords only, so its score stays below 0.5.

Warning: A new memory is processing until 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 is ready.