← Statement to Sheet / API
Get a token

Drive Statement to Sheet from your own code

Everything the web app's AI reader does goes through one public surface. (Text PDFs are parsed in the browser and never touch the API; this page is about the paid AI read.) Base URL:

https://api.skillsafe.ai/v1/app-api

Every request carries Authorization: Bearer <token> and Content-Type: application/json. Every response is a JSON envelope: {"ok":true,"data":{…}} on success and {"ok":false,"error":{"code":"…","message":"…"}} on failure. Read error.code, not only the HTTP status.

Errors

CodeMeaningWhat to do
UNAUTHORIZEDMissing, expired or wrong-app token.Mint a guest token or sign in again.
INSUFFICIENT_CREDITSBalance below min_credits.Top up. /estimate is free and tells you the hold.
VALIDATION_ERRORThe input object failed validation.Check error.details. The body is the input object itself — never wrap it in {"input": …}.
NOT_FOUNDA $files id does not exist for this token's subject.Upload again with the same token you run with.
RATE_LIMITEDToo many requests.Back off and retry.
JOB_FAILEDThe run started and did not complete.Retry with the same Idempotency-Key.

The input

One task, "extract", with two sources. Fields are scalars; the object-valued ones (hints, facts) travel as JSON strings.

FieldTypeMeaning
taskstring, requiredAlways "extract".
sourcestring"images" (page scans or photos attached as $files) or "text" (a PDF's text layer in text).
$filesarray of file idsImages uploaded first with POST /files (step 3). Up to 8 per run. The web app shrinks each to 1600 px on the long side as JPEG and deletes it after the run.
textstringThe text layer, lines in reading order, cells separated by two spaces, --- page N --- before each page. The web app sends at most 24,000 characters, cut on whole lines with a [... N lines cut ...] marker.
file_namesstringFile names, comma-separated, in $files order.
hintsstring (JSON)Optional currency, date_order (MDY/DMY), year.
factsstring (JSON)What the free parser already measured (why AI is needed, totals found, rows read).
retry_notestringSent only on the one automatic reformat retry.

The output contract

The output (output.output) is one JSON object: {"task":"extract","documents":[…],"notes":""}. Each document has source, doc_type (bank_statement · credit_card · receipt · bill · other), account (institution, name, last4, currency), period (start, end, ISO dates), totals (opening_balance, closing_balance, total_in, total_out as printed, or null), transactions[] (date, description, amount, balance, category, page, uncertain), receipt (merchant, date, items, subtotal, tax, tip, fees, discount, total, payment) or null, unreadable[] and notes.

Sign convention: amount is positive for money in and negative for money out, from the account holder's side — a card purchase is negative, a card payment positive. Amounts are JSON numbers with no separators. The model transcribes totals as printed and never adjusts a number to make them agree; the web app then reconciles opening + in − out against closing, the rows against the printed totals, and the running balance row by row. If you call the API directly, run that reconciliation yourself.

1. Get a token

A guest token is enough for /me and /estimate. Running is metered and needs a personal token from signing in. The tokens page shows yours and copies it. Replace YOUR_TOKEN below with it.

2. Check the session and the balance

/me returns subject_type, subject_id and credits. Signed in means subject_type === "user".

3. Upload the page images

POST /files as multipart/form-data with a file part; data.file.file_id is what goes in $files. Delete it afterwards with DELETE /files/FILE_ID.

curl -s -X POST "https://api.skillsafe.ai/v1/app-api/files" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@receipt.jpg;type=image/jpeg"

4. Price it before you run it

/estimate is free and starts no job. hold_credits is a reservation priced against the longest possible answer, not the price. Estimate with the real $files attached: each image adds to the hold. Images:

Text (a PDF whose text layer you already have):

5. Run and poll

/run returns a job. Always send an Idempotency-Key — the same key is the same run, so a retry cannot bill twice. The web app derives it from a hash of the input.

6. Stream it

/run-stream answers with server-sent events and ends with a done frame carrying the whole output, charged_credits and truncated. If truncated is true the balance ran low mid-answer: parse the complete rows that arrived and top up for the rest.