Skip to main content

Best Practices

Security, performance, and optimization tips for using the RenderDoc API.

Overview

Follow these best practices to build secure, reliable, and performant document generation integrations with RenderDoc. These guidelines cover API key security, error handling, performance optimization, and document generation best practices.

info

Important: These best practices apply specifically to API usage. For template design and dashboard features, see the User Guide.


API Key Security

1. Never Hardcode API Keys

❌ Bad:

const apiKey = 'rd_sk_abc123xyz456...'; // Hardcoded - dangerous!

fetch('https://api.renderdoc.dev/api/v1/documents/generate', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});

✅ Good:

const apiKey = process.env.RENDERDOC_API_KEY; // From environment variable

fetch('https://api.renderdoc.dev/api/v1/documents/generate', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});

2. Use Environment Variables

Store API keys in environment variables, never in code:

.env file:

RENDERDOC_API_KEY=rd_sk_abc123xyz456...

.gitignore:

.env
.env.local
.env.production

Node.js:

require('dotenv').config();
const apiKey = process.env.RENDERDOC_API_KEY;

Python:

import os
api_key = os.environ.get('RENDERDOC_API_KEY')

PHP:

$apiKey = getenv('RENDERDOC_API_KEY');

3. Use Separate Keys for Different Environments

Create separate API keys for development, staging, and production:

DEV_RENDERDOC_API_KEY=rd_sk_dev123...
STAGING_RENDERDOC_API_KEY=rd_sk_staging456...
PROD_RENDERDOC_API_KEY=rd_sk_prod789...

Benefits:

  • Isolate test traffic from production
  • Revoke dev keys without affecting production
  • Monitor usage separately

4. Rotate Keys Regularly

Rotate API keys every 90 days:

// Store key creation date in your database
const KEY_AGE_LIMIT_DAYS = 90;

function checkKeyAge(createdAt) {
const ageInDays = (Date.now() - createdAt) / (1000 * 60 * 60 * 24);

if (ageInDays > KEY_AGE_LIMIT_DAYS) {
console.warn('API key is old! Rotate immediately.');
// Alert admin, create new key in dashboard
}
}

5. Never Log API Keys

❌ Bad:

console.log('Sending with key:', apiKey); // Logs sensitive data!
logger.debug({ apiKey, templateId }); // Exposes key in logs!

✅ Good:

console.log('Generating document...'); // No sensitive data
logger.debug({ templateId, format }); // Safe information only

// If you must log for debugging, redact the key
const redactedKey = apiKey.substring(0, 10) + '***';
console.log('Using key:', redactedKey); // Safe

6. Restrict API Key Permissions

When creating API keys in the dashboard:

  • ✅ Only grant necessary permissions (document generation only)
  • ✅ Set expiration dates
  • ✅ Name keys descriptively ("Production Server", "Staging API")
  • ❌ Don't create overly permissive keys

Error Handling

1. Always Check Response Status

❌ Bad:

const response = await fetch(url, options);
const data = await response.json(); // Might fail if status is 4xx/5xx

✅ Good:

const response = await fetch(url, options);

if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.code} - ${error.message}`);
}

const data = await response.json();

2. Implement Retry Logic with Exponential Backoff

✅ Production-ready retry logic:

async function generateDocumentWithRetry(docData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api.renderdoc.dev/api/v1/documents/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RENDERDOC_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(docData)
});

// Handle rate limiting
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
console.log(`Rate limited. Retrying in ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue;
}

// Handle server errors (5xx) - retry
if (response.status >= 500) {
const delay = Math.min(2 ** attempt * 1000, 30000); // Max 30s
console.log(`Server error. Retrying in ${delay}ms...`);
await sleep(delay);
continue;
}

// Handle client errors (4xx) - don't retry
if (response.status >= 400) {
const error = await response.json();
throw new Error(`${error.code}: ${error.message}`);
}

// Success
return await response.json();
} catch (error) {
// Last attempt - throw error
if (attempt === maxRetries - 1) {
throw error;
}

// Network error - retry with exponential backoff
const delay = Math.min(2 ** attempt * 1000, 30000);
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await sleep(delay);
}
}
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

3. Handle Specific Error Codes

