Developers

Authenticate with your API key. REST, SDK, Python, and MCP use the same parsing and validation. Samples use your production URL. The reference below lists every endpoint, field, status code, and limit.

API

Call the API with your key. Same parsing over REST or MCP.

REST API

POST /api/v1/parse with your API key in the Authorization header. Upload a file or send base64, get JSON back, plus Excel or CSV when you ask.

Remote MCP (optional)

Connect once so coding assistants can call parse_document with the same rules as the API.

Same result everywhere

The console, REST, and MCP run the same operation, with the same validation and the same JSON.

POST /api/v1/parse (multipart file upload)
curl -X POST https://doc2json.manoonlabs.com/api/v1/parse \
  -H "Authorization: Bearer mld2j_..." \
  -F file=@invoice.pdf \
  -F schema=invoice

Reference

API docs

REST and MCP for doc2json. Create an API key from the console and replace mld2j_... in the samples with your secret.

Authentication

Create a key on API Keys. The full secret is shown once. Later, the console shows the prefix. Send it on every REST and MCP call, as a bearer token or in an x-api-key header. These samples use mld2j_... where that secret goes. The key acts as the workspace it belongs to. A call that authenticates is listed in Plan (usage for the period). The key's last used time still updates on each call.
Authorization
Authorization: Bearer mld2j_...

TypeScript SDK

The official SDK wraps the REST calls below with typed results and errors. Use it in Node, Bun or Deno. curl still works, and every sample below stays in curl for direct calls. Install with npm install @manoonlabs/doc2json; zod is a peer dependency, only needed for Zod schemas.
terminal
npm install @manoonlabs/doc2json
parse-simple.ts
import { Doc2Json } from "@manoonlabs/doc2json";

const doc2json = new Doc2Json({ apiKey: "mld2j_..." });

const { values } = await doc2json.parse("invoice.pdf");

// The model picks the keys. Pass schema to choose them yourself (same JSON field name as REST).
console.log(values);

REST

The base URL is https://doc2json.manoonlabs.com. POST and PATCH bodies are JSON objects with Content-Type: application/json. An unknown key returns 400, Unexpected key. A missing required key returns 400, Missing key. Invalid JSON returns 400, Request body must be JSON. GET routes read their input from the path. Delete a layout on Layouts. Create and revoke keys on API Keys. The Playground runs the same parse and returns the same JSON. POST /api/v1/parse also takes multipart/form-data, so curl can upload a file with -F.
GET/api/v1/whoami

Identity

Return the workspace and token for this bearer key, and the credits left this month.

  • There is no request body.
  • credits_remaining is the credits left this month. It does not change the balance.
  • Use this to confirm a key before you parse.
GET /api/v1/whoami
curl -X GET https://doc2json.manoonlabs.com/api/v1/whoami \
  -H "Authorization: Bearer mld2j_..."
POST/api/v1/parse

Parse a document

Extract keys from a PDF, PNG, JPEG, or text document. Upload the file as multipart/form-data, send it as base64 in a JSON body, or send plain text in the text parameter. Add export to also get the result as an Excel or CSV file, or citations to get the page and box each value was read from. The bytes are discarded after the request.

Request body (multipart/form-data or JSON)

KeyTypeRequiredDescription
file / file_base64 / textfile | stringYes, exactly oneThe document. Send exactly one of these three. PDF, PNG, JPEG, and UTF-8 text files (.txt, .md, .csv, .eml) work. Free workspaces allow 10 MB per document; paid workspaces allow 25 MB.
  • file: The raw document as a multipart upload, for example -F file=@invoice.pdf.
  • file_base64: The same file as a standard base64 string in a JSON body.
  • text: Plain text of the document, such as an invoice email, in a JSON body or as a multipart field (-F "text=<invoice.txt"). 1 credit per started 3,000 characters.
