Skip to content

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 PNG

Security 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=86400

The 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

  1. Set appropriate expiration — Match your use case
  2. Don't over-expose — Shorter is safer
  3. Regenerate on content change — New signature for new content
  4. Log URL generation — Track usage patterns
  5. Handle expiration gracefully — Regenerate when needed

Generate stunning Open Graph images programmatically.