Skip to content
JamFrame
Docs menu

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.

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 answers 410.
  • 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 cacheSeconds get 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.
  • HEAD requests never render and never cost anything.
  • Each answer has an ETag. Send it back in If-None-Match to get a 304 when nothing changed. The X-Render-Cache header says HIT or MISS.

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.
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.

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.