Exeq Documentation

Exeq lets you embed PDF form building and document signing into any website. Everything runs client-side in the browser — no backend required.

Live Demo

Try the full experience — build a template or sign a sample NDA.

Installation

1npm install @unlev/exeq

Import the components and styles in your React app:

1import { DesignerView, SignerView } from '@unlev/exeq';
2import '@unlev/exeq/styles';

Template Editor

The editor lets you open a PDF and place form fields on it. Everything runs locally in the browser — your files are never uploaded to any server. You can pre-fill or pre-sign fields that belong to you (the "Sender" role) before exporting the template for signers.

1import { DesignerView } from '@unlev/exeq';
2import '@unlev/exeq/styles';
3
4function Editor() {
5  return (
6    <DesignerView
7      apiKey="your-api-key"
8      initialPdfUrl="/contracts/blank.pdf"
9      onSave={(template) => {
10        // Save the template JSON to your server
11        console.log(template);
12      }}
13    />
14  );
15}

DesignerView Props

PropTypeRequiredDescription
apiKeystringYesYour Exeq API key
initialPdfUrlstringNoURL to a PDF to pre-load
initialTemplateTemplateNoExisting template to resume editing
onSave(template: Template) => voidNoCalled when "Export Template" is clicked. If omitted, downloads JSON.
onChange(template: Template) => voidNoFires on every change to fields / signer roles / loaded PDF. Use for draft persistence (e.g. localStorage) so a refresh or accidental close doesn't lose work.

What the editor does

  • Renders each PDF page as an image
  • Drag fields from the palette onto the page, or click to place
  • Select a field to edit its properties (type, label, placeholder, required, assignee)
  • Drag fields to reposition, resize via the corner handle
  • Assign fields to signer roles — "Sender" is for you (the host), other roles are for signers
  • Pre-fill or pre-sign Sender fields before exporting
  • Blackout and whiteout fields for redaction
  • Export the template JSON

Document Signing

Shows a PDF with pre-placed fields for the signer to fill out. Fields belonging to the Sender (pre-filled by the host) appear as read-only. The signer fills their fields, then the completed PDF is generated client-side.

1import { SignerView } from '@unlev/exeq';
2import '@unlev/exeq/styles';
3
4function Signer() {
5  return (
6    <SignerView
7      apiKey="your-api-key"
8      initialPdfUrl="/contracts/template.pdf"
9      initialTemplate={template}
10      initialSigner="Signer 1"
11      initialValues={{
12        "Full Name": "Jane Smith",
13        "Email": "jane@example.com",
14      }}
15      onComplete={(blob) => {
16        // Upload the signed PDF to your server
17        const formData = new FormData();
18        formData.append('file', blob, 'signed.pdf');
19        fetch('/api/upload', { method: 'POST', body: formData });
20      }}
21    />
22  );
23}

SignerView Props

PropTypeRequiredDescription
apiKeystringYesYour Exeq API key
initialPdfUrlstringNoURL to the PDF
initialTemplateTemplateNoTemplate object with fields and signer roles
initialSignerstringNoSigner role name. Defaults to "Signer 1"
initialValuesRecord<string, string>NoPre-fill fields by label (case-insensitive) or ID
callbackUrlstringNoURL to POST the signed PDF to
onComplete(blob: Blob) => voidNoCallback with the signed PDF Blob
submitLabelstringNoLabel for the final submit button. Defaults to "Complete"
signerOrderstring[]NoSigning order for multi-party documents. Signers complete one at a time. If omitted, non-Sender roles go first, then Sender.
transformsRecord<string, (v: string) => string>NoCustom transform functions for formula fields. Merges with built-in transforms.

What the signer sees

  • PDF rendered with all fields visible
  • Fields assigned to this signer are editable (highlighted)
  • Fields assigned to other roles show as read-only with pre-filled values
  • Prev/Next buttons navigate through fields in document order
  • "Complete" generates the final PDF with all values overlaid

Multi-party Signing

For documents that require multiple signers, use the signerOrder prop to define who signs in what order. Signers complete their fields one at a time — only the current signer's fields are active, while other signers' fields appear greyed out.

1<SignerView
2  apiKey="your-api-key"
3  initialPdfUrl="/contracts/template.pdf"
4  initialTemplate={template}
5  signerOrder={['Signer 1', 'Sender']}  // recipient signs first, then sender
6  onComplete={(blob) => {
7    // Final PDF with all signatures from all parties
8    uploadToServer(blob);
9  }}
10/>

If signerOrder is omitted, the default order is: non-Sender roles first (in their template order), then Sender last. The PDF is only generated after the final signer completes their fields.

Mail Merge (Pre-fill Fields)

Use the initialValues prop to pre-fill form fields programmatically. This enables a mail-merge workflow where your app fills in the data and the user only needs to review and sign — no manual data entry required.

