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.
key=sk_quick_123abc) — identifies the user account (for credit deduction)s=<hmac>&t=<timestamp>) — proves the request came from your websiteBoth 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_secretfield
Example Implementations
For complete tutorials with multiple frameworks and patterns, see:
- JavaScript/Node.js Tutorial - Next.js, Express, vanilla Node.js
- PHP/Laravel Tutorial - Coming soon
- Python Tutorial - Coming soon
JavaScript (Node.js / Frontend)
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)
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
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
{{-- 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
Error Responses
Missing Signature Parameters
{
"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
{
"error": "Invalid signature format",
"code": "invalid_signature_format",
"expected": "64-character hex string"
}Expired Timestamp (>5 minutes)
{
"error": "Request timestamp expired",
"code": "expired_timestamp",
"max_age_seconds": 300
}Invalid Signature
{
"error": "Invalid signature",
"code": "invalid_signature"
}Missing API Key
{
"error": "API key not found",
"code": "missing_api_key"
}Testing
Generate a test URL:
# 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
method + path + timestamp + apiKey (no separators)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
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>Testing with curl
# 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