Skip to content

File rebuild (round-trip)

CloakAPI can round-trip a whole document, not just text. You attach a file (see File attachments — note the caution there: on the blind relay, file extraction and tokenisation happen client-side via the browser extension or desktop app, not on our servers), and once the model responds you can turn the (detokenised) response back into a real file — a .docx, .pdf, .xlsx, and 13 other formats — via POST /v1/reconstruct.

How it works

1. Client uploads a file (multipart or base64) alongside a chat/messages call
2. Gateway [FileIngestMiddleware] converts the file to Markdown via MarkItDown
(raw bytes discarded — never stored, never forwarded)
3. PrivacyTokenisationMiddleware replaces PII in the Markdown with [TOKEN_*]
placeholders before the request leaves the trust boundary
4. Upstream LLM provider sees only tokenised Markdown, returns a tokenised response
5. Caller detokenises the response client-side ("balanced" / "continuity" tiers)
using the alias map it already holds for the conversation
6. Caller calls POST /v1/reconstruct with the detokenised text + a target format
7. Gateway shells out to the `cloak-reconstruct` CLI (packages/reconstruction),
which regenerates a real file and returns it as a binary download with
receipt headers

At the max privacy tier, detokenisation happens entirely on the desktop app — the gateway never sees plaintext PII, so there is nothing for it to reconstruct; the desktop app rebuilds the file locally instead. /v1/reconstruct is for the balanced and continuity tiers, where the caller (portal, desktop app, or an SDK-based integration) holds the alias map and asks the gateway to do the file assembly work.

Request

POST /api/v1/reconstruct

