Every successful screenshot response carries an X-Cache-URL header: a public CDN link to the file that was just rendered. You can put that link in an <img> tag, an email or an og:image tag with no API key and no signature. This guide explains how long the link lives, what the TTL changes, how it compares with signed URLs and your own storage, and a renewal pattern for links that must stay up. It is part of the guides; the rules for the cache itself are in the caching reference.
Step by step: Show link previews in your app and Reuse screenshots for 0 credits walk through this task with screenshots of every step.
Tested on 2026-10-18: the Node.js, Python, Ruby, PHP and Go listings ran against a local stub that returns the API's JSON shape, and the request bodies they sent were checked. The Java listing was not run (no JVM on the test machine). Header formats were checked against the API source. No request went to the live API.
Where the link comes from
sequenceDiagram
participant App as Your server
participant API as RenderScreenshot API
participant CDN as CDN (stored file)
participant Viewer as Browser or crawler
App->>API: GET or POST /v1/screenshot (rs_live_ key)
API->>CDN: store the render as a new object
API-->>App: image bytes + X-Cache-URL, X-Cache-Expires<br/>(or JSON: image.url, cache.expires_at)
App->>Viewer: HTML with the X-Cache-URL link
Viewer->>CDN: GET the link (no key)
CDN-->>Viewer: image, until the TTL ends
The API call needs your secret key, so it happens on your server. Only the CDN link reaches the public.
The minimal requests
GET, image response. The link is in the headers:
curl -sS -D headers.txt -o shot.png \ -H "Authorization: Bearer $RS_API_KEY" \ "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&preset=og_card&cache_ttl=604800" /usr/bin/grep -i '^x-cache' headers.txt
On an image response, X-Cache-Expires is an HTTP date, such as Sun, 25 Oct 2026 03:00:00 GMT.
POST, JSON response. Send Accept: application/json and read the same link from the body:
curl -sS https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H 'Content-Type: application/json' -H 'Accept: application/json' \ -d '{"url": "https://example.com", "preset": "og_card", "cache": {"ttl": 604800}}'
The link is in image.url and in cache.url. The expiry is cache.expires_at, in ISO 8601. Prefer JSON when you store the link, because you do not download image bytes that you do not need.
How long the link lives
| Setting | Value | Effect on the link |
|---|---|---|
cache.ttl / cache_ttl default |
86400 s (24 hours) | The link is kept for 24 hours |
| Minimum | 3600 s (1 hour) | Shorter values fail with invalid_cache_ttl |
| Maximum | 2592000 s (30 days) | No link lives longer than 30 days |
cache.available / cache_available |
true |
An identical render inside the TTL is returned, with its existing link, for 0 credits |
cache.refresh / cache_refresh |
true |
A new render, a new object and a new link with a full TTL |
DELETE /v1/cache/:key, POST /v1/cache/purge |
The object is removed before its TTL ends |
Three rules follow:
- The link is not permanent. Treat
X-Cache-Expiresas its end. Do not rely on it after that time. - Each fresh render gets a new link. Two identical requests without
cache.availablegive two different links. Save the link you published. - A renewal should be a fresh render. When you renew a link that is about to expire, send
cache.refresh, so the new object starts a full TTL. A reused render keeps the age of the original.
X-Cache-URL, signed URL or your own storage
| Need | Use | Why |
|---|---|---|
| Show an image that you render on your server anyway, for up to 30 days | X-Cache-URL |
No signing, no storage. Loading the link uses no credits. |
An og:image that renders on the first crawler fetch, with nothing stored on your side |
Signed URL | The URL itself is the request. Expiry up to 30 days. |
| A link that must work for months or years | Your own bucket, with the storage option or your own upload | Only a file you own has no TTL |
| The image changes often and must always be current | A signed URL with cache_ttl set to how stale it may be |
Each fetch after the TTL renders again |
| A link you can revoke at once | X-Cache-URL plus a purge |
Purging removes the object; a signed URL works until it expires unless you revoke the whole key pair |
A signed URL in a tag costs a credit for each fetch that is not a cache hit. An X-Cache-URL link costs one credit when you render it and nothing when people load it.
SDK tabs: render and keep the link
Each listing forces a fresh render with a 30-day TTL and prints the link and its expiry. Store both next to the record that owns the image. The method names are on the SDK pages.
Node.js
import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const res = await client.takeJson( TakeOptions.url('https://example.com').preset('og_card').cacheTtl(2592000).cacheRefresh(), ); console.log(res.image.url, res.cache.expires_at);
Python
import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY']) res = client.take_json( TakeOptions.url('https://example.com').preset('og_card').cache_ttl(2592000).cache_refresh() ) print(res['image']['url'], res['cache']['expires_at'])
Ruby
require 'renderscreenshot' client = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) res = client.take_json( RenderScreenshot::TakeOptions.url('https://example.com').preset('og_card').cache_ttl(2_592_000).cache_refresh ) puts res['image']['url'], res['cache']['expires_at']
PHP
<?php use RenderScreenshot\Client; use RenderScreenshot\TakeOptions; $client = new Client(getenv('RS_API_KEY')); $res = $client->takeJson( TakeOptions::url('https://example.com')->preset('og_card')->cacheTtl(2592000)->cacheRefresh() ); echo $res['image']['url'], ' ', $res['cache']['expires_at'], PHP_EOL;
Go
const ttl = 2592000 requested := time.Now() res, err := client.TakeJSON(ctx, rs.URL("https://example.com").Preset("og_card").CacheTTL(ttl).CacheRefresh(), ) if err != nil { log.Fatal(err) } // rs-go's CacheInfo has no expiry field; a forced refresh expires TTL seconds after the request. expires := requested.Add(ttl * time.Second) fmt.Println(res.Image.URL, expires.UTC().Format(time.RFC3339))
Java
ScreenshotResponse res = client.takeJson( TakeOptions.url("https://example.com").preset("og_card").cacheTtl(2592000).cacheRefresh() ); System.out.println(res.getImage().getUrl());
A renewal pattern
For links that must stay up longer than one TTL, renew them before they expire:
- Store
urlandexpires_atfor each image. - Run a daily job that selects images whose
expires_atis less than 7 days away. - Render each one again with
cache.refreshand a 30-day TTL, then save the new link and expiry. - Publish the new link. Pages that are still cached with the old link keep working until the old expiry.
This costs one credit per image per renewal, about once a month at the maximum TTL. If that is more than a stored file would cost you, copy each render into your own bucket instead; custom storage needs a paid plan, and its path template must contain {hash} and {ext}.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The image link stopped working | The TTL ended, or the entry was purged | Render again with cache.refresh; store the new link |
| Two links for the same page | Two fresh renders | Send cache.available, or keep the first link |
invalid_cache_ttl |
TTL in minutes or milliseconds, or outside 3600 to 2592000 | Send seconds inside the range |
| Expiry parse error in your code | Header and JSON use different formats | The header is an HTTP date; cache.expires_at is ISO 8601 |
| The old image still shows after a purge | A browser, platform or your own CDN kept a copy | Publish a new link instead of reusing the old one |
No X-Cache-URL header |
The request failed, or it was async with a webhook | Check the status and the error code; webhook payloads carry the URL instead |
Create a free account to see the headers on your own renders.