An API key authenticates every request to https://api.fryri.com/v1. Create a key in the developer console, then send it as a Bearer token in the Authorization header. A request without a live key fails with 401 unauthorized.
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"
},
...
],
"next_cursor": "eyJwIjp7ImkiOjQ1OSwiayI6..."
}Keys#
The console shows a key once, when it is created. A key works on the library of its own account and on the end users of that account. A key revoked in the console stops at once, and its next request fails with 401 unauthorized.
It's recommended to keep keys on your server, in an environment variable such as FRYRI_API_KEY.
Each key can carry a monthly spend cap, set in the console. A key over its cap fails with 429 key_budget_reached.
Billing#
Every call spends from the prepaid credit of the account, separate from any Fryri plan. A newly verified account starts with $1 of credit. Every call needs credit, including the free ones. Without it, the call fails with 402 insufficient_credits.
Top-ups, usage and the request log are in the developer console. The price list covers each call, and POST /v1/search and POST /v1/answer report their cost in cost_usd.
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. The Retry-After header gives the seconds to wait.
Errors#
Every error returns one envelope. error.code is stable and machine-readable, and error.message is prose that may change. error.retryable says whether the same call, sent again unchanged, can succeed. request_id is the id to quote when reporting a problem, and the X-Request-ID header carries the same value.
| 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. |
410 | endpoint_retired | false | Call the endpoint in error.use. |
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 streamed answer reports a failure after its first event as an event of "type": "error", with the same code, message and retryable fields.
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.
Unauthenticated request
curl -X POST https://api.fryri.com/v1/search \
-H "Content-Type: application/json" \
-d '{"query": "x"}'Response
{
"error": {
"code": "unauthorized",
"message": "Invalid or missing API key. Pass it as 'Authorization: Bearer fryri_sk_...'.",
"retryable": false
},
"request_id": "b4de2b67719241b4b319d6f8fc7cb0d4"
}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
{
"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"
}The machine-readable contract#
- OpenAPI spec: the source of truth for every call. Generate a typed client from it, or hand it to an agent.
- Interactive docs: try every call in the browser.
A breaking change to a /v1 call ships as a new path. A path from the earlier API fails with 410 endpoint_retired, and the migration table names each replacement.
Tip: Branch on
error.codeanderror.retryable, never onerror.message.
Warning: A key in browser code is visible to every visitor. Keep keys on your server.