Skip to main content

Generate PDF Documents

Create professional PDF documents from templates using the Rynko API.


Overview​

Rynko allows you to generate PDF documents dynamically from templates. This tutorial covers:

  • Creating a PDF template (via AI, gallery, or manually)
  • Generating PDFs via the API
  • Batch document generation
  • Webhook notifications for async processing

This is perfect for invoices, receipts, reports, contracts, certificates, and any document you need to generate programmatically.


Prerequisites​

Before starting, make sure you have:

  • An active Rynko account
  • An API key with document generation permissions
  • A published PDF template (see Create a Template)

How PDF Generation Works​

Rynko uses templates to generate PDFs:

  1. Create a Template - Design your PDF layout in the visual editor
  2. Call the API - Send a request with your template ID and variables
  3. Receive the PDF - Get a download URL for the generated document
Template + Variables → API Call → PDF Document

Quick Start​

Generate your first PDF in under a minute:

Step 1: Get Your Template ID​

  1. Log in to your Rynko Dashboard
  2. Go to Templates
  3. Find your template and copy the Template ID (UUID or slug)

Step 2: Make an API Call​

curl -X POST https://api.rynko.dev/api/documents/generate \
-H "Authorization: Bearer fm_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"templateId": "invoice-template",
"format": "pdf",
"variables": {
"invoiceNumber": "INV-2025-001",
"customerName": "Acme Corp",
"total": "$1,250.00"
}
}'

Step 3: Wait for Completion​

Document generation is an async operation. The initial response returns job info:

{
"jobId": "job_abc123xyz",
"status": "queued",
"statusUrl": "https://api.rynko.dev/api/documents/jobs/job_abc123xyz",
"estimatedWaitSeconds": 5
}

Poll the job status endpoint until status is completed:

curl https://api.rynko.dev/api/documents/jobs/job_abc123xyz \
-H "Authorization: Bearer fm_your_api_key"

Step 4: Download Your PDF​

When the job completes, you'll get the download URL:

{
"jobId": "job_abc123xyz",
"status": "completed",
"format": "pdf",
"downloadUrl": "https://cdn.rynko.dev/documents/doc_abc123xyz.pdf",
"expiresAt": "2025-12-30T12:00:00.000Z"
}

Download your PDF from the downloadUrl before it expires.


Code Examples​

JavaScript / Node.js​

const API_KEY = process.env.RYNKO_API_KEY;

async function generateInvoice(invoiceData) {
const response = await fetch('https://api.rynko.dev/api/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'invoice-template',
format: 'pdf',
variables: {
invoiceNumber: invoiceData.number,
invoiceDate: invoiceData.date,
customerName: invoiceData.customer.name,
customerAddress: invoiceData.customer.address,
items: invoiceData.items,
subtotal: invoiceData.subtotal,
tax: invoiceData.tax,
total: invoiceData.total,
},
}),
});

const job = await response.json();

if (!response.ok) {
throw new Error(job.message);
}

console.log('Job queued:', job.jobId);

