Skip to content

Node.js Quickstart

This guide walks you through integrating Glyph into a Node.js application. You will learn how to generate PDFs, handle errors gracefully, and deploy to serverless environments.

Install the required dependencies:

Terminal window
npm install node-fetch

Store your API key securely in environment variables:

.env
GLYPH_API_KEY=gk_your_api_key

Load it in your application:

config.js
import 'dotenv/config';
export const GLYPH_API_KEY = process.env.GLYPH_API_KEY;
export const GLYPH_API_URL = 'https://api.glyph.you';

The simplest way to generate a PDF is with the /v1/create endpoint. Send your data, and Glyph returns a finished PDF.

import { writeFileSync } from 'fs';
import { GLYPH_API_KEY, GLYPH_API_URL } from './config.js';
async function generateInvoice(invoiceData) {
const response = await fetch(`${GLYPH_API_URL}/v1/create`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${GLYPH_API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
data: invoiceData,
intent: 'professional invoice',
style: 'stripe-clean',
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Glyph API error: ${error.error} (${error.code})`);
}
const result = await response.json();
// Decode base64 and save to file
const base64Data = result.url.split(',')[1];
const buffer = Buffer.from(base64Data, 'base64');
writeFileSync(`invoice-${Date.now()}.pdf`, buffer);
return {
filename: result.filename,
size: result.size,
sessionId: result.sessionId,
};
}
// Usage
const invoice = await generateInvoice({
company: { name: 'Acme Inc', email: 'billing@acme.com' },
customer: { name: 'John Doe', email: 'john@example.com' },
items: [
{ description: 'Consulting', hours: 10, rate: 150, total: 1500 },
{ description: 'Development', hours: 20, rate: 125, total: 2500 },
],
subtotal: 4000,
tax: 320,
total: 4320,
invoiceNumber: 'INV-2026-001',
date: '2026-01-15',
dueDate: '2026-02-15',
});
console.log(`Generated: ${invoice.filename} (${invoice.size} bytes)`);

For production applications, wrap the API in a reusable client class with proper error handling:

glyph-client.js
import { GLYPH_API_KEY, GLYPH_API_URL } from './config.js';
export class GlyphError extends Error {
constructor(message, code, details = null, statusCode = null) {
super(message);
this.name = 'GlyphError';
this.code = code;
this.details = details;
this.statusCode = statusCode;
}
}
export class GlyphClient {
constructor(apiKey = GLYPH_API_KEY, baseUrl = GLYPH_API_URL) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new GlyphError(
error.error || 'Request failed',
error.code || 'UNKNOWN_ERROR',
error.details,
response.status
);
}
return response;
}
/**
* Generate a PDF from structured data
*/
async create(data, options = {}) {
const response = await this.request('/v1/create', {
method: 'POST',
headers: { 'Accept': 'application/json' },
body: JSON.stringify({
data,
intent: options.intent,
style: options.style || 'stripe-clean',
format: options.format || 'pdf',
templateId: options.templateId,
ttl: options.ttl,
}),
});
return response.json();
}
/**
* Generate a PDF from raw HTML
*/
async createFromHtml(html, options = {}) {
const response = await this.request('/v1/create', {
method: 'POST',
headers: { 'Accept': 'application/json' },
body: JSON.stringify({
html,
format: options.format || 'pdf',
ttl: options.ttl,
}),
});
return response.json();
}
/**
* Capture a URL as PDF
*/
async createFromUrl(url, options = {}) {
const response = await this.request('/v1/create', {
method: 'POST',
headers: { 'Accept': 'application/json' },
body: JSON.stringify({
url,
format: options.format || 'pdf',
ttl: options.ttl,
}),
});
return response.json();
}
/**
* Apply AI modifications to a session
*/
async modify(sessionId, instruction, options = {}) {
const response = await this.request('/v1/modify', {
method: 'POST',
body: JSON.stringify({
sessionId,
instruction,
region: options.region,
}),
});
return response.json();
}
/**
* Generate final PDF from a session
*/
async generate(sessionId, options = {}) {
const response = await this.request('/v1/generate', {
method: 'POST',
body: JSON.stringify({
sessionId,
format: options.format || 'pdf',
options: options.renderOptions,
}),
});
// Return raw binary buffer
return Buffer.from(await response.arrayBuffer());
}
/**
* Get PDF as Buffer from create result
*/
static decodeDataUrl(dataUrl) {
const base64Data = dataUrl.split(',')[1];
return Buffer.from(base64Data, 'base64');
}
}

Usage:

import { GlyphClient, GlyphError } from './glyph-client.js';
const glyph = new GlyphClient();
try {
// Generate invoice
const result = await glyph.create({
company: { name: 'Acme Inc' },
customer: { name: 'Jane Doe' },
items: [{ description: 'Service', total: 500 }],
total: 500,
}, {
intent: 'professional invoice',
style: 'stripe-clean',
});
// Save PDF
const pdfBuffer = GlyphClient.decodeDataUrl(result.url);
writeFileSync('invoice.pdf', pdfBuffer);
// Optionally modify and regenerate
await glyph.modify(result.sessionId, 'Add a QR code for payment');
const finalPdf = await glyph.generate(result.sessionId);
writeFileSync('invoice-with-qr.pdf', finalPdf);
} catch (error) {
if (error instanceof GlyphError) {
console.error(`Glyph error [${error.code}]: ${error.message}`);
if (error.details) console.error('Details:', error.details);
} else {
throw error;
}
}

Create a PDF generation endpoint in your Express.js application:

server.js
import express from 'express';
import { GlyphClient, GlyphError } from './glyph-client.js';
const app = express();
app.use(express.json());
const glyph = new GlyphClient();
/**
* Middleware to handle Glyph errors
*/
function glyphErrorHandler(err, req, res, next) {
if (err instanceof GlyphError) {
const statusCode = err.statusCode || 500;
return res.status(statusCode).json({
error: err.message,
code: err.code,
details: err.details,
});
}
next(err);
}
/**
* POST /api/invoices/pdf
* Generate an invoice PDF from order data
*/
app.post('/api/invoices/pdf', async (req, res, next) => {
try {
const { orderId, customer, items, totals } = req.body;
// Validate input
if (!customer || !items || !totals) {
return res.status(400).json({
error: 'Missing required fields: customer, items, totals',
});
}
// Generate PDF
const result = await glyph.create({
company: {
name: 'Your Company',
email: 'billing@yourcompany.com',
address: '123 Business St, City, State 12345',
},
customer,
items,
subtotal: totals.subtotal,
tax: totals.tax,
total: totals.total,
invoiceNumber: `INV-${orderId}`,
date: new Date().toISOString().split('T')[0],
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0],
}, {
intent: 'professional invoice',
style: 'stripe-clean',
});
// Return PDF as download
const pdfBuffer = GlyphClient.decodeDataUrl(result.url);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${result.filename}"`,
'Content-Length': pdfBuffer.length,
});
res.send(pdfBuffer);
} catch (error) {
next(error);
}
});
/**
* POST /api/reports/pdf
* Generate a report PDF from HTML
*/
app.post('/api/reports/pdf', async (req, res, next) => {
try {
const { title, content, styles } = req.body;
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui, sans-serif; padding: 40px; }
h1 { color: #1a1a1a; border-bottom: 2px solid #eee; padding-bottom: 10px; }
${styles || ''}
</style>
</head>
<body>
<h1>${title}</h1>
${content}
</body>
</html>
`;
const result = await glyph.createFromHtml(html);
const pdfBuffer = GlyphClient.decodeDataUrl(result.url);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${result.filename}"`,
});
res.send(pdfBuffer);
} catch (error) {
next(error);
}
});
// Apply error handler
app.use(glyphErrorHandler);
// General error handler
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Terminal window
# Generate an invoice
curl -X POST http://localhost:3000/api/invoices/pdf \
-H "Content-Type: application/json" \
-d '{
"orderId": "12345",
"customer": { "name": "John Doe", "email": "john@example.com" },
"items": [
{ "description": "Widget", "quantity": 2, "unitPrice": 25, "total": 50 }
],
"totals": { "subtotal": 50, "tax": 4, "total": 54 }
}' \
--output invoice.pdf
# Generate a report from HTML
curl -X POST http://localhost:3000/api/reports/pdf \
-H "Content-Type: application/json" \
-d '{
"title": "Q1 2026 Report",
"content": "<p>Revenue increased by 15% compared to Q4 2025.</p>"
}' \
--output report.pdf

