Generating Documents
This guide covers everything you need to know about generating documents with Rynko.
Overview
Rynko generates documents by:
- Taking a template (PDF or Excel)
- Substituting variables with your data
- Rendering the final document
- 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
templateId | string | Yes | Template ID (UUID, shortId like atpl_abc123, or slug) |
format | string | Yes | Output format: 'pdf', 'excel', or 'csv' |
variables | object | Yes | Key-value pairs for template variable substitution |
filename | string | No | Custom filename without extension (max 200 chars) |
webhookUrl | string | No | Per-request webhook URL for completion notification |
metadata | object | No | Custom metadata (flat, max 10KB) — see Metadata |
useDraft | boolean | No | Use draft version instead of published (default: false) |
useCredit | boolean | No | Use 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' },
});
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
| Format | Value | Extension | Best For |
|---|---|---|---|
'pdf' | .pdf | Invoices, contracts, reports, printable documents | |
| Excel | 'excel' | .xlsx | Data exports, financial reports, editable spreadsheets |
| CSV | 'csv' | .csv | Raw data exports, system integrations |
// PDF document
{ format: 'pdf' }
// Excel spreadsheet
{ format: 'excel' }
// CSV export
{ format: 'csv' }
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}`);
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:
| Event | Description |
|---|---|
document.generated | Document successfully generated |
document.failed | Document generation failed |
document.downloaded | Document 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, ornull
// 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 Case | Example 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
| Status | Description |
|---|---|
queued | Job is waiting in the queue |
processing | Job is being rendered |
completed | Document is ready for download |
failed | Generation failed (check errorMessage) |
cancelled | Job 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({ ... });