filenamestringNoNames the document: the document column and export.filename. Defaults to the uploaded file's name, or "text" for text; set it when you send base64. Paths are stripped, control characters removed, at most 120 characters.
schemastring | objectNoThe JSON key is schema. Send the slug of a saved layout, or a layout definition for this call only. In multipart, send the slug, or the definition as a JSON string. Without it, the model picks the keys.
export"xlsx" | "csv"NoAdds an export property with the result as an Excel or CSV file in base64. The JSON data is always returned. Case does not matter. Leave it out for JSON only; any other value, json included, is 400 and spends nothing.
citationsbooleanNoAdds a citations object that says where each value was read. In multipart, send "true" or "false". Leave it out, or send false, for the plain result. It does not change the cost.
strictbooleanNoOnly matters for long documents, which are read in chunks of pages. By default a chunk that cannot be read leaves a partial result (see Partial results). With true, any chunk that cannot be read fails the whole parse with 502 CHUNK_FAILED and no credits are used. In multipart, send "true" or "false".
  • Send exactly one of file (multipart), file_base64 (base64), or text. Free workspaces allow 10 MB per document; paid workspaces allow 25 MB. The type is read from the file bytes, not the name, so a .csv or .eml file is read as text. A missing, doubled, or rejected document is turned away before it spends credits.
  • schema is optional (that is the JSON key name; it names a saved layout or carries an inline layout). A string must be the slug of a layout saved in this workspace (lowercase letters, digits, underscores; hyphens in the string are treated as underscores). An object is used for this call only. An unknown slug, or an object that fails the layout rules, is rejected before it spends credits.
  • A 200 has status "ok", credits_used, credits_remaining, and values, the extracted data. Dates inside values are yyyy-mm-dd. A value the model cannot find is null, except that a key the layout types as an array is always an array, [] when there are no rows. The shape is the same with or without export; xlsx and csv only add the export property. See Excel and CSV export below.
  • The workspace's plan sets a monthly credit allotment, shared by the API, MCP, and the Playground. One credit is one page: an image is 1 page, a PDF costs its page count, and text costs 1 credit per started 3,000 characters (at least 1). export and citations do not change the cost. Each plan's allotment and pages per document are on the pricing page. A document over the pages per document limit returns 400 with pages and max_pages, and spends nothing. A 402 does not spend. A failed parse returns status "error", message, credits_used, and credits_remaining, and the 402 also includes credits_required and upgrade.
  • With citations true, a 200 also has citations, keyed by the path of each non-null value, such as "Invoice Number" or "Line Items[0].Amount". Each entry has page (from 1), text (the printed text the value was read from), bbox ([x0, y0, x1, y1] as fractions of the page from the top left, or null), status, and source. status is verified when the text was found on the page, fuzzy for a close match, mismatch when the found text does not hold the value, other_page when it was found on a different page, ambiguous when the quoted text is at several places on the page and nothing says which one holds the value, and unverified when it could not be checked. A value the model gave no quote for is never verified: it is fuzzy when it was found once beside the other cells of its row, and unverified otherwise. A date is verified only when a date can be read from the matched text. source is text_layer for a box measured from the PDF's own text, model for a box read from a scan or image, or null. Text documents get no boxes. See Citations below.
  • A long document is read in chunks of pages. The 200 then has warnings, a list that is empty when nothing was noticed. Each warning has code, message, and, when it concerns pages, pages ([first, last], from 1). Codes: CHUNK_FAILED (those pages could not be read) and EMPTY_CHUNK (the model returned nothing for those pages).
  • Partial results: when some pages could not be read, the 200 has status "partial" instead of "ok", failed_page_ranges (a list of [first, last] page ranges that are missing from values), and a CHUNK_FAILED warning for each. values holds what the other pages gave. Only the pages that were read are charged: credits_used counts them and the rest are refunded. Send strict true to get a 502 with code CHUNK_FAILED and failed_page_ranges instead, and no charge. If no page could be read the parse fails as a model error.
  • Each workspace may parse once per second. A second parse inside that second is 429, does not spend credits, and does not call the model. The 429 body includes retry_after. REST also sends Retry-After.
  • Every parse that starts returns job_id in the JSON body and X-Job-Id on the response, including failures after the job row is created and 504 timeouts, so you can poll GET /api/v1/parse/jobs/{job_id}. X-Job-Id on the request is optional. When you send it, it must be a UUID (versions 1 through 5) and acts as an idempotency key for this workspace for about 24 hours: the same key with the same request returns the original job_id without a new run or charge; the same key with a different request (document, schema, citations or strict) returns 409 with failure_code IDEMPOTENCY_KEY_REUSED. Prefer: respond-async keeps the id even if the HTTP connection drops; without Prefer, send your own X-Job-Id before a long sync parse so a timeout still leaves you an id to poll.
  • Prefer: respond-async returns 202 Accepted immediately with job_id, X-Job-Id, and Location pointing at the job URL. Poll until job_status is succeeded or failed. A retry with the same X-Job-Id and the same request returns that same 202 (no second charge). A failure before any charge releases the key so a later retry can run. A failure after the charge keeps the key: the same key returns that failed job for about 24 hours, so use a new UUID to try again.