Implement robust error handling with automatic retries for transient failures:

error-handling.js
import { GlyphClient, GlyphError } from './glyph-client.js';
const RETRYABLE_CODES = ['RATE_LIMIT_EXCEEDED', 'INTERNAL_ERROR'];
const MAX_RETRIES = 3;
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function generateWithRetry(glyph, data, options = {}, attempt = 1) {
try {
return await glyph.create(data, options);
} catch (error) {
if (!(error instanceof GlyphError)) throw error;
// Handle rate limiting with exponential backoff
if (error.code === 'RATE_LIMIT_EXCEEDED') {
const retryAfter = error.details?.retryAfter || 60;
console.log(`Rate limited. Waiting ${retryAfter}s before retry...`);
await sleep(retryAfter * 1000);
return generateWithRetry(glyph, data, options, attempt + 1);
}
// Handle monthly limit
if (error.code === 'MONTHLY_LIMIT_EXCEEDED') {
throw new Error(
`Monthly PDF limit reached (${error.details?.used}/${error.details?.limit}). ` +
`Upgrade at ${error.details?.upgrade}`
);
}
// Handle validation errors (not retryable)
if (error.code === 'VALIDATION_ERROR') {
const fields = error.details?.map(d => `${d.path.join('.')}: ${d.message}`);
throw new Error(`Validation failed: ${fields?.join(', ')}`);
}
// Retry transient errors
if (RETRYABLE_CODES.includes(error.code) && attempt < MAX_RETRIES) {
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
await sleep(delay);
return generateWithRetry(glyph, data, options, attempt + 1);
}
throw error;
}
}
// Usage
const glyph = new GlyphClient();
try {
const result = await generateWithRetry(glyph, {
company: { name: 'Acme Inc' },
customer: { name: 'Jane Doe' },
items: [{ description: 'Service', total: 100 }],
total: 100,
});
console.log('Generated:', result.filename);
} catch (error) {
console.error('Failed to generate PDF:', error.message);
}

