npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@meistrari/document-sdk

v1.22.0

Published

SDK para a API de Processamento de Documentos, com suporte a extração de PDF, templates, conversão para imagem e mais.

Downloads

34,004

Readme

Document Processing SDK

A TypeScript SDK for the Document Processing API that provides methods for PDF extraction, splitting, template generation, PDF-to-image conversion, merging, cropping, file metadata, document parsing, HTML/Markdown/JSON-to-PDF, and Office document conversion operations.

Installation

npm install @meistrari/document-sdk
# or
pnpm add @meistrari/document-sdk
# or
yarn add @meistrari/document-sdk
# or
bun add @meistrari/document-sdk

Quick Start

Using DataToken Authentication

import { docClient } from '@meistrari/document-sdk'

const client = docClient({
  apiUrl: 'https://your-api-url.com',
  dataToken: 'your-data-token'
})

Using API Key Authentication

import { docClient } from '@meistrari/document-sdk'

const client = docClient({
  apiUrl: 'https://your-api-url.com',
  apiKey: 'Bearer your-api-key'
})

Using DocClient Class Directly

import { DocClient } from '@meistrari/document-sdk'

const client = new DocClient({
  apiUrl: 'https://your-api-url.com',
  dataToken: 'your-data-token'
})

Base URL: the API serves every operation under /v1. You can pass either the bare host (https://your-api-url.com) or an URL that already includes the prefix (https://your-api-url.com/v1) — the client normalizes it so /v1 is never missing or duplicated.

Client options

const client = docClient({
  apiUrl: 'https://your-api-url.com',
  dataToken: 'your-data-token', // or apiKey — exactly one of the two
  maxWaitMs: 15 * 60 * 1000, // how long a call waits for its job (default: 15 min)
  pollIntervalMs: 500, // initial delay between polls (default: 500ms, floor: 50ms)
  maxPollIntervalMs: 5000 // ceiling for the backoff between polls (default: 5s)
})

A call that looks synchronous is a submission followed by polling (see Runtime support). The first poll is immediate — a request served from the cache is already complete when it is accepted — and the interval then grows ×1.5 per poll up to maxPollIntervalMs. When maxWaitMs runs out the call throws a ServerError whose details carry the job_id; the job itself keeps running, and getJobStatus(job_id) still collects it.

The three durations must be finite, non-negative numbers: NaN or a negative value — what Number(process.env.X) produces from a missing variable — throws a ValidationError at construction instead of hanging or hot-looping later.

Runtime support

One build, no runtime-specific entry points. The client uses ofetch on top of the runtime's own fetch, so it runs on Node (18+), Bun, Deno, edge runtimes and in the browser — including inside a client bundle, where it must not reference anything Node-only.

Two consequences worth knowing:

  • No custom HTTP agent, and therefore the runtime's default timeouts apply (300s per request on Node). This is safe because no request is long-lived: every operation submits asynchronously and the result arrives through short GET /v1/jobs/:id polls, so the 15-minute ceiling belongs to the polling loop (maxWaitMs), not to one held connection. See Async Processing & Job Status.
  • In a browser, the API must send CORS headers for your origin, and a cross-origin response without them surfaces as NetworkError rather than the typed error, because the browser rejects it before the status is visible.

Operations

Extract Pages from PDF

Extract specific pages from PDF documents. Supports single output or multiple outputs. Ranges that overlap a document are clipped to available page boundaries.

// Single output (all pages merged into one PDF)
const result = await client.extract({
  type: 'page',
  indexes: [1, 3, -1, '2-4'], // Support for ranges and negative indexing
  files: [
    {
      file_url: 'vault://document-123',
      filename: 'document.pdf'
    }
  ]
})

// Multiple outputs (each group creates a separate PDF)
const result = await client.extract({
  type: 'page',
  indexes: [[1, 2], [3, 4], [5, '6-10']], // Each array creates a separate output file
  files: [
    {
      file_url: 'vault://document-123',
      filename: 'document.pdf'
    }
  ]
})

Index formats supported:

  • Positive integers: 1, 2, 3 (1-based indexing)
  • Negative integers: -1, -2 (last page, second to last, etc.)
  • Ranges as strings: '2-4', '1-3'

With multiple outputs, output_names names each file (without extension, one per index group) and continue_on_error: true collects a failing group instead of failing the whole request:

const result = await client.extract({
  type: 'page',
  indexes: [[1, 2], [90, 91]],
  output_names: ['capa', 'anexo'],
  continue_on_error: true,
  files: [{ file_url: 'vault://document-123', filename: 'document.pdf' }]
})

// result.files  = [{ original_file_url, file_url, indexes }]
// result.errors = [{ group_index: 1, indexes: [90, 91], error: '…' }] — only when a group failed

Split PDF

Split PDF documents into smaller chunks by page count.

const result = await client.split({
  files: [
    {
      file_url: 'vault://document-123',
      filename: 'large-document.pdf'
    }
  ],
  chunk_size: 5, // Pages per chunk (default: 1)
  max_chunk_bytes: 47185920, // Optional byte ceiling per chunk (45 MB here)
  max_chunk_chars: 200000, // Optional character ceiling per chunk
  type: 'page'
})

max_chunk_bytes and max_chunk_chars apply on top of chunk_size — a chunk closes on whichever limit it reaches first. Reach for max_chunk_bytes when the consumer has a hard size limit (an LLM upload cap, say): page count alone cannot bound the output, because a few scanned pages can outweigh hundreds of text ones. Reach for max_chunk_chars when the limit is counted in text instead (a model's input window): bytes do not track text density and on real documents run against it — a scanned page is megabytes with almost no text, while a page of log output is a hundred kilobytes and thousands of characters, so an all-text document passes any byte budget and still overruns. Sending either without chunk_size drops the page ceiling entirely. A single page over a ceiling still ships as its own chunk — splitting cannot shrink it.

The character count is an estimate of the glyphs each page draws, read from the PDF's content streams — not a bound on what a text extractor will produce. It runs below an extractor: a few percent on ordinary documents (extraction synthesizes whitespace at line breaks the page never draws), and further on ones that map a glyph to several characters via /ToUnicode or replace a drawn span with /ActualText. Size the ceiling with headroom against your real limit rather than at it. Pages the estimator cannot read at all count as zero and are logged server-side at warn, so a budget that stopped bounding a page is never silent.

// result.files = [{ original_file_url, file_url, pages: [1, 2, 3, 4, 5] }, ...]

Generate Document from Template

Generate documents from .docx templates with dynamic data substitution. Supports Mustache templating with loops and conditionals.

const result = await client.template({
  template: {
    // Provide either inline `content` (base64 / data: URI) or a `file_url`.
    content: 'base64-encoded-docx-content',
    filename: 'template.docx'
  },
  data: {
    name: 'John Doe',
    company: 'Acme Corp',
    user: {
      email: '[email protected]'
    },
    // Arrays for loops
    items: [
      { name: 'Item 1', price: 100 },
      { name: 'Item 2', price: 200 }
    ]
  },
  options: {
    outputFormat: 'permalink', // 'base64' | 'vault_url' | 'permalink'
    outputFilename: 'contract.docx', // Custom output filename
    permalink: {
      enabled: true, // Create a public download URL
      expiresIn: 3600 // Optional lifetime, in seconds
    }
  }
})

// Response: { file_url?, base64?, permalink? }

Extract Template Placeholders

List the placeholder variables a template references, without rendering it.

const result = await client.templatePlaceholders({
  template: {
    content: 'base64-encoded-docx-content',
    filename: 'template.docx'
  }
})

// result.placeholders = ['name', 'company', ...]

Convert PDF to Images

Convert specific PDF pages to high-quality JPEG images.

const result = await client.pdfToImage({
  file_url: 'vault://document-123',
  filename: 'document.pdf',
  pages: [1, 2, 5], // Pages to convert (1-indexed)
  quality: 'auto' // optional: 'auto' (default) | 'standard' | 'high'
})

// Response: result.files = [{ file_url, page, width, height }]
// Output dimensions vary per page — always read width/height for downstream cropping.
//
// quality:
//   'auto'     — hybrid: extracts the original embedded image when a page is
//                dominated by one, otherwise rasterizes at an adaptive resolution
//                that preserves the native pixels of any downscaled XObjects
//                (capped at 6× / 4000px). Output dimensions vary per page —
//                always read metadata.width/height for downstream cropping.
//   'standard' — legacy fixed scale (≈192 DPI). Output dimensions match the
//                pre-quality-flag behavior.
//   'high'     — forces adaptive rasterization up to the cap, skipping the
//                direct-extraction gate.

Merge Files

Merge multiple PDFs or audio files into a single file.

// Merge PDFs
const result = await client.merge({
  files: [
    { file_url: 'vault://doc1', filename: 'chapter1.pdf' },
    { file_url: 'vault://doc2', filename: 'chapter2.pdf' }
  ],
  output_filename: 'complete-book.pdf'
})

// Merge audio files (mp3, wav, m4a, aac, ogg, flac, wma, opus, webm)
const result = await client.merge({
  files: [
    { file_url: 'vault://audio1', filename: 'intro.mp3' },
    { file_url: 'vault://audio2', filename: 'main.mp3' }
  ],
  output_filename: 'complete-audio.mp3'
})

Dropping repeated pages

dedupe_pages (PDF only) keeps the first occurrence of each page and drops the later ones. Pages are compared by what they draw, not by their bytes, so the same page recognizes itself across files that number, compress or name their resources differently — which is what two downloads of the same court case look like. Two downloads that share a tail merge into one document instead of repeating it:

const result = await client.merge({
  files: [
    { file_url: 'vault://download-1', filename: 'process-part-1.pdf' }, // pages A B C D
    { file_url: 'vault://download-2', filename: 'process-part-2.pdf' } // pages C D E F
  ],
  dedupe_pages: true
})

result.dedupe
// {
//   input_pages: 8,
//   output_pages: 6,                                   // A B C D E F
//   removed: [
//     { file_index: 1, page: 1, duplicate_of: { file_index: 0, page: 3 } },
//     { file_index: 1, page: 2, duplicate_of: { file_index: 0, page: 4 } }
//   ],
//   skipped_files: []
// }

The comparison is conservative: a page re-encoded by a different producer may not match one it is visually identical to, so it stays. A file the API cannot read is merged whole and listed in skipped_files, never losing pages and never taking pages from another file. dedupe comes back only when the flag is set, and omitting the flag leaves the request (and its cache entry) exactly as it was.

File Metadata

Read what a file is without processing it.

const result = await client.metadata({
  file_url: 'vault://document-123',
  filename: 'document.pdf' // optional
})

// result: { file_url, filename?, mime_type?, size?, pages?, width?, height?, duration? }
// pages for PDFs, width/height for images, duration (seconds) for audio.

Read a PDF's Text Layer

The text a PDF already carries, read without OCR or a model: milliseconds per page, against minutes for parse on a long statement. A scanned page has no text layer and comes back empty, so check page_stats and send only those pages to parse.

const result = await client.pdfText({
  file_url: 'vault://statement-123',
  filename: 'statement.pdf', // optional
  password: 'secret' // optional, for encrypted PDFs
})

// result: { file_url, filename?, text, pages, chars, page_stats }
const pageTexts = result.text.split('\f') // one entry per page, in order
const needsOcr = result.page_stats
  .filter(page => page.chars === 0 || page.degraded)
  .map(page => page.page)

Each line of text is one line of the page, left to right, so a table row stays on one line. Text rotated into the margin (a signature stamp) follows the page's lines. degraded marks a page whose text layer is incomplete — a glyph with no Unicode mapping, a font that could not be decoded.

Crop Image

Crop images using coordinates.

const result = await client.crop({
  file_url: 'vault://image-123',
  filename: 'image.jpg',
  coordinates: {
    x1: 10, // Left
    y1: 10, // Top
    x2: 200, // Right
    y2: 200 // Bottom
  }
})

// Response includes dimensions
// result.dimensions = { width, height }

Convert HTML to PDF

Render HTML to PDF with headless Chromium. Accepts a whole document or a bare body fragment.

const result = await client.htmlToPdf({
  htmlContent: '<!DOCTYPE html><html><body><h1>Invoice</h1></body></html>',
  filename: 'invoice.pdf',
  options: {
    customCss: 'h1 { color: #003366; }', // appended last, so it overrides the document
    content: {
      logo: 'data:image/png;base64,...', // Optional logo
      title: 'Report Title',
      footerText: 'Confidential'
    },
    align: {
      headerLogo: 'right', // 'left', 'center', 'right', 'none'
      headerTitle: 'center',
      pageNumber: 'center',
      footerText: 'left'
    },
    pdfOptions: {
      format: 'A4',
      orientation: 'portrait',
      preferCSSPageSize: false, // set true to honour the document's own @page rules
      margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' }
    }
  }
})

The rendered page has no network access by default. Remote images, stylesheets and web fonts are refused, so a document must be self-contained (inline <style>, data: URIs) to render as authored. Opt in with allowRemoteContent: true:

const result = await client.htmlToPdf({
  htmlContent: '<img src="https://cdn.example.com/logo.png">',
  options: { allowRemoteContent: true }
})

Even then, requests to loopback, private, carrier-grade-NAT and link-local addresses (including cloud metadata endpoints) stay blocked, as does every non-http(s) scheme — file: above all, and ws:/wss: under both policies.

options.content.logo follows the same policy: it is fetched server-side to be inlined into the PDF header, so with the default it must be a data: URI.

Scripts in the document do NOT run by default; pass javascriptEnabled: true to enable them. The script is yours, but it executes in our Chromium, so it is opt-in — a static document renders identically either way.

Rendering a file instead of inline content

file_url is the other source, and the better one whenever the document is already a file:

const result = await client.htmlToPdf({
  file_url: 'vault://doc-123',
  filename: 'certidao.pdf',
})

It is not bounded by what fits in a JSON body, it never lands in the job's stored input, and the worker detects the encoding from the bytes (BOM, then <meta charset>, then UTF-8) instead of assuming what you decoded it as. That last part matters for any source still emitting windows-1252: reading those bytes as UTF-8 does not fail — it renders � where the accents were.

Exactly one of htmlContent and file_url per request.

Page defaults

Unlike markdownToPdf, this operation adds no page setup you did not ask for: margins default to 0 (your own CSS, @page included, is what governs) and the header/footer band appears only once content or align asks for it. printBackground is on and the format is A4, as everywhere else. A margin may be a CSS length or a number read as px — and 0 means zero.

Convert Markdown to PDF

Convert Markdown content to PDF with customizable styling.

const result = await client.markdownToPdf({
  markdownContent: '# My Document\n\nThis is **bold** text.',
  options: {
    theme: 'tela-default', // 'tela-default', 'legal-document', 'invoice'
    content: {
      logo: 'data:image/png;base64,...', // Optional logo
      title: 'Report Title',
      footerText: 'Confidential'
    },
    align: {
      headerLogo: 'right', // 'left', 'center', 'right', 'none'
      headerTitle: 'center',
      pageNumber: 'center',
      footerText: 'left'
    },
    customCss: 'h1 { color: #003366; }',
    pdfOptions: {
      format: 'A4',
      margin: {
        top: '20mm',
        right: '15mm',
        bottom: '20mm',
        left: '15mm'
      }
    }
  }
})

Formatting warnings

Markdown never fails to convert: marked is a total function, so a table missing its delimiter row silently becomes a paragraph, and a row one cell short is padded out with a blank — shifting every value after it one column. Both come back as a 200 with a PDF that is wrong.

The response carries a warnings array describing what the document asked for and did not get. It is advisory: the PDF is always produced, and it is up to the caller to decide whether the source is worth regenerating. This matters most when the markdown is machine-written, where nobody proof-reads it before it ships.

const result = await client.markdownToPdf({ markdownContent })

for (const warning of result.warnings ?? []) {
  console.log(`[${warning.severity}] ${warning.code} @ line ${warning.line}`)
  console.log(warning.message)
}
{
  "file_url": "vault://...",
  "warnings": [
    {
      "code": "table_row_column_mismatch",
      "severity": "error",   // the PDF is definitely not what the source asked for
      "stage": "markdown",   // found in the source text
      "message": "Linha da tabela tem 3 célula(s) e o cabeçalho tem 4: …",
      "line": 47,
      "snippet": "| Honorários | R$ 7.236,52 | R$ 7.858,80 |"
    },
    {
      "code": "unstyled_class",
      "severity": "error",
      "stage": "render",     // found in the rendered page
      "message": "A classe CSS \"demonstrativo-meta__rotulo\" é usada 9x …",
      "snippet": "Processo5002962-93.2024.8.21.0070"
    }
  ]
}

Two stages produce them. stage: "markdown" covers what the parser degraded — table_missing_delimiter, table_delimiter_mismatch, table_row_column_mismatch, table_indented_as_code, html_unclosed_tag, html_mismatched_nesting, html_stray_close, html_unclosed_comment, unknown_directive, list_marker_switch, unclosed_emphasis, unclosed_code_span. stage: "render" covers what only the rendered page knows — unstyled_class (a class no stylesheet in the document defines, which is what happens when customCss carrying it never arrives), broken_image and empty_render.

severity separates the two kinds: error means the output is definitely not what the source asked for, warning that it is probably not what the author intended. Neither fails the request.

Two things worth knowing about the field itself:

  • warnings: [] means the document was inspected and came out clean. The field is absent on a result cached from before this shipped — deduplicated requests replay the stored result, and the lint cannot run retroactively.
  • The list is capped at 50 findings, with a final warnings_truncated entry when it overflows.

Convert JSON to PDF

Convert JSON data to a formatted PDF document with syntax highlighting.

const result = await client.jsonToPdf({
  jsonData: {
    request: {
      method: 'POST',
      url: '/api/users'
    },
    response: {
      status: 200,
      data: { id: 123 }
    }
  },
  options: {
    content: {
      title: 'API Request Log',
      footerText: 'Generated in DEV'
    },
    align: {
      headerTitle: 'center',
      pageNumber: 'center'
    },
    jsonFormatting: {
      spacing: 2,
      highlightTheme: 'github', // 'github', 'stackoverflow-light', 'atom-one-light', 'googlecode'
      fontFamily: 'Fira Code, monospace',
      fontSize: '12px',
      lineHeight: '1.4'
    },
    pdfOptions: {
      format: 'A4',
      printBackground: true,
      displayHeaderFooter: true
    }
  }
})

Parse Documents

Universal parser that extracts Markdown text from PDFs, images, audio, Office files, emails, CSV and raw text. Omit parser_type to auto-detect from the file's magic bytes.

const result = await client.parse({
  file_url: 'vault://document-123',
  filename: 'document.pdf',
  parser_type: 'pdf', // optional: 'pdf' | 'image' | 'audio' | 'office' | 'email' | 'csv' | 'raw-text'
  // password, language, mime_type and polling_interval_ms are optional hints
})

// result.text        — Markdown content (always present on success)
// result.parser_type — resolved parser
// result.mime_type   — MIME resolved from magic bytes; also persisted on the job,
//                      so webhook `data.mimeType` carries it even when the request
//                      declared none
// result.cost        — provider cost in USD (0 for inline parsers)
// result.pages       — page count (PDF/multi-page)
// result.attachments — extracted files (email only)
// result.assets      — embedded Office images uploaded to Vault and linked in Markdown;
//                      best-effort Vision content is appended to result.text

Usage attribution

Optional fields that attribute the parse in the usage view. They do not change the result. All ids must be UUIDs.

| Field | Meaning | |---|---| | canvas_event_id | Event id of this parse call | | parent_canvas_event_id | Event id of the direct usage consumer | | root_canvas_event_id | Root consumer event id — requires parent_canvas_event_id | | project_id, canvas_id, prompt_version_id | What the parse is attributed to | | environment | Runtime label for the usage view, e.g. completion-api (1–64 chars) |

Positional source layout (PDF)

Markdown flattens tables: a demonstrativo's competência, parcela and valor can end up as three separate runs of text, and nothing in the sequence proves which value belongs to which row. include_source_layout keeps the positional evidence the OCR step already produced.

const result = await client.parse({
  file_url: 'vault://document-id',
  filename: 'demonstrativo.pdf',
  parser_type: 'pdf',
  include_source_layout: true,
})

// result.source_layout — reference to the artifact, never the artifact itself:
// {
//   version: 'v1',
//   coordinate_space: 'normalized',
//   file_url: 'vault://source-layout-id',
//   page_count: 10,
//   byte_size: 284120,
//   sha256: 'e110c8bde…',
// }

Download source_layout.file_url from Vault to get a SourceLayoutDocument: one entry per page, each with its lines and tokens carrying text, confidence, a polygon and (for tokens) the detected break_type. Verify the bytes against sha256 and byte_size before trusting them.

For the demonstrativo table above, the artifact page looks like this (abridged — polygons inlined for readability):

{
  "version": "v1",
  "coordinate_space": "normalized",
  "pages": [
    {
      "page_number": 8,
      "width": 1653,
      "height": 2339,
      "unit": "pixels",
      "lines": [
        {
          "text": "Competência Parcela Valor",
          "confidence": 0.9948,
          "polygon": [{ "x": 0.08, "y": 0.248 }, { "x": 0.64, "y": 0.248 }, { "x": 0.64, "y": 0.2645 }, { "x": 0.08, "y": 0.2645 }]
        },
        {
          "text": "01/2024 12 R$ 132,40",
          "confidence": 0.9887,
          "polygon": [{ "x": 0.08, "y": 0.3105 }, { "x": 0.64, "y": 0.3105 }, { "x": 0.64, "y": 0.327 }, { "x": 0.08, "y": 0.327 }]
        }
      ],
      "tokens": [
        {
          "text": "01/2024",
          "confidence": 0.9934,
          "break_type": "SPACE",
          "polygon": [{ "x": 0.08, "y": 0.3105 }, { "x": 0.255, "y": 0.3105 }, { "x": 0.255, "y": 0.327 }, { "x": 0.08, "y": 0.327 }]
        },
        {
          "text": "12",
          "confidence": 0.9887,
          "break_type": "SPACE",
          "polygon": [{ "x": 0.3, "y": 0.3105 }, { "x": 0.395, "y": 0.3105 }, { "x": 0.395, "y": 0.327 }, { "x": 0.3, "y": 0.327 }]
        },
        {
          "text": "R$ 132,40",
          "confidence": 0.9902,
          "break_type": "HYPHEN",
          "polygon": [{ "x": 0.47, "y": 0.3105 }, { "x": 0.64, "y": 0.3105 }, { "x": 0.64, "y": 0.327 }, { "x": 0.47, "y": 0.327 }]
        }
      ]
    }
  ]
}

That is the whole point of the artifact, and it is worth reading the numbers: the three tokens 01/2024, 12 and R$ 132,40 share the same Y band (0.3105 … 0.327) — they are one row — while sitting in three disjoint X bands (0.08…0.255, 0.30…0.395, 0.47…0.64) — they are three columns. Group tokens by Y to recover rows, cluster their X ranges to recover columns, and R$ 132,40 is provably the Valor of competência 01/2024, not a number that merely appeared nearby in the text. The page_number plus the polygon are also a citation: they point back at the exact region of the source PDF.

Reading it, in short:

  • page_number is global to the document — page 8 is page 8 of the PDF.
  • Every coordinate is normalized to [0, 1] with the origin top-left, so nothing needs width/height to be interpreted; they are informational.
  • lines are the provider's own grouping (useful as a sanity check); tokens are what you want for column reconstruction.
  • break_type is what followed the token (SPACE, WIDE_SPACE, HYPHEN); absent when the provider reported nothing.
  • Vertices are in provider order (top-left, top-right, bottom-right, bottom-left) and are not guaranteed to be axis-aligned on skewed scans — use the min/max of the polygon rather than assuming [0] and [2] are the corners you want.

Notes:

  • Coordinates are always normalized — origin top-left, both axes in [0, 1] — so a consumer never has to branch on page dimensions.
  • Page numbers are global to the document, not per internal chunk.
  • A blank page appears with lines: [] and tokens: []. A page the provider never returned fails the parse instead of being silently omitted.
  • No extra provider call and no per-coordinate price: the layout rides on the same OCR response the Markdown is built from.
  • The flag is part of the cache key. A document parsed earlier without layout is re-parsed the first time it is requested with layout; later identical requests reuse the cached execution as usual.
  • PDF only. Combining it with an explicit non-pdf parser_type is a 400.

Convert Office Documents

Convert Word/Excel/etc. to PDF or HTML.

const result = await client.officeConvert({
  file_url: 'vault://document-123',
  format: 'pdf' // 'pdf' (default) | 'html'
})

// format: 'pdf'  → { file_url, format: 'pdf' }
// format: 'html' → { html, format: 'html', files: [{ name, fileUrl, mimeType? }] }

Async Processing & Job Status

A plain call (await client.extract(...)) already runs as a job: the SDK submits it with async: true, delivery: 'poll' and polls GET /v1/jobs/:id until it finishes, so you get the operation's result without the API holding a connection open for the whole job.

To take delivery yourself, pass async: true with a webhook_url. The call then resolves to an AsyncAcceptedResponse ({ job_id, status: 'processing', ... }) instead of the result, and the API POSTs the final result to your webhook. You can also poll the job directly:

const job = await client.extract({
  type: 'page',
  indexes: [1, 2],
  files: [{ file_url: 'vault://document-123', filename: 'document.pdf' }],
  async: true,
  webhook_url: 'https://example.com/webhook',
  webhook_headers: { Authorization: 'Bearer my-token' }, // optional
  idempotency_key: 'unique-key', // optional dedup key
  metadata: { completionId: 'c-1' }, // optional; echoed back as the webhook's top-level `metadata`
  no_cache: true // optional; bypass cache and force a fresh run
})

const status = await client.getJobStatus(job.job_id)
// status: { job_id, execution_id, status, lane, operation, filename, progress, result, error, created_at, completed_at }

getJobStatus is authenticated and scoped to your workspace: a job that belongs to another workspace throws the same NotFoundError as one that does not exist.

metadata and no_cache are available on every operation (sync or async). metadata is echoed verbatim as the webhook payload's top-level metadata; no_cache skips execution reuse (cache) and forces a fresh run without changing the dedupe key.

The webhook is a POST whose body carries event (job.completed | job.failed), the flat job_id / status / result | error, a data envelope (jobId, executionId, operation, lane, filename, mimeType, result, …) and your metadata. It is signed: the x-webhook-signature header is sha256= + HMAC-SHA256 of `${x-webhook-timestamp}.${rawBody}`, keyed with the hex SHA-256 of the job_id. webhook_headers are sent alongside.

Error Handling

The SDK provides typed error classes for different scenarios. All of them extend DocError, which carries code, statusCode and details (the API's error envelope, when there is one).

| Class | When | |---|---| | ValidationError | 4xx: bad input, a document the operation rejected, or an invalid client option | | AuthenticationError | 401 | | NotFoundError | 404: unknown job, or an apiUrl that is not the doc API | | ServerError | 5xx, a job that failed server-side, or a job that outlived maxWaitMs | | NetworkError | The request never produced a response (DNS, reset, CORS in a browser) |

A job that fails is mapped from its persisted error exactly as the HTTP status used to be, so the class you catch does not depend on whether the failure happened at submission or during processing. Polling tolerates up to 3 consecutive failed polls before giving up on a job that is still running.

import {
  AuthenticationError,
  DocError,
  NetworkError,
  NotFoundError,
  ServerError,
  ValidationError
} from '@meistrari/document-sdk'

try {
  const result = await client.extract(request)
}
catch (error) {
  if (error instanceof ValidationError) {
    console.log('Validation failed:', error.message)
  }
  else if (error instanceof AuthenticationError) {
    console.log('Authentication failed:', error.message)
  }
  else if (error instanceof NetworkError) {
    console.log('Network error:', error.message)
  }
  else if (error instanceof ServerError) {
    console.log('Server error:', error.message, error.statusCode)
  }
  else if (error instanceof NotFoundError) {
    console.log('Not found:', error.message)
  }
  else if (error instanceof DocError) {
    console.log('Doc error:', error.code, error.statusCode, error.details)
  }
}

TypeScript Support

The SDK is written in TypeScript and provides comprehensive type definitions:

import type {
  Align,
  // Async & jobs
  AsyncAcceptedResponse,
  AsyncFields,
  AuthStrategy,
  Content,
  // Config
  DocConfig,
  DocLane,
  DocOperation,
  DocStatus,
  ElementPosition,
  ExtractError,
  // Extract
  ExtractRequest,
  ExtractResponse,
  // Common
  FileInput,
  IndexesInput,
  JobError,
  JobStatus,
  // JSON to PDF
  JsonFormatting,
  JsonHighlightTheme,
  JsonPdfOptions,
  JsonToPdfOptions,
  JsonToPdfRequest,
  JsonToPdfResponse,
  // HTML to PDF
  HtmlToPdfFileRequest,
  HtmlToPdfInlineRequest,
  HtmlToPdfOptions,
  HtmlToPdfRequest,
  HtmlToPdfResponse,

  // Markdown to PDF
  DocumentWarning,
  MarkdownPdfOptions,
  MarkdownToPdfOptions,
  MarkdownToPdfRequest,
  MarkdownToPdfResponse,
  PdfOptionsMargin,
  Theme,
  // Merge
  MergePageRef,
  MergeRequest,
  MergeResponse,
  // Metadata
  MetadataRequest,
  MetadataResponse,
  // Office Convert
  OfficeConvertHtmlResponse,
  OfficeConvertPdfResponse,
  OfficeConvertRequest,
  OfficeConvertResponse,
  // Parse
  ParseAsset,
  ParseAttachment,
  ParseRequest,
  ParseResponse,
  ParserType,
  // Parse — positional source layout artifact
  SourceLayoutBreakType,
  SourceLayoutDocument,
  SourceLayoutLine,
  SourceLayoutPage,
  SourceLayoutRef,
  SourceLayoutToken,
  SourceLayoutVertex,
  // PDF text
  PdfTextPageStats,
  PdfTextRequest,
  PdfTextResponse,
  // PDF to Image
  PdfToImageRequest,
  PdfToImageResponse,
  // Template placeholders
  PlaceholdersRequest,
  PlaceholdersResponse,
  SingleIndex,
  // Split
  SplitRequest,
  SplitResponse,
  // Crop
  CropRequest,
  CropResponse,
  TemplateInput,
  TemplateOptions,
  // Template
  TemplateRequest,
  TemplateResponse,
} from '@meistrari/document-sdk'

File URL Formats

The SDK supports multiple file URL formats:

  • Vault URLs: vault://file-id
  • External URLs: https://example.com/file.pdf
  • Base64: For template content (inline base64 encoded data)

Build & Development

The SDK is a workspace of the doc monorepo, which uses bun. From packages/sdk:

bun install          # at the repo root

bun run build        # unbuild → dist/ (ESM + CJS + types)
bun run test         # unit tests
bun run test:runtime # builds, then checks the bundle is browser-safe and the API handshake
bun run check        # tsc --noEmit
bun run lint

Releases are cut by .github/workflows/publish.yaml when packages/sdk/package.json changes on main — bump version there to publish.

License

UNLICENSED