Developer documentation
The API reference.
The Fryri API stores the notes, documents and conversations your app adds. It finds them again by meaning or by exact words, and it answers questions with citations and retrieved sources. It keeps a private library for each of your users, all under one account.
| Call | What it does | Price |
|---|---|---|
POST /v1/memories | Adds a note, document or conversation snapshot. | The model cost of reading it |
GET /v1/memories | Lists memories, newest first. | Free |
GET /v1/memories/{memory_id} | Returns one memory with its status, its text and its original file. | Free |
DELETE /v1/memories/{memory_id} | Deletes one memory. | Free |
POST /v1/search | Returns the memories that match a query, best match first. | $0.0025 per call |
POST /v1/answer | Answers a question with citations and retrieved context. | $0.02 per answer |
DELETE /v1/end-users/{end_user_id} | Erases one end user and all their memories. | Free |
Each of these calls starts at https://api.fryri.com/v1. A breaking change to
one of them ships as a new path.
A call rejects any field it does not define with 422 invalid_request, and error.message names the field. A typo such as enduser_id fails at once
instead of adding to the wrong library.
Browse endpoints
Quickstart
The quickstart adds a memory for one of your users, waits until search can find it, then searches and asks a question. Each step is one call.
- Create a key in the developer console.
A newly verified account starts with $1 of credit. Export the key as
FRYRI_API_KEY:export FRYRI_API_KEY="fryri_sk_..." - The first add with an
end_user_idcreates the private library of that user. The memory comes back with itsidand thestatusprocessing. Add a memory: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" }' - Search and answers find a memory once its
statusisready. A short note is usually ready within seconds. Read the memory until then:curl "https://api.fryri.com/v1/memories/file_NkolfLrElMmM2fCn?end_user_id=user_42" \ -H "Authorization: Bearer $FRYRI_API_KEY" - Each search result is a memory with its most relevant passage in
textand ascorein[0, 1]. Search the library of the user: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 }' - An answer carries the
answertext and thesources, distinguishing cited evidence from retrieved context. Ask a question: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" }'
Complete example
Python, with requests:
import os
import time
import requests
API = "https://api.fryri.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FRYRI_API_KEY']}"}
USER = "user_42"
def call(method, path, **kwargs):
response = requests.request(method, API + path, headers=HEADERS, **kwargs)
response.raise_for_status()
return response.json()
# Store
memory = call("POST", "/memories", json={
"text": "Project Atlas uses Python and FastAPI. The launch review is on 14 October.",
"title": "Project Atlas notes",
"end_user_id": USER,
})
# Wait until ready
while memory["status"] == "processing":
time.sleep(2)
memory = call("GET", f"/memories/{memory['id']}", params={"end_user_id": USER})
# Recall
search = call("POST", "/search", json={
"query": "When is the Atlas launch review?", "end_user_id": USER,
})
answer = call("POST", "/answer", json={
"question": "When is the Atlas launch review?", "end_user_id": USER,
})
# Read response
print(search["results"][0]["text"])
print(answer["answer"]) TypeScript on Node 18 or later. Save it as quickstart.mts and run npx tsx quickstart.mts:
const API = "https://api.fryri.com/v1";
const USER = "user_42";
async function call(method: string, path: string, body?: object) {
const response = await fetch(API + path, {
method,
headers: {
Authorization: "Bearer " + process.env.FRYRI_API_KEY,
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
});
if (!response.ok) throw new Error(response.status + " " + (await response.text()));
return response.json();
}
// Store
let memory = await call("POST", "/memories", {
text: "Project Atlas uses Python and FastAPI. The launch review is on 14 October.",
title: "Project Atlas notes",
end_user_id: USER,
});
// Wait until ready
while (memory.status === "processing") {
await new Promise((resolve) => setTimeout(resolve, 2000));
memory = await call("GET", "/memories/" + memory.id + "?end_user_id=" + USER);
}
// Recall
const search = await call("POST", "/search", {
query: "When is the Atlas launch review?",
end_user_id: USER,
});
const answer = await call("POST", "/answer", {
question: "When is the Atlas launch review?",
end_user_id: USER,
});
// Read response
console.log(search.results[0]?.text);
console.log(answer.answer); The OpenAPI spec at https://api.fryri.com/v1/openapi.json describes every call on this page, for a client of your own. https://api.fryri.com/v1/docs lets you try each call in the browser.
Authentication
An API key authenticates every request. Create keys in the developer console, which shows each key once. Send the key as a Bearer token:
Authorization: Bearer $FRYRI_API_KEY A missing, revoked or mistyped key fails with 401 unauthorized. A key works on the library of its own account
and on the end users of that account. It's recommended to keep keys on your server, in an
environment variable such as FRYRI_API_KEY.
Credit
Calls spend from the account's prepaid credit, separate from any Fryri plan. A newly
verified account starts with $1 of credit. Every call needs credit, including the free
ones. Without it, a call fails with 402 insufficient_credits. Top-ups, spend caps per key, usage and
the request log are in the developer console. Pricing lists what each call costs.
Rate limits
Each key has a limit of requests per minute, set by the plan of its account:
| Plan | Requests per minute |
|---|---|
| Free | 30 |
| Standard | 60 |
| Pro | 120 |
| Premium | 240 |
| Any plan, with prepaid credit | At least 120 |
A request over the limit fails with 429 rate_limited. Its Retry-After header gives the seconds to wait before the next try. Requests with a missing or
invalid key have a separate limit: after 100 of them in one minute from one client,
the next requests from that client also fail with 429 until the minute ends.
Request IDs
Every response carries an X-Request-ID header, and an error
body repeats it as request_id. It's recommended to log it
and quote it when you contact support.
Memories
A memory is one thing your app adds: a note, document or conversation snapshot.
The memory object
Add, list and get return the same memory object:
| Field | Type | Description |
|---|---|---|
id | string | The memory id, such as file_NkolfLrElMmM2fCn. |
type | string | text (plain text and Markdown), document (other files), or conversation (ordered turns). |
title | string or null | The title sent with the add, else the file name. null for text added without a title. |
status | string | processing until search can find the memory, then ready. failed when it cannot be processed. |
status_reason | string or null | Why the memory is failed or still processing. See the table below. |
created_at | string | When the memory was added, as an ISO 8601 time in UTC. |
source_ref | string or null | Your own id for the original, such as a file path, a URL or a record id, as sent with the add. See Replace by reference. |
sha256 | string or null | The SHA-256 of the content as it was added, in hex: file bytes, UTF-8 text, or canonical JSON for a conversation. Compare it with the hash of your own copy to skip sending unchanged content. null when Fryri holds no hash for the memory. |
status and status_reason go
together as follows:
status | status_reason | Meaning |
|---|---|---|
processing | null | Fryri is reading and indexing the memory. |
processing | queued | The memory was added in the Fryri app and waits for the daily document allowance of the account, then processes on its own. Memories added through the API never wait. |
ready | null | Search and answers can find the memory. |
failed | processing_failed | Reading the memory failed. Adding the same content again starts a new attempt. |
failed | unsupported_format | Fryri cannot read the format of the file. |
failed | reading_allowance_reached | The monthly allowance of the account for reading scanned pages is used up. |
POST /v1/memories
POST /v1/memories adds a note, document or conversation snapshot. The
memory comes back at once with the status processing, and search finds it once it is ready. A short note is usually ready within seconds. A long
document, or a large batch added at once, can take a few minutes.
Send exactly one of text, file or messages as JSON, or upload a file as multipart/form-data. A JSON body
must carry exactly one of the three. Otherwise, the request fails with 422 invalid_request.
JSON body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
text | string | One of three | Text to remember, such as a note, a message or a transcript. Length [1, 200000]. | |
file | object | One of three | A document sent inline, with the three fields below. A file over about 8 MB goes as a multipart/form-data upload instead. | |
messages | array | One of three | An ordered conversation snapshot of 1 to 200 user or assistant turns, as described below. | |
file.name | string | Yes, in file | The file name with its extension, such as contract.pdf. Length [1, 400]. | |
file.data | string | Yes, in file | The file bytes, base64-encoded, up to about 8 MB of file. | |
file.type | string | No | The MIME type, such as application/pdf. Fryri works it out from file.name when it is absent. | |
title | string | No | A name for the memory. Length [1, 400]. | |
source_ref | string | No | Your own id for the original, such as a file path, a URL or a record id. Adding again with the same source_ref replaces the earlier memory. See Replace by reference. Length [1, 2000]. | |
end_user_id | string | No | Adds the memory to one of your end users, created on first use. Length [1, 256]. |
messages is a JSON array of 1 to 200 turns. Each turn has role (user or assistant),
nonblank content and optional created_at with a time zone. Array order is authoritative. The rendered transcript, including turn headings,
must fit within 30,000 characters. Send a complete updated snapshot with the same source_ref to replace an earlier one. See Conversation snapshots.
Multipart form fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
file | file | Yes | The document to remember, up to 50 MB. | |
title | string | No | As in the JSON body. | |
source_ref | string | No | As in the JSON body. | |
end_user_id | string | No | As in the JSON body. |
Header
| Header | Type | Required | Default | Description |
|---|---|---|---|---|
Idempotency-Key | string | No | 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. |
It's recommended to send an Idempotency-Key with every add,
so a retry after a dropped connection never adds the memory twice.
The response is a memory object with two more
fields. duplicate is true when
identical content was already stored. id is then the existing
memory, and nothing new is added. replaced lists the ids of
the earlier memories that this add replaced, and it is empty when nothing was replaced.
Reading and indexing the content bills its model cost to
the prepaid credit of the account: about $0.003 for a short note. A long document costs
more, because the cost follows the length of the content.
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": []
} Duplicates
The same request, sent again, returns the existing memory with duplicate set to true.
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": []
} Replace by reference
A memory is one copy of one original. An add with a source_ref that earlier memories carry replaces them. The new content gets a new id, the earlier memories stop appearing in search and answers
at once, and replaced lists their ids. An add of unchanged
content returns the existing memory with duplicate set to true and keeps it. A failed add leaves the earlier memory in
place.
Here src/auth.py was added once before and has changed since. Keep a codebase in sync has a
complete script for a whole folder or repository.
Request
curl -X POST https://api.fryri.com/v1/memories \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-F "file=@src/auth.py" \
-F "source_ref=myrepo/src/auth.py" \
-F "end_user_id=user_42" Response
{
"id": "file_hqjP2q_DlrHZT-4U",
"type": "document",
"title": "auth.py",
"status": "processing",
"status_reason": null,
"created_at": "2026-09-25T09:07:09.917301Z",
"source_ref": "myrepo/src/auth.py",
"sha256": "785108e6758610a47355924cf936ce293ac1c6e5e6c7cd9b8752c19c78c00e26",
"duplicate": false,
"replaced": [
"file_kR1jHO7CDRwaoCEw"
]
} Upload a file
A multipart/form-data upload takes a file of up to 50 MB.
The file type comes from the content type of the part, or from the file name when that is
missing or application/octet-stream.
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": []
} Supported files
A file memory holds a document. Fryri reads these files:
| Kind | Files |
|---|---|
| Documents | PDF, Word (.docx), PowerPoint (.pptx), Excel (.xlsx), OpenDocument (.odt, .ods, .odp), RTF and EPUB |
| Text and data | Plain text, Markdown, CSV, JSON and HTML |
.eml and .msg | |
| Code | Source code files and Jupyter notebooks |
An add of a photo, a video or an audio file, judged by its content type or its file
extension, fails with 422 unsupported_format and stores nothing.
Add a web page
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.
Request
curl -X POST https://api.fryri.com/v1/memories \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Example Domain. This domain is for use in documentation examples.",
"title": "Example Domain",
"source_ref": "https://example.com",
"end_user_id": "user_42"
}'GET /v1/memories
GET /v1/memories lists memories, newest first. The list covers
every note, document and conversation snapshot in the library, however it arrived. Listing is free.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
limit | integer | No | 20 | Memories on this page. Default value is 20. Range [1, 100]. |
cursor | string | No | The next_cursor from the previous page. | |
source_ref_prefix | string | No | Lists only the memories whose source_ref starts with this text, such as myrepo/. Characters like % and _ match themselves. Length [1, 2000]. | |
end_user_id | string | No | Lists the memories of one of your end users. An id with no memories returns an empty list. |
The response holds memories, an array of memory objects, and next_cursor. Pass next_cursor back as cursor for the next page; it is null on the last page. A cursor from a different list starts
again at the first page.
Request
curl "https://api.fryri.com/v1/memories?end_user_id=user_42&limit=2" \
-H "Authorization: Bearer $FRYRI_API_KEY" Response
{
"memories": [
{
"id": "file_tjSa4DvE15aMARb4",
"type": "document",
"title": "Example Domain.html",
"status": "ready",
"status_reason": null,
"created_at": "2026-09-25T09:03:01.562079Z",
"source_ref": "https://example.com",
"sha256": "ff67a9d764d6a2367a187734e697f6a53217db9a21c101d410a113ca871a299d"
},
{
"id": "file_khWTQ9FQRD0vNdAx",
"type": "text",
"title": "Atlas budget",
"status": "ready",
"status_reason": null,
"created_at": "2026-09-25T09:02:58.286297Z",
"source_ref": null,
"sha256": "c6a6c1b533130aa2c02bb580edb9263885ac229f1ebd7016e1a341885a908ee4"
}
],
"next_cursor": "eyJwIjp7ImkiOjQ1OSwiayI6..."
} List one folder or codebase
source_ref_prefix lists the memories you added under one
prefix of source_ref, such as the files of one repository.
Request
curl "https://api.fryri.com/v1/memories?end_user_id=user_42&source_ref_prefix=myrepo%2F" \
-H "Authorization: Bearer $FRYRI_API_KEY" Response
{
"memories": [
{
"id": "file_hqjP2q_DlrHZT-4U",
"type": "document",
"title": "auth.py",
"status": "processing",
"status_reason": null,
"created_at": "2026-09-25T09:07:09.917301Z",
"source_ref": "myrepo/src/auth.py",
"sha256": "785108e6758610a47355924cf936ce293ac1c6e5e6c7cd9b8752c19c78c00e26"
}
],
"next_cursor": null
}GET /v1/memories/{memory_id}
GET /v1/memories/{memory_id} returns one memory with
its status, its text and a link to its original file. Reading is free.
Poll it after an add until status is ready or failed. A short note
is usually ready within seconds. A long document can take a few minutes.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
memory_id | string | Yes | The memory id, in the path. | |
cursor | string | No | The next_cursor from the previous read of this memory. | |
end_user_id | string | No | The end user the memory belongs to. |
A memory of an end user must be read with its end_user_id.
Otherwise, the request fails with 404 not_found. A cursor from another memory, or from an earlier
version of its text, fails with 422 invalid_cursor.
The response is a memory object with six more fields:
| Field | Type | Description |
|---|---|---|
summary | string or null | A one-line summary of a document, once it is processed. |
text | string or null | The text of the memory: a note as it was added, or the extracted text of a document. |
lines | object or null | For a document, which lines text holds: start and end (1-based and inclusive) out of total. |
next_cursor | string or null | More text follows. Pass it back as cursor to read the next part. |
messages | array or null | The original turns of a processed conversation, on its first page only, as described below. |
download_url | string or null | A link to the original file, valid for 5 minutes. null when no original file is available. |
A processed conversation also returns messages on its first page: original role, content and created_at, with 1-based turn, line_start and line_end. Other memories and later pages return messages: null.
The text of a long document arrives in parts. lines counts the
same lines as location.line_start in a search result.
Request
curl "https://api.fryri.com/v1/memories/file_NkolfLrElMmM2fCn?end_user_id=user_42" \
-H "Authorization: Bearer $FRYRI_API_KEY" Response
{
"id": "file_NkolfLrElMmM2fCn",
"type": "text",
"title": "Project Atlas notes",
"status": "ready",
"status_reason": null,
"created_at": "2026-09-25T09:02:54.915644Z",
"source_ref": "crm:note:881",
"sha256": "886d5886b41216cf0dc8e2234f4ffb24cb5666425200e5e2c5ee33d841b91be4",
"summary": "A brief note about Project Atlas, which uses Python and FastAPI, with a launch review scheduled for 14 October.",
"text": "Project Atlas uses Python and FastAPI. The launch review is on 14 October.",
"lines": {
"start": 1,
"end": 1,
"total": 1
},
"next_cursor": null,
"download_url": "https://****.r2.cloudfla..."
}DELETE /v1/memories/{memory_id}
DELETE /v1/memories/{memory_id} deletes one memory.
It stops appearing in search and answers at once, together with anything learned from it.
Deleting is free.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
memory_id | string | Yes | The memory id, in the path. | |
end_user_id | string | No | The end user the memory belongs to. |
A memory of an end user must be deleted with its end_user_id.
Otherwise, the request fails with 404 not_found. A second delete of the same memory also fails with 404. To erase everything stored for one end user, call DELETE /v1/end-users/{end_user_id}.
Request
curl -X DELETE "https://api.fryri.com/v1/memories/file_khWTQ9FQRD0vNdAx?end_user_id=user_42" \
-H "Authorization: Bearer $FRYRI_API_KEY" Response
{
"id": "file_khWTQ9FQRD0vNdAx",
"deleted": true
}POST /v1/search
POST /v1/search returns the memories that match a query, best
match first. It matches by meaning and by exact words, so a query does not need the wording
of the memory. Each result is a memory with its most relevant passage.
Search writes no answer. For an answer with sources, call POST /v1/answer. Each
search costs $0.0025, reported in cost_usd, and a failed
search costs nothing.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | What to find, in natural language or exact words. Length [1, 8000]. | |
limit | integer | No | 10 | Maximum results on this page. Default value is 10. Range [1, 50]. |
cursor | string | No | The next_cursor from the previous page of the same query. A cursor from another query or end user starts again at the first page. | |
end_user_id | string | No | Searches the memories of one of your end users. |
Response fields:
| Field | Type | Description |
|---|---|---|
results[].id | string | The memory id, for GET /v1/memories/{memory_id} and DELETE /v1/memories/{memory_id}. |
results[].type | string | The type of the memory, as in the memory object. |
results[].title | string or null | The title of the memory. |
results[].text | string | The most relevant passage of the memory, up to 1,200 characters. |
results[].score | number | Relevance in [0, 1]. Higher is a better match. |
results[].score_basis | string | How score was measured: relevance (the calibrated mixed score), cosine (how close the meanings are) or lexical (a keyword-only match, which stays below 0.5). |
results[].location | object or null | Where text sits in the memory. Conversations can also include turn_start and turn_end. Lines use line_start and line_end for a document. null when the match covers the whole memory. |
results[].created_at | string | When the memory was added. |
results[].source_ref | string or null | Your own pointer back to the original. |
next_cursor | string or null | Pass it back as cursor for the next page. null on the last page. |
cost_usd | number | What this call cost, in US dollars. |
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
}POST /v1/answer
POST /v1/answer answers a question from memories and lists
cited evidence and retrieved context separately. One call finds what is relevant, reads it and writes the answer. An
answer draws only on the memories of the library, and it never adds or changes a memory.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
question | string | Yes | The question, in natural language. Length [1, 50000]. | |
end_user_id | string | No | Answers from the memories of one of your end users. | |
stream | boolean | No | false | Delivers the answer as server-sent events while it is written. Default value is false. |
Response fields:
| Field | Type | Description |
|---|---|---|
answer | string | The answer, as Markdown text. |
sources[].type | string | text, document or conversation for a memory, or reference for a quoted passage of a reference work. |
sources[].id | string or null | The memory id, for a memory source. |
sources[].title | string or null | The title of the memory, or the citation of a reference. |
sources[].text | string or null | Up to 300 characters of source text or retrieved context. For a numbered citation, these are the delivered source lines. |
sources[].cited | boolean | True when the answer cites verified, delivered source lines. False means retrieved context. |
sources[].citation | integer or null | The number in the answer's [N] marker. Null for retrieved context. |
sources[].location | object or null | Inclusive 1-based line_start and line_end. Conversation citations also have turn_start and turn_end. |
sources[].source_ref | string or null | Your reference for this source, when supplied on add. |
sources[].text_truncated | boolean | True when the cited range exceeds the 300-character quote. Read the memory to continue. |
cost_usd | number | What this call cost, in US dollars. |
sources lists numbered citations first, then retrieved context.
Read cited to distinguish the two. An answer costs $0.02 and
covers up to 32,000 input tokens and 4,000 output tokens. A longer run bills further $0.02
steps. Fryri charges the $0.02 only when the answer completes.
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...",
"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
} Streaming
With stream set to true, the
response is text/event-stream. Each event is one data: line that holds a JSON object with a type:
type | Fields | Meaning |
|---|---|---|
status | text | What the answer is doing right now, such as looking through the library. |
delta | text | The next piece of the answer. |
reset | Discard the text so far. The answer starts again. | |
done | answer, sources, cost_usd | The last event. It carries the same body as a response without stream. |
error | error | The last event when the answer stops early, with code, message and retryable. |
The stream ends with done or error.
The answer in done is final, so
it replaces the joined delta text. Lines that start with : are heartbeats that keep the connection open. A failure
before the first event, such as a missing key, returns the JSON error envelope with its HTTP
status.
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
}' Response
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}End users
end_user_id scopes a call to one of your own users. Each end
user has a private library. Calls with the same id only ever reach that library, and calls
without end_user_id use the library of your own account.
The id is any stable id you already use for the user, [1, 256] printable characters. Fryri keeps it exactly as sent, so user_42 and User_42 are two different end users. POST /v1/memories, POST /v1/search and POST /v1/answer create the end user the first time they
use its id. Reads and deletes never create one, so a mistyped id reads as an empty library.
The ids belong to your account, so every key of the account reaches the same end users. Spend always lands on your account. The developer console lists your end users and what each one spent.
DELETE /v1/end-users/{end_user_id}
DELETE /v1/end-users/{end_user_id} permanently
deletes one end user and everything stored for them: memories, their original files, and
everything learned from them. It takes effect at once and cannot be undone. Your own memories
and your other end users stay as they are.
Erasing the same id again fails with 404 not_found, and the next add, search or answer with the same
id starts a new, empty library. It's recommended to call it from your own account-deletion flow. Deleting is
free.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
end_user_id | string | Yes | The id you passed as end_user_id, in the path. Length [1, 256]. An id with a slash goes in percent-encoded, such as team%2F42. |
Request
curl -X DELETE "https://api.fryri.com/v1/end-users/user_42" \
-H "Authorization: Bearer $FRYRI_API_KEY" Response
{
"deleted": true,
"end_user_id": "user_42"
}Pricing
Every call spends from the prepaid credit of its account at these prices. Search and answer
responses report their cost in cost_usd.
| Call | Price |
|---|---|
POST /v1/memories | The model cost of reading and indexing the content: about $0.003 for a short note. A long document costs more. |
POST /v1/search | $0.0025 per call. |
POST /v1/answer | $0.02 per answer. One answer covers up to 32,000 input tokens and 4,000 output tokens; a longer run bills further $0.02 steps. |
GET /v1/memories, GET /v1/memories/{memory_id}, DELETE /v1/memories/{memory_id}, DELETE /v1/end-users/{end_user_id} | Free. |
A newly verified account starts with $1 of credit. Every call needs credit, including the free ones. Top-ups, spend caps per key, usage and the request log are in the developer console.
Errors
Every error returns one JSON envelope with its HTTP status. error.code is a stable machine code to branch on, and error.message is prose that may change. error.retryable says whether the same call, sent again
unchanged, can succeed. It's recommended to read it instead of inferring from the status. request_id matches the X-Request-ID header.
Some errors carry extra fields: use on a 410, errors (every field
problem) on a 422, and key_monthly_budget_usd with key_spend_this_month_usd on key_budget_reached.
| HTTP | code | retryable | Recovery |
|---|---|---|---|
4xx | |||
401 | unauthorized | false | Send a live key as Authorization: Bearer $FRYRI_API_KEY. |
401 | sandbox_discontinued | false | Create a live key in the developer console. Sandbox keys no longer work. |
401 | account_deleted | false | Log in to restore the account. Its keys then work again. |
402 | insufficient_credits | false | Add credit in the developer console, then retry. |
404 | not_found | false | Check the id, and send the end_user_id the memory belongs to. |
405 | method_not_allowed | false | Use the method that error.message names. |
409 | conflict | false | Read the current state, then resend. |
409 | idempotency_key_reused | false | Send a new Idempotency-Key for a new request. |
409 | memory_format_conflict | false | Identical conversation bytes already exist in another format. Remove that source explicitly before importing them as a conversation. |
410 | endpoint_retired | false | Call the endpoint in error.use. See Retired endpoints. |
413 | file_too_large | false | Send a smaller file. error.message names the limit. |
413 | request_too_large | false | Send a smaller request body. error.message names the limit. |
422 | invalid_request | false | Fix the field named in error.message ({field}: {reason}), or remove a field the call does not define. error.errors lists every problem. |
422 | unsupported_format | false | Add text or a document. Photos, video and audio can't be added. |
422 | invalid_cursor | false | Read the memory again without cursor. |
429 | rate_limited | true | Wait for the Retry-After header, then retry. |
429 | key_budget_reached | false | Raise or clear the spend cap of the key in the developer console, or use another key. |
5xx | |||
500 | internal_error | true | Retry once. Quote request_id if it persists. |
502 | upstream_error | true | Retry. |
502 | ingest_failed | true | Retry the add with the same Idempotency-Key. |
503 | unavailable | true | Fryri is busy. Retry after a few seconds. |
An add that the upload pipeline refuses carries that pipeline's own code, HTTP status and retryable,
such as storage_quota_exceeded (413)
or upload_rate_limit (429, with Retry-After). A 5xx error
carries the same envelope, with retryable set to true.
Missing key
curl -X POST https://api.fryri.com/v1/search \
-H "Content-Type: application/json" \
-d '{
"query": "x"
}' Response 401
{
"error": {
"code": "unauthorized",
"message": "Invalid or missing API key. Pass it as 'Authorization: Bearer fryri_sk_...'.",
"retryable": false
},
"request_id": "b4de2b67719241b4b319d6f8fc7cb0d4"
} Unknown memory id
curl "https://api.fryri.com/v1/memories/file_AAAAAAAAAAAAAAAA" \
-H "Authorization: Bearer $FRYRI_API_KEY" Response 404
{
"error": {
"code": "not_found",
"message": "No memory with that id in this library.",
"retryable": false
},
"request_id": "b8bc43a847964c60a8c05612506be894"
} Unknown field
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.",
"enduser_id": "user_42"
}' Response 422
{
"error": {
"code": "invalid_request",
"message": "enduser_id: Extra inputs are not permitted",
"retryable": false,
"errors": [
{
"type": "extra_forbidden",
"loc": [
"body",
"enduser_id"
],
"msg": "Extra inputs are not permitted",
"input": "user_42",
"url": "https://errors.pydantic.dev/2.10/v/extra_forbidden"
}
]
},
"request_id": "92fa7a87447044adafc2b36aa8a50203"
}Retired endpoints
On 25 September 2026 the API moved to the seven calls on this page. A path from the earlier
API fails with 410 endpoint_retired, and error.use names the call to make instead, when one exists.
| Old path | Now | Call instead |
|---|---|---|
POST /v1/capture | 410 | POST /v1/memories. file.data, file.name and file.type replace content_base64, filename and content_type. |
POST /v1/capture/batch | 410 | POST /v1/memories, once per item. |
POST /v1/chat | 410 | POST /v1/answer. The answer text moves from reply to answer. |
GET /v1/search | 405 | POST /v1/search, with query in the JSON body in place of q. Results carry type and text in place of kind and snippet. |
/v1/documents | 410 | GET /v1/memories and GET /v1/memories/{memory_id}. |
/v1/library/export | 410 | GET /v1/memories. |
/v1/grep, /v1/graph, /v1/entities | 410 | POST /v1/search. |
/v1/facts | 410 | POST /v1/memories to add, POST /v1/search to find. |
GET /v1/end-users | 410 | The developer console shows the list of end users. DELETE /v1/end-users/{end_user_id} stays. |
/v1/usage, /v1/runs, /v1/wallet | 410 | The developer console. |
/v1/webhooks | 410 | No replacement. Read the memory with GET /v1/memories/{memory_id} until status is ready. |
/v1/tools, /v1/byo-key, /v1/storage, /v1/research, /v1/conversations, /v1/calendar, /v1/evolving | 410 | No replacement. These left the public API. |
Retired path
curl -X POST https://api.fryri.com/v1/capture \
-H "Authorization: Bearer $FRYRI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "x"
}' Response 410
{
"error": {
"code": "endpoint_retired",
"message": "`POST /v1/capture` was retired on 2026-09-25. Use POST /v1/memories.",
"retryable": false,
"use": "POST /v1/memories"
},
"request_id": "f13dd44eafa741f4953f93df4e9410a4"
} Old search method
curl "https://api.fryri.com/v1/search" \
-H "Authorization: Bearer $FRYRI_API_KEY" Response 405
{
"error": {
"code": "method_not_allowed",
"message": "`GET /v1/search` is not supported. Use POST.",
"retryable": false
},
"request_id": "913f828f5e7d4c8699222e09845a89be"
} Removed on 26 September 2026
On 26 September 2026 the API narrowed to text and documents. A request that still uses one of these parts fails as follows:
| Removed | Now | Instead |
|---|---|---|
url on POST /v1/memories | 422 invalid_request | Fetch the page in your own code and add its text, with its address as source_ref. See Add a web page. |
Photos, video and audio on POST /v1/memories | 422 unsupported_format | Add text or a document. See Supported files. |
web_search on POST /v1/answer | 422 invalid_request | Leave the field out. An answer draws only on memories. |
The capture.completed and capture.failed webhooks | No deliveries | Read the memory with GET /v1/memories/{memory_id} until status is ready or failed. |
POST /mcp | 410 endpoint_retired | Call the /v1 routes over HTTP. |
| The Python and TypeScript client files | No longer served | Call the API over HTTP, as the examples on this page do. |