Overview
This guide covers battle-tested patterns for building reliable, scalable, and secure integrations with the Bluma API. Follow these practices to avoid common pitfalls and build production-grade applications.
Security
API Key Management
Environment Variables Always store API keys in environment variables// ✅ CORRECT
const apiKey = process . env . BLUMA_API_KEY ;
// ❌ WRONG
const apiKey = 'bluma_live_...' ;
Never Commit Keys Add to .gitignore: .env
.env.local
.env.production
* .key
secrets/
Rotate Regularly Rotate production keys quarterly:
Create new key
Update application
Deploy
Delete old key
Separate Keys Use different keys per environment:
Development: bluma_test_dev
Staging: bluma_test_staging
Production: bluma_live_prod
Webhook Security
Always verify webhook signatures:
import crypto from 'crypto' ;
function verifyWebhook ( payload , signature , secret ) {
const expectedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
// Use constant-time comparison to prevent timing attacks
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( `sha256= ${ expectedSignature } ` )
);
}
app . post ( '/webhooks/bluma' ,
express . raw ({ type: 'application/json' }),
( req , res ) => {
const signature = req . headers [ 'x-bluma-signature' ];
const payload = req . body . toString ();
if ( ! verifyWebhook ( payload , signature , WEBHOOK_SECRET )) {
return res . status ( 401 ). send ( 'Unauthorized' );
}
// Process webhook...
res . sendStatus ( 200 );
}
);
Never skip signature verification in production. Unverified webhooks are a major security risk.
Client-Side Security
Never expose API keys in client-side code
// ❌ WRONG - API key exposed to users
const response = await fetch ( 'https://api.getbluma.com/api/v1/videos' , {
headers: {
'Authorization' : `Bearer ${ BLUMA_API_KEY } ` // Visible in browser!
}
});
Solution: Proxy requests through your backend:
// ✅ CORRECT - Frontend
const response = await fetch ( '/api/videos' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ template_id: 'meme-dialogue' , context: { ... } })
});
// ✅ CORRECT - Backend
app . post ( '/api/videos' , async ( req , res ) => {
// Verify user is authenticated
if ( ! req . user ) {
return res . status ( 401 ). send ( 'Unauthorized' );
}
// Call Bluma API with server-side key
const response = await fetch ( 'https://api.getbluma.com/api/v1/videos' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . BLUMA_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ( req . body )
});
const data = await response . json ();
res . json ( data );
});
Error Handling
Comprehensive Error Handling
async function generateVideo ( templateId , context ) {
try {
const response = await fetch ( 'https://api.getbluma.com/api/v1/videos' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . BLUMA_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
template_id: templateId ,
context
})
});
// Handle non-2xx responses
if ( ! response . ok ) {
const error = await response . json ();
throw new BlumaAPIError ( error );
}
return await response . json ();
} catch ( error ) {
// Network errors
if ( error . code === 'ECONNREFUSED' ) {
throw new Error ( 'Cannot connect to Bluma API. Please check your internet connection.' );
}
// API errors
if ( error instanceof BlumaAPIError ) {
switch ( error . status ) {
case 401 :
throw new Error ( 'Invalid API key. Please check your configuration.' );
case 402 :
throw new Error ( 'Insufficient credits. Please purchase more at getbluma.com/billing' );
case 429 :
throw new Error ( 'Rate limit exceeded. Please slow down requests.' );
case 500 :
throw new Error ( 'Bluma API error. Please try again later.' );
default :
throw error ;
}
}
throw error ;
}
}
class BlumaAPIError extends Error {
constructor ( errorResponse ) {
super ( errorResponse . error . detail );
this . name = 'BlumaAPIError' ;
this . status = errorResponse . error . status ;
this . type = errorResponse . error . type ;
this . title = errorResponse . error . title ;
}
}
Retry Logic with Exponential Backoff
async function retryWithBackoff ( fn , maxRetries = 3 ) {
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
return await fn ();
} catch ( error ) {
// Don't retry client errors (4xx except 429)
if ( error . status >= 400 && error . status < 500 && error . status !== 429 ) {
throw error ;
}
// Last attempt - throw error
if ( attempt === maxRetries - 1 ) {
throw error ;
}
// Exponential backoff: 1s, 2s, 4s, 8s...
const delay = Math . min ( 1000 * Math . pow ( 2 , attempt ), 10000 );
console . log ( `Retry attempt ${ attempt + 1 } after ${ delay } ms` );
await new Promise ( resolve => setTimeout ( resolve , delay ));
}
}
}
// Usage
const video = await retryWithBackoff (() =>
generateVideo ( 'meme-dialogue' , { prompt: 'Funny cat video' })
);
Handling Rate Limits
async function handleRateLimit ( response ) {
if ( response . status === 429 ) {
const retryAfter = parseInt ( response . headers . get ( 'Retry-After' ) || '60' );
console . log ( `Rate limited. Retrying in ${ retryAfter } seconds...` );
await new Promise ( resolve => setTimeout ( resolve , retryAfter * 1000 ));
// Retry the request
return true ;
}
return false ;
}
async function apiCall ( url , options ) {
let response = await fetch ( url , options );
if ( await handleRateLimit ( response )) {
// Retry after rate limit delay
response = await fetch ( url , options );
}
return response ;
}
Request Batching
Instead of making many sequential requests:
// ❌ SLOW - Sequential requests
for ( const template of templates ) {
await generateVideo ( template , context );
}
Batch them in parallel:
// ✅ FAST - Parallel requests
const promises = templates . map ( template =>
generateVideo ( template , context )
);
const videos = await Promise . all ( promises );
Respect rate limits when batching. If you hit rate limits, use a queue-based approach.
Request Queue
For high-volume applications, implement a queue:
import Queue from 'bull' ; // or p-queue, bottleneck, etc.
const videoQueue = new Queue ( 'video-generation' , {
redis: process . env . REDIS_URL ,
limiter: {
max: 50 , // Max 50 requests
duration: 3600000 // Per hour (matches Starter tier)
}
});
// Add jobs to queue
videoQueue . add ({
template_id: 'meme-dialogue' ,
context: { prompt: 'Funny video' }
});
// Process queue
videoQueue . process ( async ( job ) => {
const { template_id , context } = job . data ;
return await generateVideo ( template_id , context );
});
// Monitor progress
videoQueue . on ( 'completed' , ( job , result ) => {
console . log ( `Video ${ result . id } completed` );
});
videoQueue . on ( 'failed' , ( job , error ) => {
console . error ( `Video generation failed:` , error );
});
Caching
Cache frequently accessed data:
import NodeCache from 'node-cache' ;
const cache = new NodeCache ({ stdTTL: 3600 }); // 1 hour TTL
async function getTemplates () {
// Check cache first
const cached = cache . get ( 'templates' );
if ( cached ) {
return cached ;
}
// Fetch from API
const response = await fetch ( 'https://api.getbluma.com/api/v1/templates' , {
headers: { 'Authorization' : `Bearer ${ API_KEY } ` }
});
const templates = await response . json ();
// Cache for next time
cache . set ( 'templates' , templates );
return templates ;
}
What to cache:
✅ Template list (changes rarely)
✅ Your credit balance (update every 5-10 minutes)
✅ Template details (static information)
What NOT to cache:
❌ Video status (needs to be real-time)
❌ Download URLs (expire after 1 hour)
❌ API keys
Webhook Best Practices
app . post ( '/webhooks/bluma' ,
express . raw ({ type: 'application/json' }),
async ( req , res ) => {
// Verify signature
if ( ! verifySignature ( req . body . toString (), req . headers [ 'x-bluma-signature' ])) {
return res . status ( 401 ). send ( 'Unauthorized' );
}
const event = JSON . parse ( req . body . toString ());
// ✅ Respond immediately
res . sendStatus ( 200 );
// ⏳ Process asynchronously
processEventAsync ( event ). catch ( error => {
console . error ( 'Event processing failed:' , error );
});
}
);
async function processEventAsync ( event ) {
// Long-running tasks here
// - Download video
// - Send notifications
// - Update database
}
Implement Idempotency
// Using Redis for deduplication
import { createClient } from 'redis' ;
const redis = createClient ({ url: process . env . REDIS_URL });
await redis . connect ();
app . post ( '/webhooks/bluma' ,
express . raw ({ type: 'application/json' }),
async ( req , res ) => {
const event = JSON . parse ( req . body . toString ());
// Check if already processed
const processed = await redis . get ( `webhook: ${ event . id } ` );
if ( processed ) {
console . log ( `Duplicate webhook ${ event . id } , skipping` );
return res . sendStatus ( 200 ); // Still return 200!
}
// Mark as processed (expire after 24 hours)
await redis . setex ( `webhook: ${ event . id } ` , 86400 , 'true' );
// Process event
await processEvent ( event );
res . sendStatus ( 200 );
}
);
Monitor Delivery Failures
async function checkWebhookHealth () {
const response = await fetch (
'https://api.getbluma.com/api/v1/webhooks/webhook_abc123/deliveries' ,
{
headers: { 'Authorization' : `Bearer ${ API_KEY } ` }
}
);
const { deliveries } = await response . json ();
// Check for consecutive failures
const failures = deliveries
. filter ( d => d . status_code >= 400 )
. slice ( 0 , 5 );
if ( failures . length >= 3 ) {
// Alert team
await sendAlert ( 'Webhook deliveries failing!' );
}
}
// Run health check hourly
setInterval ( checkWebhookHealth , 3600000 );
Monitoring & Logging
Structured Logging
import winston from 'winston' ;
const logger = winston . createLogger ({
level: 'info' ,
format: winston . format . json (),
transports: [
new winston . transports . File ({ filename: 'error.log' , level: 'error' }),
new winston . transports . File ({ filename: 'combined.log' })
]
});
async function generateVideo ( templateId , context ) {
const startTime = Date . now ();
logger . info ( 'Video generation started' , {
template_id: templateId ,
context_length: JSON . stringify ( context ). length
});
try {
const response = await fetch ( 'https://api.getbluma.com/api/v1/videos' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({ template_id: templateId , context })
});
if ( ! response . ok ) {
const error = await response . json ();
logger . error ( 'Video generation failed' , {
template_id: templateId ,
error_type: error . error . type ,
error_detail: error . error . detail ,
status_code: error . error . status
});
throw new Error ( error . error . detail );
}
const video = await response . json ();
const duration = Date . now () - startTime ;
logger . info ( 'Video generation completed' , {
video_id: video . id ,
template_id: templateId ,
duration_ms: duration ,
credits_charged: video . credits_charged
});
return video ;
} catch ( error ) {
logger . error ( 'Video generation exception' , {
template_id: templateId ,
error_message: error . message ,
stack: error . stack
});
throw error ;
}
}
Metrics Tracking
import StatsD from 'hot-shots' ;
const statsd = new StatsD ({
host: process . env . STATSD_HOST ,
prefix: 'bluma.'
});
async function generateVideo ( templateId , context ) {
const startTime = Date . now ();
statsd . increment ( 'video.generation.started' );
try {
const video = await blumaAPI . generateVideo ( templateId , context );
statsd . timing ( 'video.generation.duration' , Date . now () - startTime );
statsd . increment ( 'video.generation.success' );
statsd . gauge ( 'video.generation.credits' , video . credits_charged );
return video ;
} catch ( error ) {
statsd . increment ( 'video.generation.error' );
statsd . increment ( `video.generation.error. ${ error . status || 'unknown' } ` );
throw error ;
}
}
Usage Monitoring
async function monitorUsage () {
const response = await fetch (
'https://api.getbluma.com/api/v1/credits/balance' ,
{
headers: { 'Authorization' : `Bearer ${ API_KEY } ` }
}
);
const { credits , monthly_allowance } = await response . json ();
const percentUsed = (( monthly_allowance - credits ) / monthly_allowance ) * 100 ;
// Alert if running low
if ( percentUsed > 90 ) {
await sendAlert ( `⚠️ CRITICAL: 90%+ of credits used ( ${ credits } remaining)` );
} else if ( percentUsed > 75 ) {
await sendAlert ( `⚠️ WARNING: 75%+ of credits used ( ${ credits } remaining)` );
}
// Track metric
statsd . gauge ( 'bluma.credits.remaining' , credits );
statsd . gauge ( 'bluma.credits.percent_used' , percentUsed );
}
// Check every 30 minutes
setInterval ( monitorUsage , 1800000 );
Data Validation
import { z } from 'zod' ;
const videoRequestSchema = z . object ({
template_id: z . enum ([
'meme-dialogue' ,
'ugc-text-overlay' ,
'ugc-review' ,
'news-anchor'
]),
context: z . object ({
prompt: z . string (). min ( 10 ). max ( 500 )
})
});
async function generateVideo ( data ) {
// Validate input
const validated = videoRequestSchema . parse ( data );
// Call API with validated data
const response = await fetch ( 'https://api.getbluma.com/api/v1/videos' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ( validated )
});
return response . json ();
}
import DOMPurify from 'isomorphic-dompurify' ;
function sanitizePrompt ( userPrompt ) {
// Remove HTML/scripts
const clean = DOMPurify . sanitize ( userPrompt , { ALLOWED_TAGS: [] });
// Limit length
const truncated = clean . slice ( 0 , 500 );
// Remove excessive whitespace
const normalized = truncated . replace ( / \s + / g , ' ' ). trim ();
return normalized ;
}
// Usage
const userPrompt = req . body . prompt ;
const safePrompt = sanitizePrompt ( userPrompt );
await generateVideo ( 'meme-dialogue' , { prompt: safePrompt });
Configuration Management
Environment-Based Config
// config/index.js
const environments = {
development: {
apiKey: process . env . BLUMA_TEST_KEY ,
apiUrl: 'https://api.getbluma.com/api/v1' ,
webhookSecret: process . env . BLUMA_WEBHOOK_SECRET_TEST ,
logLevel: 'debug' ,
retryAttempts: 3
},
production: {
apiKey: process . env . BLUMA_LIVE_KEY ,
apiUrl: 'https://api.getbluma.com/api/v1' ,
webhookSecret: process . env . BLUMA_WEBHOOK_SECRET_PROD ,
logLevel: 'info' ,
retryAttempts: 5
}
};
const env = process . env . NODE_ENV || 'development' ;
const config = environments [ env ];
// Validation
if ( ! config . apiKey ) {
throw new Error ( `BLUMA_ ${ env === 'production' ? 'LIVE' : 'TEST' } _KEY is required` );
}
export default config ;
Feature Flags
const features = {
useWebhooks: process . env . FEATURE_WEBHOOKS === 'true' ,
enableBatching: process . env . FEATURE_BATCHING === 'true' ,
cacheTemplates: process . env . FEATURE_CACHE_TEMPLATES === 'true'
};
async function generateVideo ( templateId , context ) {
if ( features . enableBatching ) {
return await queueVideoGeneration ( templateId , context );
} else {
return await generateVideoSync ( templateId , context );
}
}
Testing
Integration Tests
import { describe , it , expect , beforeAll } from '@jest/globals' ;
describe ( 'Bluma API Integration' , () => {
let testApiKey ;
beforeAll (() => {
testApiKey = process . env . BLUMA_TEST_KEY ;
if ( ! testApiKey ) {
throw new Error ( 'BLUMA_TEST_KEY required for tests' );
}
});
it ( 'should generate a video' , async () => {
const response = await fetch ( 'https://api.getbluma.com/api/v1/videos' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ testApiKey } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
template_id: 'meme-dialogue' ,
context: { prompt: 'Test video for integration tests' }
})
});
expect ( response . status ). toBe ( 200 );
const video = await response . json ();
expect ( video ). toHaveProperty ( 'id' );
expect ( video ). toHaveProperty ( 'status' );
expect ( video . status ). toBe ( 'queued' );
});
it ( 'should handle invalid template gracefully' , async () => {
const response = await fetch ( 'https://api.getbluma.com/api/v1/videos' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ testApiKey } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
template_id: 'invalid-template' ,
context: { prompt: 'Test' }
})
});
expect ( response . status ). toBe ( 400 );
const error = await response . json ();
expect ( error . error . type ). toBe ( 'validation_error' );
});
it ( 'should handle rate limits' , async () => {
// Make many requests to trigger rate limit
const promises = Array . from ({ length: 200 }, () =>
fetch ( 'https://api.getbluma.com/api/v1/templates' , {
headers: { 'Authorization' : `Bearer ${ testApiKey } ` }
})
);
const responses = await Promise . all ( promises );
const rateLimited = responses . some ( r => r . status === 429 );
// If rate limited, check headers
const limitedResponse = responses . find ( r => r . status === 429 );
if ( limitedResponse ) {
expect ( limitedResponse . headers . get ( 'Retry-After' )). toBeTruthy ();
}
});
});
Mock for Unit Tests
// __mocks__/bluma-api.js
export class MockBlumaAPI {
async generateVideo ( templateId , context ) {
return {
id: 'mock_video_123' ,
status: 'completed' ,
template_id: templateId ,
url: 'https://cdn.getbluma.com/videos/mock_video_123.mp4' ,
credits_charged: 5
};
}
async getVideo ( videoId ) {
return {
id: videoId ,
status: 'completed' ,
url: `https://cdn.getbluma.com/videos/ ${ videoId } .mp4`
};
}
}
// Usage in tests
import { MockBlumaAPI } from './__mocks__/bluma-api' ;
describe ( 'Video Service' , () => {
it ( 'should process completed videos' , async () => {
const api = new MockBlumaAPI ();
const video = await api . generateVideo ( 'meme-dialogue' , { prompt: 'Test' });
expect ( video . status ). toBe ( 'completed' );
});
});
Deployment Checklist
Before deploying to production:
Common Anti-Patterns
❌ Polling Too Frequently
// ❌ DON'T DO THIS
setInterval ( async () => {
const video = await getVideoStatus ( videoId );
if ( video . status === 'completed' ) {
// Process video
}
}, 1000 ); // Checking every second wastes API calls!
Solution: Use webhooks or poll every 5-10 seconds:
// ✅ BETTER
const checkInterval = setInterval ( async () => {
const video = await getVideoStatus ( videoId );
if ( video . status === 'completed' ) {
clearInterval ( checkInterval );
// Process video
}
}, 5000 ); // Every 5 seconds
❌ Not Handling Async Errors
// ❌ DON'T DO THIS
app . post ( '/generate' , ( req , res ) => {
generateVideo ( req . body . template_id , req . body . context );
res . send ( 'Started' ); // What if generateVideo fails?
});
Solution: Always handle promise rejections:
// ✅ CORRECT
app . post ( '/generate' , async ( req , res ) => {
try {
const video = await generateVideo ( req . body . template_id , req . body . context );
res . json ( video );
} catch ( error ) {
console . error ( 'Video generation failed:' , error );
res . status ( 500 ). json ({ error: error . message });
}
});
❌ Ignoring Rate Limits
// ❌ DON'T DO THIS
for ( let i = 0 ; i < 1000 ; i ++ ) {
await generateVideo ( 'meme-dialogue' , { prompt: `Video ${ i } ` });
}
// Will hit rate limit immediately!
Solution: Use a queue or check rate limit headers:
// ✅ CORRECT
import pLimit from 'p-limit' ;
const limit = pLimit ( 10 ); // Max 10 concurrent requests
const promises = Array . from ({ length: 1000 }, ( _ , i ) =>
limit (() => generateVideo ( 'meme-dialogue' , { prompt: `Video ${ i } ` }))
);
await Promise . all ( promises );
Next Steps
Summary
Following these best practices will help you build:
Secure integrations that protect API keys and verify webhooks
Reliable systems with proper error handling and retries
Scalable applications that respect rate limits and use queuing
Observable services with comprehensive logging and monitoring
Maintainable codebases with clean patterns and tests
Remember: Test thoroughly in the test environment before deploying to production!