Signed URLs
Turn one render request into a public link that makes the image or PDF when someone opens it, with no API key in the page. Use it in img tags, emails and link previews.
A signed URL is a link that renders an image or PDF when someone opens it. You make it once on your server,
with your API key. Then you can put it anywhere: in an <img> tag, an email or an og:image tag. The
link holds no API key, and nobody can change its values.
Create a signed URL
Send a render request to POST /v1/signed-urls. Nothing is rendered or charged yet.
curl https://api.jamframe.com/v1/signed-urls \
-H "Authorization: Bearer $JAMFRAME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "clx8m2k3p0001abcd1234efgh",
"modifications": {
"title": "How we cut render times in half",
"author": "Jane Doe"
},
"response": { "format": "png" },
"expiresIn": 2592000
}'const res = await fetch("https://api.jamframe.com/v1/signed-urls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JAMFRAME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: "clx8m2k3p0001abcd1234efgh",
modifications: {
title: "How we cut render times in half",
author: "Jane Doe",
},
response: { format: "png" },
expiresIn: 2592000, // 30 days
}),
});
const { data } = await res.json();
console.log(data.url);import os
import requests
res = requests.post(
"https://api.jamframe.com/v1/signed-urls",
headers={"Authorization": f"Bearer {os.environ['JAMFRAME_API_KEY']}"},
json={
"templateId": "clx8m2k3p0001abcd1234efgh",
"modifications": {
"title": "How we cut render times in half",
"author": "Jane Doe",
},
"response": {"format": "png"},
"expiresIn": 2592000, # 30 days
},
)
print(res.json()["data"]["url"])The answer:
{
"data": {
"url": "https://api.jamframe.com/r/s/cJa3HMV6TZzf.zPcqxCsIwFAXQXwnX….kDVEKgTbbJVl2AuLI64vRg.png",
"templateId": "clx8m2k3p0001abcd1234efgh",
"format": "png",
"expiresAt": "2026-12-14T15:01:30.000Z"
}
}
JamFrame checks the request as it would check a render. An unknown template is a 404, and a wrong value
is a 400. Keys that match nothing come back in warnings.
The request
| Field | What it does |
|---|---|
templateId |
Required. A template in your workspace, or a library template. |
modifications |
The values to fill in, as in a render. Up to 50 keys, and values up to 2000 characters. See Modification keys. |
response.format |
png (the default), jpeg (or jpg), webp or pdf. |
response.scale |
1, 2 or 3, for images. |
response.quality |
1 to 100, for jpeg and webp. |
response.includePages |
For an image: one page number (the default is page 1). For a PDF: up to 20 pages (the default is all). |
response.size |
Another size, such as story. See Smart Resize. |
expiresIn |
Seconds until the link stops working. The default is 604800 (7 days). The most is 31536000 (1 year). |
cacheSeconds |
How long one render of the link is reused. The default, and the most, is 2592000 (30 days). 0 renders on every visit. |
A signed URL can’t take async, webhookUrl, response.type, response.sizes, translate or ai.
Those are a 400. If the link would be too long, that’s a 400 too: send fewer or shorter
modifications.
Use the link
The link looks like https://api.jamframe.com/r/s/….png. It ends in .png, .jpg, .webp or .pdf,
and is usually 200 to 300 characters long. Opening it answers with the file itself, so you can use it
anywhere an image link works:
<img src="https://api.jamframe.com/r/s/cJa3HMV6TZzf.zPcq….png" alt="How we cut render times in half" />
<meta property="og:image" content="https://api.jamframe.com/r/s/cJa3HMV6TZzf.zPcq….png" />
- The values are part of the signature. Change any character and the link answers
403. - After
expiresIn, the link answers410. - JamFrame ignores anything after a
?, so tracking tags that email tools add don’t break the link. - When something goes wrong, the link answers with a short JSON error, not a picture.
- Signed URLs don’t run AI. AI layers show their saved text and images.
Cache and credits
- The first visit renders the file. It costs credits like any render: 1 per image, or 1 per PDF page.
- Later visits within
cacheSecondsget the same file from the cache. They’re free. - Saving the template in the editor starts a fresh cache. The next visit renders the new version.
- Browsers and CDNs may keep their own copy for up to one day.
HEADrequests never render and never cost anything.- Each answer has an
ETag. Send it back inIf-None-Matchto get a304when nothing changed. TheX-Render-Cacheheader saysHITorMISS.
Limits
Anyone can open a signed URL, so it has tighter limits than the API. Requests and new renders have their own limits per minute, for each visitor, link and workspace. See Public links and forms.
| Limit | Value |
|---|---|
| Output size | 20 megapixels per page: width × height × scale² |
| Time | 10 seconds to render all the pages |
When too many renders run at once, the link answers 503 with a Retry-After header. Try again after
that many seconds.
Revoke all signed URLs
POST /v1/signed-urls/rotate-key gives your workspace a new signing secret. Every signed URL made before
stops working at once and answers 403. Links you make afterwards use the new secret.
curl -X POST https://api.jamframe.com/v1/signed-urls/rotate-key \
-H "Authorization: Bearer $JAMFRAME_API_KEY"
{ "data": { "rotatedAt": "2026-11-14T15:20:00.000Z" } }
- You can’t revoke one link on its own. To limit the risk, give links a shorter
expiresIn. - Browsers and CDNs may still show a copy they already have, for up to a day.
- Revoking an API key doesn’t stop the signed URLs it made. Rotate the signing key for that.
Errors when a link is opened
| Status | error |
When |
|---|---|---|
| 400 | INVALID_PARAMETERS |
The request no longer fits the template, for example a page that was removed. |
| 402 | CREDITS_EXHAUSTED |
Your organization is out of render credits, or the workspace reached its credit cap, and the file isn’t in the cache. |
| 403 | INVALID_SIGNATURE |
The link was changed, or the signing key was rotated. |
| 403 | UNAVAILABLE |
Your organization is on hold or suspended. |
| 404 | NOT_FOUND |
The template was deleted. |
| 410 | URL_EXPIRED |
The link is past its expiresIn. |
| 422 | MEDIA_UNAVAILABLE |
An image in the template or the values couldn’t be loaded. |
| 429 | RATE_LIMITED |
Too many requests, or too many failed renders. |
| 503 | BUSY |
Too many renders at once. Try again shortly. |
| 500 | RENDER_FAILED |
Something else went wrong. |
Signed URLs or dynamic URLs?
Both are public links that render when opened. They suit different jobs.
| Signed URL | Dynamic URL | |
|---|---|---|
| Made with | The API: POST /v1/signed-urls |
The app: the template’s Integrations, API & Forms dialog, Dynamic URL tab |
| Values | Fixed when you make the link. Nobody can change them. | In the link’s query string. Anyone with the link can change them. |
| Links | One per set of values | One per template, for any values |
| Templates | Your workspace’s, and library templates | Your workspace’s |
| Spending limit | No cap. Each link renders one fixed request, and repeat visits come from the cache. | A credit cap, 100 credits for a new link |
| Expiry | Up to 1 year, 7 days by default | None, unless you set one |
| Cache | Up to 30 days, 30 days by default | Up to 30 days, 5 minutes by default |
| Revoke | All at once, by rotating the signing key | Turn it off, or click New link |
Use a signed URL when your server knows the values, such as a blog post’s title for its og:image. Use a
dynamic URL when the values come from somewhere without an API key, such as an email tool’s merge tags. See
Dynamic URLs.
Related
- The full render request: Render images and PDFs.
- Links you make in the app: Dynamic URLs.
- Every field: the Signed URLs endpoints in the API reference (opens in a new tab).
Last updated
Still stuck?
Email [email protected] and a real person will reply. Tell us the template you were using and what you expected to happen.