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.
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
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
- Generating Documents: Review API details in Generating Documents
- Error Handling: See all error codes in Error Handling
- Rate Limiting: Understand limits in Rate Limiting
Related: Generating Documents | Error Handling | Rate Limiting | Authentication