• Skip to main content
  • Skip to navigation
  • Skip to search
    Petunia™
    FeaturesPricingIntegrationsAboutContact
    Log inStart free trialSign up
    Loading
    Petunia™

    Reimagining customer communication for the modern business.

    Product

    • Features
    • Pricing
    • Integrations
    • Roadmap
    • What's New

    Resources

    • Help Center
    • Documentation
    • Guides
    • API Reference
    • Community
    • Support

    Company

    • About Us
    • Careers
    • Blog
    • Press
    • Contact

    © 2026 Gray Group International LLC. All rights reserved.·
    Made by gardenpatch 🌱

    Privacy PolicyTerms of ServiceCookie Policy

    Petunia™ is a trademark of Gray Group International LLC. The Petunia name, brand, product design, and content are proprietary. Unauthorized use, imitation, or copying is prohibited.

    Documentation

    INTERNAL_API_AUTHENTICATION

    docs/features/INTERNAL_API_AUTHENTICATION.md
    Docs homeGuidesSupport
    Quick links
    Start here
    How the docs are organized.
    Environment setup
    Configure env + run locally.
    Unified Inbox
    Inbox concepts & behavior.
    Voice AI setup
    Providers, Twilio, testing.
    Pricing model
    Source-of-truth pricing.
    Operations runbook
    How to operate safely.

    Internal API Authentication

    This document describes the authentication system for internal API routes and cron jobs in Petunia.

    Overview

    Internal APIs (cron jobs, service-to-service calls) require authentication to prevent unauthorized access. We use HMAC-based request signing for secure authentication.

    Authentication Methods

    1. HMAC Signature (Recommended)

    Modern HMAC-SHA256 request signing with replay attack prevention.

    Features:

    • HMAC-SHA256 signatures
    • Timestamp-based replay protection (5-minute window)
    • Body hash verification
    • Timing-safe comparison

    Security Benefits:

    • Prevents replay attacks
    • Verifies request integrity
    • No bearer token in logs
    • Better audit trail

    2. CRON_SECRET (Legacy)

    Simple bearer token authentication for backward compatibility.

    Usage: Authorization: Bearer ${CRON_SECRET}

    Note: This is maintained for backward compatibility but HMAC signatures are recommended for new code.

    Environment Variables

    INTERNAL_API_SECRET (Required)

    Secret key for HMAC request signing.

    Generate:

    openssl rand -base64 32
    

    Add to .env.local:

    INTERNAL_API_SECRET="your-generated-secret-here"
    

    CRON_SECRET (Legacy)

    Bearer token for cron job authentication.

    Note: Supported for backward compatibility. New code should use INTERNAL_API_SECRET.

    Usage

    Protecting an API Route

    import { NextRequest, NextResponse } from 'next/server';
    import { verifyInternalRequest } from '@/lib/middleware/security/internal-api-auth';
    
    export async function GET(request: NextRequest) {
      // Verify this is a legitimate internal request
      const authResult = await verifyInternalRequest(request, {
        mode: 'both', // Accept both HMAC signature and CRON_SECRET
      });
    
      if (authResult) {
        return authResult; // Return 401 error response
      }
    
      // Continue with authenticated request
      return NextResponse.json({ success: true });
    }
    

    Authentication Modes

    Mode: 'signature' (Recommended)

    Only accept HMAC signatures. Most secure option.

    const authResult = await verifyInternalRequest(request, {
      mode: 'signature',
    });
    

    Mode: 'cron-secret' (Legacy)

    Only accept CRON_SECRET bearer token.

    const authResult = await verifyInternalRequest(request, {
      mode: 'cron-secret',
    });
    

    Mode: 'both' (Default, Migration)

    Accept either HMAC signature or CRON_SECRET. Use during migration.

    const authResult = await verifyInternalRequest(request, {
      mode: 'both', // Default
    });
    

    Optional Authentication

    Allow requests without authentication but log when missing:

    const authResult = await verifyInternalRequest(request, {
      mode: 'both',
      required: false, // Don't reject unauthenticated requests
    });
    

    Custom Options

    const authResult = await verifyInternalRequest(request, {
      mode: 'signature',
      maxAge: 2 * 60 * 1000, // 2 minutes (default: 5 minutes)
      required: true,
      unauthorizedMessage: 'Custom error message',
    });
    

    Making Authenticated Requests

    Using HMAC Signature (Recommended)

    import { signRequest } from '@/lib/utils/request-signing';
    
    const secret = process.env.INTERNAL_API_SECRET;
    const body = { action: 'cleanup' };
    
    // Sign the request
    const headers = signRequest(secret, { body });
    
    // Make the request
    const response = await fetch('https://api.petunia.ai/api/cron/cleanup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...headers, // Add signature header
      },
      body: JSON.stringify(body),
    });
    

    Using CRON_SECRET (Legacy)

    const response = await fetch('https://api.petunia.ai/api/cron/cleanup', {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${process.env.CRON_SECRET}`,
      },
    });
    

    Signature Format

    The signature is sent in the X-Internal-Signature header:

    X-Internal-Signature: ts=1234567890,sig=abcdef0123456789...
    

    Components:

    • ts: Unix timestamp in milliseconds
    • sig: HMAC-SHA256 hex signature of timestamp.body.metadata

    Security Considerations

    Replay Attack Prevention

    Signatures expire after 5 minutes (configurable via maxAge). Requests with timestamps older than this are rejected.

    Timing-Safe Comparison

    Signature verification uses timing-safe comparison to prevent timing attacks.

    Body Integrity

    The request body is included in the signature, preventing tampering.

    Secret Rotation

    To rotate the INTERNAL_API_SECRET:

    1. Generate a new secret: openssl rand -base64 32
    2. Update environment variable in production
    3. Update all calling services
    4. Monitor logs for authentication failures
    5. Remove old secret after migration

    Vercel Cron Jobs

    Vercel cron jobs automatically include the CRON_SECRET in the Authorization header when configured in vercel.json:

    {
      "crons": [
        {
          "path": "/api/cron/data-retention",
          "schedule": "0 3 * * *"
        }
      ]
    }
    

    For HMAC signatures in Vercel cron: You'll need to use a separate service to trigger the cron with signed requests, or continue using CRON_SECRET for Vercel-triggered crons.

    Migration Guide

    From CRON_SECRET to HMAC Signatures

    1. Add INTERNAL_API_SECRET to environment:

      openssl rand -base64 32
      
    2. Update route to accept both:

      const authResult = await verifyInternalRequest(request, {
        mode: 'both', // Accept either method
      });
      
    3. Update calling code to use HMAC signatures:

      const headers = signRequest(process.env.INTERNAL_API_SECRET, { body });
      
    4. Test thoroughly with both methods

    5. Switch to signature-only mode:

      const authResult = await verifyInternalRequest(request, {
        mode: 'signature', // Only accept HMAC signatures
      });
      
    6. Remove CRON_SECRET (optional, can keep for Vercel crons)

    Examples

    Example: Data Retention Cron

    // app/api/cron/data-retention/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import { verifyInternalRequest } from '@/lib/middleware/security/internal-api-auth';
    
    export async function GET(request: NextRequest) {
      // Verify authentication
      const authResult = await verifyInternalRequest(request, {
        mode: 'both', // Accept both methods during migration
      });
    
      if (authResult) {
        return authResult;
      }
    
      // Run data retention logic
      // ...
    
      return NextResponse.json({ success: true });
    }
    

    Example: Internal Service Call

    // Service making internal API call
    import { signRequest } from '@/lib/utils/request-signing';
    
    async function triggerCleanup() {
      const secret = process.env.INTERNAL_API_SECRET;
      const body = { dryRun: false };
    
      const headers = signRequest(secret, { body });
    
      const response = await fetch('http://localhost:3000/api/cron/cleanup', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...headers,
        },
        body: JSON.stringify(body),
      });
    
      return response.json();
    }
    

    Testing

    Unit Tests

    import { signRequest, verifyRequest } from '@/lib/utils/request-signing';
    
    test('should sign and verify request', () => {
      const secret = 'test-secret';
      const body = { test: 'data' };
    
      const headers = signRequest(secret, { body });
      const result = verifyRequest(secret, headers['X-Internal-Signature'], { body });
    
      expect(result.valid).toBe(true);
    });
    

    Integration Tests

    import { NextRequest } from 'next/server';
    import { verifyInternalRequest } from '@/lib/middleware/security/internal-api-auth';
    import { signRequest } from '@/lib/utils/request-signing';
    
    test('should authenticate valid request', async () => {
      const secret = process.env.INTERNAL_API_SECRET;
      const headers = signRequest(secret);
    
      const request = new NextRequest('http://localhost/api/cron/test', {
        headers: {
          'X-Internal-Signature': headers['X-Internal-Signature'],
        },
      });
    
      const result = await verifyInternalRequest(request, { mode: 'signature' });
    
      expect(result).toBeNull(); // Null means authenticated
    });
    

    Monitoring

    Logs

    Successful authentication:

    [internal-api-auth] Request authenticated { path: '/api/cron/cleanup', ip: '10.0.0.1', mode: 'both' }
    

    Failed authentication:

    [internal-api-auth] Unauthorized internal API access attempt { path: '/api/cron/cleanup', ip: '1.2.3.4' }
    

    Metrics

    Monitor authentication failures to detect:

    • Configuration issues
    • Potential attacks
    • Secret rotation problems

    Troubleshooting

    "Missing signature header"

    Cause: Request doesn't include X-Internal-Signature header

    Solution: Use signRequest() to generate headers before making request

    "Signature expired"

    Cause: Request timestamp is older than maxAge (default 5 minutes)

    Solutions:

    • Ensure clocks are synchronized
    • Increase maxAge if needed
    • Check for network delays

    "Invalid signature"

    Causes:

    • Wrong secret
    • Body doesn't match
    • Metadata doesn't match

    Solutions:

    • Verify INTERNAL_API_SECRET matches on both sides
    • Ensure body is identical when signing and verifying
    • Include metadata in both sign and verify calls

    "Internal API secret not configured"

    Cause: INTERNAL_API_SECRET environment variable not set

    Solution: Add to .env.local and restart server

    Reference

    Files

    • /lib/utils/request-signing.ts - Signing utilities
    • /lib/middleware/security/internal-api-auth.ts - Authentication middleware
    • /tests/lib/utils/request-signing.test.ts - Signing tests
    • /tests/lib/middleware/security/internal-api-auth.test.ts - Middleware tests

    API

    signRequest(secret, options)

    • Creates HMAC signature for a request
    • Returns headers object with X-Internal-Signature

    verifyRequest(secret, signatureHeader, options)

    • Verifies HMAC signature
    • Returns verification result with valid, error, timestamp, age

    verifyInternalRequest(request, options)

    • Middleware for Next.js API routes
    • Returns null if authenticated, NextResponse error if not
    On this page
    OverviewAuthentication Methods1. HMAC Signature (Recommended)2. CRON_SECRET (Legacy)Environment VariablesINTERNAL_API_SECRET (Required)CRON_SECRET (Legacy)UsageProtecting an API RouteAuthentication ModesOptional AuthenticationCustom OptionsMaking Authenticated RequestsUsing HMAC Signature (Recommended)Using CRON_SECRET (Legacy)Signature FormatSecurity ConsiderationsReplay Attack PreventionTiming-Safe ComparisonBody IntegritySecret RotationVercel Cron JobsMigration GuideFrom CRON_SECRET to HMAC SignaturesExamplesExample: Data Retention CronExample: Internal Service CallTestingUnit TestsIntegration TestsMonitoringLogs