Documentation

Parse documents in the browser in five minutes

qovox-parser is a client-side SDK. You give it a File, it gives you structured JSON and Markdown. Documents are processed on the user’s device and are never uploaded.

5-minute quickstart

  1. Create a license key

    Sign in to the dashboard, open License keys, click New key and add the domains that will run your app (for example app.company.com, or localhost while developing). Copy the key: it is shown once.

  2. Install the SDK

    Run npm install qovox-parser, or add the script tag from Installation.

  3. Initialize with your key

    Call QovoxParser.init({ licenseKey }) once, when your page or app starts. It validates your key and domain and resolves to a parser instance.

  4. Parse a file

    Pass any File (from an <input type="file"> or drag and drop) to parser.parse(file) and use doc.markdown, doc.blocks or doc.text.

  5. Ship it

    Deploy to a domain on your allow-list. Requests from any other domain fail with DOMAIN_NOT_ALLOWED.

app.js
import { QovoxParser } from 'qovox-parser';

const parser = await QovoxParser.init({
  licenseKey: 'QVX-XXXX-XXXX-XXXX-XXXX',
  // apiBase: 'http://localhost:3000',   // only while developing against a local license server
});

document.querySelector('#file').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  const doc = await parser.parse(file);
  console.log(doc.markdown);          // "# Quarterly Report\n\n..."
  console.log(doc.stats.pages, 'pages in', doc.stats.ms, 'ms');
});

The license key is not a secret from your users’ browser (it has to be in your page), which is why it is locked to your domains. Copying it to another site does not work.

Installation

npm / pnpm / yarn

terminal
npm install qovox-parser

Script tag (self-hosted)

Download docparser.min.js from the dashboard, put it on your own domain and load it with a normal script tag. The SDK exposes a global QovoxParser.

index.html
<script src="/docparser.min.js" defer></script>

Supported browsers: current Chrome, Edge, Firefox and Safari (16.4+). The SDK needs File.arrayBuffer(), DecompressionStream and DOMParser.

Framework examples

main.js
import { QovoxParser } from 'qovox-parser';

const out = document.querySelector('#out');
let parser;

async function start() {
  try {
    parser = await QovoxParser.init({ licenseKey: 'QVX-XXXX-XXXX-XXXX-XXXX' });
  } catch (err) {
    out.textContent = `License problem: ${err.code}`;   // LICENSE_INVALID, DOMAIN_NOT_ALLOWED, ...
  }
}
start();

document.querySelector('#file').addEventListener('change', async (e) => {
  const doc = await parser.parse(e.target.files[0]);
  out.textContent = doc.markdown;
});

API reference

QovoxParser.init(options)

Validates your license and returns a ready-to-use parser. Call it once per page load and reuse the instance. Returns Promise<Parser>. Rejects with a license error if the key, domain or subscription is not valid.

OptionTypeDefaultDescription
licenseKey requiredstring-Key from the dashboard, format QVX-XXXX-XXXX-XXXX-XXXX.
apiBasestringproduction license APIBase URL of the license server. Set to http://localhost:3000 when developing against a local server.
threadsnumber | 'auto''auto'Worker threads for parsing. Multi-threading needs a cross-origin-isolated page; otherwise a single worker is used.
reportUsagebooleanfalseWhen true, sends anonymous counters (number of documents and pages) to your usage analytics. Never any content or file names.
onLicense(info) => void-Called after every successful license check with { plan, expiresAt, features }.
onError(err) => void-Called when a background license refresh fails (the parser keeps working during the offline grace period).
example
const parser = await QovoxParser.init({
  licenseKey: 'QVX-XXXX-XXXX-XXXX-XXXX',
  reportUsage: true,
  onLicense: ({ plan, expiresAt }) => console.log(`licensed (${plan}) until ${expiresAt}`),
});

parser.parse(file, options?)

Parses a PDF or DOCX file entirely in the browser. Returns Promise<ParsedDocument>. The file type is detected from its contents, not its name.

ParameterTypeDescription
file requiredFile | BlobThe document. Maximum size depends on device memory; 40 MB in the fallback engine.
options.signalAbortSignalCancel a long parse. The promise rejects with an AbortError.
options.onProgress({ page, total }) => voidCalled as pages complete.
options.pagesnumber[]Only parse these 1-based page numbers (PDF only).
example
const controller = new AbortController();

const doc = await parser.parse(file, {
  signal: controller.signal,
  onProgress: ({ page, total }) => (bar.value = page / total),
});

The ParsedDocument result

FieldTypeDescription
file{ name, size, type }Basic file info. type is 'pdf' or 'docx'.
metaobjectTitle, author, page count and other document properties when available. May contain a warning string (for example for scanned PDFs).
blocksBlock[]Structured content in reading order (see below).
markdownstringThe whole document as Markdown: headings, lists, tables, bold/italic and links (DOCX).
textstringPlain text with blocks separated by blank lines.
statsobject{ pages, blocks, headings, tables, words, characters, ms }