For large documents or real-time progress feedback, use streaming:

streaming.js
import { GLYPH_API_KEY, GLYPH_API_URL } from './config.js';
async function modifyWithStreaming(sessionId, instruction) {
const response = await fetch(`${GLYPH_API_URL}/v1/modify?stream=true`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${GLYPH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ sessionId, instruction }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let currentEvent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line
for (const line of lines) {
if (line.startsWith('event: ')) {
currentEvent = line.slice(7);
continue;
}
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
switch (currentEvent) {
case 'start':
console.log(`Processing with ${data.model}, ETA: ${data.estimatedTime}ms`);
break;
case 'delta':
process.stdout.write('.'); // Progress indicator
break;
case 'changes':
console.log('\nChanges:', data.changes);
break;
case 'complete':
console.log(`\nCompleted in ${data.usage.processingTimeMs}ms`);
return data;
case 'error':
throw new Error(`${data.code}: ${data.error}`);
}
}
}
}
throw new Error('Stream ended without complete event');
}
handler.js
import { GlyphClient, GlyphError } from './glyph-client.js';
const glyph = new GlyphClient(process.env.GLYPH_API_KEY);
export async function generateInvoice(event) {
try {
const body = JSON.parse(event.body);
const result = await glyph.create(body.data, {
intent: 'professional invoice',
style: body.style || 'stripe-clean',
});
// Return base64-encoded PDF for API Gateway
return {
statusCode: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${result.filename}"`,
},
body: result.url.split(',')[1], // Base64 data
isBase64Encoded: true,
};
} catch (error) {
const statusCode = error instanceof GlyphError ? error.statusCode || 500 : 500;
return {
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error.message,
code: error.code || 'INTERNAL_ERROR',
}),
};
}
}
api/generate-pdf.js
import { GlyphClient, GlyphError } from '../lib/glyph-client.js';
const glyph = new GlyphClient(process.env.GLYPH_API_KEY);
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const result = await glyph.create(req.body.data, {
intent: req.body.intent || 'professional document',
style: req.body.style || 'stripe-clean',
});
const pdfBuffer = GlyphClient.decodeDataUrl(result.url);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
res.send(pdfBuffer);
} catch (error) {
const statusCode = error instanceof GlyphError ? error.statusCode || 500 : 500;
res.status(statusCode).json({
error: error.message,
code: error.code || 'INTERNAL_ERROR',
});
}
}
export const config = {
api: {
bodyParser: {
sizeLimit: '1mb',
},
responseLimit: false, // Allow large PDF responses
},
};
worker.js
const GLYPH_API_URL = 'https://api.glyph.you';
export default {
async fetch(request, env) {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
try {
const body = await request.json();
const response = await fetch(`${GLYPH_API_URL}/v1/create`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.GLYPH_API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
data: body.data,
intent: body.intent || 'professional document',
style: body.style || 'stripe-clean',
}),
});
if (!response.ok) {
const error = await response.json();
return new Response(JSON.stringify(error), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
});
}
const result = await response.json();
// Decode base64 and return as binary
const binaryString = atob(result.url.split(',')[1]);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Response(bytes, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${result.filename}"`,
},
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
},
};

