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: 'excel',
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);

Generation Options

The generate() method accepts these parameters:

ParameterTypeRequiredDescription
templateIdstringYesTemplate ID (UUID, shortId like atpl_abc123, or slug)
formatstringYesOutput format: 'pdf', 'excel', or 'csv'
variablesobjectYesKey-value pairs for template variable substitution
filenamestringNoCustom filename without extension (max 200 chars)
webhookUrlstringNoPer-request webhook URL for completion notification
metadataobjectNoCustom metadata (flat, max 10KB) — see Metadata
useDraftbooleanNoUse draft version instead of published (default: false)
useCreditbooleanNoUse purchased credits for watermark-free output (default: false)
const job = await client.documents.generate({
templateId: 'invoice-template',
format: 'pdf',
variables: { invoiceNumber: 'INV-001', total: 99.99 },
filename: 'invoice-acme-january',
useDraft: false,
useCredit: true,
webhookUrl: 'https://your-server.com/webhooks/rynko',
metadata: { orderId: 'ord_123' },
});
tip

Use useDraft: true during development to test unpublished templates. Set useCredit: true in production to generate watermark-free documents using purchased credits.


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

FormatValueExtensionBest For
PDF'pdf'.pdfInvoices, contracts, reports, printable documents
Excel'excel'.xlsxData exports, financial reports, editable spreadsheets
CSV'csv'.csvRaw data exports, system integrations
// PDF document
{ format: 'pdf' }

// Excel spreadsheet
{ format: 'excel' }

// CSV export
{ format: 'csv' }
warning

The format value is 'excel', not 'xlsx'. Using 'xlsx' will result in a validation error.


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

Use the REST API to check batch status and retrieve individual job results:

async function waitForBatch(batchId) {
const apiBase = 'https://api.rynko.dev/api/v1';
const headers = { Authorization: `Bearer ${process.env.RYNKO_API_KEY}` };

while (true) {
const res = await fetch(`${apiBase}/documents/batches/${batchId}`, { headers });
const batch = await res.json();

console.log(`Progress: ${batch.completedJobs}/${batch.totalJobs}`);

if (batch.status === 'completed' || batch.status === 'partial') {
return batch;
}

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

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

const batch = await waitForBatch(result.batchId);
console.log(`Completed: ${batch.completedJobs}, Failed: ${batch.failedJobs}`);
note

Batch status can be queued, processing, completed, partial (some jobs failed), or failed.


Async Generation with Webhooks

For production use, we recommend webhooks instead of polling:

1. Create Webhook Subscription

Configure webhooks in the Rynko Dashboard under Settings > Webhooks, or via the REST API:

const webhook = await fetch('https://api.rynko.dev/api/webhook-subscriptions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RYNKO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://your-server.com/webhooks/rynko',
events: ['document.generated', 'document.failed', 'document.downloaded'],
}),
});

Available webhook events:

EventDescription
document.generatedDocument successfully generated
document.failedDocument generation failed
document.downloadedDocument was downloaded

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 payload = req.body;

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

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

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

Downloading Documents

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({
templateId: 'invoice-template',
format: 'pdf',
variables: { invoiceNumber: 'INV-001' },
});
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));
# Python SDK
job = client.documents.generate(
template_id='invoice-template',
format='pdf',
variables={'invoiceNumber': 'INV-001'},
)
completed = client.documents.wait_for_completion(job['jobId'])

# Download using httpx
import httpx
response = httpx.get(completed['downloadUrl'])
with open('document.pdf', 'wb') as f:
f.write(response.content)

Custom Filenames

Specify a custom filename (without extension, max 200 characters):

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

// The download URL will serve the file with this filename

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 payload = req.body;

if (payload.event === 'document.generated') {
const { jobId, downloadUrl, metadata } = payload.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' }

Job Statuses

StatusDescription
queuedJob is waiting in the queue
processingJob is being rendered
completedDocument is ready for download
failedGeneration failed (check errorMessage)
cancelledJob was cancelled before completion

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 response = await fetch(completed.downloadUrl);
const buffer = Buffer.from(await response.arrayBuffer());
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({ ... });