POST /api/v1/parse (multipart file upload)
curl -X POST https://doc2json.manoonlabs.com/api/v1/parse \
  -H "Authorization: Bearer mld2j_..." \
  -F file=@invoice.pdf \
  -F schema=invoice
GET/api/v1/parse/jobs/{job_id}

Get parse job

Return a parse job's status and, when it has finished, the stored result for this workspace.

  • job_status is queued, running, succeeded, failed, or expired.
  • A finished succeeded job includes the same values and credits fields as a synchronous 200, with warnings and, for a partial result, failed_page_ranges.
  • A running job whose process stopped is picked up by this call: the chunks that were not finished run now, then the job settles, before the response is sent.
  • Another workspace's id or an unknown id returns 404. A result past the 24-hour window returns 410.
GET /api/v1/parse/jobs/{job_id}
curl -X GET https://doc2json.manoonlabs.com/api/v1/parse/jobs/cm7job \
  -H "Authorization: Bearer mld2j_..."
GET/api/v1/parse/jobs

List parse jobs

List this workspace's parse jobs, newest first. Parsed values are not included.

  • Query parameters: page, page_size (max 100), status, from, to (ISO dates), and client_job_id (the X-Job-Id UUID, exact match in this workspace).
  • Each item has job_id, status, created_at, schema, credits_used, and client_job_id when set.
GET /api/v1/parse/jobs
curl -X GET https://doc2json.manoonlabs.com/api/v1/parse/jobs?page=1 \
  -H "Authorization: Bearer mld2j_..."
GET/api/v1/schemas

List layouts

List the extraction layouts saved in this workspace.

  • There is no request body.
  • Each item has id, name, slug, keyCount, createdAt, and updatedAt.
  • Read one layout when you need its definition.
  • schemas is an empty array when the workspace has none.
GET /api/v1/schemas
curl -X GET https://doc2json.manoonlabs.com/api/v1/schemas \
  -H "Authorization: Bearer mld2j_..."
GET/api/v1/schemas/{slug}

Read a layout

Read one saved extraction layout by slug.

  • The slug is one path segment. URL-encode it. "invoice" is /api/v1/schemas/invoice.
  • The response adds schema, the definition (JSON key name unchanged).
  • A missing slug returns 404 with status "error" and message "That layout was not found.".
GET /api/v1/schemas/invoice
curl -X GET https://doc2json.manoonlabs.com/api/v1/schemas/invoice \
  -H "Authorization: Bearer mld2j_..."
POST/api/v1/schemas

Create a layout

Save an extraction layout in this workspace. The slug is derived from the display name.

Request body (JSON)

