Skip to content
Back to blog
react embedded editor api developer guide

How to Build an In-App PDF Editor with an Embedded Document Editor API

pdfs.build Team

Every B2B SaaS platform eventually hits the same customer request: “Can we customize our invoices, reports, or contracts to match our branding?”

At first, you might offer a few toggle switches: upload a logo PNG, select a primary hex color, or type custom footer notes. But as enterprise customers join, their requirements escalate:

At this crossroads, engineering teams face a daunting choice: spend 6–9 months building a fragile in-house drag-and-drop document builder, or integrate an embedded document editor API.

In this guide, we will walk through how to build a production-grade, white-label in-app PDF editor using React and the pdfs.build Embedded SDK (@pdfsbuild/react).


The Build vs. Buy Dilemma: Why Canvas Editors Fail at PDFs

Building an in-app document editor from scratch appears deceptively simple until you encounter print pagination.

Browser canvas libraries (Fabric.js, Konva, or HTML contenteditable blocks) treat pages as continuous web viewports or single-page canvases. When your users export a 5-page financial statement or a 40-item invoice, classical issues emerge:

  1. Broken Table Pagination: Tables split arbitrarily across pages, cutting text rows in half.
  2. Missing Running Headers: Page numbers like “Page 2 of 4” require complex DOM measuring hacks.
  3. Rendering Inconsistency: What the user sees on their 4K monitor does not match what prints on 300 DPI A4 paper.
  4. Heavy Headless Browsers: Converting the client DOM to PDF requires spinning up headless Chromium (Puppeteer/Playwright) on your backend, costing 150MB+ RAM per render.

With an embedded document editor API, your frontend embeds a native document designer backed by Typst—a typesetting engine designed specifically for paginated print documents. Your backend controls tenant permissions and handles rendering via a deterministic REST API.


Architectural Overview

The embedded document editor architecture operates on a zero-trust, scoped-session security model:

  1. Your Backend: Authenticates the tenant in your application (e.g., Acme Corp user #42).
  2. Session Exchange: Your backend calls the pdfs.build API using your secret API key to mint a short-lived (15-minute) session token scoped to a single template.
  3. Frontend Mount: Your React frontend receives the session token and mounts the <PdfReportEditor> component.
  4. Direct Studio Interaction: Your user designs the template with AI assistance or visual controls. Changes compile in WebAssembly (Wasm) in their browser with instant live preview.
  5. Programmatic Rendering: When your application needs to generate the final PDF (e.g., during monthly billing runs), your backend calls the render API passing dynamic JSON data against the user’s saved template.

Your root API key never leaves your secure server environment.


Step 1: Minting a Tenant-Scoped Session Token

On your server, expose an endpoint (e.g., /api/templates/session) that checks your user’s permissions and requests an ephemeral session token from pdfs.build:

// app/api/templates/session/route.ts (Next.js 15 Server Route)
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  // 1. Verify user authentication in your application
  const user = await authenticateUser(request);
  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { templateSlug } = await request.json();

  // 2. Request a scoped session token from pdfs.build
  const response = await fetch(
    `https://api.pdfs.build/v2/organizations/${process.env.PDFS_BUILD_ORG_ID}/templates/${templateSlug}/sessions`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.PDFS_BUILD_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        // Attribute usage and enforce isolation per tenant
        externalTenantId: user.organizationId,
        ttlSeconds: 900, // 15 minutes
      }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    return NextResponse.json({ error: error.message }, { status: response.status });
  }

  const { sessionToken, expiresAt } = await response.json();
  return NextResponse.json({ sessionToken, expiresAt });
}

Step 2: Installing the React SDK

In your React or Next.js web application, install the official component package:

npm install @pdfsbuild/react

@pdfsbuild/react includes the full visual editor: AI chat assistant, Typst syntax canvas, live compiled PDF preview, JSON schema validator, and sample data controls.


Step 3: Mounting the White-Label Editor Component

Create an editor view in your frontend application. Wrap the editor inside <PdfReportProvider> and pass your session token and theming options:

// components/DocumentEditorModal.tsx
"use client";

import React, { useEffect, useState } from "react";
import { PdfReportProvider, PdfReportEditor } from "@pdfsbuild/react";

interface Props {
  templateSlug: string;
  onClose: () => void;
}

export function DocumentEditorModal({ templateSlug, onClose }: Props) {
  const [sessionToken, setSessionToken] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchSession() {
      try {
        const res = await fetch("/api/templates/session", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ templateSlug }),
        });
        const data = await res.json();
        setSessionToken(data.sessionToken);
      } finally {
        setLoading(false);
      }
    }
    fetchSession();
  }, [templateSlug]);

  if (loading) {
    return <div className="p-8 text-center text-sm text-gray-500">Loading document editor...</div>;
  }

  if (!sessionToken) {
    return <div className="p-8 text-center text-red-500">Failed to start editor session.</div>;
  }

  return (
    <div className="fixed inset-0 z-50 bg-slate-900/80 backdrop-blur-sm flex flex-col">
      <div className="h-14 border-b border-gray-200 bg-white px-6 flex items-center justify-between">
        <h2 className="font-semibold text-gray-900">Custom Invoice Designer</h2>
        <button
          onClick={onClose}
          className="px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-100 rounded-lg"
        >
          Exit Editor
        </button>
      </div>

      <div className="flex-1 w-full overflow-hidden">
        <PdfReportProvider
          token={sessionToken}
          theme={{
            primaryColor: "#2563eb", // Match your SaaS brand
            borderRadius: "0.5rem",
            fontFamily: "Inter, sans-serif",
          }}
        >
          <PdfReportEditor className="w-full h-full" />
        </PdfReportProvider>
      </div>
    </div>
  );
}

Step 4: Composable Parts vs. Out-of-the-Box Studio

While <PdfReportEditor /> provides the complete designer experience with toolbar, preview, chat, and code panels, @pdfsbuild/react also offers composable parts if you prefer a customized layout:


Step 5: Rendering User Templates at Scale

Once your customer has customized and saved their template, your backend generates production documents by calling the standard render endpoint:

// Example: Generating an invoice on schedule
const renderResponse = await fetch(
  `https://api.pdfs.build/v2/organizations/${ORG_ID}/templates/${templateSlug}/render`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      data: {
        invoice_number: "INV-2026-9021",
        date: "2026-09-21",
        customer: { name: "Global Enterprise Ltd", tax_id: "US-849201" },
        items: [
          { description: "Enterprise SaaS Subscription (Annual)", amount: 12000 },
        ],
      },
    }),
  }
);

const pdfBlob = await renderResponse.arrayBuffer();

If the customer customized their template layout, their changes are automatically reflected in the output. If the JSON data violates the template’s schema contract, the render fails cleanly before any document is generated.


Conclusion

Shipping an in-app document designer no longer requires building a custom canvas from scratch or managing heavy headless browsers.

With the embedded document editor API, you get:

Explore the complete Embedded Editor Guide and start building today with @pdfsbuild/react.

Back to blog