// Poll for completion
let completed;
while (true) {
const statusRes = await fetch(
`https://api.rynko.dev/api/documents/jobs/${job.jobId}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
completed = await statusRes.json();

if (completed.status === 'completed') break;
if (completed.status === 'failed') throw new Error(completed.errorMessage);

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

console.log('PDF generated:', completed.downloadUrl);
return completed;
}

// Usage
generateInvoice({
number: 'INV-2025-001',
date: 'December 29, 2025',
customer: {
name: 'Acme Corp',
address: '123 Business St, New York, NY 10001',
},
items: [
{ description: 'Consulting', quantity: 10, price: '$1,500.00' },
{ description: 'Software License', quantity: 1, price: '$500.00' },
],
subtotal: '$2,000.00',
tax: '$170.00',
total: '$2,170.00',
});

Python​

import requests
import os

API_KEY = os.environ.get('RYNKO_API_KEY')
API_URL = 'https://api.rynko.dev/api/documents/generate'

def generate_invoice(invoice_data):
response = requests.post(
API_URL,
headers={
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
},
json={
'templateId': 'invoice-template',
'format': 'pdf',
'variables': {
'invoiceNumber': invoice_data['number'],
'invoiceDate': invoice_data['date'],
'customerName': invoice_data['customer']['name'],
'customerAddress': invoice_data['customer']['address'],
'items': invoice_data['items'],
'subtotal': invoice_data['subtotal'],
'tax': invoice_data['tax'],
'total': invoice_data['total'],
},
},
)

job = response.json()

if not response.ok:
raise Exception(job.get('message', 'Unknown error'))

print(f"Job queued: {job['jobId']}")

# Poll for completion
import time
while True:
status_res = requests.get(
f"https://api.rynko.dev/api/documents/jobs/{job['jobId']}",
headers={'Authorization': f'Bearer {API_KEY}'}
)
completed = status_res.json()

if completed['status'] == 'completed':
break
if completed['status'] == 'failed':
raise Exception(completed.get('errorMessage', 'Generation failed'))

time.sleep(2)

print(f"PDF generated: {completed['downloadUrl']}")
return completed

# Usage
generate_invoice({
'number': 'INV-2025-001',
'date': 'December 29, 2025',
'customer': {
'name': 'Acme Corp',
'address': '123 Business St, New York, NY 10001',
},
'items': [
{'description': 'Consulting', 'quantity': 10, 'price': '$1,500.00'},
{'description': 'Software License', 'quantity': 1, 'price': '$500.00'},
],
'subtotal': '$2,000.00',
'tax': '$170.00',
'total': '$2,170.00',
})

Using the Rynko SDK​

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 Corp',
total: '$2,170.00',
},
});

console.log('Job queued:', job.jobId);

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

Batch Generation​

Generate multiple documents in a single API call:

const response = await fetch('https://api.rynko.dev/api/documents/generate/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'invoice-template',
format: 'pdf',
documents: [
{
variables: {
invoiceNumber: 'INV-001',
customerName: 'Customer A',
total: '$500.00',
},
},
{
variables: {
invoiceNumber: 'INV-002',
customerName: 'Customer B',
total: '$750.00',
},
},
{
variables: {
invoiceNumber: 'INV-003',
customerName: 'Customer C',
total: '$1,200.00',
},
},
],
}),
});

const batch = await response.json();
console.log('Batch ID:', batch.batchId);
console.log('Documents:', batch.documents);

Batch Response​

{
"batchId": "batch_xyz789",
"status": "processing",
"total": 3,
"completed": 0,
"failed": 0,
"documents": [
{ "id": "doc_001", "status": "pending" },
{ "id": "doc_002", "status": "pending" },
{ "id": "doc_003", "status": "pending" }
]
}

Check Batch Status​

const status = await fetch(
`https://api.rynko.dev/api/documents/batches/${batchId}`,
{
headers: { 'Authorization': `Bearer ${API_KEY}` },
}
).then(r => r.json());

console.log('Batch status:', status);

Webhook Notifications​

Get notified when documents are generated:

Configure Webhook​

  1. Go to Settings → Webhooks in your dashboard
  2. Add a webhook endpoint URL
  3. Select events: document.generated, document.failed, batch.completed

Webhook Payload​

{
"event": "document.generated",
"timestamp": "2025-12-29T10:30:00.000Z",
"data": {
"id": "doc_abc123",
"templateId": "invoice-template",
"format": "pdf",
"downloadUrl": "https://cdn.rynko.dev/documents/doc_abc123.pdf",
"expiresAt": "2025-12-30T10:30:00.000Z",
"metadata": {
"invoiceNumber": "INV-2025-001"
}
}
}

Webhook Handler Example​

// Express.js webhook handler
app.post('/webhooks/rynko', (req, res) => {
const { event, data } = req.body;

switch (event) {
case 'document.generated':
console.log(`Document ready: ${data.downloadUrl}`);
// Download and store the PDF
// Notify the user
break;

case 'document.failed':
console.error(`Document failed: ${data.errorMessage}`);
// Handle the error
// Retry if appropriate
break;

case 'batch.completed':
console.log(`Batch complete: ${data.total} documents`);
// Process all documents
break;
}

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

Output Formats​

Rynko supports multiple output formats:

FormatExtensionUse Case
pdf.pdfDocuments, invoices, reports
excel.xlsxSpreadsheets, data exports

Generate Excel​

const result = await fetch('https://api.rynko.dev/api/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'report-template',
format: 'excel', // Excel format
variables: {
reportTitle: 'Monthly Sales Report',
month: 'December 2025',
data: salesData,
},
}),
}).then(r => r.json());

Custom Filenames​

Specify a custom filename for the generated document:

const result = await fetch('https://api.rynko.dev/api/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'invoice-template',
format: 'pdf',
filename: 'Invoice-{{invoiceNumber}}-{{customerName}}', // Dynamic filename
variables: {
invoiceNumber: 'INV-2025-001',
customerName: 'AcmeCorp',
},
}),
}).then(r => r.json());

// Result: Invoice-INV-2025-001-AcmeCorp.pdf

Metadata​

Add metadata to track documents:

