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:
  1. Create new key
  2. Update application
  3. Deploy
  4. 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;
}

Performance Optimization

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

Respond Immediately

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

Validate Inputs

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();
}

Sanitize User Input

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:
  • API keys stored in environment variables
  • .env files added to .gitignore
  • Using production API key (bluma_live_*)
  • Error handling implemented for all API calls
  • Retry logic with exponential backoff
  • Rate limit handling configured
  • Webhook signature verification enabled
  • Webhook idempotency implemented
  • Logging configured (errors + info)
  • Monitoring/metrics tracking set up
  • Credit usage alerts configured
  • Integration tests passing
  • Load testing completed
  • Security audit performed
  • Rollback plan documented
  • On-call rotation established
  • Documentation updated

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!