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({ ... });