expired_signature

The request is a signed URL, and the time in its expires parameter has passed. The signature may be perfect, but the API no longer accepts it. You cannot fix this URL: you must sign a new one. This page is part of the error code reference.

HTTP status 401 Unauthorized
error.type authentication_error
error.retryable false
Credits used 0

Example response

{
  "error": {
    "type": "authentication_error",
    "code": "expired_signature",
    "message": "Signed URL has expired",
    "retryable": false,
    "docs": "https://docs.renderscreenshot.com/errors/expired_signature"
  },
  "request_id": "req_abc123"
}

The API checks the expiry before it checks the signature. An expired URL reports expired_signature even if its signature is also wrong. After you fix the expiry, a remaining signature problem shows up as invalid_signature.

Common causes

  • A URL signed once and stored. The URL was generated at deploy time, or saved in a database or a static HTML file, and it outlived its expiry.
  • Social crawlers that come back later. You put a signed URL in og:image. The first share worked. Days later a platform fetches the image again, or a new post re-uses the cached page, and the URL has expired.
  • Seconds and milliseconds. expires is a Unix timestamp in seconds. Date.now() in JavaScript returns milliseconds, which gives a value about 1,000 times too large.
  • A duration instead of a timestamp. expires=3600 means one hour after 1 January 1970, not one hour from now.
  • Clock drift. The server that signs the URL has the wrong time. Keep it in sync with NTP.

Check a URL

Paste a failing URL into this script to see which case you have:

// check-expiry.mjs (Node 22+). Usage: node check-expiry.mjs '<signed URL>'
const signed = new URL(process.argv[2]);
const expires = Number(signed.searchParams.get('expires'));
const now = Math.floor(Date.now() / 1000);
const MAX_LIFETIME = 30 * 24 * 3600; // documented maximum: 30 days

if (!Number.isInteger(expires) || expires <= 0) {
  console.log('No valid expires parameter. Sign the URL again.');
} else if (expires > 1e11) {
  console.log('expires looks like milliseconds. Divide by 1000 before signing.');
} else if (expires < now) {
  console.log(`Expired ${now - expires} s ago, at ${new Date(expires * 1000).toISOString()}.`);
} else if (expires - now > MAX_LIFETIME) {
  console.log('Expiry is more than 30 days ahead. Use a shorter lifetime.');
} else {
  console.log(`Valid for ${Math.round((expires - now) / 3600)} more hours. The expiry is not the problem.`);
}

How to fix it

  1. Generate signed URLs when you render the HTML that contains them, not once at build time. Each page view then carries a URL with a full lifetime.
  2. Choose a lifetime that covers how long the URL must work after the page is served. For og:image, a longer lifetime (up to 30 days) covers crawlers that fetch later.
  3. For an image that must work for longer than 30 days, do not use a signed URL. Use the public CDN link from the X-Cache-URL header, or store a copy yourself.
  4. Do not retry the same URL. It returns the same error every time.

This Ruby helper signs a new og_card URL each time a page renders. The rs_secret_ key stays on the server; only the rs_pub_ key ID appears in the URL.

require 'renderscreenshot'

SIGNER = RenderScreenshot::Client.new(
  ENV.fetch('RS_API_KEY'),
  signing_key: ENV.fetch('RS_SECRET_KEY'),     # rs_secret_..., server side only
  public_key_id: ENV.fetch('RS_PUBLIC_KEY_ID') # rs_pub_...
)

OG_LIFETIME = 7 * 24 * 3600 # seconds; must stay at or under 30 days

# Call this while you render the HTML page, never once at deploy time.
def og_image_url(page_url)
  SIGNER.generate_url(
    RenderScreenshot::TakeOptions.url(page_url).preset('og_card'),
    expires_at: Time.now + OG_LIFETIME
  )
end

puts og_image_url(ARGV.fetch(0))

In a Rails view, that becomes <meta property="og:image" content="<%= og_image_url(request.original_url) %>">.

Tested on 2026-10-15 with Ruby 4.0.7 plus the renderscreenshot gem 1.0.0, and Node 22.18, against a local mock. The generated URL had expires 168 hours ahead and returned 200. The same URL with a past timestamp returned the expired_signature envelope above, and the check script reported the expired, millisecond and over-30-day cases correctly.

  • Signed URLs for the signing method.
  • Signed URLs for OG images for a full meta tag setup.
  • invalid_signature when the expiry is fine but the signature does not match.

Related in this topic

Was this page helpful?