Keys are matched against field labels (case-insensitive) first, then field IDs. Field labels are enforced to be unique in the designer, so there are no conflicts.

1<SignerView
2  apiKey="your-api-key"
3  initialPdfUrl="/contracts/lease.pdf"
4  initialTemplate={leaseTemplate}
5  initialSigner="Tenant"
6  initialValues={{
7    "Tenant Name": "Jane Smith",
8    "Email": "jane@example.com",
9    "Move-in Date": "2026-06-01",
10    "Monthly Rent": "$2,400",
11    "Unit Number": "4B",
12  }}
13  onComplete={(blob) => {
14    // The signed PDF has all values baked in
15    uploadToServer(blob);
16  }}
17/>

Fields matched by initialValues appear pre-filled in the signing UI. The signer can still edit them unless they belong to a different role (e.g. Sender fields are read-only).

Formulas & Transforms

Fields can reference other fields and apply transforms using the syntax {{ Source Field Label | transform }}. Formula fields auto-compute their values and are read-only to the signer.

In the designer, set a field's Formula property to reference another field. For example: {{ Contract Date | month2 }}/{{ Contract Date | day2 }}/{{ Contract Date | year }}

1<SignerView
2  initialTemplate={template}
3  transforms={{
4    // Custom transforms — any name works
5    'last4': (value) => value.slice(-4),
6    'ssn-masked': (value) => `***-**-${value.slice(-4)}`,
7    'phone-area': (value) => value.replace(/\D/g, '').slice(0, 3),
8  }}
9/>

Exeq ships with built-in transforms for common operations:

TransformDescriptionExample
month / month2Numeric month (1-12) / zero-padded3 / 03
monthname / monthshortFull / short month nameMarch / Mar
day / day2Day of month / zero-padded5 / 05
year / year24-digit / 2-digit year2026 / 26
upper / lowerUppercase / lowercaseJANE / jane
first / lastFirst / last wordJane / Smith
initialsFirst letter of each wordJS
last4 / last2Last N characters6789
first4 / first2First N characters1234
digitsStrip non-digits1234567890
number / currencyParse number / format as USD42 / $42.00
trimRemove leading/trailing whitespace

Custom transforms override built-ins with the same name. Any transform name is valid — use whatever makes sense for your use case.

Utilities & Types

Utility Functions

1import { renderPdfPages, generateFilledPdf, downloadPdf } from '@unlev/exeq';
2
3// Render PDF pages to images
4const pages = await renderPdfPages(pdfUrlOrBytes);
5
6// Generate a filled PDF
7const bytes = await generateFilledPdf({ pdfSource, fields });
8
9// Trigger browser download
10downloadPdf(bytes, 'signed-document.pdf');

See PDF Generation for the full generateFilledPdf options, batch output via createPdfBuilder, calibration, and page-size constants.

Types

1import type { FormField, Template, FieldType, RenderedPage } from '@unlev/exeq';

Additional Components

ComponentDescription
PdfViewerLow-level PDF page renderer with draggable field overlays
SignatureCanvasFreehand signature/initials drawing canvas
FieldPropertyPanelField property editor (type, label, assignee)
FieldNavigatorPrev/Next navigation through signer fields
SignerRoleSelectorManage signer roles

PDF Generation

For generating filled PDFs outside the SignerView flow — batch mail-merge, server-rendered previews, printing onto pre-printed physical forms, or any custom workflow — use generateFilledPdf and createPdfBuilder directly.

generateFilledPdf

1import { generateFilledPdf, US_LETTER } from '@unlev/exeq';
2
3// Default: output sized to the source PDF.
4const bytes = await generateFilledPdf({
5  pdfSource: '/forms/template.pdf',
6  fields,
7});
8
9// Overlay-only — blank Letter pages with no background, for printing
10// onto pre-printed physical forms.
11const overlay = await generateFilledPdf({
12  pdfSource: null,
13  fields,
14  pageSize: US_LETTER,
15});
16
17// Force-Letter output even when the source isn't Letter (e.g. a scan
18// at slightly different dimensions). Source pages are stretched to fit.
19const letter = await generateFilledPdf({
20  pdfSource: '/forms/scan.pdf',
21  fields,
22  pageSize: US_LETTER,
23});
24
25// Resolve formula fields during the render call.
26const filled = await generateFilledPdf({
27  pdfSource,
28  fields,
29  resolveFormulas: true,
30  customTransforms: { last4: v => v.slice(-4) },
31});

All options live on a single FillPdfOptions object:

