Skip to main content

Generating Documents

This guide covers everything you need to know about generating documents with Rynko.

Overview

Rynko generates documents by:

  1. Taking a template (PDF or Excel)
  2. Substituting variables with your data
  3. Rendering the final document
  4. Providing a signed download URL

Quick Start

Generate a PDF

import { Rynko } from '@rynko/sdk';

const client = new Rynko({ apiKey: process.env.RYNKO_API_KEY });

// Queue document generation (async operation)
const job = await client.documents.generate({
templateId: 'invoice-template',
format: 'pdf',
variables: {
invoiceNumber: 'INV-2025-001',
customerName: 'Acme Corporation',
amount: 1250.00
}
});

console.log('Job ID:', job.jobId);
console.log('Status:', job.status); // 'queued'

// Wait for completion to get download URL
const completed = await client.documents.waitForCompletion(job.jobId);
console.log('Download URL:', completed.downloadUrl);

Generate an Excel File

const job = await client.documents.generate({
templateId: 'monthly-report',
format: 'xlsx',
variables: {
reportMonth: 'January 2025',
data: [
{ category: 'Sales', amount: 50000 },
{ category: 'Expenses', amount: 30000 },
{ category: 'Profit', amount: 20000 }
]
}
});

const completed = await client.documents.waitForCompletion(job.jobId);
console.log('Download URL:', completed.downloadUrl);

Variable Types

Rynko supports various variable types in templates:

Simple Variables

variables: {
customerName: 'John Doe',
invoiceNumber: 'INV-001',
amount: 99.99,
isActive: true
}

Use in template: {{customerName}}, {{invoiceNumber}}, {{amount}}

Arrays (for tables/lists)

variables: {
lineItems: [
{ description: 'Widget', quantity: 2, price: 25.00 },
{ description: 'Gadget', quantity: 1, price: 49.99 }
]
}

Use in template with {{#each lineItems}}...{{/each}}

Nested Objects

variables: {
customer: {
name: 'Acme Corp',
address: {
street: '123 Main St',
city: 'New York',
zip: '10001'
}
}
}

Use in template: {{customer.name}}, {{customer.address.city}}

Dates

variables: {
invoiceDate: '2025-01-15',
dueDate: new Date('2025-02-15').toISOString()
}

Output Formats

PDF

Best for:

  • Invoices and receipts
  • Contracts and agreements
  • Reports and statements
  • Any printable document
{ format: 'pdf' }

Excel (XLSX)

Best for:

  • Data exports
  • Financial reports
  • Spreadsheet templates
  • Data that users will edit
{ format: 'xlsx' }

Batch Generation

Generate multiple documents efficiently. Each object in the documents array contains the variables for one document:

// Queue batch generation
const batch = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents: [
{ invoiceNumber: 'INV-001', customer: 'Acme Corp' },
{ invoiceNumber: 'INV-002', customer: 'Beta Inc' },
{ invoiceNumber: 'INV-003', customer: 'Gamma Ltd' }
]
});

console.log('Batch ID:', batch.batchId);
console.log('Total jobs:', batch.totalJobs);
console.log('Estimated wait:', batch.estimatedWaitSeconds, 'seconds');

Polling for Batch Completion

async function waitForBatch(client, batchId) {
while (true) {
const status = await client.documents.getBatch(batchId);

if (status.status === 'completed') {
return status.documents;
}

if (status.status === 'failed') {
throw new Error('Batch failed');
}

await new Promise(resolve => setTimeout(resolve, 1000));
}
}

const documents = await waitForBatch(client, result.batchId);
for (const doc of documents) {
console.log(`${doc.jobId}: ${doc.downloadUrl}`);
}

Async Generation with Webhooks

For production use, we recommend webhooks instead of polling:

1. Create Webhook Subscription

const webhook = await client.webhooks.create({
url: 'https://your-server.com/webhooks/rynko',
events: ['document.generated', 'document.failed']
});

2. Generate Document

const job = await client.documents.generate({
templateId: 'complex-report',
format: 'pdf',
variables: { ... }
});

// Don't poll - webhook will notify when ready
console.log('Job submitted:', job.jobId);
console.log('Status URL:', job.statusUrl);

3. Handle Webhook

app.post('/webhooks/rynko', (req, res) => {
const event = req.body;

if (event.type === 'document.generated') {
console.log('Document ready:', event.data.downloadUrl);
// Process the document...
}

if (event.type === 'document.failed') {
console.error('Generation failed:', event.data.errorMessage);
}

res.status(200).send('OK');
});

Downloading Documents

Direct Download

The downloadUrl is a signed URL valid for a limited time. It's only available after the job completes:

// Generate and wait for completion
const job = await client.documents.generate({ ... });
const completed = await client.documents.waitForCompletion(job.jobId);

// Download using fetch
const response = await fetch(completed.downloadUrl);
const buffer = await response.arrayBuffer();

// Save to file
fs.writeFileSync('document.pdf', Buffer.from(buffer));

Using SDK Helper

