Skip to content

Files from an agent, waiting for a read, and your usage

Three things a new integration meets first: an agent that holds a file (a PDF, an Office file, an image) and must put it in; a document that is read in the background, so the agent must know when it has been read; and a view of what your key holds — what was read, what failed and why, and your limits.

1. A file, from an agent

What an agent can do with a file depends on its client:

client can it run a command? how a file gets in
Claude Code, Cursor, VS Code in agent mode yes — the agent runs commands in a terminal an upload link: the agent runs the link's command where the file is. Its shell needs no API key
Claude Desktop, and any chat client without a command tool no an upload link whose command the agent hands to its user to run; or the text the agent sees, sent as content under a text name (report.txt) — the file's tables and page layout are then lost
any program that holds the API key — POST /v1/workspaces/{workspace_id}/documents (multipart), the SDK's upload_document, or limma sync <folder>

A model cannot send a file's bytes as tool arguments. A tool call's arguments are text the model writes, and a file a person attaches to a chat reaches the model as extracted text and page images, never as the bytes a reader needs. limma_ingest refuses a binary file name with text content (binary_format_needs_upload) for that reason.

Ask limma_ingest for a link instead of sending content — upload_link=True, the file's filename (its extension says how the bytes are read) and content="":

limma_ingest(content="", filename="report.pdf", upload_link=True,
             entity_name="Northwind Lumber Co.", wait_s=60)

Over REST it is the same verb: POST /v1/public/ingest with {"content": "", "upload_link": true, "filename": "report.pdf", ...}. The reply's status is awaiting_upload, its source_id is empty, and upload_link names ONE request:

"upload_link": {
  "method": "POST",
  "url": "https://limma.your-company.internal/v1/public/upload",
  "headers": {"X-Limma-Upload-Token": "lmu_…"},
  "filename": "report.pdf",
  "max_bytes": 104857600,
  "expires_at": "2026-10-20T16:05:41.722736+00:00",
  "uses": 1,
  "command": "curl -sS --fail-with-body -X POST -H 'X-Limma-Upload-Token: lmu_…' -T report.pdf 'https://limma.your-company.internal/v1/public/upload?wait_s=60'"
}

Run command in the directory that holds the file (it reads filename from there), or make the same request yourself: the file's bytes are the body — not a form around them (-T or --data-binary @report.pdf, never -F). The reply is the document door's entry for the file (documents: [{document_id, processing_status, status, ...}]) and, when the request asked to wait, its wait.

What a link is:

  • One upload. The first upload a link admits spends it — whatever then happens to the bytes. A request refused before its body is read (a multipart body: 415; an empty one: 422; one that declares more than max_bytes: 413) does not spend it. Mint another link to send the file again.
  • Fifteen minutes. After expires_at it admits nothing (410 upload_link_expired).
  • At most max_bytes — the deployment's upload limit when the link was minted (on the hosted service, the plan's largest file when that is smaller).
  • One workspace, and what the file is read under. The workspace, filename, entity_name, document_kind, retry and the sync fields (external_id, external_version, access) are fixed when the link is minted. The file is then queued exactly as POST /v1/workspaces/{workspace_id}/documents queues one.
  • The key that minted it. Minting is an ingest: it needs a key that may write (a viewer key and a key confined to documents are refused 403, a request asserting X-Limma-Principals 400), and a call that names no workspace is served from the key's default workspace. When the bytes arrive, that key is judged again by the document door's rule: revoked, made read-only or scoped elsewhere since the mint, it admits nothing (403 upload_link_key_refused; the link is not spent). The upload is made as that key alone: a request that also presents another X-API-Key is refused (400 upload_link_key_differs) — the minting key itself may ride along.

