Skip to main content

Batch Document Generation

Generate hundreds or thousands of documents in a single API call using Rynko batch generation.

When to Use Batch Generation

Batch generation is ideal for:

  • Monthly invoicing - Generate invoices for all customers at once
  • Report distribution - Create personalized reports for each team member
  • Certificate generation - Produce certificates for all event attendees
  • Contract processing - Generate contracts for multiple parties

Basic Batch Request

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

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

// Queue batch generation (async operation)
const batch = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents: [
{ invoiceNumber: 'INV-001', customerName: 'Acme Corporation', amount: 1500.00 },
{ invoiceNumber: 'INV-002', customerName: 'Beta Industries', amount: 2300.00 },
{ invoiceNumber: 'INV-003', customerName: 'Gamma Systems', amount: 890.00 },
]
});

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

Batch Response

{
"batchId": "batch_xyz789abc",
"status": "processing",
"totalDocuments": 3,
"completedDocuments": 0,
"failedDocuments": 0,
"createdAt": "2025-01-15T10:00:00Z"
}

Checking Batch Status

Poll the batch status endpoint to monitor progress:

async function waitForBatch(client, batchId) {
console.log(`Waiting for batch ${batchId}...`);

while (true) {
const status = await client.documents.getBatch(batchId);

console.log(`Progress: ${status.completedDocuments}/${status.totalDocuments}`);

if (status.status === 'completed') {
console.log('Batch completed!');
return status;
}

if (status.status === 'failed') {
throw new Error(`Batch failed: ${status.errorMessage}`);
}

// Wait 2 seconds before checking again
await new Promise(resolve => setTimeout(resolve, 2000));
}
}

const batchResult = await waitForBatch(client, result.batchId);

// Download all documents
for (const doc of batchResult.documents) {
if (doc.status === 'completed') {
console.log(`Download: ${doc.downloadUrl}`);
} else {
console.error(`Failed: ${doc.jobId} - ${doc.errorMessage}`);
}
}

For production, use webhooks instead of polling:

1. Set Up Webhook

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

console.log('Webhook secret:', webhook.secret);

2. Submit Batch

const result = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents: invoiceData,
metadata: {
batchName: 'January 2025 Invoices',
triggeredBy: 'monthly-billing-job'
}
});

console.log('Batch submitted:', result.batchId);
// Don't wait - webhook will notify when complete

3. Handle Webhook Events

import express from 'express';
import { verifyWebhookSignature } from '@rynko/sdk';

const app = express();

app.post('/webhooks/rynko', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = verifyWebhookSignature({
payload: req.body.toString(),
signature: req.headers['x-rynko-signature'],
secret: process.env.WEBHOOK_SECRET
});

switch (event.type) {
case 'batch.completed':
console.log(`Batch ${event.data.batchId} completed!`);
console.log(`Generated: ${event.data.completedDocuments}`);
console.log(`Failed: ${event.data.failedDocuments}`);
processBatchResults(event.data);
break;

case 'document.generated':
console.log(`Document ready: ${event.data.downloadUrl}`);
break;

case 'document.failed':
console.error(`Document failed: ${event.data.errorMessage}`);
break;
}

res.status(200).send('OK');
} catch (error) {
console.error('Webhook error:', error);
res.status(400).send('Invalid signature');
}
});

Real-World Example: Monthly Invoice Generation

import { Rynko } from '@rynko/sdk';
import { getCustomersWithPendingInvoices, saveInvoiceUrl } from './database';

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

// Get all customers that need invoices
const customers = await getCustomersWithPendingInvoices();

console.log(`Generating ${customers.length} invoices...`);

// Prepare batch documents - each object contains the template variables
const documents = customers.map(customer => ({
invoiceNumber: `INV-${Date.now()}-${customer.id}`,
customerName: customer.name,
customerEmail: customer.email,
address: customer.address,
lineItems: customer.pendingCharges,
subtotal: customer.subtotal,
tax: customer.tax,
total: customer.total,
dueDate: getNextMonthDate()
}));

// Submit batch (async operation)
const batch = await client.documents.generateBatch({
templateId: 'monthly-invoice',
format: 'pdf',
documents,
metadata: {
billingPeriod: getMonthYear()
}
});

console.log(`Batch submitted: ${batch.batchId}`);

// Wait for completion
const result = await waitForBatch(client, batch.batchId);

// Save download URLs to database
for (const doc of result.documents) {
if (doc.status === 'completed') {
await saveInvoiceUrl(doc.metadata.customerId, doc.downloadUrl);
}
}

console.log(`Generated ${result.completedDocuments} invoices`);
console.log(`Failed: ${result.failedDocuments}`);
}

Batch Limits

PlanMax Documents per BatchConcurrent Batches
Free101
Starter1003
Growth50010
Scale1000Unlimited

Error Handling

Some documents may fail while others succeed:

const result = await waitForBatch(client, batchId);

const successful = result.documents.filter(d => d.status === 'completed');
const failed = result.documents.filter(d => d.status === 'failed');

console.log(`Successful: ${successful.length}`);
console.log(`Failed: ${failed.length}`);

// Handle failures
for (const doc of failed) {
console.error(`Document ${doc.jobId} failed: ${doc.errorMessage}`);

// Retry logic
if (doc.errorMessage.includes('temporary')) {
await retryDocument(doc);
}
}

Best Practices

1. Use Meaningful Filenames

You can include custom filenames in the batch-level metadata for tracking:

const batch = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents: customers.map(c => ({
invoiceNumber: c.invoiceNumber,
customerName: c.name,
// ... other variables
})),
metadata: {
batchName: `invoices-${month}`,
generatedAt: new Date().toISOString()
}
});

2. Include Metadata for Tracking

Use batch-level metadata to track the generation context:

const batch = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents: invoiceVariables,
metadata: {
type: 'monthly-invoice',
billingPeriod: getMonthYear(),
triggeredBy: 'billing-cron-job'
}
});

3. Process in Chunks for Large Batches

const BATCH_SIZE = 500;

async function processLargeBatch(allDocuments) {
const batches = [];

for (let i = 0; i < allDocuments.length; i += BATCH_SIZE) {
const chunk = allDocuments.slice(i, i + BATCH_SIZE);

const batch = await client.documents.generateBatch({
templateId: 'invoice-template',
format: 'pdf',
documents: chunk
});

batches.push(batch.batchId);
}

// Wait for all batches
const results = await Promise.all(
batches.map(id => waitForBatch(client, id))
);

return results;
}

Next Steps