async function handleApiError(error) {
switch (error.code) {
case 'ERR_AUTH_001':
// Invalid API key - check key, don't retry
console.error('Invalid API key. Check RENDERDOC_API_KEY environment variable.');
alertAdmin('API key invalid or expired');
break;

case 'ERR_TMPL_002':
// Template not found - check template ID, don't retry
console.error(`Template not found: ${error.relatedInfo.templateId}`);
break;

case 'ERR_QUOTA_001':
// Document credits exhausted - purchase more credits
console.error('Document credits exhausted');
alertAdmin('Purchase more credits or upgrade plan');
break;

case 'ERR_QUOTA_003':
// Rate limit - retry after specified time
const retryAfter = error.relatedInfo.retryAfterSeconds;
console.log(`Rate limited. Retry after ${retryAfter}s`);
await sleep(retryAfter * 1000);
return generateDocument(docData); // Retry once
break;

default:
// Unknown error - log and alert
console.error('Unexpected error:', error);
alertAdmin(`API error: ${error.code}`);
}
}

4. Log Errors Properly

✅ Structured logging:

function logApiError(error, context) {
logger.error({
timestamp: new Date().toISOString(),
errorCode: error.code,
errorMessage: error.message,
statusCode: error.statusCode,
context: {
templateId: context.templateId,
format: context.format,
// Don't log sensitive variable data
},
stack: error.stack
});
}

Performance Optimization

1. Use Batch Endpoints

❌ Bad - 100 individual requests:

for (const doc of documents) {
await generateDocument({
templateId: 'tmpl_invoice',
format: 'pdf',
variables: doc.variables
});
}
// 100 API calls, 100x the time, 100x rate limit usage

✅ Good - 1 batch request:

await generateDocumentBatch({
templateId: 'tmpl_invoice',
format: 'pdf',
items: documents.map(d => ({
variables: d.variables
}))
});
// 1 API call, much faster, efficient rate limit usage

2. Implement Request Queuing

For high-volume applications, queue requests to avoid rate limits:

class DocumentQueue {
constructor(rateLimitPerMinute = 100) {
this.queue = [];
this.processing = false;
this.rateLimitPerMinute = rateLimitPerMinute;
this.requestTimes = [];
}

enqueue(docData) {
return new Promise((resolve, reject) => {
this.queue.push({ docData, resolve, reject });
this.process();
});
}

async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;

while (this.queue.length > 0) {
// Clean up old request times (>1 minute ago)
const now = Date.now();
this.requestTimes = this.requestTimes.filter(t => now - t < 60000);

// Wait if at rate limit
if (this.requestTimes.length >= this.rateLimitPerMinute) {
const oldestRequest = this.requestTimes[0];
const waitTime = 60000 - (now - oldestRequest);
await sleep(waitTime);
continue;
}

// Process next document
const { docData, resolve, reject } = this.queue.shift();
this.requestTimes.push(now);

try {
const result = await generateDocument(docData);
resolve(result);
} catch (error) {
reject(error);
}
}

this.processing = false;
}
}

// Usage
const queue = new DocumentQueue(100);
const result = await queue.enqueue(docData);

3. Validate Before Generating

Validate data before making API calls to avoid wasted requests:

function validateDocumentData(data) {
// Check required fields
if (!data.templateId) {
throw new Error('Template ID required');
}

if (!data.format) {
throw new Error('Format required (pdf or excel)');
}

// Validate format
const validFormats = ['pdf', 'excel'];
if (!validFormats.includes(data.format)) {
throw new Error(`Invalid format: ${data.format}. Must be pdf or excel.`);
}

// Validate required variables
if (!data.variables) {
throw new Error('Variables object required');
}

return true;
}

// Use it
try {
validateDocumentData(docData); // Validate first
const result = await generateDocument(docData); // Then generate
} catch (error) {
console.error('Validation error:', error.message);
}

4. Cache Template IDs

Store template IDs in your application configuration instead of fetching them repeatedly:

// config/document-templates.js
const DOCUMENT_TEMPLATES = {
INVOICE: process.env.TEMPLATE_ID_INVOICE || 'tmpl_invoice_abc123',
REPORT: process.env.TEMPLATE_ID_REPORT || 'tmpl_report_xyz456',
CERTIFICATE: process.env.TEMPLATE_ID_CERTIFICATE || 'tmpl_certificate_def789',
STATEMENT: process.env.TEMPLATE_ID_STATEMENT || 'tmpl_statement_ghi012',
};

// Usage
await generateDocument({
templateId: DOCUMENT_TEMPLATES.INVOICE,
format: 'pdf',
variables: { ... }
});

Note: Templates are managed in the RenderDoc dashboard. Store template IDs in environment variables for easy configuration across environments.


Document Generation Best Practices

1. Always Use Templates

RenderDoc requires templates for all document generation:

✅ Templates ensure:

  • Consistent branding and formatting
  • Variable validation
  • Reusability across formats (PDF/Excel)
  • Easy updates without code changes

Create templates in dashboard, reference via API:

{
templateId: 'tmpl_invoice',
format: 'pdf',
variables: {
customerName: 'John Doe',
companyName: 'Acme Corp',
invoiceNumber: 'INV-2025-001'
}
}

2. Use Descriptive Template Names

// ❌ Bad
{ templateId: 'tmpl_doc1' }

// ✅ Good
{ templateId: 'tmpl_monthly_invoice' }

3. Provide Complete Variable Data

// ❌ Bad - minimal data
{
variables: {
name: 'User'
}
}

// ✅ Good - complete data
{
variables: {
customerName: 'John Doe',
companyName: 'Acme Corp',
invoiceNumber: 'INV-2025-001',
invoiceDate: '2025-01-07',
lineItems: [
{ description: 'Service A', quantity: 2, price: 50 },
{ description: 'Service B', quantity: 1, price: 100 }
],
totalAmount: 200,
currency: 'USD'
}
}

4. Choose the Right Format

Select the appropriate output format for your use case:

PDF Format (format: 'pdf'):

  • ✅ Invoices, receipts, certificates
  • ✅ Official documents requiring fixed layout
  • ✅ Print-ready documents
  • ✅ Documents with complex styling

Excel Format (format: 'excel'):

  • ✅ Reports with data analysis needs
  • ✅ Documents recipients will edit
  • ✅ Data exports and spreadsheets
  • ✅ Financial statements with formulas
info

Document Credits: Each document generation consumes credits from your account. Monitor your credit balance in the dashboard and purchase additional credits as needed.


Testing

1. Use Test API Keys in Development

const apiKey = process.env.NODE_ENV === 'production'
? process.env.RENDERDOC_API_KEY_PROD
: process.env.RENDERDOC_API_KEY_TEST;

Benefits:

  • Isolated from production metrics
  • Can delete/reset without risk
  • Test without affecting production data

2. Test with Real Data

✅ Production-like test data:

const testDocument = {
templateId: 'tmpl_invoice',
format: 'pdf',
variables: {
customerName: 'John Doe',
companyName: 'Test Corp',
invoiceNumber: 'TEST-INV-001',
invoiceDate: '2025-01-07',
lineItems: [
{ description: 'Test Service', quantity: 1, price: 100 }
],
totalAmount: 100
}
};

3. Test Error Scenarios

// Test with invalid template ID
await expect(generateDocument({ templateId: 'invalid', format: 'pdf' }))
.rejects.toThrow('ERR_TMPL_002');

// Test with missing required variable
await expect(generateDocument({
templateId: 'tmpl_invoice',
format: 'pdf',
variables: {} // Missing required variables
})).rejects.toThrow('ERR_VALID_005');

// Test rate limiting
const promises = Array(105).fill().map(() => generateDocument(testDocument));
const results = await Promise.allSettled(promises);
const rateLimited = results.filter(r => r.reason?.code === 'ERR_QUOTA_003');
expect(rateLimited.length).toBeGreaterThan(0);

4. Test Document Rendering

Preview documents before production:

// Use preview mode for testing
const preview = await generateDocument({
templateId: 'tmpl_invoice',
format: 'pdf',
preview: true, // Preview mode - no credits consumed
variables: testVariables
});

Monitoring and Logging

1. Log All Document Generations

async function generateDocument(docData) {
const startTime = Date.now();

try {
const result = await fetch(url, options);

// Log success
logger.info({
event: 'document_generated',
jobId: result.id,
templateId: docData.templateId,
format: docData.format,
duration: Date.now() - startTime,
timestamp: new Date().toISOString()
});

return result;
} catch (error) {
// Log failure
logger.error({
event: 'document_generation_failed',
templateId: docData.templateId,
format: docData.format,
errorCode: error.code,
errorMessage: error.message,
duration: Date.now() - startTime,
timestamp: new Date().toISOString()
});

throw error;
}
}

2. Monitor API Health

async function checkApiHealth() {
try {
const response = await fetch('https://api.renderdoc.dev/api/health');

if (!response.ok) {
alertAdmin('RenderDoc API is down!');
}
} catch (error) {
alertAdmin('Cannot reach RenderDoc API');
}
}

// Check every 5 minutes
setInterval(checkApiHealth, 300000);

3. Track Success Rates

const metrics = {
generated: 0,
failed: 0,
rateLimited: 0
};

async function generateDocumentWithMetrics(docData) {
try {
const result = await generateDocument(docData);
metrics.generated++;
return result;
} catch (error) {
if (error.code === 'ERR_QUOTA_003') {
metrics.rateLimited++;
} else {
metrics.failed++;
}
throw error;
}
}

