Dynamic URLs
A Dynamic URL is one address you can put in an <img> tag, an email, or behind a
QR code. The data for each document travels in the link itself, signed, so the
same URL serves every recipient — a certificate per student, an invoice per
customer — and nothing calls our API until someone actually opens it.
https://dyn.automette.com/d/abc123def456ghij/certificate.pdf?d=eyJuYW1lIjoiQWxpY2UifQ&s=a1b2c3d4
└── token ────┘ └─ filename ─┘ └── payload ──┘ └ signature ┘One URL serves everyone. ?d=<Alice> and ?d=<Bob> are the same Dynamic
URL. Create a second one only when you want separate controls — its own expiry,
or a signing secret you can revoke without touching the first. Never one per
recipient.
Create one
In the dashboard, Dynamic URLs → New URL. Or:
curl -X POST https://automette.com/api/v1/dynamic_urls \
-H "Authorization: Bearer $AUTOMETTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "template_id": "cm4tpl8e2...", "name": "Cohort 2026 certificates" }'The response carries the signing secret and a sample_url that works as-is.
Keep the secret somewhere your server can read it.
Sign your own URLs
This is the point of the feature. You build addresses locally and we hear nothing until a recipient opens one.
The signed message is:
{token}/{extension}?{query}with the query parameters sorted and s left out. No hostname and no path —
so a signature keeps working even if the URL is served from somewhere else later.
import { createHmac } from "crypto"
const SECRET = process.env.AUTOMETTE_SECRET
const TOKEN = "abc123def456ghij"
function dynamicUrl(data) {
const d = Buffer.from(JSON.stringify(data)).toString("base64url")
const s = createHmac("sha256", SECRET).update(`${TOKEN}/pdf?d=${d}`).digest("hex")
return `https://dyn.automette.com/d/${TOKEN}/certificate.pdf?d=${d}&s=${s}`
}What you can put in the payload
The same field names the render API takes. Canvas templates use
{layerId}.{property}; Typst and Word templates use their field names.
{ "name.text": "Alice Rao", "course.text": "Data Structures", "grade.text": "A" }Which format you get
The extension decides. certificate.pdf renders a PDF, certificate.png an
image. The extension is part of the signature, so a signature made for one
format is refused on another.
The filename before the extension is yours — it is what a browser calls the
download and is not signed. certificate.pdf and cert.pdf are the same
document.
Rotating a secret
A Dynamic URL holds up to five signing secrets, and any unrevoked one works. So rotating is not a cutover:
- Add a secret.
- Migrate whatever signs URLs to the new one, at your own pace.
- Revoke the old one when Last used shows nothing is still on it.
Nothing breaks in between, which matters because a live URL sits in someone else's email template where you cannot reach it.
Revoking is immediate. Anything still signing with that secret starts returning 403 straight away — which is why Last used is worth checking first.
Controls
expires_at | After this, the URL returns 410. The cache lifetime is capped so expiry is exact. |
max_renders | Counts renders, not views. A cached document keeps serving after the cap. |
cache_ttl | How long the CDN keeps a copy. Defaults to a day; set it low for something you edit often. |
formats | Which extensions this URL will serve. |
status | disabled returns 410 and clears the CDN copy. |
What happens on a request
1. CDN has it? → bytes, ~15ms
2. No → your document is looked up, the signature and controls checked
3. Rendered before → streamed from storage
4. First time → rendered now, stored, returnedA document is only ever rendered once per distinct payload, however many people open it and from wherever.
Editing the template
Change a template and every Dynamic URL on it re-renders on its next request.
There is nothing to purge and no URL to reissue. The CDN may serve the previous
version until cache_ttl lapses, so lower it on templates you are actively
working on.
Last updated on