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.
Installation
Section titled “Installation”Install the required dependencies:
npm install node-fetchpnpm add node-fetchyarn add node-fetchbun add node-fetchEnvironment Setup
Section titled “Environment Setup”Store your API key securely in environment variables:
GLYPH_API_KEY=gk_your_api_keyLoad it in your application:
import 'dotenv/config';
export const GLYPH_API_KEY = process.env.GLYPH_API_KEY;export const GLYPH_API_URL = 'https://api.glyph.you';Basic PDF Generation
Section titled “Basic PDF Generation”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, };}
// Usageconst 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)`);Glyph API Client
Section titled “Glyph API Client”For production applications, wrap the API in a reusable client class with proper error handling:
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; }}Express.js Integration
Section titled “Express.js Integration”Create a PDF generation endpoint in your Express.js application:
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 handlerapp.use(glyphErrorHandler);
// General error handlerapp.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}`);});Testing the Express Endpoints
Section titled “Testing the Express Endpoints”# Generate an invoicecurl -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 HTMLcurl -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.pdfError Handling
Section titled “Error Handling”Implement robust error handling with automatic retries for transient failures:
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; }}
// Usageconst 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);}Streaming Responses
Section titled “Streaming Responses”For large documents or real-time progress feedback, use streaming:
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');}Serverless Deployment
Section titled “Serverless Deployment”AWS Lambda
Section titled “AWS Lambda”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', }), }; }}Vercel Serverless Functions
Section titled “Vercel Serverless Functions”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 },};Cloudflare Workers
Section titled “Cloudflare Workers”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' }, }); } },};Batch Processing
Section titled “Batch Processing”Generate multiple PDFs efficiently:
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;}
// Usageconst 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);TypeScript Support
Section titled “TypeScript Support”Full TypeScript definitions for the client:
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;}Next Steps
Section titled “Next Steps”- POST /v1/create — Full API reference for document generation
- Batch Generation — Generate multiple PDFs in one request
- Streaming — Real-time modification feedback
- Error Codes — Complete error reference
- Rate Limits — Understand usage limits by tier