// Report metrics every hour
setInterval(() => {
logger.info({
event: 'document_metrics',
generated: metrics.generated,
failed: metrics.failed,
rateLimited: metrics.rateLimited,
successRate: (metrics.generated / (metrics.generated + metrics.failed)) * 100
});

// Reset counters
metrics.generated = 0;
metrics.failed = 0;
metrics.rateLimited = 0;
}, 3600000);

4. Alert on Critical Errors

function alertAdmin(message, severity = 'warning') {
// Send to monitoring service (e.g., Sentry, DataDog, PagerDuty)
if (severity === 'critical') {
// Page on-call engineer
sendSlackAlert(`🚨 CRITICAL: ${message}`);
sendPagerDutyAlert(message);
} else if (severity === 'warning') {
// Log and notify
sendSlackAlert(`⚠️ WARNING: ${message}`);
}

logger.log(severity, message);
}

Security Considerations

1. Sanitize User Input

Always sanitize data before including in documents:

function sanitizeInput(input) {
if (typeof input !== 'string') return input;

// Remove potentially dangerous content
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/javascript:/gi, '');
}

// Use it
const docData = {
templateId: 'tmpl_invoice',
format: 'pdf',
variables: {
customerName: sanitizeInput(user.name),
companyName: sanitizeInput(user.companyName),
// ... sanitize all user-provided data
}
};

2. Validate Variable Data Types

function validateVariables(variables, schema) {
for (const [key, expectedType] of Object.entries(schema)) {
const value = variables[key];
const actualType = typeof value;

if (expectedType === 'array' && !Array.isArray(value)) {
throw new Error(`Variable ${key} should be an array`);
}

if (expectedType !== 'array' && actualType !== expectedType) {
throw new Error(`Variable ${key} should be ${expectedType}, got ${actualType}`);
}
}

return true;
}

// Use it
const schema = {
customerName: 'string',
totalAmount: 'number',
lineItems: 'array'
};
validateVariables(docData.variables, schema);

3. Implement Rate Limiting on Your End

Even though RenderDoc has rate limits, add your own to prevent abuse:

const userRequestCounts = new Map();

function checkUserRateLimit(userId, maxPerHour = 50) {
const now = Date.now();
const oneHourAgo = now - 3600000;

// Get user's request history
let requests = userRequestCounts.get(userId) || [];

// Remove requests older than 1 hour
requests = requests.filter(time => time > oneHourAgo);

// Check limit
if (requests.length >= maxPerHour) {
throw new Error('User document generation limit exceeded');
}

// Record request
requests.push(now);
userRequestCounts.set(userId, requests);
}

Production Checklist

Before deploying to production, verify:

API Configuration

  • ✅ API key stored in environment variable (not hardcoded)
  • ✅ Using production API key (rd_sk_...)
  • ✅ API key has appropriate permissions
  • ✅ API key expiration date set (if applicable)

Error Handling

  • ✅ Retry logic implemented with exponential backoff
  • ✅ All error codes handled appropriately
  • ✅ Errors logged with structured logging
  • ✅ Critical errors trigger alerts

Performance

  • ✅ Using batch endpoints for multiple documents
  • ✅ Request queuing implemented for high volume
  • ✅ Rate limit headers monitored
  • ✅ Input validation before API calls

Security

  • ✅ User input sanitized
  • ✅ Variable data types validated
  • ✅ API keys not logged
  • ✅ HTTPS enforced for all API calls

Monitoring

  • ✅ All document generations logged
  • ✅ Success/failure metrics tracked
  • ✅ Health checks configured
  • ✅ Alerts set up for critical errors

Testing

  • ✅ Tested with production-like data
  • ✅ Error scenarios tested
  • ✅ Rate limiting tested
  • ✅ Document rendering verified

Common Pitfalls

1. Not Handling Rate Limits

❌ Problem:

for (const doc of documents) {
await generateDocument({ ... }); // Will hit rate limit quickly
}

✅ Solution: Use batch endpoints or implement queuing (see Performance Optimization).

2. No Retry Logic

❌ Problem: Single network error causes document generation to fail permanently.

✅ Solution: Implement retry with exponential backoff (see Error Handling).

3. Hardcoded Template IDs

❌ Problem:

const TEMPLATE_ID = 'tmpl_abc123'; // Hardcoded

✅ Solution:

const TEMPLATE_ID = process.env.INVOICE_TEMPLATE_ID;

4. Not Validating Before Generating

❌ Problem: Wasting API calls and credits on invalid data.

✅ Solution: Validate required fields, variable types, and format before API calls.


Next Steps


Related: Generating Documents | Error Handling | Rate Limiting | Authentication