@meistrari/document-sdk
v1.18.0
Published
SDK para a API de Processamento de Documentos, com suporte a extração de PDF, templates, conversão para imagem e mais.
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, 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-sdkQuick 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/v1is never missing or duplicated.
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/:idpolls, 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
NetworkErrorrather 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'
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)
type: 'page'
})max_chunk_bytes applies on top of chunk_size — a chunk closes on whichever
limit it reaches first. Reach for it 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. Sending it without
chunk_size drops the page ceiling entirely. A single page heavier than the
ceiling still ships as its own chunk — splitting cannot shrink it.
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'
})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'
}
}
}
})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.textPositional 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_numberis 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 needswidth/heightto be interpreted; they are informational. linesare the provider's own grouping (useful as a sanity check);tokensare what you want for column reconstruction.break_typeis 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: []andtokens: []. 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-
pdfparser_typeis 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
Every operation accepts async: true (with a required webhook_url). The call then
resolves to an AsyncAcceptedResponse ({ job_id, status: 'processing', ... }) instead
of the synchronous result, and the API delivers 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 }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.
Error Handling
The SDK provides typed error classes for different scenarios:
import {
AuthenticationError,
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)
}
}TypeScript Support
The SDK is written in TypeScript and provides comprehensive type definitions:
import type {
Align,
// Async & jobs
AsyncAcceptedResponse,
AsyncFields,
Content,
// Config
DocConfig,
ElementPosition,
ExtractError,
// Extract
ExtractRequest,
ExtractResponse,
// Common
FileInput,
IndexesInput,
JobStatus,
// JSON to PDF
JsonToPdfRequest,
JsonToPdfResponse,
// HTML to PDF
HtmlToPdfRequest,
HtmlToPdfResponse,
// Markdown to PDF
MarkdownToPdfRequest,
MarkdownToPdfResponse,
// Merge
MergeRequest,
MergeResponse,
// Metadata
MetadataRequest,
MetadataResponse,
// Office Convert
OfficeConvertRequest,
OfficeConvertResponse,
// Parse
ParseRequest,
ParseResponse,
ParserType,
// Parse — positional source layout artifact
SourceLayoutBreakType,
SourceLayoutDocument,
SourceLayoutLine,
SourceLayoutPage,
SourceLayoutRef,
SourceLayoutToken,
SourceLayoutVertex,
// 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
# Install dependencies
pnpm install
# Build the SDK
pnpm build
# Run tests
pnpm test
# Lint code
pnpm lintLicense
UNLICENSED
