OLPDF Documentation
The complete reference for the OLPDF REST API, Studio editor, PDF toolkit, and self-hosting infrastructure.
Overview
Base URL, versioning, and content type conventions
Base URL
https://api.olpdf.xyzProtocol
HTTPS only
Format
JSON (UTF-8)
Versioning
/v1 prefix
All request bodies must be application/json. Binary uploads (PDF ingestion) use multipart/form-data. Every response includes an X-Request-ID header for distributed tracing.
Quick Start
Extract and edit your first document in under 3 minutes
Upload a PDF to extract its semantic model
Use the AI instruction endpoint to edit it
Export back to PDF or EPUB3
# 1. Upload + extract curl -X 700">POST https://api.olpdf.xyz/v1/extract \ -H "Authorization: Bearer free_beta_key" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/report.pdf", "mode": "semantic"}' # Returns: { S3 : S4 , S5 : [...] } # 2. Issue an AI instruction curl -X 700">POST https://api.olpdf.xyz/api/ai/documents/doc_abc123/instruction \ -H "Authorization: Bearer free_beta_key" \ -H "Content-Type: application/json" \ -d '{"instruction": "Summarize the executive summary section into 3 bullet points"}' # 3. Export to PDF curl -X 700">POST https://api.olpdf.xyz/api/documents/doc_abc123/export/pdf \ -H "Authorization: Bearer free_beta_key" \ --output result.pdf
import requests BASE = "https://api.olpdf.xyz" HEADERS = {"Authorization": "Bearer free_beta_key"} # 1. Extract doc = requests.post(f"{BASE}/v1/extract", headers=HEADERS, json={ "url": "https://example.com/report.pdf", "mode": "semantic" }).json() doc_id = doc["document_id"] # 2. AI edit requests.post(f"{BASE}/api/ai/documents/{doc_id}/instruction", headers=HEADERS, json={ "instruction": "Convert all headers to Title Case" }) # 3. Export pdf = requests.post(f"{BASE}/api/documents/{doc_id}/export/pdf", headers=HEADERS) open("result.pdf", "wb").write(pdf.content)
Code Examples
Extract and edit a PDF in curl, Python, Node, Java, Go, and Rust
curl -X 700">POST https://api.olpdf.xyz/v1/extract \ -H "Authorization: Bearer <your_key>" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/invoice.pdf","mode":"semantic"}'
Authentication
Bearer JWT tokens and persistent API keys
All /api/* endpoints require authentication via one of two mechanisms. Worker callback routes (/api/worker/*) use QStash signature verification instead.
Bearer JWT (User Sessions)
Issued by Supabase Auth. Short-lived (1 hour), auto-refreshed by the SDK. Use for user-facing integrations.
Authorization: Bearer eyJhbGci...
API Key (Machine-to-Machine)
Long-lived keys generated in the dashboard. Stored as SHA-256 hashes server-side. Never expire unless rotated.
X-API-Key: olpdf_live_xxxxxxxxxxxx
Warning
free_beta_key is a public testing credential with a 10 req/min limit. Generate a personal API key in your dashboard for production use.Error Responses
// 401 — Missing or invalid credential { "error": "api_error", "message": "Authentication required (JWT or API Key)" } // 403 — Valid credential but insufficient permissions { "error": "api_error", "message": "You do not have permission to access this resource" }
Rate Limits
Per-IP burst limits enforced via Redis sliding window
| Tier | Limit | Window | Scope |
|---|---|---|---|
| Free (beta key) | 10 requests | 1 minute | Per IP address |
| API Key (personal) | 120 requests | 1 minute | Per key |
| PDF Toolkit ops | 20 requests | 1 minute | Per user |
| AI Instructions | 30 requests | 1 hour | Per document |
When a limit is exceeded the API returns 429 Too Many Requests with a Retry-After header in seconds.
Documents
Create, read, update, export, and version documents
Initialize a new empty document. Returns the document_id used in all subsequent calls.
Parameters
| Parameter | Description |
|---|---|
titleRequiredstring | Human-readable document name |
metaobject | Arbitrary key-value metadata (author, tags, etc.) |
source_urlstring | If provided, the PDF at this URL is fetched and extracted immediately |
Request Body
{
"title": "Q3 2026 Investor Report",
"meta": { "author": "Finance Team", "department": "ir" },
"source_url": "https://example.com/q3-report.pdf"
}Response
{
"document_id": "doc_7f3a2b91",
"title": "Q3 2026 Investor Report",
"status": "extracting",
"created_at": "2026-05-09T10:23:00Z",
"blocks_count": 0
}Retrieve the full semantic JSON model including all blocks, metadata, and version history.
Response
{
"document_id": "doc_7f3a2b91",
"title": "Q3 2026 Investor Report",
"status": "ready",
"blocks": [
{ "id": "blk_001", "type": "heading", "level": 1, "text": "Executive Summary", "page": 1, "bbox": [72, 48, 540, 72] },
{ "id": "blk_002", "type": "paragraph", "text": "Revenue grew 34% YoY...", "page": 1, "bbox": [72, 80, 540, 120] }
],
"page_count": 12,
"created_at": "2026-05-09T10:23:00Z",
"updated_at": "2026-05-09T10:24:15Z"
}Generate a signed temporary URL for read-only PDF preview. Valid for 15 minutes.
Response
{
"preview_url": "https://r2.olpdf.xyz/previews/doc_7f3a2b91.pdf?token=...",
"expires_at": "2026-05-09T10:38:00Z"
}Replace the block array. Use after making manual edits to the semantic model.
Parameters
| Parameter | Description |
|---|---|
blocksRequiredBlock[] | Full replacement block array |
titlestring | Update the document title |
Request Body
{
"blocks": [
{ "id": "blk_001", "type": "heading", "level": 1, "text": "Updated Executive Summary" },
{ "id": "blk_002", "type": "paragraph", "text": "Revenue grew 41% YoY after adjustment..." }
]
}Response
{ "document_id": "doc_7f3a2b91", "blocks_count": 2, "updated_at": "2026-05-09T10:30:00Z" }Compile the semantic model back to binary. Supported formats: pdf, epub, docx.
Parameters
| Parameter | Description |
|---|---|
formatRequiredstring | pdf | epub | docx |
pdf_standardstring | pdf-a (archival) | tagged (accessibility). Defaults to standard PDF. |
Response
// Content-Type: application/pdf (or application/epub+zip) // Binary stream. Save directly to disk.
Create a named version snapshot. Snapshots are immutable and can be used to roll back.
Request Body
{ "version_name": "pre-review-v1.0" }Response
{
"snapshot_id": "snap_a1b2c3",
"version_name": "pre-review-v1.0",
"blocks_count": 48,
"created_at": "2026-05-09T11:00:00Z"
}Permanently delete a document and all associated blobs from R2 storage. Irreversible.
Response
{ "deleted": true, "document_id": "doc_7f3a2b91" }AI Operations
Gemini-powered structural editing with full audit trail
AI operations use a propose → review → acceptflow. The model never directly mutates your document; it proposes a diff which you can inspect and accept or discard. Every accepted change is logged with the original instruction, the model's reasoning, and a full before/after diff.
Tip
auto_accept: true in the instruction body to skip manual review. Useful for batch processing pipelines.Send a natural language command to Gemini 1.5 Pro. Returns a proposed diff.
Parameters
| Parameter | Description |
|---|---|
instructionRequiredstring | Plain-language edit command |
scopestring[] | Optional block IDs to restrict the edit scope to specific sections |
auto_acceptboolean | If true, immediately applies the diff without manual review |
Request Body
{
"instruction": "Extract all action items into a numbered list at the end of each section",
"scope": ["blk_010", "blk_011", "blk_012"],
"auto_accept": false
}Response
{
"log_id": "log_x9y8z7",
"status": "pending_review",
"instruction": "Extract all action items...",
"diff": {
"added": [{ "id": "blk_013", "type": "list", "items": ["Schedule Q4 review", "Approve budget"] }],
"modified": [],
"removed": []
},
"model": "gemini-1.5-pro",
"tokens_used": 1240
}Full audit trail of all AI operations on this document, including diffs and token usage.
Response
{
"logs": [
{
"log_id": "log_x9y8z7",
"instruction": "Extract all action items...",
"status": "accepted",
"created_at": "2026-05-09T10:45:00Z",
"tokens_used": 1240
}
],
"total": 1
}Permanently apply the proposed diff to the document. Transitions log status to accepted.
Response
{ "log_id": "log_x9y8z7", "status": "accepted", "blocks_affected": 3 }Discard a pending diff. No changes are made to the document. Log is marked as discarded.
Response
{ "log_id": "log_x9y8z7", "status": "discarded" }PDF Toolkit
Low-level binary PDF operations — merge, split, redact, compress
Combine multiple PDFs into a single output file. Page order follows the document_ids array.
Parameters
| Parameter | Description |
|---|---|
document_idsRequiredstring[] | Ordered list of document IDs to merge |
output_titlestring | Title for the resulting merged document |
Request Body
{ "document_ids": ["doc_aaa", "doc_bbb", "doc_ccc"], "output_title": "Combined Report" }Response
{ "document_id": "doc_merged_001", "page_count": 36, "size_bytes": 1204300 }Split a PDF into multiple documents by page ranges.
Parameters
| Parameter | Description |
|---|---|
document_idRequiredstring | Source document to split |
rangesRequiredobject[] | Array of {start, end} page ranges (1-indexed) |
Request Body
{
"document_id": "doc_7f3a2b91",
"ranges": [{ "start": 1, "end": 5 }, { "start": 6, "end": 12 }]
}Response
{ "documents": [{ "document_id": "doc_split_a", "pages": 5 }, { "document_id": "doc_split_b", "pages": 7 }] }Reduce file size by downsampling images and removing metadata. Choose from three quality presets.
Parameters
| Parameter | Description |
|---|---|
document_idRequiredstring | Document to compress |
qualitystring | screen | ebook | printer. Defaults to ebook. |
Response
{ "document_id": "doc_compressed_001", "original_bytes": 8200000, "compressed_bytes": 1940000, "reduction_pct": 76 }Rotate specific pages in a document.
Request Body
{ "document_id": "doc_7f3a2b91", "pages": [3, 4], "degrees": 90 }Response
{ "document_id": "doc_rotated_001", "pages_modified": 2 }Apply a text or image watermark across all pages.
Parameters
| Parameter | Description |
|---|---|
document_idRequiredstring | Source document |
textstring | Watermark text (e.g. DRAFT) |
opacitynumber | 0.0–1.0. Defaults to 0.3 |
anglenumber | Rotation in degrees. Defaults to 45 |
Request Body
{ "document_id": "doc_7f3a2b91", "text": "CONFIDENTIAL", "opacity": 0.25, "angle": 45 }Response
{ "document_id": "doc_watermarked_001" }Permanently black out coordinate areas across one or more pages. Irreversible vector-level redaction — not just a visual overlay.
Parameters
| Parameter | Description |
|---|---|
document_idRequiredstring | Source document |
areasRequiredobject[] | Array of {page, x, y, w, h} bounding boxes in PDF points |
Request Body
{
"document_id": "doc_7f3a2b91",
"areas": [
{ "page": 1, "x": 100, "y": 200, "w": 150, "h": 20 },
{ "page": 2, "x": 72, "y": 340, "w": 300, "h": 12 }
]
}Response
{ "document_id": "doc_redacted_001", "areas_redacted": 2 }Locate and classify all interactive form fields (text, checkbox, radio, dropdown) in a PDF.
Response
{
"fields": [
{ "id": "fld_001", "name": "FullName", "type": "text", "page": 1, "bbox": [100, 200, 300, 220] },
{ "id": "fld_002", "name": "Agree", "type": "checkbox", "page": 3, "bbox": [72, 400, 90, 418] }
]
}Flatten a PDF form with provided field values. Produces a non-interactive filled PDF.
Request Body
{
"document_id": "doc_7f3a2b91",
"data": { "FullName": "Jane Doe", "Agree": true, "Date": "2026-05-09" }
}Response
{ "document_id": "doc_filled_001" }Books
Multi-chapter publication workspace with cross-chapter consistency
A Book is a container that aggregates individual Documents as ordered chapters. The consistency engine uses RAG across all chapter embeddings to detect terminology drift, name mismatches, and font hierarchy violations before export.
Create a new book workspace.
Request Body
{ "title": "OLPDF Technical Handbook", "description": "Internal developer documentation" }Response
{ "book_id": "book_abc123", "title": "OLPDF Technical Handbook", "chapters": [] }Append an existing document as a chapter. Chapter order is controlled by chapter_number.
Parameters
| Parameter | Description |
|---|---|
document_idRequiredstring | Existing document to attach as a chapter |
chapter_numberRequiredinteger | Insertion position (1-indexed) |
titlestring | Override chapter title (defaults to document title) |
Request Body
{ "document_id": "doc_ch1", "chapter_number": 1, "title": "Introduction" }Response
{ "book_id": "book_abc123", "chapter_id": "ch_001", "chapter_number": 1 }Run a full cross-chapter consistency analysis using pgvector embeddings. Returns a report of issues.
Response
{
"issues": [
{ "type": "terminology", "description": "Chapter 2 uses 'semantic DOM', Chapter 4 uses 'block tree'", "chapters": [2, 4] },
{ "type": "font_hierarchy", "description": "H2 font size inconsistency between chapters 1 and 3", "chapters": [1, 3] }
],
"score": 0.87
}Compile all chapters into a single publication. Formats: epub (EPUB3), pdf.
Response
// Binary stream — application/epub+zip or application/pdfTemplates
Reusable structural scaffolds for new documents
List all available templates (system-provided + user-created).
Response
{
"templates": [
{ "template_id": "tmpl_invoice", "name": "Invoice", "category": "finance", "is_system": true },
{ "template_id": "tmpl_mytemplate", "name": "My Template", "category": "custom", "is_system": false }
]
}Save the current block structure of a document as a reusable template.
Request Body
{ "source_document_id": "doc_7f3a2b91", "name": "Q3 Report Template", "category": "finance" }Response
{ "template_id": "tmpl_q3_2026", "name": "Q3 Report Template" }Create a new document pre-populated with the template's block structure.
Request Body
{ "title": "Q4 2026 Investor Report" }Response
{ "document_id": "doc_new_from_template", "blocks_count": 24 }Delete a user-created template. System templates cannot be deleted.
Response
{ "deleted": true }Annotations
Comments and notes attached to document blocks
List all annotations on a document.
Response
{ "annotations": [{ "annotation_id": "ann_001", "block_id": "blk_005", "text": "Verify this figure", "author": "user@email.com" }] }Add an annotation to a specific block.
Parameters
| Parameter | Description |
|---|---|
block_idRequiredstring | Block to attach the annotation to |
textRequiredstring | Annotation content |
typestring | comment | highlight | todo. Defaults to comment |
Remove an annotation.
Response
{ "deleted": true }Webhooks
Real-time event push to your own HTTPS endpoints
Webhooks are signed with HMAC-SHA256 using your webhook secret. Always verify the X-OLPDF-Signature header before processing events.
Available Events
| Event | Description |
|---|---|
| document.created | A new document was created |
| document.ready | PDF extraction completed successfully |
| document.failed | Extraction or OCR job failed |
| ai.instruction.accepted | An AI diff was accepted |
| export.completed | An export job finished — download URL included |
| book.consistency_report | Cross-chapter analysis complete |
Register a new webhook endpoint.
Request Body
{ "url": "https://yourapp.com/webhooks/olpdf", "events": ["document.ready", "export.completed"] }Response
{ "webhook_id": "wh_001", "secret": "whsec_xxxxxxxxxxxx" }List all registered webhooks for your account.
Delete a webhook registration.
Response
{ "deleted": true }Payload Example — document.ready
{
"event": "document.ready",
"timestamp": "2026-05-09T10:24:15Z",
"data": {
"document_id": "doc_7f3a2b91",
"blocks_extracted": 48,
"page_count": 12
}
}Note
API Keys
Create and manage persistent machine-to-machine credentials
List all API keys for your account. Secret values are never returned after creation.
Response
{ "keys": [{ "key_id": "key_001", "name": "CI Pipeline", "prefix": "olpdf_live_xxxx", "created_at": "2026-05-01" }] }Generate a new API key. The full key is returned only once — store it immediately.
Request Body
{ "name": "Production Backend" }Response
{
"key_id": "key_002",
"name": "Production Backend",
"key": "olpdf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"created_at": "2026-05-09T10:00:00Z"
}Warning
key value is shown exactly once. OLPDF stores only the SHA-256 hash. Copy it now.Usage examples
# Step 1 — create an API key (one time) curl -X 700">POST https://api.olpdf.xyz/api/api-keys \ -H "Authorization: Bearer <supabase_jwt>" \ -H "Content-Type: application/json" \ -d '{"name": "Production Backend"}' # => { S3 : S4 } ← save this, shown once # Step 2 — use the key in any request curl -X 700">POST https://api.olpdf.xyz/v1/extract \ -H "Authorization: Bearer olpdf_live_xxxx..." \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/doc.pdf","mode":"semantic"}'
Revoke an API key immediately. Any requests using this key will return 401.
Response
{ "deleted": true }Error Reference
Standard error envelope and HTTP status codes
All errors follow a consistent envelope format:
{
"error": "api_error",
"message": "Human-readable description of what went wrong",
"code": "DOCUMENT_NOT_FOUND" // optional machine-readable code
}| HTTP Status | Meaning | Common Cause |
|---|---|---|
| 400 | Bad Request | Malformed JSON, missing required field |
| 401 | Unauthorized | Missing or invalid JWT / API key |
| 403 | Forbidden | Accessing another user's resource |
| 404 | Not Found | Document or resource doesn't exist |
| 409 | Conflict | Version snapshot name already exists |
| 413 | Payload Too Large | Upload exceeds 10 MB limit |
| 422 | Unprocessable Entity | File is not a valid PDF or is password-protected |
| 429 | Too Many Requests | Rate limit exceeded — check Retry-After header |
| 500 | Internal Server Error | Unexpected error — include X-Request-ID in support tickets |
| 503 | Service Unavailable | Database or AI model temporarily unavailable |
Self-Hosting
Deploy your own OLPDF instance on Fly.io or Docker
OLPDF is fully open-source under the MIT license. The stack comprises a Next.js frontend (Vercel or self-hosted), a FastAPI backend (Fly.io or Docker), and a Modal GPU worker for OCR. This guide covers Fly.io deployment.
Prerequisites
1. Clone and install
git clone https://github.com/chidi09/olpdf.git
cd olpdf
pnpm install2. Deploy Redis on Fly.io
fly redis create --name olpdf-redis --region ams --no-replicas
# Copy the redis: C0 3. Deploy the FastAPI backend
# Launch the app (reads apps/api/fly.toml) cd apps/api fly launch --no-deploy # Set required secrets fly secrets set \ SUPABASE_URL="https://xxx.supabase.co" \ SUPABASE_SERVICE_ROLE_KEY="eyJ..." \ SUPABASE_JWT_SECRET="your-jwt-secret" \ GEMINI_API_KEY="AIza..." \ R2_ACCESS_KEY_ID="..." \ R2_SECRET_ACCESS_KEY="..." \ R2_ENDPOINT="https://xxx.r2.cloudflarestorage.com" \ R2_BUCKET_NAME="olpdf-documents" \ QSTASH_TOKEN="..." \ QSTASH_CURRENT_SIGNING_KEY="..." \ MODAL_WORKER_URL="https://..." \ WORKER_SECRET="your-shared-secret" \ REDIS_URL="rediss://default:xxx@in-koala-xxx.upstash.io:6379" \ ALLOWED_ORIGINS="https://yourdomain.com" # Deploy fly deploy
4. Deploy the Next.js frontend to Vercel
vercel --prod # Add these environment variables in Vercel dashboard: # NEXT_PUBLIC_SUPABASE_URL=https: C0 # NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # NEXT_PUBLIC_API_URL=https: C1
5. Run Supabase migrations
supabase db push
# Or apply manually:
psql $SUPABASE_DB_URL < supabase/migrations/*.sql6. Deploy the Modal OCR worker
cd apps/worker
pip install modal
modal deploy modal_worker.py
# Note the deployed function URL — set it as MODAL_WORKER_URLTip
fly logs -a olpdf to tail live logs. The /health endpoint probes the database and returns 200 healthy or 503 degraded — use it as your uptime monitor target.Fly.io machine configuration (fly.toml)
app = 'olpdf' primary_region = 'ams' [http_service] internal_port = 8000 force_https = true auto_stop_machines = 'stop' auto_start_machines = true [[http_service.checks]] path = '/health' interval = '30s' timeout = '5s' [[vm]] memory = '2gb' cpu_kind = 'shared' cpus = 2
Embed SDK — Overview
The @olpdf/embed package lets you embed the full OLPDF editor inside any web application via a lightweight postMessage bridge. The host page renders an iframe pointing at your OLPDF instance; the SDK handles all communication transparently.
No React, Vue, or Angular required in the host app.
Secure cross-origin communication with origin validation.
Receive the complete DocumentModel on every edit.
Installation
Or load from CDN (no bundler required):
Events
Subscribe to events using editor.on(event, handler). Each call returns an unsubscribe function.
| Event | Payload | Description |
|---|---|---|
| READY | — | Editor mounted and ready to accept commands. |
| MODEL_UPDATE | { documentId, documentModel } | Full AST after any edit. Replaces deprecated BLOCK_CHANGE. |
| PAGE_ADDED | { pageIndex, width, height } | Layout engine added an overflow page. |
| PAGE_REMOVED | { pageIndex } | Layout engine removed an overflow page. |
| SAVE | { documentId, blockCount, pageCount } | Document saved. |
| EXPORT_COMPLETE | { url } | Export finished; download URL (24h TTL). |
Framework Guides
The SDK is framework-agnostic — it only needs a mounted DOM element as a container. The golden rule: call new OlPDFEmbed(el, opts) after the element is in the DOM, and always call editor.destroy() when the component unmounts to avoid iframe leaks.
Next.js
Because OlPDFEmbed interacts with the DOM directly, you must mark the file with 'use client'. Use useRef to get a stable reference to the container div, and useEffect to instantiate the editor after the component mounts. Return editor.destroy()from the effect cleanup so React Hot Reload and strict-mode double-invocation don't leak iframes.
'use client'; import { useEffect, useRef } from 'react'; import { OlPDFEmbed } from '@olpdf/embed'; interface PDFEditorProps { documentId: string; token: string; onSave?: (model: object) => void; } export default function PDFEditor({ documentId, token, onSave }: PDFEditorProps) { const containerRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!containerRef.current) return; const editor = new OlPDFEmbed(containerRef.current, { host: 'https://olpdf.xyz', documentId, token, }); // READY fires when the iframe is mounted and the editor is initialised editor.on('READY', () => console.log('OLPDF editor ready')); // MODEL_UPDATE carries the full AST after every edit editor.on('MODEL_UPDATE', ({ documentModel }) => { onSave?.(documentModel); }); // PAGE_ADDED / PAGE_REMOVED fire when layout reflows overflow pages editor.on('PAGE_ADDED', ({ pageIndex, width, height }) => { console.log(`Page ${pageIndex + 1} added (${width}×${height}pt)`); }); // Always destroy on unmount to remove the iframe and event listeners return () => editor.destroy(); }, [documentId, token, onSave]); return ( <div ref={containerRef} className="w-full h-screen" // Prevent parent scroll from interfering with the editor style={{ overflow: 'hidden' }} /> ); }
Svelte
Use bind:this to obtain the DOM element, then instantiate inside onMount. Svelte guarantees the element exists by the time onMount fires. Destroy in onDestroy.
<script lang="ts"> import { onMount, onDestroy } from 'svelte'; import { OlPDFEmbed } from '@olpdf/embed'; export let documentId: string; export let token: string; let container: HTMLDivElement; let editor: OlPDFEmbed; onMount(() => { editor = new OlPDFEmbed(container, { host: 'https://olpdf.xyz', documentId, token, }); editor.on('READY', () => console.log('Editor ready')); editor.on('MODEL_UPDATE', ({ documentModel }) => { // Persist or sync the full AST model console.log('Model updated', documentModel); }); editor.on('EXPORT_COMPLETE', ({ url }) => { // url is a 24-hour signed download link window.open(url, '_blank'); }); }); onDestroy(() => { editor?.destroy(); }); </script> <div bind:this={container} class="w-full h-screen overflow-hidden" />
Nuxt / Vue 3
Use the Composition API with ref() for the container element and onMounted for instantiation. In Nuxt, wrap in <ClientOnly> or add process.client guard to avoid SSR errors since the SDK requires a browser DOM.
<!-- components/PDFEditor.vue --> <script setup lang="ts"> import { ref, onMounted, onUnmounted, watch } from 'vue'; import { OlPDFEmbed } from '@olpdf/embed'; const props = defineProps<{ documentId: string; token: string; }>(); const emit = defineEmits<{ modelUpdate: [model: object]; }>(); const container = ref<HTMLDivElement | null>(null); let editor: OlPDFEmbed | null = null; function initEditor() { if (!container.value) return; editor?.destroy(); editor = new OlPDFEmbed(container.value, { host: 'https://olpdf.xyz', documentId: props.documentId, token: props.token, }); editor.on('MODEL_UPDATE', ({ documentModel }) => { emit('modelUpdate', documentModel); }); } onMounted(initEditor); // Re-init when props change watch(() => [props.documentId, props.token], initEditor); onUnmounted(() => editor?.destroy()); </script> <template> <div ref="container" class="w-full h-screen overflow-hidden" /> </template>
Astro
Astro is server-first. Place the <script> tag inside the .astro file (not the frontmatter fence). Astro bundles and defers client-side scripts automatically. No client:* directive is needed since this is vanilla JS, not a framework component.
--- // src/pages/editor/[id].astro import Layout from '../layouts/Layout.astro'; const { id } = Astro.params; // Fetch a short-lived embed token from your API const token = await getEmbedToken(id); --- <Layout> <div id="olpdf-container" class="w-full h-screen overflow-hidden"></div> </Layout> <script define:vars={{ documentId: id, token }}> // This script runs in the browser after hydration import { OlPDFEmbed } from '@olpdf/embed'; const container = document.getElementById('olpdf-container'); const editor = new OlPDFEmbed(container, { host: 'https://olpdf.xyz', documentId, token, }); editor.on('READY', () => { console.log('OLPDF editor ready'); }); editor.on('MODEL_UPDATE', ({ documentModel }) => { // Send back to your server or store locally fetch('/api/save', { method: '700">POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documentId, documentModel }), }); }); </script>
Analog (Angular)
Analog is the meta-framework built on Angular. Use @ViewChild to obtain the native DOM element and ngAfterViewInit to instantiate the editor — this lifecycle hook guarantees the view is fully rendered. Implement OnDestroy to clean up.
// pdf-editor.component.ts import { Component, Input, Output, EventEmitter, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { OlPDFEmbed } from '@olpdf/embed'; @Component({ selector: 'app-pdf-editor', standalone: true, template: ` <div #container class= S3 ></div> `, }) export class PDFEditorComponent implements AfterViewInit, OnDestroy { @Input() documentId!: string; @Input() token!: string; @Output() modelUpdate = new EventEmitter<object>(); @ViewChild('container') containerRef!: ElementRef<HTMLDivElement>; private editor?: OlPDFEmbed; ngAfterViewInit(): void { this.editor = new OlPDFEmbed(this.containerRef.nativeElement, { host: 'https://olpdf.xyz', documentId: this.documentId, token: this.token, }); this.editor.on('READY', () => { console.log('OLPDF ready'); }); this.editor.on('MODEL_UPDATE', ({ documentModel }) => { this.modelUpdate.emit(documentModel); }); this.editor.on('EXPORT_COMPLETE', ({ url }) => { window.open(url, '_blank'); }); } ngOnDestroy(): void { this.editor?.destroy(); } }
Wix (Velo)
In Wix Velo, all custom code runs in the Page Code panel (not a file you upload). Add an HTML Component element to your page, give it an ID (e.g. #editorBox), then call $w('#editorBox').getEl() inside $w.onReady to get the underlying DOM element. Use the Wix NPM Packages panel to install @olpdf/embed.
// Page Code panel (Wix Velo) import wixData from 'wix-data'; import wixUsers from 'wix-users'; import { OlPDFEmbed } from '@olpdf/embed'; $w.onReady(async () => { // Get the current user's session token for the embed const token = await wixUsers.currentUser.getToken(); // #editorBox is an HTML Component element on your Wix page const containerEl = $w('#editorBox').getEl(); const editor = new OlPDFEmbed(containerEl, { host: 'https://olpdf.xyz', documentId: $w('#documentIdInput').value, // e.g. from a text input token, }); editor.on('READY', () => { $w('#statusText').text = 'Editor loaded'; }); editor.on('MODEL_UPDATE', async ({ documentModel }) => { // Save the updated document model to Wix Data await wixData.save('Documents', { _id: $w('#documentIdInput').value, model: JSON.stringify(documentModel), }); $w('#statusText').text = 'Saved ✓'; }); editor.on('EXPORT_COMPLETE', ({ url }) => { $w('#downloadButton').link = url; $w('#downloadButton').show(); }); });
WordPress
WordPress has no npm build pipeline by default, so load the SDK from a CDN via wp_enqueue_scriptand wire up the editor with wp_add_inline_script. For block-based themes, create a custom block or use a Classic Widget with the HTML widget.
<?php // functions.php — enqueue the SDK and initialise the editor add_action('wp_enqueue_scripts', function () { // Only load on the document editor page template if (!is_page_template('page-pdf-editor.php')) return; wp_enqueue_script( 'olpdf-embed', 'https://cdn.jsdelivr.net/npm/@olpdf/embed/dist/index.js', [], // no dependencies '1.0', true // load in footer, after DOM is ready ); // Pass server-side data to the script safely $document_id = get_query_var('document_id'); $embed_token = olpdf_generate_embed_token($document_id); // your helper wp_add_inline_script('olpdf-embed', sprintf(' (function() { var container = document.getElementById("olpdf-editor"); if (!container) return; var editor = new OlPDFEmbed(container, { host: "https://olpdf.xyz", documentId: %s, token: %s, }); editor.on("READY", function () { console.log("OLPDF ready"); }); editor.on("MODEL_UPDATE", function (payload) { fetch(ajaxurl, { method: "700">POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ action: "olpdf_save", document_id: %s, model: JSON.stringify(payload.documentModel), nonce: "%s", }), }); }); })(); ', wp_json_encode($document_id), wp_json_encode($embed_token), wp_json_encode($document_id), wp_create_nonce('olpdf_save') )); }); // Add the AJAX handler for saving add_action('wp_ajax_olpdf_save', function () { check_ajax_referer('olpdf_save', 'nonce'); $document_id = sanitize_text_field($_POST['document_id']); $model = wp_unslash($_POST['model']); update_post_meta($document_id, '_olpdf_model', $model); wp_send_json_success(); });
Add the container div to your page template (page-pdf-editor.php):
<!-- page-pdf-editor.php --> <?php get_header(); ?> <main> <div id="olpdf-editor" style="width:100%; height:90vh; overflow:hidden;"></div> </main> <?php get_footer(); ?>
.NET — WinForms / WPF / MAUI
Embed OLPDF into any Windows desktop app using WebView2— Microsoft's Chromium-based webview. Install the Olpdf NuGet package for the API client and Microsoft.Web.WebView2 to host the editor iframe natively.
$ dotnet add package Microsoft.Web.WebView2
// OlpdfWindow.cs — WinForms example using Microsoft.Web.WebView2.WinForms; using Olpdf; using System.Text.Json; public partial class OlpdfWindow : Form { private readonly WebView2 _view = new() { Dock = DockStyle.Fill }; private readonly OlpdfClient _api; public OlpdfWindow(string apiKey, string documentId, string embedToken) { _api = new OlpdfClient(apiKey); Controls.Add(_view); Text = "OLPDF Editor"; ClientSize = new Size(1280, 800); _ = InitAsync(documentId, embedToken); } private async Task InitAsync(string documentId, string embedToken) { // Boot WebView2 runtime await _view.EnsureCoreWebView2Async(); // Load the embed URL — OLPDF renders inside the webview _view.Source = new Uri( $"https://olpdf.xyz/embed/{documentId}?token={embedToken}"); // Receive postMessage events from the iframe _view.CoreWebView2.WebMessageReceived += async (_, e) => { var msg = JsonSerializer.Deserialize<OlpdfMessage>(e.WebMessageAsJson); if (msg?.Type == "MODEL_UPDATE" && msg.Data is not null) { // Persist the updated AST via the REST API await _api.SaveDocumentAsync(documentId, msg.Data); } if (msg?.Type == "EXPORT_COMPLETE") { System.Diagnostics.Process.Start(new ProcessStartInfo { FileName = msg.Url, UseShellExecute = true }); } }; } } // OlpdfMessage.cs public record OlpdfMessage( [property: JsonPropertyName("type")] string Type, [property: JsonPropertyName("data")] DocumentModel? Data, [property: JsonPropertyName("url")] string? Url );
Something missing?
Open an issue or submit a PR on GitHub.