Each entry of blocks has a type:

typeFields
headinglevel (1–6), text, page?
paragraphtext, page?
list_itemtext, ordered, index?, depth?, page?
tablerows: string[][] (DOCX tables)
output
{
  "file":  { "name": "report.pdf", "size": 2232, "type": "pdf" },
  "meta":  { "format": "pdf", "pages": 2, "title": "Quarterly Report" },
  "blocks": [
    { "type": "heading",   "level": 1, "text": "Quarterly Report", "page": 1 },
    { "type": "paragraph", "text": "Revenue grew 18% quarter over quarter...", "page": 1 },
    { "type": "list_item", "ordered": false, "text": "Annual recurring revenue reached $4.2M", "page": 1 }
  ],
  "markdown": "# Quarterly Report\n\nRevenue grew 18%...",
  "stats": { "pages": 2, "words": 105, "ms": 28 }
}

Other methods & properties

MemberDescription
parser.licenseRead-only { plan, expiresAt, features } from the last successful license check.
parser.refreshLicense()Forces an immediate license check. Resolves with the new license or rejects with a license error.
parser.dispose()Stops background refresh timers and terminates workers. Call it when your component unmounts.
QovoxParser.versionSDK version string.

TypeScript types

qovox-parser.d.ts
export interface InitOptions {
  licenseKey: string;
  apiBase?: string;
  threads?: number | 'auto';
  reportUsage?: boolean;
  onLicense?: (info: LicenseInfo) => void;
  onError?: (err: QovoxError) => void;
}
export interface LicenseInfo { plan: 'dev' | 'enterprise'; expiresAt: string; features: Record<string, unknown> }
export type Block =
  | { type: 'heading'; level: number; text: string; page?: number }
  | { type: 'paragraph'; text: string; page?: number }
  | { type: 'list_item'; text: string; ordered: boolean; index?: number; depth?: number; page?: number }
  | { type: 'table'; rows: string[][] };
export interface ParsedDocument {
  file: { name: string; size: number; type: 'pdf' | 'docx' };
  meta: Record<string, unknown>;
  blocks: Block[];
  markdown: string;
  text: string;
  stats: { pages: number | null; blocks: number; headings: number; tables: number; words: number; characters: number; ms: number };
}
export class QovoxError extends Error { code: string; status?: number }
export interface Parser {
  license: LicenseInfo;
  parse(file: File | Blob, options?: { signal?: AbortSignal; onProgress?: (p: { page: number; total: number }) => void; pages?: number[] }): Promise<ParsedDocument>;
  refreshLicense(): Promise<LicenseInfo>;
  dispose(): void;
}
export const QovoxParser: { init(options: InitOptions): Promise<Parser>; version: string };

How licensing works

  1. init() sends POST /v1/heartbeat with your license key and SDK version. The browser adds the page’s Origin header; page scripts cannot change it.
  2. The license server checks that the key exists, is not revoked, its subscription is valid, and that the origin matches one of the key’s allowed domains.
  3. On success it returns a short-lived (1 hour) signed session token. The SDK refreshes it in the background about every 50 minutes.
  4. If a refresh fails because the network is down, parsing keeps working for an offline grace period (24 hours on Dev, 7 days on Enterprise). Revocation and expiry take effect at the next successful check.

What is sent: license key, page origin (domain), SDK version, and, only if you enable reportUsage, counts of documents and pages. Never sent: file contents, file names, extracted text.

Allowed domains

Error codes

Errors are instances of QovoxError with a stable code string. Always branch on err.code, never on the message text.

License errors (from init() / refreshLicense())

