A signed image URL lets you generate an image without calling the API: you build the URL yourself — template, values, and a signature made with your signing secret — and put it wherever an image URL goes: an og:image tag, an email, a CMS field, a spreadsheet cell. The first request renders the image and redirects to the file; every later request with the same values is a redirect to that file, with no rendering and no quota. This page explains the format, the signature, code for three languages, and the rules that come with it.
When to use one
The API (POST /v1/images) and signed URLs make the same images. The difference is who makes the request:
POST /v1/images |
Signed URL | |
|---|---|---|
| Who calls | your server, with an API key | whoever fetches the URL: a crawler, a mail client, a browser |
| When it renders | when you call | on the first fetch |
| What you need | an HTTP request and a key | an HMAC function and the signing secret |
| Good for | apps, batches, webhooks | static sites, themes, emails, no-code, spreadsheets |
Use a signed URL where you cannot keep an API key or send a POST: a Hugo or Jekyll template, a Shopify or Ghost theme, a mailing tool's merge field, a no-code builder. Use the API when you want the file in hand before anyone sees it, or need batches.
The format
https://shareglint.com/i/{account_id}/{template}.{ext}?{field}={value}&…&s={signature}
account_id— your account'sacc_…id. It is public, not a credential.template— a built-in template slug or one of yours.ext— the format:png,jpg,webp,pdf, orgifandmp4for animated templates. The extension is part of what is signed, so a signature for.pngdoes not open.jpg.- Query parameters — the template's fields, plus optional
localeandsize(one of og 1200×630, square 1080×1080, portrait 1080×1350, story 1080×1920, pinterest 1000×1500, youtube 1280×720). s— the signature.
The signature
- Take every query parameter except
s. - Sort them by name, in byte order.
- Percent-encode each name and value per RFC 3986 — everything except
A–Z a–z 0–9 - _ . ~, so a space is%20, not+— and join them asname=valuewith&. - The canonical string is the path,
?, and the joined parameters (the?stays even with no parameters). sis the first 32 lowercase hex characters ofHMAC-SHA256(canonical string, signing secret)(HMAC, RFC 2104).
Test your implementation against this vector before publishing anything. With the secret sign_example, the path /i/acc_01k8example0000000000000000/blog-og.png and the parameters title=Hello, world!, author=Ann & Bob, locale=en:
canonical /i/acc_01k8example0000000000000000/blog-og.png?author=Ann%20%26%20Bob&locale=en&title=Hello%2C%20world%21
s cdc6c6ad85a071f083b5f1374b0a6d98
The published URL may list parameters in any order and encode spaces as + or %20: the server decodes it and rebuilds the canonical string itself. Values are signed exactly as sent — they are not trimmed.
Code
Node.js:
import { createHmac } from 'node:crypto';
// RFC 3986: encodeURIComponent leaves ! ' ( ) * alone.
const enc = (v) => encodeURIComponent(String(v))
.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
export function signedImageUrl(account, secret, template, fields, ext = 'png') {
const path = `/i/${account}/${template}.${ext}`;
const query = Object.keys(fields).sort()
.map((name) => `${enc(name)}=${enc(fields[name])}`)
.join('&');
const s = createHmac('sha256', secret).update(`${path}?${query}`).digest('hex').slice(0, 32);
return `https://shareglint.com${path}?${query}&s=${s}`;
}
PHP:
function signed_image_url(string $account, string $secret, string $template, array $fields, string $ext = 'png'): string
{
$path = "/i/{$account}/{$template}.{$ext}";
ksort($fields, SORT_STRING);
$query = implode('&', array_map(
fn ($name, $value) => rawurlencode((string) $name).'='.rawurlencode((string) $value),
array_keys($fields), $fields,
));
$s = substr(hash_hmac('sha256', $path.'?'.$query, $secret), 0, 32);
return 'https://shareglint.com'.$path.'?'.$query.'&s='.$s;
}
Python:
import hashlib, hmac
from urllib.parse import quote
def signed_image_url(account, secret, template, fields, ext="png"):
path = f"/i/{account}/{template}.{ext}"
query = "&".join(
f"{quote(str(k), safe='')}={quote(str(v), safe='')}" for k, v in sorted(fields.items())
)
s = hmac.new(secret.encode(), f"{path}?{query}".encode(), hashlib.sha256).hexdigest()[:32]
return f"https://shareglint.com{path}?{query}&s={s}"
All three give the vector above. Google Sheets has its own version in Apps Script on the Google Sheets page, and n8n's Crypto node (HMAC, SHA256, hex, first 32 characters) signs without code. An AI assistant connected to the MCP server can build signed URLs for you with its create_signed_url tool.
What happens on a request
- First request for a template and set of values: the image renders (about a second for a still image), one render is counted, and the answer is a
302redirect to the file. - Every later request with the same values: a
302to the existing file — no render, no quota. - The redirect may be cached for 1 hour(s), the file itself for a year, so crawlers come back through the signed URL now and then — which is what makes per-platform view counts possible in the dashboard's analytics.
Because the URL renders on demand, it never "expires" the way a stored file can: if the file is gone after your plan's retention period, the next request simply renders it again.
Rules and limits
- Unknown fields are an error. A field the template does not declare returns
400, so a typo cannot quietly ship an image without its title. (POST /v1/imagesis more lenient.) sizeis reserved for the size preset; a template field namedsizecannot be set through a signed URL.- Errors are short plain-text bodies:
400unknown field or locale,402quota used up,403missing or wrong signature,404unknown account or template,429rate limit,503render still running — retry afterRetry-After. - Rate limits: 120 correctly signed requests a minute per account (requests with a bad signature never count against yours) and 600 a minute per IP address.
- The secret. The signing secret is separate from API keys: a leaked secret gives no API access, but it lets someone render on your quota. Rotating it (
POST /v1/account/signing-secret) breaks every URL signed with the old secret at once, including published ones. - Never sign in the browser. Anything in client-side JavaScript is public; sign on a server, at build time or in a serverless function.
Signed URLs and auto OG URLs
There is a second kind of image URL: auto OG (/u/{account}.png?url=…), which reads a page's title, description and image and renders a card for it. It is not signed; instead it only works for domains you allow in the dashboard. Use auto OG when the data is already on the page (one tag in a theme covers every page), and signed URLs when you choose the values — a different headline, an email's recipient name, a template other than the default card. The OG images without code page covers auto OG.
Cost
One render per distinct template, set of values, format and size; repeat requests are free. Signed URLs work on every plan, the Free plan included (100 renders a month, with a watermark); Starter is $9 a month for 2,000 renders (pricing). The full reference, with the same vector, is in the docs.