ReflowPDF API api.reflowpdf.com/v1

    The engine in WebAssembly

    The same engine in your own pages and servers: PDFs printed where your data is, with no request to us and no browser to run.

    npm install @reflowpdf/engine
    In a page
    import { createEngine } from '@reflowpdf/engine'
    
    const engine = await createEngine({ licenseKey: 'rpdfl_…' })
    
    const pdf = await engine.pdf('<h1>Hello, PDF</h1>')   // Uint8Array
    const url = URL.createObjectURL(new Blob([pdf], { type: 'application/pdf' }))

    @reflowpdf/engine is the engine the editor and this API print with, compiled to WebAssembly: paged media (@page, running headers and footers, page counters), footnotes, tables across pages, flexbox and grid, hyphenation in 91 languages; tagged PDF (PDF/UA-1 and PDF/UA-2), PDF/A-3a and PDF/A-1b, PDF/X-4 and PDF/X-1a; pages as images and the layout. (Czech, Indonesian, Macedonian and Serbian lay out without automatic hyphenation in the package: their patterns are offered only under the GPL. The API hyphenates them.) The same document gives the same PDF here, in the editor and from the API.

    The APIThe engine
    Where it printsour serversthe browser of your user, or your own server
    Your datasent to the APInever leaves the page
    Billedby the page, with your plana license by installation, any number of pages
    Templatesthe gallery and your library, by idany HTML template with data (the same language)
    Fitsservers, jobs, webhooksinteractive pages, offline apps, private data

    The license#

    • Free to use. The engine is free for anything: without a license every page carries a small “ReflowPDF trial” mark in its corner; in a page on localhost (*.localhost, 127.0.0.1) a small “ReflowPDF development” mark instead. (A license cannot name localhost: anyone's loopback is everyone's.) The terms: ReflowPDF Engine License.
    • A license removes the mark on the installations it covers. An installation is a website with its subdomains (example.com, app.example.com, or *.example.com for every subdomain; a site and its subdomains are one), a computer, virtual machine or container that runs the engine, or a deployed serverless function; development, testing and CI are not installations. Start covers one installation, US$149 a month or US$1,490 a year. Business covers up to five, US$299 a month or US$2,990 a year, with a reply from support within a business day. Neither limits pages or prints at once; both allow any commercial use in your own websites and online services, including ones you sell to your customers. Inside software that others install (desktop or mobile apps, appliances, libraries, SDKs) the engine needs a separate agreement: hello@reflowpdf.com. Buy it at app.reflowpdf.com → account menu → Engine licenses. Its key (rpdfl_…) goes into your page: it is public, and works only on the license's sites.
    • How it is checked. The engine asks this API for the license's token (GET /v1/engine/license?key=…), signed (Ed25519). A token lasts at most 42 days and at most a week past the paid period, and the engine asks for a new one before it ends. It keeps the token in the page's storage and checks it offline: its signature, its end, and the page's host against its sites. A print makes no request of its own, unless it needs a catalogue font or a printer profile the engine does not have yet.
    • On a server (Node, Deno, Bun, a serverless function) there is no site to check: give the engine your API key as well, and the token is issued for the license's owner. A server is an installation of its own.
    • The check runs in the code you give your users, so it is not a lock: the license's terms are the rule.
    import { createEngine } from '@reflowpdf/engine'
    
    const engine = await createEngine({
      licenseKey: process.env.REFLOWPDF_LICENSE_KEY,
      apiKey: process.env.REFLOWPDF_API_KEY,          // a secret: servers only
    })
    console.log(await engine.license())          // { status: 'licensed', host: null, license: { … } }
    statusmeans
    licensedprints without the mark
    trialevery page carries the mark; reason says why: no-license, expired, domain (this site is not the license's), server (no page here, and no API key given), invalid-…
    developmentlocalhost: the development mark
    first-partyReflowPDF's own sites: without the mark, as the editor and the docs' examples print

    Printing#

    const { pdf, missingVariables, missingGlyphs, missingFonts, skippedResources, license } = await engine.render(html, {
      data: { invoice: { number: 'INV-1042' }, items },   // fill the HTML as a template
      paper: { size: 'A4', orientation: 'portrait', margin: '18mm 20mm' },   // where the document's CSS names none
      baseUrl: 'https://example.com/invoices/',            // where its relative addresses point
      title: 'Invoice INV-1042', lang: 'en',
      pdfUa: 1,                                            // tagged by default; 2 for PDF/UA-2
      timeLimitMs: 30000,
    })
    Option
    dataFill the HTML as a template, in the template language of the API and the editor. Values it does not give print empty and are named in missingVariables; a template that does not read is an EngineError template_error.
    paper{ size, orientation, margin }: A4 with 20 mm margins (default), A3, A5, B4, B5, Letter, Legal, Ledger, Tabloid, or two lengths. The document's own @page wins, property by property, as in the API. Headers and footers sit in these margins: a taller header needs a larger one, and margin: 0 leaves it no room.
    baseUrlWhere relative addresses point. In a page, the page's address; on a server, none.
    title, langThe PDF's title (default: <title>) and the document's language when it names none.
    tagged, pdfUa, pdfA, pdfXTagged PDF (PDF/UA-1) by default; pdfUa: 2 claims PDF/UA-2; pdfA: '3a' | '1b' PDF/A; pdfX: { standard, outputIntent, profile } PDF/X (what each claims and which go together.
    headerFooter, watermarkA line of text in the margins of every page; a stamp, text or image, under or over the content.
    encryption, attachments, metadataAES-256 with a user and owner password and permissions (extraction for accessibility is always allowed); embedded files (data: bytes or text); author, subject, keywords.
    compression, objectStreamsLossless output tuning: fast, default, smallest; a classic cross-reference table when false.
    colorScheme, defaultFontSize, printScale, shrinkToFit, baseCss, inputFormat, allowRemote, maxPagesHow the document lays out, as the API's options, in camelCase.
    timeLimitMs, signalThe print's time limit, from its start, fetching its files and laying it out included (default 60 s); an AbortSignal, honoured between its steps (a layout under way ends at the limit).

    The document's scripts do not run: send the HTML as it should print (a chart drawn, not the code that draws it).

    Images, the layout, print conditions#

    const { images } = await engine.images(html, { format: 'webp', pages: [1], dpi: 200 })   // [{ page, bytes, width, height }]
    const { pages, elements } = await engine.layout(html)                                 // where every element with an id lands
    
    // PDF/X: ReflowPDF's FOGRA39 and SWOP (CGATS TR 001), fetched for a licensed engine when named, or your printer's profile
    await engine.registerOutputIntent('FOGRA51', await (await fetch('/profiles/PSOcoated_v3.icc')).arrayBuffer())
    const proof = await engine.pdf(html, { pdfX: { standard: 'x-4', outputIntent: 'FOGRA51' }, pdfA: '3a' })

    Images are made from the PDF itself, its fields and marks included, on white paper, and are lossless (PNG or WebP, 18–1200 dpi). ReflowPDF's print conditions, its own profiles FOGRA39, CGATS21-2-CRPC5 and CGATS21-2-CRPC6 (built from Fogra's and ISO's data) and Adobe's U.S. Web Coated (SWOP) v2 (CGATS TR 001), are not in the package: a licensed engine fetches their profiles from this API (sending its license's token) the first time a document names one, checks them against the SHA-256 it knows, and keeps them. The trial and development use your own: registerOutputIntent. engine.outputIntents() lists the conditions and the ones you registered.

    The document's stylesheets (<link>, @import), @font-face fonts and images are fetched with fetch (in a page, under its CORS rules); one that cannot be fetched prints without and is named in skippedResources. Give fetch: null to createEngine to fetch nothing, or your own fetch (it runs in the page's thread: a function cannot go to a Worker).

    Fonts#

    The engine carries Liberation Sans, Serif and Mono (metric-compatible with Arial, Helvetica, Times and Courier, and what sans-serif, serif and monospace print with) and DejaVu for wide Unicode coverage. For the other scripts it carries Noto faces: Chinese, Japanese, Korean, Arabic, Hebrew, the Indic scripts, Thai, Ethiopic, Tibetan, mathematics, symbols, emoji and others. Each face is fetched when a document first draws with it: a Latin document fetches one or two.

    A licensed engine also prints with ReflowPDF's catalogue, the 1,922 families of Google Fonts the API and the editor print with. A family your document names that no face of its own answers (font-family: Inter, sans-serif) is asked of this API (GET /v1/fonts/{family}, with the license's token), its faces given by their headers, and the faces its text draws with are fetched: an invoice in Inter fetches Inter Regular and Bold, 0.7 MB, once. The same document prints the same here, in the editor and from the API. The trial and development print such a family with the next of its list and name it in missingFonts, as a licensed engine does a family the catalogue has not; give it with the document's @font-face, or once for every print. The characters no face has are in missingGlyphs.

    await engine.registerFont({ family: 'Inter', data: await (await fetch('/fonts/Inter-Regular.woff2')).arrayBuffer(), weight: 400 })

    Without the network#

    An offline app, or a page about to lose the network, fetches what it will need now:

    await engine.preload({ fonts: 'own', languages: ['en', 'de'] })   // or fonts: 'all' (every script, 19 MB), or ['Noto Sans JP']
    // { faces: 15, languages: ['en', 'de'], missing: [] }

    A print that cannot get a file it would use goes on without it (a face's text with the next face, a language without hyphenation) and names the file in skippedResources. A Service Worker that caches the engine's files keeps them across visits.

    Where it runs#

    HowTested
    BrowsersWith a bundler, or from our address at an exact version: https://api.reflowpdf.com/sdk/engine/1.0.3/dist/index.js. It prints in a Web Worker, so the page stays responsive.Chromium, Firefox, WebKit; a Vite 5 build
    Node.js 20.19+, 22.12+import or require; its files are read from the package.Node 20.19, 22
    Deno 2, Bun 1The package installed into node_modules by the line above (Bun: bun add @reflowpdf/engine). Deno: --allow-read for its files, --allow-net for a license's token and the documents' resources.Deno 2.9, Bun 1.4
    Cloudflare WorkersThe module imported (wasm), its other files from an address (assetsUrl), below. Workers Paid: a first print takes about a second of CPU.workerd (the Workers runtime)
    AWS Lambda, Vercel and Netlify Functions, Cloud RunAs Node, with 1 GB of memory or more (a process with the engine takes 0.5–0.6 GB).—

    Not in Vercel Edge Functions: they take 1–4 MB of code, and the module alone is 5.8 MB compressed: call the API from there.

    What it downloads#

    SizeWhen
    The npm package25.9 MB (46.8 MB installed)once
    The module (flexpdf_wasm_bg.wasm)21.9 MB: 5.8 MB as brotli, 8.3 MB as gzip (our address sends either)at the start; a browser keeps it cached and compiled
    The faces' headers (fonts-headers.bin)1.3 MB; 0.3 MB as brotliat the first print
    A face the text draws withLiberation 0.4 MB; Noto of a script 0.05–0.3 MB; Japanese 2.4 MB, Chinese 4.6 MBwhen a document first needs it
    A language's hyphenationtens of KBwhen a document of that language hyphenates

    A Latin document's first print in a page fetches 24 MB, 6.5 MB compressed, and the next prints nothing more. Every file is asked for by the engine's version (?v=): files you serve cached for good are replaced by an upgrade. The module, fonts and dictionaries are checked against their SHA-256, and files of another version than the code are refused.

    Static hosts limit a file's size (Cloudflare Pages and Workers assets to 25 MiB): the module (21.9 MB) fits; serve the files from a CDN or an object store if yours takes less.

    With a bundler#

    The engine's files are not code: serve them yourself (copy them to your public files and say where), or take them from our address at the same version. Its Worker is found where the bundler put it (webpack 5, Vite's build), else among those files (assets/worker.js): with Rollup, esbuild, Vite's dev server or any other, copying the files is all — tested with each.

    cp -r node_modules/@reflowpdf/engine/assets public/reflowpdf-engine
    • Serve .wasm as application/wasm, compressed: a browser compiles it while it downloads and keeps it compiled.
    • Start the engine once (createEngine) and print many times. Measured on a server: the gallery's invoice prints in 0.9 s the first time (the fonts are read) and in 0.14 s after.

    In a Cloudflare Worker#

    A Worker compiles no WebAssembly from bytes it fetched: import the module, and give the engine its files' address.

    import { createEngine } from '@reflowpdf/engine'
    import wasm from '@reflowpdf/engine/assets/flexpdf_wasm_bg.wasm'
    
    let engine
    export default {
      async fetch(request, env) {
        engine ??= await createEngine({ wasm, memoryLimitMb: 64, assetsUrl: 'https://api.reflowpdf.com/sdk/engine/1.0.3/assets/', licenseKey: env.REFLOWPDF_LICENSE_KEY, apiKey: env.REFLOWPDF_API_KEY })
        return new Response(await engine.pdf(await request.text()), { headers: { 'Content-Type': 'application/pdf' } })
      },
    }

    In workerd a first print took 0.66 s and the next ones 0.04 s. A Worker has 128 MB of memory, and the module's grows with the document: about 20 MB for a page, 46 MB for 37 pages, 136 MB for 147, past a Worker. memoryLimitMb lets an instance go after a large print (the next takes a fresh one); a document of a hundred pages and more belongs on a server, or the API.

    Content Security Policy#

    A page with a CSP lets the engine compile its module and start its Worker: script-src 'self' 'wasm-unsafe-eval' (and https://api.reflowpdf.com, if the engine comes from there), worker-src 'self' blob: (blob: when it comes from another origin), connect-src with the files' origin and https://api.reflowpdf.com (the license's token; a print condition's profile). Without 'wasm-unsafe-eval' the engine says so (engine_failed).

    Errors#

    A print the engine could not make is an EngineError with a code: invalid_html, invalid_option (its message names the option), template_error (the template does not read, or asks for more than a fill may make), unfulfillable (a claim the document cannot keep, a print condition it cannot get, a page it does not have; the message says which), document_too_deep (flatten the nesting), time_limit (the print took longer than timeLimitMs), layout_failed, engine_failed (its files could not be had or are not this version's, or the module could not be compiled; the message says which), closed. After time_limit or layout_failed the next print gets a fresh module.

    The token endpoint#

    GET https://api.reflowpdf.com/v1/engine/license?key=rpdfl_… answers the token (EngineLicenseToken, in the reference); with Authorization: Bearer rpdf_… of the license's owner, one valid on a server. The engine asks for it itself; ask yourself only to pass a token with engine.setLicense(token) — e.g. fetched by your server for a page that should not reach the API.