KeyTypeRequiredDescription
namestringYesDisplay name: letters, digits, spaces, underscore, and hyphen, up to 80 characters. The API slug is lowercase with underscores.
schemaobjectYesThe layout definition (JSON key name is schema). See Layout format for the key types and limits.
  • Two display names that produce the same slug (case and spacing ignored) are treated as duplicates.
  • 201 returns the saved layout, including its definition.
  • A duplicate returns 409 with status "error" and message "A layout with that name already exists in this workspace.".
  • A Free workspace can save 10 saved layouts. A paid workspace can save 100. Past that, the status is 409.
POST /api/v1/schemas
curl -X POST https://doc2json.manoonlabs.com/api/v1/schemas \
  -H "Authorization: Bearer mld2j_..." \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Invoice",
  "schema": {
    "type": "object",
    "properties": {
      "vendor": {
        "type": "string"
      },
      "invoiceNumber": {
        "type": "string"
      },
      "issued": {
        "type": "date"
      },
      "total": {
        "type": "number"
      },
      "lineItems": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "description": {
              "type": "string"
            },
            "amount": {
              "type": "number"
            }
          },
          "required": [
            "description",
            "amount"
          ]
        }
      }
    },
    "required": [
      "vendor",
      "invoiceNumber",
      "total"
    ]
  }
}'
PATCH/api/v1/schemas/{slug}

Update a layout

Change the display name and/or definition. Renaming updates the slug to match.

Request body (JSON)

KeyTypeRequiredDescription
namestringOne ofNew display name, same rules as create. At most 80 characters.
schemaobjectOne ofThe new layout definition (JSON key name is schema). It replaces the old one.
  • The path is the current slug, URL-encoded as one segment.
  • Send name, schema, or both. Sending neither returns 400.
  • 200 returns the layout after the change.
  • A duplicate slug returns 409. A missing layout returns 404.
PATCH /api/v1/schemas/invoice
curl -X PATCH https://doc2json.manoonlabs.com/api/v1/schemas/invoice \
  -H "Authorization: Bearer mld2j_..." \
  -H "Content-Type: application/json" \
  -d '{
  "schema": {
    "type": "object",
    "properties": {
      "total": {
        "type": "number"
      }
    }
  }
}'

Excel and CSV export

Add export=xlsx or export=csv to a parse, as a form field or a JSON key. The response is the same JSON, plus an export object with the file as base64. One parse, no extra credits. Leave export out for JSON only.
invoice.xlsx
curl -s -X POST https://doc2json.manoonlabs.com/api/v1/parse \
  -H "Authorization: Bearer mld2j_..." \
  -F file=@invoice.pdf \
  -F schema=invoice \
  -F export=xlsx \
| jq -r '.export.content' | base64 -d > invoice.xlsx

export object

KeyTypeRequiredDescription
export.format"xlsx" | "csv"AlwaysThe export value you sent.
export.filenamestringAlwaysA suggested name, <name>-<yyyy-mm-dd>.<ext> in UTC. The name is the document name (filename, else the uploaded file name), else the saved layout name, else "doc2json".
export.contentTypestringAlwaysapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet for xlsx, text/csv; charset=utf-8 for csv.
export.contentstringAlwaysThe file bytes as standard base64. Decode and write them to disk.

Excel (.xlsx)

  • The Document sheet has one row: a document column, then every top-level key.
  • Nested objects become dotted columns, for example vendor.address.city.
  • Each list of objects gets its own sheet, named after its path (lineItems, or lineItems.taxes for a list inside a list). Its first columns tie each row back: document, then a row number per level ("lineItems #").
  • document is the document name: filename, else the uploaded file name, else 1.
  • Numbers and dates (yyyy-mm-dd) are real number and date cells. true and false stay booleans. Lists of plain values are joined with "; ". Headers are bold and frozen.

