OpenAI-compatible API
OpenAI-compatible API
Point any OpenAI client at Paperwork. Parse, extract, and redact documents through /chat/completions.
Paperwork serves an OpenAI-compatible surface. Set the base URL, send your usual
CloudRaker API key, and pick a paperwork-* model. The façade translates each
call into exactly one capability run and returns the result in OpenAI’s
chat.completion shape.
Use it when your stack already speaks OpenAI: LiteLLM, LangChain, Continue, Cursor, or the official SDKs. For new code, the native capability endpoints give you more control.
This is not a chat model. There is no conversation state, no tool calling, and no token stream. Every request must attach a file. Token counts are always zero — this API does not bill by token.
Base URL
Both production forms reach the same worker. api.paperwork.sh rewrites /v1/*
to the internal /openai/v1/* prefix, because every OpenAI client appends
/chat/completions to a /v1 base.
Authentication
Send your organization API key as the Bearer token. Nothing else changes.
The tenant comes from the key’s organization. OpenAI-Organization,
OpenAI-Project, OpenAI-Beta, and x-stainless-* headers are ignored, never
rejected. You cannot select another organization with a header.
A missing or bad key returns the OpenAI error shape:
Models
GET /models lists them. GET /models/{model} returns one.
paperwork-auto runs extract when response_format.type is json_schema or
json_object. Otherwise it parses.
paperwork-extract-cited exists for model pickers. Tools such as Cursor and
Continue cannot send a custom body, so the model name is the only channel for
turning citations on.
An unknown model returns 404:
Your first call
Attach a file part and send it. This parses a document to markdown:
The response is a standard chat.completion with two additions:
Response headers carry x-request-id, x-cloudraker-run-id, and
idempotent-replay: true on a replay.
What content holds
content is always a string, never null.
Citations, per-document results, and signed output URLs stay in the cloudraker
key. They never enter content, so JSON.parse(content) keeps working.
File inputs
Walk order is message order. Every file part is collected.
Counts: parse and redact take exactly one file. Extract takes 1 to 100. Zero
files is a 400 with code: "missing_file".
The Files API
The OpenAI file endpoints work, over the same corpus as
/v1/files:
purpose is accepted, stored, and echoed back. There is one corpus, so it
selects nothing.
Message text
Text parts are joined with a blank line: every system and developer text
first, then every user text. assistant and tool messages are ignored — the
façade is one-shot and stateless.
A dropped prompt is reported, never silent: the response carries
cloudraker.ignoredInstructions: true.
Structured output
The extraction schema dialect is
stricter than OpenAI’s. $defs, $ref, oneOf, anyOf, allOf, const, and
pattern are refused, the root must be an object, depth is capped at 5, and the
schema must stay under 64 KB. A rejection is a 400 with
code: "invalid_schema" and param: "response_format.json_schema.schema".
Paperwork options
Non-OpenAI knobs live under one namespaced cloudraker key. The official SDKs
reach it with extra_body.
An unknown key inside cloudraker is a 400 with
param: "cloudraker.<key>". Any other key is namespaced on purpose: OpenAI keeps
adding top-level fields.
Streaming
stream: true returns text/event-stream. There is no token stream underneath,
so this is a keep-alive wrapper, not fake token output. Use it when the work
takes longer than the synchronous cap.
The sequence is:
- An SSE comment as the first byte:
: request=req_… status=processing. Every SDK decoder drops comments, so the first chunk comes later — size client timeouts on the run, not on time-to-first-byte. - The same comment every 10 seconds while the run works.
- When the run resolves, a comment carrying the run id
(
: run=exr_… request=req_… status=processed), then a chunk withdelta: {"role":"assistant","content":""}, then one chunk carrying the wholecontent. - A chunk with
finish_reason: "stop". - A usage chunk with
"choices": [], if you sentstream_options: {"include_usage": true}. data: [DONE].
A failure after the headers sends one data: {"error":{…}} frame and closes,
with no [DONE].
x-request-id and idempotent-replay never reach a streaming client: both are
written after the body starts. The keep-alive comment carries the request id and
the run id instead.
Usage and cost
prompt_tokens, completion_tokens, and total_tokens are always 0. This
API does not bill by token, and the public envelope carries no token count.
usage.cloudraker passes through the run’s own usage, which parse, extract, and
redact do not produce. It is {} on every response.
A façade run costs exactly what the same native /v1 call costs. Read the balance in
Settings → Billing.
Timeouts and long work
A non-streaming call waits 90 seconds by default, a streaming call up to 120.
Set cloudraker.wait to change it, up to 120.
If the run is still working at the cap, you get 504:
The run keeps going. Read it at GET /v1/runs/{id},
or send cloudraker.webhook and get told when it finishes. An SDK retry after a
504 is a replay, not a second run: the façade derives an idempotency key from
your organization, the request body, and the current hour.
Errors
Errors use OpenAI’s envelope, not the capability envelope:
Every error carries x-request-id. Both official SDKs surface it as
response._request_id. Quote it in support requests.
Rate limits
The façade draws on the same per-organization budgets as /v1: at least 67
requests per minute overall, and 20 per minute on
POST /chat/completions and POST /files. A 429 keeps its Retry-After
header. See Rate limits.
Request size
A request body must stay under 25 MB, on /chat/completions and /files
alike. For larger documents, register the file first — with POST /files here,
or the presigned flow on POST /v1/files — and
send {"type":"file","file":{"file_id":"…"}}.
Ignored and refused parameters
Chat parameters that have no meaning here are ignored, never rejected:
temperature, top_p, presence_penalty, frequency_penalty, logit_bias,
seed, stop, max_tokens, max_completion_tokens, reasoning_effort,
store, service_tier, user, safety_identifier, modalities,
parallel_tool_calls, and prompt_cache_key. Unknown fields are ignored too, so
an SDK upgrade never breaks a call.
These return a 400 with code: "unsupported_parameter", because silence would
be a lie:
Clients
Vercel AI SDK
Call openai.chat(id), not openai(id). openai(id) targets OpenAI’s
Responses API, which this façade does not implement.
What this API does not do
- No conversation state. Each request is one run.
- No tool calling, no
/responses, no/batches, no embeddings. - No token counts and no per-request cost.
- No browser (CORS) support.
- No space scoping. Façade runs land in a hidden
workspace, expire with their
ttl, and are not indexed. Use the native endpoints to keep results.