Skavio Processing API: a quote-controlled Flow workflow in Python and Node.js

Skavio Processing API lets a server-side application upload an invoice, estimate its credit requirement, submit a Flow extraction job and download authenticated results. The important part is not the choice of runtime: it is preserving the approved payload, submission key and job ID so that retries remain the same request. This quickstart outlines that lifecycle with the official Python and Node.js clients and the published OpenAPI contract.

1. Establish the contract baseline and install the client

Start with the [official developer kit](https://github.com/skavio-eu/skavio-processing-api) and [API documentation](https://www.skavio.eu/api/docs/). Create an account and company profile in the [web workspace](https://www.skavio.eu/dashboard/), then create a read/write API key. Keep the secret server-side in SKAVIO_API_KEY. Requests authenticate with Authorization: Bearer; never put the key in browser code or URLs. Keep the version numbers separate. Research checked on 7 October 2026 identified an API 1.9.0 repository snapshot captured on 4 October. Routes remain under /v1. The fetched live contract declares OpenAPI specification format 3.1.0, while the SDK README identifies SDK version 1.1.0. These describe different things: API metadata, specification format and client-library version. The research did not confirm the live contract’s info.version. The documented requirements are Python 3.10+ and Node.js 22+. At the research check, the SDK README stated that the packages were not yet published to PyPI or npm. Install the client locally from the repository: ```sh git clone https://github.com/skavio-eu/skavio-processing-api.git cd skavio-processing-api # Run the installation command for your chosen language. python3 -m pip install ./sdk/python npm install ./sdk/javascript # Private local state for the examples below. mkdir -m 700 .quickstart-state ``` The documented imports are from skavio import SkavioClient in Python and import { SkavioClient } from '@skavio/sdk' in Node.js. Configure the client using the README’s constructor options and API server URL. For a reproducible integration, pin a reviewed repository commit rather than relying on a moving branch, and check the current [OpenAPI contract](https://www.skavio.eu/api/openapi.json) before deployment.

2. Upload a small invoice and let the server estimate the work

Use the developer kit’s synthetic invoice for the first run, not a production document. Follow the SDK README’s upload example: POST /v1/uploads uses the multipart field files, and the first uploaded source ID is uploads[0].id. Build a Flow request with operation set to flow-extract, upload_ids containing that source ID, and fields copied from the documented Flow example. Do not guess the field schema: use the published contract and example. POST /v1/estimate returns estimated_credits without starting processing. Copy that value into max_credits before submitting to POST /v1/jobs. This sets a submission ceiling based on the server’s estimate rather than a client-side price calculation. The canonical Flow rate is 25 shared credits per page, with a minimum of 25 credits per document. Web tools and API jobs use the same company wallet. Submission reserves the fixed measured quote; success charges it once, and permanent failure releases the reservation. An estimate is not itself a processing job. The next examples accept an already configured client, an upload response and the documented fields definition. They illustrate persistence and submission—not standalone scripts—and were not independently executed. As written, they accept the returned estimate. Add your application’s budget or approval check before saving and submitting the request.

3. Persist the payload and key before sending the job

For an idempotent retry, keep both the submission key and the payload unchanged. Uploading the invoice again can produce different upload_ids, so recovery must load the original submission record instead of rebuilding it. Choose one language for this example. Both helpers use one private state file for one logical request and assume a single writer. They save the payload and key before submission, then save the full submission response. If a connection drops after the server accepts the job but before the response is saved, the next call retries with the original key and payload. Python: ```python import json from pathlib import Path from uuid import uuid4 STATE = Path('.quickstart-state/flow.json') def save_state(value): temporary = STATE.with_suffix('.tmp') temporary.write_text(json.dumps(value), encoding='utf-8') temporary.replace(STATE) def submit_flow(client, upload_response=None, fields=None): if STATE.exists(): state = json.loads(STATE.read_text(encoding='utf-8')) else: if upload_response is None or fields is None: raise ValueError('First run requires an upload and fields.') payload = { 'operation': 'flow-extract', 'upload_ids': [upload_response['uploads'][0]['id']], 'fields': fields, } estimate = client.estimate(payload) payload['max_credits'] = estimate['estimated_credits'] # Add your budget or approval check here before saving. state = {'payload': payload, 'key': str(uuid4())} save_state(state) if 'submission' not in state: state['submission'] = client.submit_job( state['payload'], idempotency_key=state['key'] ) save_state(state) return state['submission'] ``` On the first run, call submit_flow(client, upload_response, fields_from_example). To recover that same request, call submit_flow(client) without uploading again. Node.js: ```javascript import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs'; const statePath = '.quickstart-state/flow.json'; function saveState(value) { const temporary = `${statePath}.tmp`; writeFileSync(temporary, JSON.stringify(value), { mode: 0o600 }); renameSync(temporary, statePath); } export async function submitFlow(client, uploadResponse, fields) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); } else { if (!uploadResponse || fields == null) { throw new Error('First run requires an upload and fields.'); } const payload = { operation: 'flow-extract', upload_ids: [uploadResponse.uploads[0].id], fields, }; const estimate = await client.estimate(payload); payload.max_credits = estimate.estimated_credits; // Add your budget or approval check here before saving. state = { payload, key: randomUUID() }; saveState(state); } if (!Object.hasOwn(state, 'submission')) { state.submission = await client.submitJob(state.payload, state.key); saveState(state); } return state.submission; } ``` On the first run, call await submitFlow(client, uploadResponse, fieldsFromExample). Recovery uses await submitFlow(client). Read the returned job ID according to the documented submission response schema. The saved response preserves it for subsequent polling. Use a separate state record for each new logical request; do not delete a pending record just to retry. In production, replace this single-file example with a durable job store and concurrency control.

4. Poll the saved job, then download its results

Submission returns HTTP 202: accepted is not completed. Resume using the saved job ID, not a fresh submission. The SDK helpers are client.wait('job', job_id) in Python and await client.wait('job', jobId) in Node.js. Check the returned status after wait(); only proceed to results when the job is completed. A failed job needs failure handling, not a download attempt. A polling timeout does not stop processing on the server. If you implement polling yourself, use GET /v1/jobs/{id}. The documentation recommends intervals of 3–10 seconds, with backoff for HTTP 429 and transient server errors. An ambiguous submission retry must retain the original idempotency key and unchanged payload. Completed jobs expose result.files[].download_path. Pass those paths to client.download(download_path) in Python or await client.download(downloadPath) in Node.js, then save the returned content as described in the SDK README. SDK downloads strip credentials when leaving the API origin; preserve that safeguard rather than forwarding the bearer key to another host. Move the downloaded artifacts into your application’s storage before retention expires. The supplied API limits specify seven-day retention. Treat a completed extraction as ready for review: check invoice dates, totals and line items before using the output downstream.

5. Extend through the contract, not guessed client methods

Once the lifecycle works, keep your integration tied to the published contract. The SDK’s call() method accepts an exact OpenAPI operationId; the documented example is usage_v1_usage_get with the query month set to 2026-10. Operation IDs are unique and case-sensitive, but they are not necessarily generated client method names. A documented endpoint also does not bypass authorization. For the next step, open the [Processing API documentation](https://www.skavio.eu/api/docs/) alongside your [company workspace](https://www.skavio.eu/dashboard/). Set up the key, run the synthetic fixture in your chosen runtime, and verify recovery before connecting production documents.

Open the Processing API documentation