CSV (.csv)

  • One row per item of the largest list in the result, usually the line items. Its columns are prefixed with its path, for example lineItems.amount.
  • Every other key is repeated on each row, so the file stands on its own in a spreadsheet or a database import.
  • A result with no lists is a single row. Other lists of objects, including lists inside a line item, are written into one cell as JSON. Excel has every list as its own sheet.
  • UTF-8 with a byte order mark, so Excel shows accents and currency symbols correctly. Lines end with CRLF. Text that starts with =, +, -, or @ gets a leading apostrophe, so it is not run as a formula.
invoice.xlsx
Document sheet
document    | vendor           | invoiceNumber | issued     | total  | currency
------------|------------------|---------------|------------|--------|---------
invoice.pdf | Northwind Supply | INV-1042      | 2026-10-25 | 1280.5 | USD

lineItems sheet
document    | lineItems # | description | amount
------------|-------------|-------------|-------
invoice.pdf | 1           | Cable       | 1280.5

Good to know

  • The JSON is always in the response. export only adds the file next to it, from the same parse, for the same credits.
  • A failed parse never has export. Check status first.
  • If the file cannot be built, the response is the plain JSON without export. The parse itself still counts.
  • MCP's parse_document returns JSON only. The Playground has a Download menu for Excel and CSV.

Citations

Add citations=true to a parse, as a form field or a JSON key, to see where each value came from. The response is the same JSON, plus a citations object keyed by each value's path, with the page, the printed text, and a box on the page. Works for PDFs and images, at no extra credits.
POST /api/v1/parse (multipart, citations=true)
curl -X POST https://doc2json.manoonlabs.com/api/v1/parse \
  -H "Authorization: Bearer mld2j_..." \
  -F file=@invoice.pdf \
  -F schema=invoice \
  -F citations=true

One citations entry

KeyTypeRequiredDescription
pagenumber | nullAlwaysThe page the value was read from, counting from 1. An image is page 1. null for a text document.
textstringAlwaysThe printed text the value was read from, as it appears on the page.
bbox[x0, y0, x1, y1] | nullAlwaysThe box around that text, as fractions of the page width and height from the top left, so it fits any render size. null when no box could be placed, and always null for text documents.
statusstringAlwaysHow well the cited text was checked against the document. See the statuses below.
source"text_layer" | "model" | nullAlwaystext_layer when the box was measured from the PDF's own text, model when it was read from a scan or an image, null when there is no box.

Status

  • verified: The cited text was found on the page and holds the value.
  • fuzzy: A close match was found, for example with scan noise or a reflowed line.
  • mismatch: The text was found, but the value does not appear in it. Worth a human look.
  • other_page: The text was found, but on a different page than cited. page points to where it was found.
  • unverified: The text was not found in the document's own text, for example on a scan or photo. The box, if any, is the model's reading.

Good to know

  • citations is off by default. Send citations: true in a JSON body, or citations=true as a multipart field. MCP parse_document takes the same citations argument.
  • The Playground always asks for citations, so you can click a value to see its box on the page.
  • Keys are the path of each value, such as vendor or lineItems[0].amount. A null value, or one that could not be placed, has no entry.
  • Works for PDFs (digital and scanned) and for PNG and JPEG images. Text documents get the cited text with no page or box.
  • Same credits as a plain parse. Citations work together with export.

MCP

MCP is JSON-RPC 2.0 at POST https://doc2json.manoonlabs.com/mcp. It uses the same bearer token and the same operations as REST. Protocol 2025-06-18. The server name is doc2json. Tool arguments are the REST JSON keys. Where REST reads {slug} from the path, the argument is slug.
initializepingtools/listtools/call

Argument checks fail as a JSON-RPC error, code -32602, on HTTP 200. That covers an unknown tool, an unexpected key, or a missing key. Once the operation runs, the HTTP status stays 200. isError is true when the operation fails, and result.content[0].text is the same JSON a REST call would return. initialize, ping, and tools/list answer without a key. Any other method without a valid key is HTTP 401 with a JSON-RPC error, code -32001. Any method other than POST is HTTP 405. A notification with no id, such as notifications/initialized, returns HTTP 202 and an empty body.

