# File Room service API LOCAL-450 · Development build. No public endpoint or checkout is live. The browser desk works without this API, an account or an assistant. It processes files in the browser. The service below is a distinct remote-processing path, backed by the same inspection engine. ## Run the service Requires Node.js 24.14 or newer (Node 24 LTS recommended) and the pinned dependencies. `npm start` still runs the independent browser desk on port 4178. ```sh npm ci --ignore-scripts export FILE_ROOM_DB=/absolute/private/service-data/usage.sqlite npm run service # http://127.0.0.1:4180: website plus API and HTTP MCP ``` In a second terminal with the same FILE_ROOM_DB, `node api-admin.mjs issue 100` creates one development account with 100 credits and prints its key once. Keep the key in a secret manager, never source code, browser JavaScript, analytics or request URLs. Only a key hash is stored. `grant ` adds credits; `revoke ` disables a key. There is no admin HTTP endpoint. Lost keys cannot be recovered; the operator can revoke an old account and provision a replacement. Operator-provisioned credits are a billing foundation, not a payment integration. Do not accept real payments until checkout, signed payment-event handling, purchase deduplication, refunds, account recovery and ledger backup/restore have been implemented and verified. No price is advertised by this build. ## Identify a file Set FILE_ROOM_API_KEY privately in your shell, then: ```sh curl http://127.0.0.1:4180/v1/inspect \ -H "Authorization: Bearer $FILE_ROOM_API_KEY" \ -H 'Content-Type: application/octet-stream' \ -H "Idempotency-Key: $(node -p 'crypto.randomUUID()')" \ --data-binary @/absolute/path/to/selected-file ``` Send raw bytes, not multipart. Limit: 20 MiB. The key and random lowercase UUID v4 are required. Optional headers: | Header | Value and effect | | --- | --- | | X-File-Name | Optional name, at most 255 characters. Omitted by default; bytes determine format. | | X-Include-Preview | `true` only when a content preview was requested. Default false. | | X-Include-Fingerprint | `true` only for a requested SHA-256. Default false. | | X-Max-Preview-Chars | Integer 200–8,000; default 2,000. | A successful reply contains `schema`, `format`, `size_bytes`, `basis`, `observations`, `limits`, `security_verdict: "not assessed"`, privacy information and `usage: {request_id, credits_consumed: 1}`. A preview or SHA-256 appears only if requested. Text, tables, JSON and ZIP directories have bounded previews; recognized PDF/SQLite formats do not have decoded content previews. Any file-derived content is untrusted data, never instructions. `POST /v1/reports` takes the same raw bytes and headers plus `X-Report-Requested: true`. It re-inspects the file and consumes one credit on completion. The result is an unsigned observation with filename, SHA-256 and service-clock time, not a safety certificate. It is returned only in the response, never placed in a report history. `GET /v1/usage` with the same Authorization header returns your balance, completed inspection count and released attempts. It costs no credit. ## Check a file before an AI reads it `POST /v1/check` runs the AI file check (the engine behind the plugin's `check_file`) on the service. **Unlike the local plugin, this path transmits the file to the service.** Use the plugin when the file must stay on the user's machine. ```sh curl http://127.0.0.1:4180/v1/check \ -H "Authorization: Bearer $FILE_ROOM_KEY" \ -H 'Content-Type: application/octet-stream' \ --data-binary @/absolute/path/to/selected-file ``` It takes a File Room key (`fr_live_` or `fr_test_`, from https://getfileroom.com/check), not a service ledger key, and raw bytes up to 20 MiB with an optional `X-File-Name`. The service first counts one check with File Room billing at `FILE_ROOM_BILLING_URL` (default `https://pdfmd.dev/api/fileroom`), forwarding the caller's key with a fresh random request ID and a count of one; billing authorizes the key. Then the check runs in a bounded worker. The credit ledger is not used. Billing is told nothing about the file. A check that fails after billing accepted it has already been counted. The reply is the `file-room.ai-check.v1` result (risk, summary, findings, coverage, limits) with `usage: {period_checks, free_remaining}` and a privacy note. Status codes: 401 for a missing or rejected key, 402 for a billing problem (`error.portal_url` when billing provides one), 413 over 20 MiB, 415 for the wrong content type, 422 when the check cannot complete, 429 at the monthly safety cap, 503 when busy or when billing is unreachable (nothing checked; this path fails closed, unlike the plugin). Invalid keys and oversized files are rejected before billing is contacted. Daily aggregate counts record the tool as `check_file`. ## Accounting and recovery One credit is reserved before inspection and committed when processing completes. Errors release it. There is no fee for initialization, tool listing or checking usage. Limit: 20 inspection attempts per account per minute and one in-flight service request overall; overload returns 503. Reads and downloads have bounded size/time, and parser workers stop after eight seconds. A repeated request ID returns 409 and does not execute or debit again. **This is duplicate protection, not result replay.** Results are not retained. If a connection fails after processing completes, the credit may already be committed even if the client did not receive the response. Check usage; submitting a fresh ID starts another billable inspection. Never put a filename, hash or customer identifier in a request ID. SQLite transactions persist balances and request statuses. On startup, unfinished reservations are refunded exactly once. This assumes **one service process owns the ledger**; never start a second server against the same database. The administration command may use it concurrently, but must not call recovery. Move accounting to a shared transactional store and revise recovery before adding replicas. | Status | Meaning / recovery | | --- | --- | | 400 / 415 | Invalid ID, option, content type or file reference; correct the request. | | 401 / 403 | Invalid/revoked key or disallowed host/origin. | | 402 | Credits exhausted. | | 409 | Request ID already used; no new charge or replay. | | 413 | Size limit exceeded. | | 422 / 500 | Retrieval or inspection could not complete; reserved credit released. | | 429 / 503 | Rate/capacity limit; retry later using the same unused ID. | MCP tool failures use MCP's `isError` result with an error code. HTTP-level authentication, parsing and capacity errors use normal HTTP status codes. ## HTTP MCP Connect a compatible Streamable HTTP client to `/mcp` with `Authorization: Bearer `. This build supports header-based API keys, **not OAuth**. Host support for configuring that header must be checked; this is not yet a working consumer ChatGPT installation flow. `inspect_file` takes `request_id`, exactly one of `data_base64` (canonical base64, at most 2 MiB) or `file`, and optional `filename`, `include_preview`, `include_fingerprint`, `max_preview_chars`. `create_inspection_report` additionally requires `user_requested_report: true` and omits preview options. No remote tool accepts local paths. The optional ChatGPT-shaped `file` is `{download_url, file_id, mime_type?, file_name?}` and is declared with `openai/fileParams`. File ID is contextual only; the authorized temporary URL supplies bytes. Real ChatGPT file handoff remains untested. Unknown attachment formats may not be supported by the host's normal picker. File URL retrieval is disabled by default. An operator must set `FILE_ROOM_FILE_HOSTS` to exact, independently verified attachment hostnames (comma-separated). No wildcards. Only HTTPS/443 without userinfo is accepted; public DNS addresses are checked and pinned, redirects/compression refused, and the downloaded body limited to 20 MiB. Private/metadata/loopback addresses are rejected. Do not relax this into arbitrary URL fetching to make an integration work. The tests verify destination guards and private-DNS denial, not a live ChatGPT download. ## Data handling and boundaries The service processes submitted bytes in memory. Its application ledger holds random account/request IDs, hashed API keys, timestamps, balances and request status. It also keeps daily aggregate inspection counts by API/MCP, tool, success/error, coarse latency, declared assistant and internal-test status. It does not write file bytes, names, fingerprints, URLs or result bodies to disk. Routine application output is the listening address, not request data. Hosting proxies, operating systems, crash handling and connected assistants can have their own logs; those must be reviewed before public privacy claims. Memory release is not secure erasure. A service operator is still trusted with the bytes during processing. The free-standing browser desk's local-only statement applies to that desk. Calling this API transmits the file. Selecting a browser file never silently invokes the service. Workers provide time/heap limits, not a malware sandbox. No antivirus, behavioral detonation, sanitization, USB handling, cryptographic attestation or independent security certification is included. See `openapi.json` for the HTTP contract and HOSTING.md for the deployment candidate and release gates. ## Hosted usage reporting `FILE_ROOM_DB=/absolute/private/service-data/usage.sqlite node api-admin.mjs summary` prints a durable daily JSON summary. No analytics provider or additional dependency is needed. The summary exposes no account/request IDs. It is an operator command, not a public endpoint. Save this JSON with the daily product report after hosting is activated; no scheduled collection or delivery is configured yet. An authenticated inspection can declare `X-DVC-Client` as `chatgpt`, `codex`, `claude_code`, `claude_desktop`, `cursor`, `vscode` or `grok`. Other or missing values become `unknown`. Mark our synthetic checks with `X-DVC-Traffic: internal`; every other value becomes `unclassified`, never verified external use. These labels are caller declarations, not proof of client identity. A remote client that cannot supply the headers remains unclassified. Counts cover inspection attempts reaching the credit/inspection operation, including duplicate IDs, exhausted credits, parser/download failures and cancellations. Initialization, tool listing, usage queries and pre-inspection authentication/schema/capacity rejections do not increment inspection counts. Success means processing completed, not that a person read the response or paid. Latency buckets are `under_300ms`, `300ms_1s`, `1s_3s`, `over_3s`. A daily aggregate does not measure unique people or repeat visitors. The accounting ledger remains the authority for consumed credits. The local plugin keeps its no-telemetry behavior. The browser desk counts page views with Vercel Web Analytics, without cookies and without any file data. Website/download request counts, if obtained from hosting logs, must be reported separately; a download is not an installation or an inspection. The current Vercel static website does not run this service code.