CodeHTTPMeaningHow to fix
LICENSE_INVALID401The key is missing, malformed or unknown.Check for typos or stray whitespace. Make sure you copied the full key from the dashboard. Create a new key if it was lost.
DOMAIN_NOT_ALLOWED403The page’s domain is not on the key’s allow-list (or the browser sent no Origin, e.g. a file:// page).Add the domain in Dashboard → License keys → Edit. Add localhost for local development. Serve the page over http(s), not file://.
LICENSE_EXPIRED403The subscription or the key’s own expiry date has passed.Renew or change your plan in Dashboard → Billing. The key works again at the next license check.
LICENSE_REVOKED403The key was revoked in the dashboard.Create a new key and deploy it. Revoked keys cannot be restored.
ACCOUNT_FROZEN403The account that owns the key was suspended by an administrator.Contact support at qovox222@gmail.com.
SERVER_PAUSED / MAINTENANCE503The license service is temporarily paused or under maintenance.Nothing to fix: the SDK keeps working during the offline grace period and retries automatically. Check the status badge in the site header.
RATE_LIMITED429Too many license checks from one IP.Call init() once per page load and reuse the parser. Retry after the Retry-After delay.
NETWORK_ERROR-The license server could not be reached and no valid grace-period token exists.Check connectivity, your apiBase and your Content-Security-Policy connect-src.

Parsing errors (from parse())

CodeMeaning
UNSUPPORTED_TYPEThe file is neither a PDF nor a DOCX.
NOT_PDF / NOT_DOCXThe file looks like one of the formats but has no valid structure (for example a ZIP that is not a Word document).
ENCRYPTED_PDFThe PDF is password-protected.
CORRUPTThe file is damaged or truncated.
EMPTYThe file has zero bytes.
TOO_LARGEThe file, or its decompressed contents, exceeds the safety limit.
UNSUPPORTED_BROWSERA required browser API (such as DecompressionStream) is missing.
error handling
try {
  const parser = await QovoxParser.init({ licenseKey });
  const doc = await parser.parse(file);
} catch (err) {
  switch (err.code) {
    case 'DOMAIN_NOT_ALLOWED': showBanner('This site is not licensed for qovox-parser.'); break;
    case 'LICENSE_EXPIRED':     showBanner('Document parsing is paused: license expired.'); break;
    case 'ENCRYPTED_PDF':       showToast('Please remove the password and try again.'); break;
    default:                      console.error(err);
  }
}

License REST API

The SDK talks to this API for you. You only need it if you build your own client or verify tokens on your server. Errors always look like { "ok": false, "error": { "code", "message" } }.

POST /v1/heartbeat

request
POST /v1/heartbeat
Content-Type: application/json
Origin: https://app.company.com        (added by the browser)

{ "licenseKey": "QVX-XXXX-XXXX-XXXX-XXXX", "sdkVersion": "1.0.0" }
200 OK
{
  "ok": true,
  "token": "eyJhbGciOiJFZERTQSIs...",          // EdDSA (Ed25519) signed JWT, 1 hour
  "expiresAt": "2026-10-06T12:00:00.000Z",
  "refreshAfterSeconds": 3000,
  "plan": "dev",
  "features": { "watermark": false, "offlineGraceHours": 24 }
}

Token claims: iss (qovox-license), sub (license id), aud (the allowed host that was checked), plan, features, iat, exp, jti. Fetch the verification key from GET /v1/public-key (a JWK).

verify a token (Node.js)
const crypto = require('crypto');

const { jwk } = await (await fetch(`${API}/v1/public-key`)).json();
const key = crypto.createPublicKey({ key: jwk, format: 'jwk' });

function verify(token) {
  const [h, p, sig] = token.split('.');
  const ok = crypto.verify(null, Buffer.from(`${h}.${p}`), key, Buffer.from(sig, 'base64url'));
  const claims = JSON.parse(Buffer.from(p, 'base64url'));
  return ok && claims.exp > Date.now() / 1000 ? claims : null;
}

Other endpoints

EndpointAuthPurpose
POST /v1/usagesession tokenReport { documents, pages } counters (used when reportUsage is on).
POST /v1/auth/register · /login-Create an account / sign in; returns a dashboard token.
GET /v1/licenses · POST /v1/licensesdashboard tokenList keys / create a key (name, allowedOrigins[]); the full key is returned once.
PATCH /v1/licenses/:id · POST /v1/licenses/:id/revoke · DELETE /v1/licenses/:iddashboard tokenEdit name or domains / revoke / delete.
GET /v1/usage?days=30dashboard tokenDaily usage series and per-key totals.
GET /v1/billing · POST /v1/billing/subscribe · /canceldashboard tokenBilling summary and (mock) plan changes.
GET /health-Liveness probe.

Headers & CSP

Single-threaded parsing works on any page. To enable multi-threaded parsing the page must be cross-origin isolated, which needs these response headers on your HTML:

response headers
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

require-corp blocks cross-origin images, scripts and iframes that do not opt in with CORS or Cross-Origin-Resource-Policy. Test your page after enabling it.

If you use a Content-Security-Policy, allow the SDK’s worker and the license endpoint:

Content-Security-Policy
script-src 'self' 'wasm-unsafe-eval';
worker-src 'self' blob:;
connect-src 'self' https://your-license-api.example.com;

Your license server must be reachable from your users’ browsers. The /v1/heartbeat endpoint answers CORS preflights for any origin, because the allowed-domain list is the real access control.

Troubleshooting

SymptomLikely cause and fix
DOMAIN_NOT_ALLOWED on localhostAdd localhost (or 127.0.0.1) to the key’s domains. Open the page through a dev server rather than file://.
Console shows a CORS error on /v1/heartbeatThe license server is down or the URL is wrong. Check apiBase; run npm start in site/server for local development.
Blocked by Content-Security-PolicyAdd the license API origin to connect-src and blob: to worker-src.
Empty blocks and a meta.warning about scansThe PDF has no text layer (it is an image). Run OCR first.
Parsing works, then stops after a day offlineThe offline grace period ended. Reconnect so the SDK can refresh its token.
RATE_LIMITED in testsYou call init() repeatedly. Create the parser once and reuse it.