This endpoint currently sits behind the same session gate as the rest of the authenticated /v1/* surface — auth:sanctum, verified.json, and legal.accepted (see apps/gateway/routes/api.php:692-999, group starting at line 692; the route itself is registered at line 999). In practice today that means it’s reachable from an authenticated portal session (browser cookie), not yet from a bearer cloak_... API key — direct API-key support for this endpoint is in active development.

{
"response_text": "Dear [TOKEN_PERSON_1], your invoice for [TOKEN_ORG_1] is attached.",
"alias_map": {
"conversationId": "conv_9f1c2b7a",
"hmacPrefix": "a1b2",
"entries": [
{ "placeholder": "[TOKEN_PERSON_1]", "original": "Henry James" },
{ "placeholder": "[TOKEN_ORG_1]", "original": "Acme Corp" }
]
},
"target_format": "docx",
"input_mime": "application/pdf",
"fidelity": "structural"
}
FieldTypeRequiredNotes
response_textstringyesThe (already detokenised) text to rebuild. Max 10 MB (MAX_RESPONSE_TEXT_BYTES).
alias_map.conversationIdstringyesMax 256 chars.
alias_map.hmacPrefixstringyesMax 16 chars — the request-scoped HMAC prefix salt CloakAPI’s tokeniser stamped on every placeholder it emitted for this conversation.
alias_map.entries[]arrayyesEach entry: { placeholder, original }, both strings, max 512 chars each.
target_formatstringyesOne of the 16 supported output formats — see below.
input_mimestringnoOriginal file’s MIME type, e.g. application/pdf. Defaults to text/plain.
fidelitystringnotextual | structural | layout | typographic. Defaults to structural.

Source: apps/gateway/app/Http/Controllers/Api/V1/ReconstructController.php:85-110.

Note that only placeholders whose prefix matches alias_map.hmacPrefix are honoured — tokens from a different conversation, or text that merely looks like a placeholder, are rejected by design (packages/reconstruction/src/alias-resolver.ts).

Supported formats

The gateway validates target_format against a fixed allowlist of 16 formats (ReconstructController::ALLOWED_FORMATS), matching the 16 “implemented” formats in the reconstruction package’s format matrix (packages/reconstruction/docs/matrix.md):

FormatMIME typeRound-trip fidelity
txttext/plainGREEN — trivial
mdtext/markdownGREEN — pass-through
htmltext/htmlGREEN
docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentYELLOW — style skeleton, new paragraphs get default style
pdfapplication/pdfYELLOW — textual fidelity, no original glyph layout
xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetGREEN — values only, formulas write-only
csvtext/csvGREEN
pptxapplication/vnd.openxmlformats-officedocument.presentationml.presentationYELLOW — structured JSON or auto-split by headings
epubapplication/epub+zipYELLOW
latexapplication/x-latexGREEN — raw .tex, run pdflatex yourself
svgimage/svg+xmlYELLOW — text-to-SVG, no original vector layout
srtapplication/x-subripGREEN
vtttext/vttGREEN
jsonapplication/jsonGREEN — RFC 8785 canonical
yamltext/yamlGREEN
xmlapplication/xmlGREEN

Not yet supported (requesting these as target_format fails Laravel validation with a 422 today — see Error codes): png/jpg image output, rtf, odt/ods/odp, mmd (Mermaid), mp3, mp4, zip. See packages/reconstruction/docs/matrix.md for the full deferred-formats rationale.

Response

On success, the gateway returns the reconstructed file as a binary download, not JSON:

HTTP/1.1 200 OK
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
Content-Disposition: attachment; filename="reconstructed.docx"
Content-Length: 48213
X-CloakAPI-Receipt-Input-Mime: application/pdf
X-CloakAPI-Receipt-Output-Mime: application/vnd.openxmlformats-officedocument.wordprocessingml.document
X-CloakAPI-Receipt-Tokeniser-Version: <version>
X-CloakAPI-Receipt-Reconstructor-Version: <version>
X-CloakAPI-Receipt-Input-Hash: sha256:...
X-CloakAPI-Receipt-Output-Hash: sha256:...
X-CloakAPI-Receipt-Fidelity-Achieved: structural
X-CloakAPI-Receipt-Unresolved-Tokens: 0
X-CloakAPI-Receipt-Partial: false
<binary file bytes>

Header source: ReconstructController.php:224-237. X-CloakAPI-Receipt-Unresolved-Tokens is a count, not a boolean — any value greater than zero means some placeholders in response_text could not be resolved against alias_map (the file is still returned; those spans are left as literal placeholder text).

file_rebuilt receipt claim (planned)

Today the receipt evidence for a reconstruction call is carried only in the X-CloakAPI-Receipt-* response headers shown above. Folding this into a signed claims.file_rebuilt block on the OpenReceipt envelope — mirroring the claims.tokenisation claim already used for file attachments — is in active development. The expected shape, once shipped, is:

{
"claims": {
"file_rebuilt": {
"target_format": "docx",
"fidelity_requested": "structural",
"fidelity_achieved": "structural",
"input_mime": "application/pdf",
"output_mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"input_hash": "sha256:...",
"output_hash": "sha256:...",
"unresolved_tokens": 0,
"partial": false
}
}
}

Treat this block as forthcoming — verify against the response headers until it ships.

Limits

LimitValueSource
Max response_text size10 MBReconstructController::MAX_RESPONSE_TEXT_BYTES
CLI process timeout60 seconds (hard)ReconstructController.php:145,184
alias_map.conversationIdmax 256 charsvalidation rule
alias_map.hmacPrefixmax 16 charsvalidation rule
alias_map.entries[].placeholder / .originalmax 512 chars eachvalidation rule

curl example

Terminal window
curl -s https://api.cloakapi.io/api/v1/reconstruct \
-H "Content-Type: application/json" \
-H "Cookie: cloakapi_session=$SESSION_COOKIE" \
-H "X-XSRF-TOKEN: $XSRF_TOKEN" \
-d '{
"response_text": "Dear [TOKEN_PERSON_1], your invoice for [TOKEN_ORG_1] is attached.",
"alias_map": {
"conversationId": "conv_9f1c2b7a",
"hmacPrefix": "a1b2",
"entries": [
{ "placeholder": "[TOKEN_PERSON_1]", "original": "Henry James" },
{ "placeholder": "[TOKEN_ORG_1]", "original": "Acme Corp" }
]
},
"target_format": "docx",
"input_mime": "application/pdf",
"fidelity": "structural"
}' \
-o reconstructed.docx -D -

(This endpoint is session-authenticated today — see Request — so a plain Authorization: Bearer cloak_... header alone won’t yet authenticate against it; use the portal’s session cookie + CSRF token as shown, or drive it from the portal/desktop UI directly, described below.)

TS and Rust SDK snippets

The CloakAPI SDKs are not yet published (npm/crates.io distribution is pending) — until they ship, call the HTTP endpoint directly as shown above. The snippets below are a preview of the planned rebuildFile / rebuild_file helpers; the exact signature may still shift slightly before release.

TypeScript (sdks/typescript, following the existing CloakClient request pattern in sdks/typescript/src/client.ts):

import { CloakClient } from '@cloakapi/sdk';
const client = new CloakClient({ sanctumToken: sessionToken });
const { bytes, mimeType, filename } = await client.rebuildFile({
responseText: detokenisedText,
aliasMap, // { conversationId, hmacPrefix, entries }
targetFormat: 'docx',
inputMime: 'application/pdf',
fidelity: 'structural',
});

Rust (sdks/rust, following the existing count_tokens pattern in sdks/rust/src/client.rs):

use cloakapi::CloakClient;
let client = CloakClient::with_sanctum_token(api_key, session_token)?;
let file = client.rebuild_file(&ReconstructRequest {
response_text: detokenised_text,
alias_map, // conversation_id, hmac_prefix, entries
target_format: "docx".into(),
input_mime: Some("application/pdf".into()),
fidelity: Some("structural".into()),
}).await?;

Portal and desktop apps

Browser portal — a download button on the response bubble, next to the receipt strip, lets you pick an output format and pulls the reconstructed file via POST /v1/reconstruct using the alias map already held for that conversation.

Desktop app — the equivalent download button in the chat composer calls the same endpoint for balanced/continuity tiers; at the max tier the Rust backend rebuilds the file locally instead (no network round-trip, since detokenisation never leaves the device at that tier).

Error codes

ResponseHTTPWhen
Laravel validation error ({"message": "...", "errors": {...}})422Missing/invalid response_text, alias_map.*, or target_format not in the allowlist
{"error": "Reconstruction failed", "code": <exit code>}422The cloak-reconstruct CLI process exited non-zero
{"error": "Internal server error"}500Temp-file creation failed, or an unexpected exception
{"error": "Internal JSON error"}500Alias map or CLI metadata failed to parse as JSON
{"error": "Reconstruction produced empty output"}500The CLI produced zero output bytes
{"verdict": "unverified", ...}403Authenticated user’s email is not verified (EnsureEmailVerified)
{"verdict": "legal_acceptance_required", "redirect": "/onboarding"}403Authenticated user hasn’t accepted ToS/Privacy Policy yet (EnsureLegalAccepted)
Unauthenticated.401No valid session (auth:sanctum failed)

Source: ReconstructController.php:104-258, EnsureEmailVerified.php, EnsureLegalAccepted.php.

Zero-payload invariant: the gateway logs only the exit code and the first 200 characters of stderr on CLI failure — never file content, alias-map entries, or response_text (ReconstructController.php:149-154,188-193). The alias-map temp file and the output temp file are both shredded (@unlink) in a finally block regardless of outcome.

  • File attachments — the ingest half of the round-trip
  • Receipts — receipt envelope format and verification
  • Privacy model — tokenisation tiers, including why max never reaches the gateway with plaintext