OptionTypeDescription
pdfSourcestring | ArrayBuffer | nullBackground PDF (URL or bytes), or null for overlay-only output.
fieldsFormField[]Fields to render on top of the background.
pageSize[number, number]Override output page size in PDF points (72pt = 1in). With a source, source pages are drawn stretched to fit. Defaults to source dims, or Letter if no source.
pageCountnumberWhen pdfSource is null, how many blank pages to render. Default 1.
resolveFormulasbooleanRun resolveAllFormulas on fields before rendering. Default false.
customTransformsTransformMapMerged with BUILTIN_TRANSFORMS during formula resolution.
calibrationCalibrationLinear offset/scale applied to field positions.

createPdfBuilder — batch / mail merge

For multi-record output (one PDF with one page set per recipient), createPdfBuilder merges records into a single document and dedupes shared resources. The source PDF, fonts, and signature PNGs are embedded once and referenced from every page.

1import { createPdfBuilder, downloadPdf, US_LETTER } from '@unlev/exeq';
2
3const builder = await createPdfBuilder({ pageSize: US_LETTER });
4for (const record of records) {
5  await builder.addRecord({
6    pdfSource: '/forms/template.pdf',  // embedded once, referenced N times
7    fields: record.fields,
8    resolveFormulas: true,
9  });
10}
11const bytes = await builder.save();
12downloadPdf(bytes, 'merged.pdf');

A 12-record batch with a 700 KB background goes from ~8 MB (twelve separate generateFilledPdf calls then merged) to ~705 KB (one shared embed across all pages). The PdfBuilder.doc property exposes the underlying pdf-lib document if you need to add an audit-trail or table-of-contents page before saving.

addRecord takes the same FillPdfOptions shape as generateFilledPdf, so you can hand the same record object to either.

applyCalibration

When a printed overlay doesn't quite register with a pre-printed physical form (the scan was cropped differently than the real paper, or the printer tray has a small offset), apply a linear calibration. Offsets are in PDF points (72pt = 1 inch).

1import { applyCalibration, generateFilledPdf, US_LETTER } from '@unlev/exeq';
2
3// Pass calibration to the renderer:
4const bytes = await generateFilledPdf({
5  pdfSource,
6  fields,
7  pageSize: US_LETTER,
8  calibration: { xOffset: -5, yOffset: 3, xScale: 1.005, yScale: 1 },
9});
10
11// ...or apply it as a standalone transform (e.g. to preview adjusted positions):
12const adjusted = applyCalibration(fields, {
13  xOffset: -5, yOffset: 3, xScale: 1.005, yScale: 1,
14});
FieldTypeMeaning
xOffsetnumber (pt)Horizontal shift; positive = right.
yOffsetnumber (pt)Vertical shift; positive = down (screen coords).
xScalenumberMultiplier on x and width.
yScalenumberMultiplier on y and height.

Page-size constants

1import { US_LETTER, US_LEGAL, A4 } from '@unlev/exeq';
2
3// US_LETTER === [612, 792]    (8.5 × 11 in)
4// US_LEGAL  === [612, 1008]   (8.5 × 14 in)
5// A4        === [595.28, 841.89]

Template JSON Schema

The template JSON exported by the editor has this structure:

1{
2  "pdfUrl": "https://yoursite.com/contract.pdf",
3  "signerRoles": ["Sender", "Signer 1"],
4  "fields": [
5    {
6      "id": "550e8400-e29b-41d4-a716-446655440000",
7      "type": "text",
8      "textSubtype": "freeform",
9      "label": "Full Name",
10      "placeholder": "Enter your full name",
11      "required": true,
12      "assignee": "Signer 1",
13      "page": 0,
14      "x": 15.5,
15      "y": 42.3,
16      "width": 25,
17      "height": 3,
18      "fontSize": 12,
19      "value": ""
20    }
21  ]
22}

Coordinate system

  • page: 0-indexed page number
  • x, y: position as percentage of page dimensions (0–100). Origin is top-left.
  • width, height: size as percentage of page dimensions (0–100)
  • fontSize: font size in points (used for text fields)

Field Types

TypeDescriptionValue formatNotes
textText inputStringSet textSubtype: freeform, number, date, email, phone
signatureFreehand signaturePNG data URLSupports ink color (black, blue)
initialsSmaller freehand drawingPNG data URLSame as signature, smaller default size
signed-dateAuto-filled dateLocale date stringAuto-fills when the signer signs
checkboxToggle checkbox"true" or ""Renders a checkmark when checked
blackoutBlack redaction rectangleN/ADesigner-only. Covers content with a solid black box.
whiteoutWhite redaction rectangleN/ADesigner-only. Covers content with a solid white box.

Typical Workflow

  1. Design — Use <DesignerView /> to create a template. Open a PDF, place fields, assign roles.
  2. Pre-fill — Fill Sender fields (company name, your signature, dates). These are baked into the template.
  3. Export — Use the onSave callback to capture the template JSON. Host the PDF on your server.
  4. Sign — Render <SignerView /> with the template. Use initialValues for mail-merge pre-fill.
  5. Collect — Receive the signed PDF via the onComplete callback.