Skip to content

Signed URLs

Anyone can change the query parameters of a design image URL, and render your design with other text, images or colors. Signed URLs stop that: when a design requires them, Pixelstack only renders URLs that carry a valid signature, and answers anything else with HTTP 403.

You sign URLs on your own server with your team’s signing secret. Signing happens locally, with no call to Pixelstack, and signed URLs don’t expire.

Requiring signed URLs

  1. Open Design Settings

    In the editor, click the Design settings button ( icon, top-right corner).

  2. Toggle the switch

    Enable Require signed URLs and click Save.

A signed URL ends with an s parameter:

https://cdn.pixelstack.io/team-123/design-456?title.text=Hello+World&badge.fill=%23fff&s=lOmI7lhSB42BkemoT0PoUQ

Your signing secret

Team owners and admins find it in Team settings → Signing Secret. Store it as an environment variable on your server, like PIXELSTACK_SIGNING_SECRET, and never send it to the browser.

Rotate replaces it with a new one. URLs signed with the old secret stop working for designs that require signed URLs, so update the secret wherever you use it.

Signing with the Pixelstack client

The client signs every URL it builds when it has a signing secret:

import { PixelstackClient } from '@pixelstackai/client';
// Reads PIXELSTACK_TEAM_ID and PIXELSTACK_SIGNING_SECRET from the environment.
// Keep this code on your server.
const pixelstack = new PixelstackClient();
const url = pixelstack.getImageUrl('my-design', {
data: { title: { text: 'Hello World' } },
});

You can also pass teamId and signingSecret to the constructor, or sign a URL you built yourself:

const signedUrl = pixelstack.signUrl(
'https://cdn.pixelstack.io/your-team-id/my-design?title.text=Hello'
);

The Next.js <PixelstackImage> component and the Nuxt module do this for you: set PIXELSTACK_SIGNING_SECRET (or the Nuxt module’s signingSecret option) and their URLs are signed.

Signing in other languages

The signature is an HMAC-SHA256, so any language can compute it:

  1. Take the URL’s query parameters, decoded, without s.
  2. Sort them by name, then by value.
  3. Encode each name and value like JavaScript’s encodeURIComponent, and join them as name=value pairs with &.
  4. Build the string v1:{teamId}/{designId}?{parameters}.
  5. Compute its HMAC-SHA256 with your signing secret, keep the first 16 bytes, and encode them as base64url without padding.
  6. Add the result to the URL as the s parameter.

In Python:

import base64, hashlib, hmac
from urllib.parse import quote
def signature(secret, team_id, design_id, params):
encode = lambda value: quote(value, safe="!'()*-._~")
query = "&".join(
f"{encode(name)}={encode(value)}" for name, value in sorted(params) if name != "s"
)
payload = f"v1:{team_id}/{design_id}?{query}"
digest = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).digest()[:16]
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode()

With the secret pxs_test-secret, the team team-123, the design design-456 and the parameters title.text=Hello World and badge.fill=#fff, the signature is lOmI7lhSB42BkemoT0PoUQ.

Signing through the API

If you’d rather not handle the secret, the API signs a URL for you with an API key from Team settings → API Keys:

GET https://api.pixelstack.io/v1/teams/{teamId}/signUrl?url={ENCODED_URL}
X-Api-Key: {your_api_key}
{
"signedUrl": "https://cdn.pixelstack.io/your-team-id/my-design?title.text=Hello&s=..."
}