// Node.js SDK
const bytes = await client.documents.download(result.downloadUrl);
fs.writeFileSync('document.pdf', bytes);
# Python SDK
bytes_data = client.documents.download(result['downloadUrl'])
with open('document.pdf', 'wb') as f:
f.write(bytes_data)

Custom Filenames

Specify a custom filename (without extension):

const result = await client.documents.generate({
templateId: 'invoice-template',
format: 'pdf',
variables: { invoiceNumber: 'INV-001' },
filename: 'invoice-acme-corp-january-2025'
});

// Download URL will use this filename
// https://storage.rynko.dev/.../invoice-acme-corp-january-2025.pdf

Metadata

Attach custom metadata to track and correlate documents across your system. Metadata is passed through to API responses and webhook payloads.

Basic Usage

const result = await client.documents.generate({
templateId: 'invoice-template',
format: 'pdf',
variables: { ... },
metadata: {
customerId: 'cust_123',
orderId: 'ord_456',
department: 'sales',
rowNumber: 42
}
});

Constraints

  • Flat structure: No nested objects allowed
  • Max size: 10 KB total
  • Value types: string, number, boolean, or null
// Valid metadata
metadata: {
orderId: 'ord_123', // string
rowNumber: 42, // number
isPriority: true, // boolean
notes: null // null
}

// Invalid - nested objects not allowed
metadata: {
customer: { id: 123, name: 'Acme' } // ❌ Nested object
}

Accessing Metadata in Responses

Metadata is returned when you check job status:

const completed = await client.documents.waitForCompletion(job.jobId);

console.log(completed.metadata);
// { customerId: 'cust_123', orderId: 'ord_456', ... }

Metadata in Webhooks

When using webhooks, metadata is included in the event payload:

app.post('/webhooks/rynko', (req, res) => {
const event = req.body;

if (event.type === 'document.generated') {
const { jobId, downloadUrl, metadata } = event.data;

// Use metadata to correlate with your records
const orderId = metadata.orderId;
await updateOrderStatus(orderId, 'document_ready', downloadUrl);
}

res.status(200).send('OK');
});

Batch Metadata

For batch generation, you can set metadata at two levels:

await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
// Batch-level metadata (applies to the batch)
metadata: {
batchRunId: 'run_20250115',
triggeredBy: 'scheduled_job'
},
documents: [
{
variables: { invoiceNumber: 'INV-001' },
// Document-level metadata (per document)
metadata: { rowNumber: 1, customerId: 'cust_001' }
},
{
variables: { invoiceNumber: 'INV-002' },
metadata: { rowNumber: 2, customerId: 'cust_002' }
}
]
});

Common Use Cases

Use CaseExample Metadata
Order tracking{ orderId: 'ord_123' }
User attribution{ userId: 'usr_456', email: 'user@example.com' }
Source tracking{ source: 'api', environment: 'production' }
Batch correlation{ batchId: 'batch_789', rowNumber: 42 }
Internal IDs{ internalRef: 'REF-2025-001' }

Error Handling

try {
const result = await client.documents.generate({
templateId: 'invalid-template',
format: 'pdf',
variables: {}
});
} catch (error) {
if (error.code === 'ERR_TMPL_001') {
console.error('Template not found');
} else if (error.code === 'ERR_VALID_002') {
console.error('Variable validation failed:', error.details);
} else if (error.code === 'ERR_QUOTA_008') {
console.error('Monthly document quota exceeded');
} else if (error.code === 'ERR_QUOTA_011') {
console.error('Insufficient document credits');
} else {
console.error('Unexpected error:', error.message);
}
}

See Error Codes for the complete error code reference.


Best Practices

1. Use Batch for Multiple Documents

Instead of making many individual requests:

// Bad: Many individual requests
for (const invoice of invoices) {
await client.documents.generate({ templateId: 'invoice', format: 'pdf', variables: invoice });
}

// Good: Single batch request - each item is a variables object
await client.documents.generateBatch({
templateId: 'invoice',
format: 'pdf',
documents: invoices // Array of variable objects
});

2. Use Webhooks for Production

Polling is fine for development, but use webhooks in production:

// Development: Polling with SDK helper
const job = await client.documents.generate({ ... });
const completed = await client.documents.waitForCompletion(job.jobId);

// Production: Webhooks
const job = await client.documents.generate({ ... });
// Handle completion in webhook endpoint - no polling needed

3. Handle URL Expiration

Download URLs expire. If you need long-term storage:

const job = await client.documents.generate({ ... });
const completed = await client.documents.waitForCompletion(job.jobId);

// Download immediately and store in your own storage
const buffer = await client.documents.download(completed.downloadUrl);
await uploadToS3(buffer, 'invoices/inv-001.pdf');

4. Validate Variables Before Generation

Check required variables before making API calls:

function validateInvoiceVariables(vars) {
const required = ['invoiceNumber', 'customerName', 'amount'];
const missing = required.filter(key => !vars[key]);

if (missing.length > 0) {
throw new Error(`Missing required variables: ${missing.join(', ')}`);
}
}

validateInvoiceVariables(variables);
const result = await client.documents.generate({ ... });