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.
expiresis 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=3600means 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
- 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.
- 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. - 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-URLheader, or store a copy yourself. - 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.
Related
- 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.