tools/call
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "parse_document",
    "arguments": {
      "file_base64": "<base64 PDF, PNG, or JPEG>",
      "schema": "invoice"
    }
  }
}

Connect a client

Point the client at the MCP URL and send the bearer token on every request. The MCP server page has the setup and tool list on one page.

  1. 1Create a key on API Keys and copy the secret.
  2. 2Add the config below. Cursor reads .cursor/mcp.json. Claude Code registers the HTTP server from a terminal.
  3. 3Call whoami to confirm the workspace, then parse_document with a document.
.cursor/mcp.json
{
  "mcpServers": {
    "doc2json": {
      "url": "https://doc2json.manoonlabs.com/mcp",
      "headers": {
        "Authorization": "Bearer mld2j_..."
      }
    }
  }
}

Layout format

A layout is an object with type: object and properties. Each property is a node with one of these types, plus an optional description the model reads with that key. array adds items, one node. object adds properties and may add required, the keys that must be present. A date is a string in yyyy-mm-dd. Keys start with a letter or underscore, then letters, digits, underscores, or hyphens.
anynullbooleanstringnumberdatearrayobject
  • Rootobject
  • Dateyyyy-mm-dd
  • Depth4 levels below the root
  • Keys100
  • Size16,384 bytes
  • Key length64 characters
  • Description400 characters, optional
  • Name length80 characters
  • Saved layouts10 on Free, 100 on paid plans
layout.json
{
  "type": "object",
  "properties": {
    "vendor": {
      "type": "string"
    },
    "invoiceNumber": {
      "type": "string"
    },
    "issued": {
      "type": "date"
    },
    "total": {
      "type": "number"
    },
    "lineItems": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "amount": {
            "type": "number"
          }
        },
        "required": [
          "description",
          "amount"
        ]
      }
    }
  },
  "required": [
    "vendor",
    "invoiceNumber",
    "total"
  ]
}

Status codes

REST uses these statuses. The samples below are the error bodies you will see most often.
  • 200The call succeeded.
  • 201A layout was created.
  • 400A failed parse returns status "error", message, credits_used, and credits_remaining. That includes an export other than xlsx or csv, and a filename that is not a string, both caught before credits are spent. Layout routes (`/api/v1/schemas`) and whoami return status "error" and message naming the field or rule that failed, with no credit fields because they never spend credits. Their success bodies start with status "ok".
  • 401The bearer token is missing, unknown, revoked, or expired. The response body is empty, and the call is omitted from Activity.
  • 402The workspace does not have enough credits for this document. The body includes credits_required, credits_used, credits_remaining, and upgrade. A 402 does not spend. Allotments per plan are on the pricing page.
  • 403Parsing is paused for this workspace.
  • 404That layout was not found.
  • 405The HTTP method does not match the route. The response body is empty.
  • 409The layout name already exists, or the workspace is at its layout limit.
  • 429This workspace already parsed in the last second. Limit is 1 parse per second per workspace. The body includes retry_after. REST also sends Retry-After. A 429 does not spend credits.
  • 502The model could not extract the document. The body has status "error", message, credits_used 0 (the charge is refunded), and credits_remaining. With strict true, a chunk that could not be read also returns 502 with failed_page_ranges, the pages that failed.
  • 503Storage or the job queue behind the parser could not be reached. The body has status "error", message, failure_code SERVICE_UNAVAILABLE, credits_used 0 (nothing is charged), and credits_remaining. REST also sends Retry-After. Retry the same request after that many seconds; a job id you sent in X-Job-Id can be reused.
  • 504The parse took longer than 100 seconds (10 minutes with Prefer: respond-async) and was stopped. Same body as 502: the charge is refunded. Try again, or send fewer pages.
400 response.json
{
  "status": "error",
  "message": "Unknown layout: invoice",
  "credits_used": 0,
  "credits_remaining": 59
}