HMAC Signature Guide

This HMAC signature guide walks you through building HMAC-SHA256 signatures for FastOG, so you can generate dynamic Open Graph images on the fly — credit-based, with a free account and 100 starter credits. Hand-roll the signature in any language here, or use the Free OG Image Tester to see the URL format in action first.

Overview

All requests to /api/v1/og must include an HMAC-SHA256 signature to prove they originated from the legitimate website. This prevents unauthorized parties from using your API credits.

Two layers of authentication:
  • API Key (in query: key=sk_quick_123abc) — identifies the user account (for credit deduction)
  • HMAC Signature (in query: s=<hmac>&t=<timestamp>) — proves the request came from your website
  • Both must pass. API key alone is NOT sufficient.

    Why query parameters? OG images are fetched by social network crawlers (LinkedIn, Twitter, Facebook) that don't send custom headers. The signature MUST be in the URL.

    Generating the Signature

    Algorithm

    timestamp = current Unix timestamp (seconds)
    method = HTTP method (e.g., "GET")
    path = request path (e.g., "/api/v1/og")
    apiKey = the full API key (e.g., "sk_quick_123abc")
    
    message = method + path + timestamp + apiKey
    signature = HMAC-SHA256(message, hmac_secret)
    
    Query parameters:
      key=<apiKey>
      s=<signature>
      t=<timestamp>

    Requirements

    • Timestamp must be within 5 minutes of the server time (prevents replay attacks)
    • Signature format: 64-character hex string (SHA256)
    • Use hash_equals() for constant-time comparison (prevents timing attacks)
    • Per-user secrets: Each API key has its own hmac_secret field

    Example Implementations

    For complete tutorials with multiple frameworks and patterns, see:

    JavaScript (Node.js / Frontend)

    javascript
    const crypto = require("crypto");
    
    function generateOgImageUrl(baseUrl, params, apiKey, hmacSecret) {
        const timestamp = Math.floor(Date.now() / 1000);
        const url = new URL(baseUrl);
    
        // Add template and content params
        for (const [key, value] of Object.entries(params)) {
            url.searchParams.set(key, value);
        }
    
        // Add auth params
        url.searchParams.set("key", apiKey);
        url.searchParams.set("t", timestamp);
    
        // Compute signature: method + path + timestamp + apiKey
        const path = url.pathname;
        const message = `GET${path}${timestamp}${apiKey}`;
        const signature = crypto
            .createHmac("sha256", hmacSecret)
            .update(message)
            .digest("hex");
    
        url.searchParams.set("s", signature);
        return url.toString();
    }
    
    // Usage in meta tag:
    const ogUrl = generateOgImageUrl(
        "https://fastog.com/api/v1/og",
        { title: "Como criar um SaaS", template: "default" },
        "sk_quick_123abc",
        process.env.HMAC_SECRET,
    );
    
    // <meta property="og:image" content="{ogUrl}" />

    PHP (Laravel)

    php
    function generateOgImageUrl(string $baseUrl, array $params, string $apiKey, string $hmacSecret): string
    {
        $timestamp = time();
        $url = $baseUrl . '?' . http_build_query($params);
    
        // Parse URL to get path
        $parsed = parse_url($baseUrl);
        $path = $parsed['path'];
    
        // Compute signature: method + path + timestamp + apiKey
        $message = 'GET' . $path . $timestamp . $apiKey;
        $signature = hash_hmac('sha256', $message, $hmacSecret);
    
        // Add auth params
        $separator = strpos($url, '?') === false ? '?' : '&';
        $url .= $separator . http_build_query([
            'key' => $apiKey,
            's' => $signature,
            't' => $timestamp,
        ]);
    
        return $url;
    }
    
    // Usage in Blade template:
    $ogUrl = generateOgImageUrl(
        'https://fastog.com/api/v1/og',
        ['title' => 'Como criar um SaaS'],
        'sk_quick_123abc',
        env('HMAC_SECRET')
    );
    
    // <meta property="og:image" content="{{ $ogUrl }}" />

    Python

    python
    import hmac
    import hashlib
    import time
    from urllib.parse import urlencode, urlparse
    
    def generate_og_image_url(base_url, params, api_key, hmac_secret):
        timestamp = int(time.time())
    
        # Parse URL to get path
        parsed = urlparse(base_url)
        path = parsed.path
    
        # Compute signature: method + path + timestamp + apiKey
        message = f"GET{path}{timestamp}{api_key}"
        signature = hmac.new(
            hmac_secret.encode(),
            message.encode(),
            hashlib.sha256
        ).hexdigest()
    
        # Build query params
        query_params = {**params, 'key': api_key, 's': signature, 't': timestamp}
    
        return f"{base_url}?{urlencode(query_params)}"
    
    # Usage:
    og_url = generate_og_image_url(
        'https://fastog.com/api/v1/og',
        {'title': 'Como criar um SaaS', 'template': 'default'},
        'sk_quick_123abc',
        os.environ['HMAC_SECRET']
    )
    
    # <meta property="og:image" content="{{ og_url }}" />

    Complete Example: Laravel Blade Template

    blade
    {{-- In your blog post view --}}
    @php
        $apiKey = 'sk_quick_123abc'; // Your full API key
        $hmacSecret = env('OG_RENDER_HMAC_SECRET');
        $timestamp = time();
        $path = '/api/v1/og';
        $message = 'GET' . $path . $timestamp . $apiKey;
        $signature = hash_hmac('sha256', $message, $hmacSecret);
    
        $ogImageUrl = 'https://fastog.com/api/v1/og?' . http_build_query([
            'template' => 'blog',
            'title' => $post->title,
            'subtitle' => $post->excerpt,
            'key' => $apiKey,
            's' => $signature,
            't' => $timestamp,
        ]);
    @endphp
    
    <head>
        <meta property="og:image" content="{{ $ogImageUrl }}" />
        <meta property="og:title" content="{{ $post->title }}" />
        <meta property="og:description" content="{{ $post->excerpt }}" />
    </head>

    Security Best Practices

  • Store secrets securely - Use environment variables or a secrets manager
  • Never expose secrets in client-side code - Sign URLs on the server only
  • Use HTTPS - Prevents MITM attacks
  • Rotate secrets periodically - Especially if compromised
  • Monitor failed signatures - Repeated failures may indicate attacks
  • Per-user isolation - Each API key has its own HMAC secret
  • Error Responses

    Missing Signature Parameters

    json
    {
        "error": "Missing signature parameters",
        "code": "missing_signature",
        "required_params": ["s", "t", "key"],
        "example": "/api/v1/og?title=Hello&key=sk_quick_123abc&s=<hmac>&t=<timestamp>"
    }

    Invalid Signature Format

    json
    {
        "error": "Invalid signature format",
        "code": "invalid_signature_format",
        "expected": "64-character hex string"
    }

    Expired Timestamp (>5 minutes)

    json
    {
        "error": "Request timestamp expired",
        "code": "expired_timestamp",
        "max_age_seconds": 300
    }

    Invalid Signature

    json
    {
        "error": "Invalid signature",
        "code": "invalid_signature"
    }

    Missing API Key

    json
    {
        "error": "API key not found",
        "code": "missing_api_key"
    }

    Testing

    Generate a test URL:

    bash
    # Node.js
    node -e "
    const crypto = require('crypto');
    const ts = Math.floor(Date.now() / 1000);
    const msg = 'GET/api/v1/og' + ts + 'sk_quick_123abc';
    const sig = crypto.createHmac('sha256', 'YOUR_HMAC_SECRET').update(msg).digest('hex');
    console.log('https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=' + sig + '&t=' + ts);
    "
    
    # Test with curl
    curl "https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=<signature>&t=<timestamp>"

    Expected responses:

    • ✅ Valid signature → Returns OG image (or validation error if template params missing)
    • ❌ Expired timestamp → {"error": "...", "code": "expired_timestamp"}
    • ❌ Invalid signature → {"error": "...", "code": "invalid_signature"}
    • ❌ Missing params → {"error": "...", "code": "missing_signature"}

    Troubleshooting

    Signature doesn't match

  • Verify you're using the correct HMAC secret (check API key in dashboard)
  • Ensure timestamp is current Unix timestamp in seconds (not milliseconds)
  • Check message format: method + path + timestamp + apiKey (no separators)
  • Verify signature is lowercase hex (64 characters)
  • Timestamp expired

    • Ensure your server clock is synchronized (use NTP)
    • Generate timestamp immediately before making the request
    • Maximum allowed drift: 5 minutes

    API key not found

    • Verify you're using the full API key (e.g. sk_quick_123abc)
    • Check the API key exists and belongs to your user
    • Ensure the key hasn't been revoked

    Getting Your HMAC Secret

  • Go to Dashboard → API Keys
  • Create a new API key (or use an existing one)
  • Copy the HMAC Secret shown alongside the API key (only shown once!)
  • Store it securely in your environment variables
  • bash
    # Add to .env
    OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>

    Testing with curl

    bash
    # Bash script to generate signed URL
    SECRET="your-hmac-secret-here"
    API_KEY="sk_quick_123abc"  # Your full API key
    TIMESTAMP=$(date +%s)
    METHOD="GET"
    PATH="/api/v1/og"
    
    MESSAGE="${METHOD}${PATH}${TIMESTAMP}${API_KEY}"
    SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
    
    curl "http://localhost:8000/api/v1/og?template=default&title=Test&key=${API_KEY}&s=${SIGNATURE}&t=${TIMESTAMP}"

    Expected: 200 OK with image data (or 422 if template params are invalid).

    Full API Reference

    Related

    Try it free — no signup required

    Preview and test dynamic OG images in seconds.

    Open the Free OG Image Tester