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/exeqImport 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
| Prop | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Your Exeq API key |
initialPdfUrl | string | No | URL to a PDF to pre-load |
initialTemplate | Template | No | Existing template to resume editing |
onSave | (template: Template) => void | No | Called when "Export Template" is clicked. If omitted, downloads JSON. |
onChange | (template: Template) => void | No | Fires 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
| Prop | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Your Exeq API key |
initialPdfUrl | string | No | URL to the PDF |
initialTemplate | Template | No | Template object with fields and signer roles |
initialSigner | string | No | Signer role name. Defaults to "Signer 1" |
initialValues | Record<string, string> | No | Pre-fill fields by label (case-insensitive) or ID |
callbackUrl | string | No | URL to POST the signed PDF to |
onComplete | (blob: Blob) => void | No | Callback with the signed PDF Blob |
submitLabel | string | No | Label for the final submit button. Defaults to "Complete" |
signerOrder | string[] | No | Signing order for multi-party documents. Signers complete one at a time. If omitted, non-Sender roles go first, then Sender. |
transforms | Record<string, (v: string) => string> | No | Custom 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:
| Transform | Description | Example |
|---|---|---|
month / month2 | Numeric month (1-12) / zero-padded | 3 / 03 |
monthname / monthshort | Full / short month name | March / Mar |
day / day2 | Day of month / zero-padded | 5 / 05 |
year / year2 | 4-digit / 2-digit year | 2026 / 26 |
upper / lower | Uppercase / lowercase | JANE / jane |
first / last | First / last word | Jane / Smith |
initials | First letter of each word | JS |
last4 / last2 | Last N characters | 6789 |
first4 / first2 | First N characters | 1234 |
digits | Strip non-digits | 1234567890 |
number / currency | Parse number / format as USD | 42 / $42.00 |
trim | Remove 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
| Component | Description |
|---|---|
PdfViewer | Low-level PDF page renderer with draggable field overlays |
SignatureCanvas | Freehand signature/initials drawing canvas |
FieldPropertyPanel | Field property editor (type, label, assignee) |
FieldNavigator | Prev/Next navigation through signer fields |
SignerRoleSelector | Manage 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:
| Option | Type | Description |
|---|---|---|
pdfSource | string | ArrayBuffer | null | Background PDF (URL or bytes), or null for overlay-only output. |
fields | FormField[] | 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. |
pageCount | number | When pdfSource is null, how many blank pages to render. Default 1. |
resolveFormulas | boolean | Run resolveAllFormulas on fields before rendering. Default false. |
customTransforms | TransformMap | Merged with BUILTIN_TRANSFORMS during formula resolution. |
calibration | Calibration | Linear 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});| Field | Type | Meaning |
|---|---|---|
xOffset | number (pt) | Horizontal shift; positive = right. |
yOffset | number (pt) | Vertical shift; positive = down (screen coords). |
xScale | number | Multiplier on x and width. |
yScale | number | Multiplier 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 numberx,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
| Type | Description | Value format | Notes |
|---|---|---|---|
text | Text input | String | Set textSubtype: freeform, number, date, email, phone |
signature | Freehand signature | PNG data URL | Supports ink color (black, blue) |
initials | Smaller freehand drawing | PNG data URL | Same as signature, smaller default size |
signed-date | Auto-filled date | Locale date string | Auto-fills when the signer signs |
checkbox | Toggle checkbox | "true" or "" | Renders a checkmark when checked |
blackout | Black redaction rectangle | N/A | Designer-only. Covers content with a solid black box. |
whiteout | White redaction rectangle | N/A | Designer-only. Covers content with a solid white box. |
Typical Workflow
- Design — Use
<DesignerView />to create a template. Open a PDF, place fields, assign roles. - Pre-fill — Fill Sender fields (company name, your signature, dates). These are baked into the template.
- Export — Use the
onSavecallback to capture the template JSON. Host the PDF on your server. - Sign — Render
<SignerView />with the template. UseinitialValuesfor mail-merge pre-fill. - Collect — Receive the signed PDF via the
onCompletecallback.