{"openapi":"3.1.0","info":{"title":"ParseDoc API","version":"1.0.0","description":"Agent-native document parsing API. Turns a PDF, scan or invoice into structured JSON or Markdown, billed per page. Two payment rails: API key (Mollie prepaid credit packs) for humans, x402 (no account) for autonomous agents."},"servers":[{"url":"https://parsedoc.io","description":"Production"}],"paths":{"/v1/signup":{"post":{"operationId":"signup","summary":"Create an account and receive an API key","description":"Keyless endpoint. Returns a one-time plaintext API key (never stored) and a free tier of 100 pages. Rate limited per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"dev@example.com"}}}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResult"}}}},"400":{"description":"Invalid email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/parse":{"post":{"operationId":"parseDocument","summary":"Parse a document to JSON or Markdown","description":"Upload a PDF, PNG or JPEG (multipart 'file') or pass a public 'url' (JSON). Documents under the sync page threshold return 200 with the result; larger documents return 202 with a poll_url. Billed per page actually processed. Send an Idempotency-Key header to replay without re-billing within 24h.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream"},"mode":{"type":"string","enum":["fast","accurate"],"default":"fast"},"output":{"type":"string","enum":["json","markdown"],"default":"json"},"ocr_engine":{"type":"string","enum":["rapidocr","easyocr","tesseract"],"description":"Optional OCR engine override for scanned inputs."},"chunks":{"type":"boolean","default":false,"description":"When true, add heading-aware RAG chunks to the output."}}}},"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Public document URL. Fetched with SSRF hardening (private and metadata addresses are rejected)."},"mode":{"type":"string","enum":["fast","accurate"],"default":"fast"},"output":{"type":"string","enum":["json","markdown"],"default":"json"},"ocr_engine":{"type":"string","enum":["rapidocr","easyocr","tesseract"]},"chunks":{"type":"boolean","default":false}}}}}},"responses":{"200":{"description":"Synchronous parse result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseSuccess"}}}},"202":{"description":"Accepted for async processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Insufficient credits (insufficient_credits): the prepaid balance cannot cover the request. Purchase a pack at /v1/credits/purchase.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"File too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported file type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Too many pages or parse failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Sync processing timeout","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/jobs/{id}":{"get":{"operationId":"getJob","summary":"Get a job status and result","description":"Poll an async job by its id. Returns the current status and, once terminal, the parse output or error_code.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Job id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Job status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Job not found (also returned for another account's job)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/usage":{"get":{"operationId":"getUsage","summary":"Get the current month usage","description":"Returns the pages and cost billed this month, the remaining free pages, the remaining prepaid credit balance (credits_remaining), and the plan. Served from the local usage aggregate, not from Mollie in real time.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Monthly usage","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Usage"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credits/purchase":{"post":{"operationId":"purchaseCredits","summary":"Start a checkout for a prepaid credit pack","description":"Returns a Mollie checkout URL for the chosen pack (starter, standard or pro). Credits are granted only after the payment is confirmed by the Mollie webhook, never at purchase time. Prepaid credits back the API key rail; a call that runs out of credits gets a 402 insufficient_credits.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["pack_id"],"properties":{"pack_id":{"type":"string","enum":["starter","standard","pro"],"description":"Credit pack tier."}}}}}},"responses":{"200":{"description":"Checkout created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseResult"}}}},"400":{"description":"Invalid or unknown pack_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/extract/invoice":{"post":{"operationId":"extractInvoice","summary":"Extract a normalized FR and UE invoice JSON","description":"Premium vertical endpoint. Returns a normalized invoice object (siren, siret, tva_intracom, numero_facture, dates, lignes, totals, iban, mentions_legales_detectees). Async by design: returns 202 with a poll_url, poll /v1/jobs/{id} for the result. Billed at the flat invoice per-page price.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream"}}}},"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Public document URL (SSRF hardened)."}}}}}},"responses":{"200":{"description":"Synchronous invoice result (single-page inputs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseSuccess"}}}},"202":{"description":"Accepted for async processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Insufficient credits (insufficient_credits): the prepaid balance cannot cover the request. Purchase a pack at /v1/credits/purchase.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"File too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported file type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Too many pages or extraction failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Sync processing timeout","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/enrich":{"post":{"operationId":"enrichDocument","summary":"Enrich text with AI (acronym expansion, completion)","description":"AI enrichment tier. Provide 'text' or a 'job_id' (exactly one): job_id enriches the markdown of one of your already parsed jobs. mode selects the work: 'acronyms' expands the acronyms found in the text, 'complete' clarifies vague or truncated text, 'both' does both (default). Synchronous: returns 200 with the enriched output. Usage-based billing: each call is billed in credits from your prepaid balance, with no free tier, drawn from the same credit balance as parse. The price charged is returned in usage.cost_usd. Send an Idempotency-Key header to replay a call without a new charge.","security":[{"bearerAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional replay key. A repeat within retention returns the stored result and is not billed again.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"Raw text to enrich. Provide this or job_id, not both."},"job_id":{"type":"string","format":"uuid","description":"Id of one of your succeeded parse jobs whose markdown is enriched. Provide this or text, not both."},"mode":{"type":"string","enum":["complete","acronyms","both"],"default":"both"},"domain":{"type":"string","description":"Optional domain hint (max 64 chars) that steers acronym disambiguation.","default":""}}}}}},"responses":{"200":{"description":"Enrichment result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichSuccess"}}}},"400":{"description":"Invalid request (missing or ambiguous text and job_id, or a source job with no text)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Insufficient credits (insufficient_credits): the prepaid balance cannot cover the worst case cost of the call. No token is spent. Purchase a pack at /v1/credits/purchase.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Source job_id not found (also returned for another account's job)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Enrichment provider failure (enrich_failed): the upstream AI call failed. Nothing was billed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/x402/parse":{"post":{"operationId":"parseDocumentX402","summary":"Parse a document, paid via x402 (no account)","description":"x402 rail for autonomous agents. A request without a valid X-PAYMENT header returns a 402 challenge; the agent pays and retries the same request. Flat price per call on Base Sepolia at the MVP. Same body and result as /v1/parse.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","contentMediaType":"application/octet-stream"}}}},"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Parse result (payment settled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseSuccess"}}}},"202":{"description":"Accepted for async processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"402":{"description":"Payment required (x402 v2 challenge)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402Challenge"}}}},"429":{"description":"Rate limit exceeded (per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/x402/invoice":{"post":{"operationId":"extractInvoiceX402","summary":"Extract an invoice, paid via x402 (no account)","description":"x402 rail for the invoice endpoint. A request without a valid X-PAYMENT header returns a 402 challenge; the agent pays and retries. Flat price per call on Base Sepolia at the MVP. Same result as /v1/extract/invoice.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","contentMediaType":"application/octet-stream"}}}},"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Invoice result (payment settled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseSuccess"}}}},"202":{"description":"Accepted for async processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"402":{"description":"Payment required (x402 v2 challenge)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402Challenge"}}}},"429":{"description":"Rate limit exceeded (per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/mcp":{"post":{"operationId":"mcp","summary":"Model Context Protocol endpoint (Streamable HTTP)","description":"Stateless MCP server over Streamable HTTP. Exposes the tools parse_document, extract_invoice and get_job_status, each a wrapper of the matching /v1 endpoint. Authenticate with Authorization: Bearer <api_key>. This is an MCP JSON-RPC transport, not a plain REST endpoint.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"MCP JSON-RPC response stream"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/health":{"get":{"operationId":"health","summary":"Service healthcheck","description":"Reports database and worker liveness. Returns 200 when the database is reachable, 503 otherwise.","responses":{"200":{"description":"Service healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"503":{"description":"Service degraded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key as a Bearer token: Authorization: Bearer <api_key>."}},"schemas":{"Error":{"type":"object","description":"Unique error envelope for every 4xx and 5xx response.","required":["error"],"properties":{"error":{"type":"object","required":["code","message","doc_url"],"properties":{"code":{"type":"string","example":"invalid_file_type"},"message":{"type":"string"},"doc_url":{"type":"string","format":"uri"}}}}},"SignupResult":{"type":"object","required":["account_id","api_key","plan","free_pages"],"properties":{"account_id":{"type":"string","format":"uuid"},"api_key":{"type":"string","description":"One-time plaintext key. Store it now: it is never returned again."},"plan":{"type":"string","example":"free"},"free_pages":{"type":"integer","example":100}}},"ParseSuccess":{"type":"object","description":"Synchronous parse result (documents under the sync page threshold).","required":["job_id","pages","cost_usd","output"],"properties":{"job_id":{"type":"string","format":"uuid"},"pages":{"type":"integer","description":"Pages actually processed and billed."},"cost_usd":{"type":"number","example":0.005},"output":{"$ref":"#/components/schemas/ParseOutput"}}},"ParseOutput":{"type":"object","description":"Structured parse output. markdown always present; tables and metadata when detected.","properties":{"markdown":{"type":"string"},"tables":{"type":"array","items":{"type":"object","additionalProperties":true}},"metadata":{"type":"object","additionalProperties":true},"chunks":{"type":"array","description":"Present only when chunks=true. Heading-aware RAG chunks with per-chunk metadata.","items":{"type":"object","additionalProperties":true}}}},"JobAccepted":{"type":"object","description":"Asynchronous acceptance (documents at or above the sync page threshold, and the invoice rail by design).","required":["job_id","status","poll_url"],"properties":{"job_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["processing"]},"poll_url":{"type":"string","example":"/v1/jobs/uuid"}}},"JobStatus":{"type":"object","required":["job_id","status"],"properties":{"job_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","processing","succeeded","failed","timeout"]},"pages":{"type":["integer","null"]},"cost_usd":{"type":["number","null"]},"output":{"oneOf":[{"$ref":"#/components/schemas/ParseOutput"},{"type":"null"}]},"error_code":{"type":["string","null"]}}},"Usage":{"type":"object","required":["month_pages","month_cost_usd","free_pages_remaining","credits_remaining","plan"],"properties":{"month_pages":{"type":"integer"},"month_cost_usd":{"type":"number"},"free_pages_remaining":{"type":"integer"},"credits_remaining":{"type":"integer","description":"Remaining prepaid page credits (accounts.credits_pages)."},"plan":{"type":"string"}}},"PurchaseResult":{"type":"object","description":"Checkout details for a prepaid credit pack. No credit is granted until the Mollie payment is confirmed by webhook.","required":["checkout_url","pack_id","credit_pages","eur_amount"],"properties":{"checkout_url":{"type":"string","format":"uri","description":"Hosted Mollie checkout page. Redirect the buyer here to pay."},"pack_id":{"type":"string","enum":["starter","standard","pro"]},"credit_pages":{"type":"integer","description":"Page credits granted once the payment is confirmed."},"eur_amount":{"type":"number","description":"Pack price in EUR."}}},"EnrichSuccess":{"type":"object","description":"Synchronous AI enrichment result. Billed in credits from the prepaid balance (usage-based, no free tier).","required":["job_id","mode","output","usage"],"properties":{"job_id":{"type":"string","format":"uuid"},"mode":{"type":"string","enum":["complete","acronyms","both"]},"output":{"$ref":"#/components/schemas/EnrichOutput"},"usage":{"$ref":"#/components/schemas/EnrichUsage"}}},"EnrichOutput":{"type":"object","description":"Enriched output. markdown is the completed text (unchanged in acronyms mode); metadata.acronyms maps each resolved acronym to its definition when the mode requests it.","required":["markdown","tables","metadata"],"properties":{"markdown":{"type":"string"},"tables":{"type":"array","items":{"type":"object","additionalProperties":true}},"metadata":{"type":"object","additionalProperties":true}}},"EnrichUsage":{"type":"object","description":"Billing for the call. cost_usd is the price charged (the raw provider token cost is never exposed). credits_debited is the number of prepaid page credits deducted.","required":["tokens_in","tokens_out","cost_usd","credits_debited"],"properties":{"tokens_in":{"type":"integer","description":"Input tokens consumed by the AI model."},"tokens_out":{"type":"integer","description":"Output tokens produced by the AI model."},"cost_usd":{"type":"number","description":"Price charged for this call in USD (credits debited times the credit unit price)."},"credits_debited":{"type":"integer","description":"Prepaid page credits deducted from the balance."}}},"Health":{"type":"object","required":["status","checks"],"properties":{"status":{"type":"string","enum":["ok","degraded"]},"checks":{"type":"object","properties":{"db":{"type":"boolean"},"worker":{"type":"boolean"}}}}},"X402Challenge":{"type":"object","description":"x402 v2 payment challenge returned on 402. The client pays with an X-PAYMENT header and retries the same request.","required":["x402Version","accepts"],"properties":{"x402Version":{"type":"integer","example":1},"error":{"type":["string","null"]},"accepts":{"type":"array","items":{"type":"object","properties":{"scheme":{"type":"string","example":"exact"},"network":{"type":"string","example":"eip155:84532"},"maxAmountRequired":{"type":"string"},"payTo":{"type":"string"},"asset":{"type":"string"}}}}}}}}}