The token is a credential, so it stays out of logs. It travels in the X-Limma-Upload-Token header, never in the URL: an access log between an agent and the API writes the request line (POST /v1/public/upload?wait_s=60) — a load balancer's, nginx's and uvicorn's do by default — and none of them writes this header unless it is configured to. The API stores only the token's SHA-256, and every line the API, the ingestion worker and the stdio MCP server log is scrubbed of anything shaped like a token. If a proxy, load balancer or WAF in front of your install logs request headers, redact X-Limma-Upload-Token there, as you would X-API-Key. The token does reach the agent — it is in the tool's reply and in the command it runs, and an MCP host that writes its servers' replies to a log of its own writes it there — which is why a link is spent by one upload and dies in fifteen minutes.

Inside your network the link names your install: its URL is <LIMMA_APP_BASE_URL>/v1/public/upload, the address your agents reach the API at.

from docs.examples.roster import new_workspace
made = new_workspace(client, name="uploads")
minted = client.post("/v1/public/ingest", json={
    "workspace_id": made, "content": "", "upload_link": True, "filename": "income.md"})
link = minted.json()["upload_link"]
token = link["headers"]["X-Limma-Upload-Token"]
print(minted.json()["status"], minted.json()["ingest_reason"])
print(link["method"], link["url"].endswith("/v1/public/upload"), link["uses"])
print(token[:4], len(token), token in link["url"])
table = b"| | 2023 | 2024 |\n|---|---|---|\n| Revenue | 380 | 412 |\n"
up = client.post("/v1/public/upload", headers={"X-Limma-Upload-Token": token},
                 content=table)
(entry,) = up.json()["documents"]
print(up.status_code, entry["document_name"], entry["status"], up.json()["wait"])
again = client.post("/v1/public/upload", headers={"X-Limma-Upload-Token": token},
                    content=table)
print(again.status_code, again.json()["detail"]["type"])
awaiting_upload upload_link_issued
POST True 1
lmu_ 47 False
200 income.md pending None
410 upload_link_spent

(This page's server runs no ingestion worker, so the file stays pending.)

Status detail.type Meaning
401 upload_link_required No X-Limma-Upload-Token header
401 upload_link_unknown A token no link has — or a link its workspace's deletion took with it
410 upload_link_spent The link carried its one upload; mint another
410 upload_link_expired Fifteen minutes have passed; mint another
400 upload_link_key_differs / principals_not_applied A second caller beside the link's key, or a request made for an end user
403 upload_link_key_refused The key that minted the link may no longer upload here
404 not_found The link's workspace was deleted as the upload arrived
413 The file is over max_bytes (a body that declares no size is cut there, and spends the link)
415 upload_body_is_the_file A multipart body: send the bytes themselves
422 upload_body_missing No body

limma_ingest refuses, before minting, what a link cannot carry (422): content that is not empty, an idempotency_key (each request mints a new link), a missing filename or one holding a path, an unknown document_kind, an invalid sync field.

2. Waiting for the read

A document is read in the background. wait_s — on limma_ingest (and POST /v1/public/ingest) and as a query parameter of the upload — bounds how long the call waits for it: 0 to 120 seconds. Left out, limma_ingest waits the deployment's default (LIMMA_INGEST_WAIT_S, 45 s). With an upload link, the mint's wait is the upload's: the link's command carries it (?wait_s=45 when the mint named none). An upload sent without wait_s does not wait, as the document door does not.

The reply's wait says why the call returned:

wait.outcome processing_status what to do
read complete, partial or failed read it. wait.held is {facts, records} the document holds now; a failed one carries wait.failure.reason
still_reading pending / processing the bound passed while it is queued or being read. Re-send the same content later — a file through a new link — and the call waits on the same document and never starts a second reading; or read GET /v1/public/usage
erased none the document was deleted while the call waited; wait.erased_at says when
no_reader pending no ingestion worker is alive: nothing was waited for, and the document is read once one starts

wait also gives wait_s (the bound) and waited_s. A wait holds no lock and no database connection: it reads the document's row, then pauses — at most a second between two reads — so deleting the document ends it within a second.

Give the call the time you asked for. A call that asks to wait 120 s answers after up to 120 s. The Python SDK and the stdio MCP server (python -m epistem.mcp.server) give such a call a read timeout of the wait plus 30 s; your own HTTP client, and any proxy in front of the API, must allow as much. An MCP client may stop waiting for a tool's reply sooner than that — the document is read all the same: call again to read its status.

3. Your usage

GET /v1/public/usage (REST and the SDK's usage(); no MCP tool — an agent's menu pays for every tool on every turn) tells the caller what its key holds and may do:

  • caller — the key's id (never the key), organization, role, permissions, workspace scope (null: every workspace of the organization), document scope (null, or how many documents it is confined to), the principals asserted for an end user, and its rate_limit (verbs: the one budget every /v1/public verb draws on but region and diff, each its own, and the upload, whose link was charged when minted; region; diff — each {limit, window_s}, per key).
  • workspaces — one entry per workspace the key reaches, in id order, a page of limit at a time (next_after for the next): documents (total and by_status), failures (total, and the newest failures_limit with each one's reason code), facts (current facts and prose statements), records (current relation rows), last_activity_at (when a document you may see last arrived or was read), default (the organization's default workspace).
  • plan — on the hosted service, the organization's plan: its name, trial_days, limits_in_force — whose limits are in force: plan (the plan's own), free_until_first_payment (a paid plan not yet paid for keeps the Free plan's limits until its first payment above $0; until_first_payment then gives the plan's own plan_limits and how they start, lifts) or trial (until the first payment, on a plan whose trial has limits of its own — none of the hosted plans has a trial; trial then gives the trial's limits, the plan's own plan_limits, ends_at and lifts), Stripe's subscription_status, its standing (active; grace, with read_only_from; or read_only — reasons says why: payment_failed, subscription_ended), subscription_ended_at and data_deleted_after (from when its content is deleted, 30 days after its subscription ended; null while it runs), and limits — the limits in force: documents_held, pages_per_month, api_calls_per_month, workspaces and max_file_mb, each {limit, used, remaining} (used and remaining are null for max_file_mb, a limit per file); pages_per_month also says where the pages went — by_kind: read (by the model) and loaded (without one) — and how a page is counted (counted_as, the rule in words). The counts are the whole organization's, in the calendar month (UTC) for the two monthly ones; a key confined to documents, or a call made for an end user, is shown the plan and its standing with limits null. Null for an organization on no plan — every one inside your own network.

