Sign in

Files and statuses

View as Markdown

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:

FieldMeaning
fileRequired. PDF, image (JPEG, PNG, TIFF, WebP) or e-invoice XML (XRechnung, UBL, CII), up to 25 MB. No archives in this version.
nameThe file name; else the upload's.
external_refYour own id, up to 200 characters (Odoo: account.move,<id>), returned with the file. Resent bytes keep the first one's.
AnswerMeaning
202 AcceptedNew file, status: "received"; reading starts at once.
200 OKThe same bytes (by SHA-256, not name) came before: that file as it is now, not read or counted again.
409 urn:ocost:file:keptThe 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, 411Too large, a format Ocost doesn't read, no Content-Length.

Every answer carries X-Ocost-Documents-Left, your plan's documents left this month.

Shell
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:

StatusMeaning
receivedStored, not read yet.
readingBeing read; while no reading service answers, Ocost retries for up to a day.
readdocuments lists each: id, kind, number, pages.
not_readStored, not read; see reason.
failedCouldn't be read; see reason.
ReasonWithMeaning
limitnot_readThe month's documents are used up; Ocost reads the file on renews_on.
model_offnot_readThis business may not have PDFs and images read; e-invoices still are.
nothing_foundnot_readNo document in it.
too_largenot_readToo many pages or too large an image.
damagedfailedNot a file Ocost can open.
unavailablefailedNo reading service answered for a day; resend the bytes to retry.
failedfailedAnything else.
othereitherA 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.

Shell
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 more is true, ask with cursor=<next_cursor> for the next page; then keep the cursor for your next poll.
  • status filters after paging, so a page may hold fewer than limit: follow more, 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

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.

AnswerMeaning
204Erased; 404 after.
409 urn:ocost:file:readingBeing read; ask again later.
409 urn:ocost:file:linkedOcost 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.