Essentials
Signed URLs
Learn how to build a link that renders a template image when someone opens it.
A signed URL is a link that renders a template image when someone opens it. You can put it in an <img> tag, an email, or an Open Graph tag, and your server does no work when the image loads. This is useful for personalized email headers, share cards, and badges.
The link carries the template id, the modifications, and a signature. Anyone who has the link can open that exact image. Nobody can change the template or the modifications without breaking the signature.
Create a signed URL with an SDK
The SDKs sign the URL on your server. They make no network request.
Examples
Open only what you need
JavaScript
const url = await bannerify.generateImageSignedUrl("tpl_xxx", {
modifications: [{ name: "headline", text: "Hello, Ada" }],
})Python
url = client.generate_image_signed_url(
"tpl_xxx",
modifications=[{"name": "headline", "text": "Hello, Ada"}],
)PHP
$url = $client->generateImageSignedUrl('tpl_xxx', [
'modifications' => [['name' => 'headline', 'text' => 'Hello, Ada']],
]);Go
url, err := client.GenerateImageSignedURL("tpl_xxx", &bannerify.CreateImageOptions{
Modifications: []bannerify.Modification{{Name: "headline", Text: ptr("Hello, Ada")}},
})Each SDK page lists its options: JavaScript, Python, PHP, and Go.
Build a signed URL yourself
You can sign a URL in any language. This is useful when no SDK covers your language, or when an SDK lacks an option you need, such as nocache in Go.
- Hash your API key with SHA-256 and write the digest in hex. This is
apiKeyHashed. Bannerify uses it to find your key, and it cannot sign anything. - Collect the parameters from the table below.
modificationsis a JSON string. - Sort the parameters by name, then write them as a query string in the form that
URLSearchParamsproduces. A space becomes+, and every character except letters, digits,*,-,., and_becomes a percent escape. - Compute the HMAC-SHA256 of that query string with your API key as the secret, and write the digest in hex. This is
sign. - Add
signto the query string.
import { createHash, createHmac } from "node:crypto"
const apiKey = process.env.BANNERIFY_API_KEY!
const params = new URLSearchParams({
templateId: "tpl_xxx",
apiKeyHashed: createHash("sha256").update(apiKey).digest("hex"),
modifications: JSON.stringify([{ name: "headline", text: "Hello, Ada" }]),
nocache: "true",
})
params.sort()
params.set("sign", createHmac("sha256", apiKey).update(params.toString()).digest("hex"))
const url = `https://api.bannerify.co/v1/templates/signedurl?${params}`
Parameters
| Parameter | Required | Description |
|---|---|---|
templateId |
Yes | The template id |
apiKeyHashed |
Yes | The SHA-256 hash of your API key, in hex |
sign |
Yes | The signature from step 4 |
modifications |
No | A JSON string. Modifications lists the fields |
format |
No | png, jpeg, or webp. The default is png |
quality |
No | An integer from 1 to 100 for jpeg and webp. The default is 90 |
nocache |
No | true to skip the cache |
Caching
Bannerify keeps the rendered image in its edge cache for up to 7 days. The cache key is the full URL, so a different modification is a different image. A request that the cache answers does not render again.
An image that people keep requesting stays cached. Cloudflare runs the cache and decides what to keep, so it can drop a copy that nobody requested for a while before the 7 days end. The next request renders the image again, and that render counts toward your quota.
- Saving a template in the editor clears the cached images of that template. A change made any other way does not clear them.
- Clear image cache on the General tab of Settings clears the cached images of every template in the project, and the cached files that they load from other sites.
nocache=trueskips the cache of the rendered image. Every request renders a new image, and the response is not stored. Each of those renders counts toward your quota. The render still uses the cached files that layers load.
Bannerify also caches each file that a layer loads from another site. It keeps the file as long as the max-age in the Cache-Control header of that site allows, but at least 60 seconds and at most 7 days. When the site sends no max-age, Bannerify keeps the file for 1 day. When the file changes, give it a new URL or use Clear image cache.
Quota and rate limits
A render counts as one generation. A request that the cache answers does not count. Quota and limits lists what counts.
Signed URL renders have their own rate limit of 100 requests every 10 seconds for each API key. A request that the cache answers does not count toward it. When you go over the limit, the request returns RATE_LIMITED.
Keep it safe
- Sign on your server. The signature uses your API key, so signing code must not run in a browser or in an app that you ship.
- The URL is safe to share. It holds the signature and a hash of your key, and neither one can sign another URL.
- Anyone who has the URL can open it as often as they like. A URL never expires, and every render that misses the cache counts toward your quota.
- The modifications sit in the URL as plain text. Do not put private data in them.
- To stop every URL that a key signed, delete the key on the API Keys page.
Troubleshooting
An image tag shows a broken image when the request fails. Open the URL in a browser tab to read the error.
| Error | Cause | Fix |
|---|---|---|
BAD_REQUEST: Missing apiKeyHashed |
The URL has no apiKeyHashed parameter |
Add it. The SDKs add it for you |
BAD_REQUEST: Invalid api key |
No key matches apiKeyHashed, for example because you deleted the key |
Sign with a key that exists |
BAD_REQUEST: Invalid sign |
The signature does not match the parameters | Check that nothing edited the URL, and that you sorted and encoded the parameters as described above |
NOT_FOUND |
The template does not exist | Check templateId |
USAGE_EXCEEDED |
You reached your generation limit | See Quota and limits |
RATE_LIMITED |
Too many renders in 10 seconds | Wait for the next 10-second window, or let the cache serve repeat requests |