A call over a plan's limit is refused 402 quota_exceeded, naming the limit, your count, the plan's allowed and the route that changes the plan (upgrade: POST /v1/billing/portal, with an admin key), and whose limits are in force (limits_in_force). On a paid plan not yet paid for (free_until_first_payment) it names the plan's own (plan_allows) and says the plan's limits start once its first payment succeeds (lifts: add a card in the portal and pay the open invoice). At a trial's own limit, trial is true and the reply names the plan's own (plan_allows), trial_ends_at and when it lifts; over pages_per_month it says how pages are counted (counted_as). Every read keeps working when only a loading limit is reached, and GET /v1/public/usage is never refused or counted. Once a subscription is read-only, every operation that loads or changes data is refused 402 account_read_only; questions, reads and deletion keep working.

Counted over the documents the caller may see: a key confined to documents, and a platform's request for an end user (X-Limma-Principals), count theirs alone, and are shown a workspace they did not name only when they may see something in it. Never a document's text, never another organization's workspace. A call that names no workspace makes none.

r = client.get("/v1/public/usage", params={"workspace_id": WS})
caller = r.json()["caller"]
print(r.status_code, caller["role"], caller["key_id"][:4] + "…", caller["permissions"])
print(caller["workspace_scope"], caller["document_scope"], caller["principals"])
print(sorted(caller["rate_limit"]))
(entry,) = r.json()["workspaces"]
print(entry["workspace_id"] == WS, entry["facts"] > 0, entry["failures"]["total"])
print("plan:", r.json()["plan"])
200 analyst key_… ['manage_catalogue', 'read', 'verify', 'write']
None None None
['diff', 'enabled', 'per', 'region', 'verbs']
True True 0
plan: None