@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.
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-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.
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/: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'
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 failedSplit 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_truncatedentry 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.textUsage 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_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
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 lintReleases are cut by .github/workflows/publish.yaml when
packages/sdk/package.json changes on main — bump version there to publish.
License
UNLICENSED
