Skip to content

Commit 62b7b34

Browse files
danmolitorclaude
andcommitted
Add merge, certify, redact to TypeScript SDK with docs
Add three missing methods to @formepdf/sdk: merge(), certify(), and redact() — matching the Python and Go SDKs. Create TypeScript SDK docs page, update README, and add to Mintlify navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a1cbcdb commit 62b7b34

5 files changed

Lines changed: 435 additions & 2 deletions

File tree

docs/docs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
},
5757
{
5858
"group": "SDKs",
59-
"pages": ["python-sdk", "go-sdk", "rust-sdk"]
59+
"pages": ["typescript-sdk", "python-sdk", "go-sdk", "rust-sdk"]
6060
},
6161
{
6262
"group": "Guides",

docs/typescript-sdk.mdx

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
title: TypeScript SDK
3+
sidebarTitle: TypeScript
4+
description: Generate, certify, redact, and merge PDFs from TypeScript with the hosted API. Zero-dependency client that works in Node.js, Deno, Bun, and edge runtimes.
5+
---
6+
7+
## Installation
8+
9+
```bash
10+
npm install @formepdf/sdk
11+
```
12+
13+
## Usage
14+
15+
Create templates in the [dashboard](https://app.formepdf.com), then render them with data:
16+
17+
```typescript
18+
import { Forme } from "@formepdf/sdk";
19+
20+
const forme = new Forme("forme_sk_...");
21+
22+
const pdf = await forme.render("invoice", {
23+
customer: "Acme Corp",
24+
items: [{ name: "Widget", qty: 5, price: 49 }],
25+
total: 245,
26+
});
27+
28+
await fs.writeFile("invoice.pdf", pdf);
29+
```
30+
31+
### Client options
32+
33+
```typescript
34+
const forme = new Forme("forme_sk_...", {
35+
baseUrl: "https://custom-api.example.com", // optional
36+
});
37+
```
38+
39+
### Methods
40+
41+
| Method | Description | Returns |
42+
|--------|-------------|---------|
43+
| `render(slug, data, options?)` | Synchronous render | `Promise<Uint8Array \| { url: string }>` |
44+
| `renderAsync(slug, data, options?)` | Start async render job | `Promise<{ jobId, status }>` |
45+
| `getJob(jobId)` | Poll async job status | `Promise<JobResult>` |
46+
| `merge(pdfs)` | Merge multiple PDFs | `Promise<Uint8Array>` |
47+
| `certify(pdf, options)` | Certify a PDF with X.509 certificate | `Promise<Uint8Array>` |
48+
| `redact(pdf, options)` | Redact content from a PDF | `Promise<Uint8Array>` |
49+
| `extract(pdf)` | Extract embedded data | `Promise<unknown \| null>` |
50+
51+
---
52+
53+
## Render to S3
54+
55+
```typescript
56+
const result = await forme.render("invoice", data, {
57+
s3: {
58+
bucket: "my-bucket",
59+
key: "invoices/001.pdf",
60+
accessKeyId: "AKIA...",
61+
secretAccessKey: "...",
62+
region: "us-east-1",
63+
},
64+
});
65+
// result = { url: "https://my-bucket.s3.amazonaws.com/invoices/001.pdf" }
66+
```
67+
68+
## Async rendering
69+
70+
```typescript
71+
const { jobId } = await forme.renderAsync("invoice", data, {
72+
webhookUrl: "https://example.com/webhook",
73+
});
74+
75+
// Poll for completion
76+
const job = await forme.getJob(jobId);
77+
if (job.status === "completed") {
78+
const pdf = Buffer.from(job.pdfBase64!, "base64");
79+
}
80+
```
81+
82+
## Certify
83+
84+
```typescript
85+
import { readFileSync } from "fs";
86+
87+
const certified = await forme.certify(pdfBytes, {
88+
certificate: readFileSync("cert.pem", "utf-8"),
89+
privateKey: readFileSync("key.pem", "utf-8"),
90+
reason: "Approved",
91+
});
92+
```
93+
94+
Or use a saved certificate on the hosted API:
95+
96+
```typescript
97+
const certified = await forme.certify(pdfBytes, {
98+
certificateId: "cert_abc123",
99+
});
100+
```
101+
102+
## Redact
103+
104+
```typescript
105+
// By text pattern
106+
const redacted = await forme.redact(pdfBytes, {
107+
patterns: [
108+
{ pattern: "Jane Doe", pattern_type: "Literal" },
109+
{ pattern: "\\d{3}-\\d{2}-\\d{4}", pattern_type: "Regex" },
110+
],
111+
});
112+
113+
// By built-in presets
114+
const redacted = await forme.redact(pdfBytes, {
115+
presets: ["ssn", "email", "phone"],
116+
});
117+
118+
// By saved redaction template
119+
const redacted = await forme.redact(pdfBytes, {
120+
template: "hipaa-patient-record",
121+
});
122+
```
123+
124+
## Merge
125+
126+
```typescript
127+
const merged = await forme.merge([pdf1, pdf2, pdf3]);
128+
```
129+
130+
## Extract embedded data
131+
132+
```typescript
133+
const data = await forme.extract(pdfBytes);
134+
if (data) {
135+
console.log(data); // the JSON that was embedded at render time
136+
}
137+
```
138+
139+
## Error handling
140+
141+
```typescript
142+
import { Forme, FormeError } from "@formepdf/sdk";
143+
144+
try {
145+
const pdf = await forme.render("invoice", data);
146+
} catch (err) {
147+
if (err instanceof FormeError) {
148+
console.error(`Error ${err.status}: ${err.message}`);
149+
}
150+
}
151+
```

packages/sdk/README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,55 @@ new Forme(apiKey: string, options?: { baseUrl?: string })
4141

4242
## Methods
4343

44-
### `render(slug, data?)`
44+
### `render(slug, data?, options?)`
4545

4646
Render a template to PDF bytes.
4747

4848
- **slug** — Template identifier
4949
- **data** — Template data (sent as JSON body, defaults to `{}`)
50+
- **options.s3** — Optional S3 upload config (`{ bucket, key, accessKeyId, secretAccessKey, region?, endpoint? }`)
51+
- **Returns** `Promise<Uint8Array>` (or `Promise<{ url: string }>` when `s3` is provided)
52+
53+
### `renderAsync(slug, data?, options?)`
54+
55+
Start an async render job.
56+
57+
- **options.webhookUrl** — URL to receive a POST when the job completes
58+
- **Returns** `Promise<{ jobId: string; status: string }>`
59+
60+
### `getJob(jobId)`
61+
62+
Poll an async job's status.
63+
64+
- **Returns** `Promise<JobResult>` — includes `pdfBase64` when complete
65+
66+
### `merge(pdfs)`
67+
68+
Merge multiple PDFs into one.
69+
70+
- **pdfs** — Array of `Uint8Array` (2–20 PDFs)
71+
- **Returns** `Promise<Uint8Array>`
72+
73+
### `certify(pdf, options)`
74+
75+
Certify a PDF with an X.509 digital certificate.
76+
77+
- **pdf** — PDF file bytes
78+
- **options.certificate** — PEM-encoded certificate string
79+
- **options.privateKey** — PEM-encoded private key string
80+
- **options.certificateId** — Or use a saved certificate ID (instead of raw PEM)
81+
- **options.reason** / **options.location** / **options.contact** — Optional metadata
82+
- **Returns** `Promise<Uint8Array>`
83+
84+
### `redact(pdf, options)`
85+
86+
Redact sensitive content from a PDF.
87+
88+
- **pdf** — PDF file bytes
89+
- **options.patterns** — Array of `{ pattern, pattern_type: 'Literal' | 'Regex' }`
90+
- **options.presets** — Built-in presets like `['ssn', 'email', 'phone']`
91+
- **options.template** — Saved redaction template slug
92+
- **options.redactions** — Explicit regions `{ page, x, y, width, height }[]`
5093
- **Returns** `Promise<Uint8Array>`
5194

5295
### `extract(pdf)`

packages/sdk/src/index.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,35 @@ export interface AsyncRenderOptions {
2121
webhookUrl?: string;
2222
}
2323

24+
export interface CertifyOptions {
25+
certificate?: string;
26+
privateKey?: string;
27+
certificateId?: string;
28+
reason?: string;
29+
location?: string;
30+
contact?: string;
31+
}
32+
33+
export interface RedactionPattern {
34+
pattern: string;
35+
pattern_type: 'Literal' | 'Regex';
36+
}
37+
38+
export interface RedactionRegion {
39+
page: number;
40+
x: number;
41+
y: number;
42+
width: number;
43+
height: number;
44+
}
45+
46+
export interface RedactOptions {
47+
redactions?: RedactionRegion[];
48+
patterns?: RedactionPattern[];
49+
presets?: string[];
50+
template?: string;
51+
}
52+
2453
export interface JobResult {
2554
id: string;
2655
status: string;
@@ -112,6 +141,79 @@ export class Forme {
112141
return res.json();
113142
}
114143

144+
async merge(pdfs: Uint8Array[]): Promise<Uint8Array> {
145+
const res = await fetch(`${this.baseUrl}/v1/merge`, {
146+
method: 'POST',
147+
headers: {
148+
'Authorization': `Bearer ${this.apiKey}`,
149+
'Content-Type': 'application/json',
150+
},
151+
body: JSON.stringify({
152+
pdfs: pdfs.map((p) => uint8ArrayToBase64(p)),
153+
}),
154+
});
155+
156+
if (!res.ok) {
157+
const message = await parseErrorMessage(res);
158+
throw new FormeError(res.status, message);
159+
}
160+
161+
return new Uint8Array(await res.arrayBuffer());
162+
}
163+
164+
async certify(pdf: Uint8Array, options: CertifyOptions): Promise<Uint8Array> {
165+
const body: Record<string, unknown> = {
166+
pdf: uint8ArrayToBase64(pdf),
167+
};
168+
169+
if (options.certificateId) {
170+
body.certificateId = options.certificateId;
171+
} else {
172+
body.certificate = options.certificate;
173+
body.privateKey = options.privateKey;
174+
}
175+
if (options.reason) body.reason = options.reason;
176+
if (options.location) body.location = options.location;
177+
if (options.contact) body.contact = options.contact;
178+
179+
const res = await fetch(`${this.baseUrl}/v1/certify`, {
180+
method: 'POST',
181+
headers: {
182+
'Authorization': `Bearer ${this.apiKey}`,
183+
'Content-Type': 'application/json',
184+
},
185+
body: JSON.stringify(body),
186+
});
187+
188+
if (!res.ok) {
189+
const message = await parseErrorMessage(res);
190+
throw new FormeError(res.status, message);
191+
}
192+
193+
return new Uint8Array(await res.arrayBuffer());
194+
}
195+
196+
async redact(pdf: Uint8Array, options: RedactOptions): Promise<Uint8Array> {
197+
const res = await fetch(`${this.baseUrl}/v1/redact`, {
198+
method: 'POST',
199+
headers: {
200+
'Authorization': `Bearer ${this.apiKey}`,
201+
'Content-Type': 'application/json',
202+
},
203+
body: JSON.stringify({
204+
pdf: uint8ArrayToBase64(pdf),
205+
...options,
206+
}),
207+
});
208+
209+
if (!res.ok) {
210+
const message = await parseErrorMessage(res);
211+
throw new FormeError(res.status, message);
212+
}
213+
214+
return new Uint8Array(await res.arrayBuffer());
215+
}
216+
115217
async extract(pdf: Uint8Array): Promise<unknown | null> {
116218
const res = await fetch(`${this.baseUrl}/v1/extract`, {
117219
method: 'POST',
@@ -140,6 +242,17 @@ export class Forme {
140242
}
141243
}
142244

245+
function uint8ArrayToBase64(bytes: Uint8Array): string {
246+
if (typeof Buffer !== 'undefined') {
247+
return Buffer.from(bytes).toString('base64');
248+
}
249+
let binary = '';
250+
for (let i = 0; i < bytes.length; i++) {
251+
binary += String.fromCharCode(bytes[i]);
252+
}
253+
return btoa(binary);
254+
}
255+
143256
async function parseErrorMessage(res: Response): Promise<string> {
144257
try {
145258
const body = await res.json();

0 commit comments

Comments
 (0)