Generate multiple PDFs efficiently:

batch.js
import { GlyphClient } from './glyph-client.js';
import { writeFileSync, mkdirSync } from 'fs';
const glyph = new GlyphClient();
async function generateBatch(invoices, outputDir = './output') {
mkdirSync(outputDir, { recursive: true });
const response = await fetch('https://api.glyph.you/v1/batch/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GLYPH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: invoices.map(invoice => ({
data: invoice,
style: 'stripe-clean',
format: 'pdf',
})),
}),
});
const { results, successCount, failCount } = await response.json();
console.log(`Generated: ${successCount} success, ${failCount} failed`);
for (const result of results) {
if (result.status === 'success') {
const buffer = Buffer.from(result.url.split(',')[1], 'base64');
writeFileSync(`${outputDir}/${result.filename}`, buffer);
console.log(`Saved: ${result.filename}`);
} else {
console.error(`Failed item ${result.index}: ${result.error}`);
}
}
return results;
}
// Usage
const invoices = [
{ company: { name: 'Acme' }, customer: { name: 'Client A' }, items: [...], total: 1000 },
{ company: { name: 'Acme' }, customer: { name: 'Client B' }, items: [...], total: 2000 },
{ company: { name: 'Acme' }, customer: { name: 'Client C' }, items: [...], total: 1500 },
];
await generateBatch(invoices);

Full TypeScript definitions for the client:

glyph-client.ts
interface GlyphCreateOptions {
intent?: string;
style?: 'stripe-clean' | 'bold' | 'minimal' | 'corporate';
format?: 'pdf' | 'png';
templateId?: string;
ttl?: number;
}
interface GlyphCreateResult {
success: boolean;
format: 'pdf' | 'png';
url: string;
size: number;
filename: string;
expiresAt: string;
sessionId: string;
analysis?: {
detectedType: string;
confidence: number;
fieldsIdentified: string[];
};
}
interface GlyphModifyResult {
html: string;
changes: string[];
tokensUsed: number;
selfCheckPassed: boolean;
}
export class GlyphClient {
constructor(apiKey?: string, baseUrl?: string);
create(data: Record<string, unknown>, options?: GlyphCreateOptions): Promise<GlyphCreateResult>;
createFromHtml(html: string, options?: Omit<GlyphCreateOptions, 'intent' | 'style' | 'templateId'>): Promise<GlyphCreateResult>;
createFromUrl(url: string, options?: Omit<GlyphCreateOptions, 'intent' | 'style' | 'templateId'>): Promise<GlyphCreateResult>;
modify(sessionId: string, instruction: string, options?: { region?: string }): Promise<GlyphModifyResult>;
generate(sessionId: string, options?: { format?: 'pdf' | 'png'; renderOptions?: object }): Promise<Buffer>;
static decodeDataUrl(dataUrl: string): Buffer;
}