const result = await fetch('https://api.rynko.dev/api/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'invoice-template',
format: 'pdf',
variables: { /* ... */ },
metadata: {
orderId: 'order-123',
customerId: 'cust-456',
source: 'api',
},
}),
}).then(r => r.json());

Metadata is returned in webhooks and can be used to correlate documents with your systems.


Real-World Example: Invoice System​

Complete example integrating with an order system:

class InvoiceService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.rynko.dev/api/v1';
}

async generateInvoice(order) {
// Calculate totals
const subtotal = order.items.reduce(
(sum, item) => sum + item.quantity * item.price,
0
);
const tax = subtotal * (order.taxRate / 100);
const total = subtotal + tax;

// Format items for template
const formattedItems = order.items.map(item => ({
description: item.name,
quantity: item.quantity,
unitPrice: this.formatCurrency(item.price),
amount: this.formatCurrency(item.quantity * item.price),
}));

// Generate PDF
const response = await fetch(`${this.baseUrl}/documents/generate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'invoice-template',
format: 'pdf',
filename: `Invoice-${order.invoiceNumber}`,
variables: {
invoiceNumber: order.invoiceNumber,
invoiceDate: this.formatDate(order.createdAt),
dueDate: this.formatDate(order.dueDate),
companyName: 'Your Company',
companyAddress: '123 Business St, City, ST 12345',
customerName: order.customer.name,
customerAddress: order.customer.address,
customerEmail: order.customer.email,
items: formattedItems,
subtotal: this.formatCurrency(subtotal),
taxRate: order.taxRate,
taxAmount: this.formatCurrency(tax),
total: this.formatCurrency(total),
paymentTerms: 'Payment due within 30 days.',
},
metadata: {
orderId: order.id,
customerId: order.customer.id,
},
}),
});

const job = await response.json();

if (!response.ok) {
throw new Error(job.message);
}

// Poll for completion
while (true) {
const statusRes = await fetch(
`${this.baseUrl}/documents/jobs/${job.jobId}`,
{ headers: { 'Authorization': `Bearer ${this.apiKey}` } }
);
const completed = await statusRes.json();

if (completed.status === 'completed') return completed;
if (completed.status === 'failed') throw new Error(completed.errorMessage);

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

formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
}

formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
}

// Usage
const invoiceService = new InvoiceService(process.env.RYNKO_API_KEY);

const order = {
id: 'order-123',
invoiceNumber: 'INV-2025-001',
createdAt: new Date(),
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
taxRate: 8.5,
customer: {
id: 'cust-456',
name: 'John Doe',
email: 'john@example.com',
address: '456 Customer Lane, New York, NY 10001',
},
items: [
{ name: 'Product A', quantity: 2, price: 99.99 },
{ name: 'Product B', quantity: 1, price: 149.99 },
],
};

invoiceService.generateInvoice(order)
.then(result => console.log('Invoice generated:', result.downloadUrl))
.catch(err => console.error('Error:', err));

PDF Design Best Practices​

Layout​

  1. Use standard page sizes - A4 for international, Letter for US
  2. Set appropriate margins - 40-60px for printing
  3. Keep it single page - When possible, fit content on one page
  4. Use visual hierarchy - Clear headings, grouped sections

Typography​

  1. Professional fonts - Arial, Helvetica, Georgia work best
  2. Readable sizes - 10-12pt for body, 14-18pt for headings
  3. Consistent alignment - Right-align numbers for easy scanning
  4. Adequate spacing - White space improves readability

Branding​

  1. Include your logo - Top-left is traditional for invoices
  2. Use brand colors - Sparingly for headers and accents
  3. Professional footer - Contact info and company details

Troubleshooting​

PDF Not Generating​

Solutions:

  1. ✅ Verify template exists and is published
  2. ✅ Check all required variables are provided
  3. ✅ Ensure API key has document generation permissions
  4. ✅ Check for validation errors in the response

Layout Issues in PDF​

Solutions:

  1. ✅ Preview the template before generating
  2. ✅ Use fixed widths for columns in tables
  3. ✅ Avoid very long unbroken text
  4. ✅ Test with maximum expected data length

Missing Data in PDF​

Solutions:

  1. ✅ Check variable names match exactly (case-sensitive)
  2. ✅ Verify nested object structure matches template
  3. ✅ Ensure array data is properly formatted
  4. ✅ Check the Variables tab in template editor

Download URL Expired​

Solutions:

  1. ✅ Download immediately after generation
  2. ✅ Use webhooks for async processing
  3. ✅ Re-generate the document if needed
  4. ✅ Consider storing documents in your own storage

Next Steps​

Now that you can generate PDF documents: