Files and statuses
A file is one PDF, image or e-invoice you send. It holds one or more documents: a scan of three invoices is one file, three documents.
Sending a file
POST /public/v1/files, multipart/form-data, scope documents:write:
| Field | Meaning |
|---|---|
file | Required. PDF, image (JPEG, PNG, TIFF, WebP) or e-invoice XML (XRechnung, UBL, CII), up to 25 MB. No archives in this version. |
name | The file name; else the upload's. |
external_ref | Your own id, up to 200 characters (Odoo: account.move,<id>), returned with the file. Resent bytes keep the first one's. |
| Answer | Meaning |
|---|---|
202 Accepted | New file, status: "received"; reading starts at once. |
200 OK | The same bytes (by SHA-256, not name) came before: that file as it is now, not read or counted again. |
409 urn:ocost:file:kept | The same bytes are kept where keys don't see them (a person's own upload). Nothing changes. |
413 urn:ocost:file:too_large, 415 urn:ocost:file:format, 411 | Too large, a format Ocost doesn't read, no Content-Length. |
Every answer carries X-Ocost-Documents-Left, your plan's documents left this month.
curl -sS https://api.ocost.io/public/v1/files \
-H "Authorization: Bearer $OCOST_KEY" \
-F "file=@scan.pdf" -F "name=scan.pdf" -F "external_ref=account.move,42"Statuses
GET /public/v1/files/{file_id} (scope documents:read) answers the file with its status:
| Status | Meaning |
|---|---|
received | Stored, not read yet. |
reading | Being read; while no reading service answers, Ocost retries for up to a day. |
read | documents lists each: id, kind, number, pages. |
not_read | Stored, not read; see reason. |
failed | Couldn't be read; see reason. |
| Reason | With | Meaning |
|---|---|---|
limit | not_read | The month's documents are used up; Ocost reads the file on renews_on. |
model_off | not_read | This business may not have PDFs and images read; e-invoices still are. |
nothing_found | not_read | No document in it. |
too_large | not_read | Too many pages or too large an image. |
damaged | failed | Not a file Ocost can open. |
unavailable | failed | No reading service answered for a day; resend the bytes to retry. |
failed | failed | Anything else. |
other | either | A reason this API doesn't name yet. |
Waiting for a file
Poll every 2 to 3 seconds for half a minute, then every 10 to 30. Most PDFs take under a minute, long ones a few.
For many files, list what changed: start with updated_since (or nothing, for all), then follow the cursor.
curl -sS -G https://api.ocost.io/public/v1/files \
-H "Authorization: Bearer $OCOST_KEY" \
--data-urlencode "updated_since=2026-11-09T08:00:00Z" \
--data-urlencode "status=read" --data-urlencode "status=not_read" \
--data-urlencode "status=failed"- Oldest change first, up to
limit(1 to 100, default 50), a file once per change. - While
moreis true, ask withcursor=<next_cursor>for the next page; then keep the cursor for your next poll. statusfilters after paging, so a page may hold fewer thanlimit: followmore, not the count.
Waiting for one file, and collecting everything changed since your last poll:
def wait_for(session, file_id, timeout=300):
started = time.monotonic()
while time.monotonic() - started < timeout:
file = session.get(f"{API}/files/{file_id}", timeout=15).json()
if file["status"] not in ("received", "reading"):
return file
time.sleep(3 if time.monotonic() - started < 30 else 15)
return file// Every file changed since the cursor kept from the last poll (null: all).
async function changedSince(cursor) {
const files = [];
for (;;) {
const params = new URLSearchParams({ limit: '100', ...(cursor ? { cursor } : {}) });
const page = await fetch(`${API}/files?${params}`, { headers: auth }).then((r) => r.json());
files.push(...page.files);
cursor = page.next_cursor ?? cursor;
if (!page.more) return { files, cursor }; // keep `cursor` for the next poll
}
}The original
GET /public/v1/files/{file_id}/content answers the bytes as sent.
Deleting a file
DELETE /public/v1/files/{file_id} (scope documents:write) erases the file, its documents, what was read, its search index and the original, at once and for good.
| Answer | Meaning |
|---|---|
204 | Erased; 404 after. |
409 urn:ocost:file:reading | Being read; ask again later. |
409 urn:ocost:file:linked | Ocost still needs it: a booked invoice (paid, matched, exported or not), a bank statement, an open question, a project's file. detail says which; delete it in Ocost once nothing does. |