Appearance
Signed URLs Deep Dive
Secure, direct access to generated images without exposing your API key.
How Signed URLs Work
1. Your Server (with API key)
↓
POST /api/signed/generate
↓
2. OG Image API
↓
Returns signed URL with signature + expiration
↓
3. Your Frontend/Email
↓
Uses signed URL directly
↓
4. Browser/Client
↓
GET /api/signed/image?...&sig=xxx&exp=yyy
↓
5. OG Image API
↓
Validates signature → Generates image → Returns PNGSecurity Model
Signature Generation
The signature is generated using:
- All image parameters
- Expiration timestamp
- Your account's secret key (server-side only)
signature = HMAC-SHA256(params + expiration, secret_key)What's Protected
- ✅ API key never exposed to client
- ✅ Parameters can't be tampered with
- ✅ URL expires after set time
- ✅ Signature is unique per parameter combination
What's NOT Protected
- ❌ Anyone with the URL can view the image (until expiration)
- ❌ URL can be shared/forwarded
Use Cases
Email Images
Perfect for marketing emails where you can't make API calls:
javascript
// Generate signed URL server-side
const signedUrl = await getSignedUrl({
title: `Welcome, ${user.name}!`,
subtitle: 'Your account is ready',
template: 'default',
expires_in: 604800 // 7 days
});
// Use in email HTML
const emailHtml = `
<img src="${signedUrl}" alt="Welcome" />
`;Public Sharing
When users share content and you want OG images without server roundtrips:
html
<!-- Generated server-side, used client-side -->
<meta property="og:image" content="https://ogimageapi.io/api/signed/image?title=Hello&sig=abc123&exp=1699999999">Third-Party Integrations
When integrating with services that need direct image URLs:
javascript
// Webhook to third-party service
await sendToSlack({
text: 'New post published!',
image_url: signedUrl
});Expiration Strategies
Short-Lived (1 hour)
For temporary previews:
javascript
const url = await getSignedUrl({
...params,
expires_in: 3600
});Medium-Lived (24 hours)
For social sharing (crawlers need time):
javascript
const url = await getSignedUrl({
...params,
expires_in: 86400
});Long-Lived (7 days)
For email campaigns:
javascript
const url = await getSignedUrl({
...params,
expires_in: 604800
});Very Long-Lived (30 days)
For static content that rarely changes:
javascript
const url = await getSignedUrl({
...params,
expires_in: 2592000
});Implementation Examples
Node.js Service
javascript
// services/og-signed.js
class OGSignedUrlService {
constructor(apiKey, baseUrl = 'https://ogimageapi.io') {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async generateSignedUrl(params) {
const queryString = new URLSearchParams(params).toString();
const response = await fetch(
`${this.baseUrl}/api/signed/generate?${queryString}`,
{
headers: {
'X-API-Key': this.apiKey
}
}
);
if (!response.ok) {
throw new Error(`Failed to generate signed URL: ${response.status}`);
}
const data = await response.json();
return {
url: data.url,
expiresAt: new Date(data.expires_at)
};
}
async generateForPost(post, expiresIn = 86400) {
return this.generateSignedUrl({
title: post.title,
subtitle: post.excerpt,
author_name: post.author.name,
template: 'blog',
theme: 'dark',
expires_in: expiresIn
});
}
}
export const ogSigned = new OGSignedUrlService(process.env.OG_IMAGE_API_KEY);Usage in Next.js
javascript
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
// Generate long-lived signed URL at build time
const { url: ogImageUrl } = await ogSigned.generateForPost(post, 2592000);
return {
props: {
post,
ogImageUrl
},
revalidate: 3600
};
}
export default function BlogPost({ post, ogImageUrl }) {
return (
<>
<Head>
<meta property="og:image" content={ogImageUrl} />
</Head>
{/* ... */}
</>
);
}Caching with Signed URLs
Signed URLs include cache headers, so images are cached:
Cache-Control: public, max-age=86400The cache duration is separate from URL expiration:
- URL expiration: When the signature becomes invalid
- Cache duration: How long CDNs/browsers cache the image
Error Handling
Expired URLs
javascript
// Returns 400 with message
{
"error": "URL expired"
}Handle gracefully:
javascript
async function getImage(signedUrl) {
const response = await fetch(signedUrl);
if (response.status === 400) {
const error = await response.json();
if (error.error === 'URL expired') {
// Regenerate signed URL
return await regenerateSignedUrl();
}
}
return response;
}Invalid Signatures
javascript
// Returns 400 with message
{
"error": "Signature invalid"
}This happens when:
- URL was modified
- Wrong secret key
- Corrupted URL
Best Practices
- Set appropriate expiration — Match your use case
- Don't over-expose — Shorter is safer
- Regenerate on content change — New signature for new content
- Log URL generation — Track usage patterns
- Handle expiration gracefully — Regenerate when needed