Skip to content

Fryri documentation

Store and recall

Updated 2026-09-27

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 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. 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 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: $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_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.