Signing

Signed delivery URLs are minted by the API and verified at the edge.

A signed URL carries exp, kid and sig and stops working when it expires. Protected projects serve nothing else. Public projects accept signed URLs too, but their unsigned URLs keep working, so only a protected project makes access depend on the signature. Mint them on your server; the signing secret never leaves img2site.

Minting a signed URL

POST /v1/projects/{slug}/signed-url with an API key that has urls:sign (or a dashboard session). The body is { file_slug, transform_chain?, kind?, expires_in_seconds? }; expires_in_seconds is 60 to 3600, default 300. The project must be active and the file ready. A chain must pass the grammar, and its watermark presets must exist; on a public project it must also be in allowed_transform_chains (a protected project signs any valid chain).

public_url is the unsigned equivalent, or null on a protected project. This endpoint has its own per-project limit on issued URLs (60 per minute by default, configurable per deployment); refused requests do not count against it. Cache signed URLs for their lifetime instead of minting one per page view.

Request and response

POST https://api.img2.site/v1/projects/my-store/signed-url
X-Api-Key: $IMG2SITE_API_KEY          (scope urls:sign)
Content-Type: application/json

{ "file_slug": "hero", "transform_chain": "resize/1024x", "expires_in_seconds": 300 }

200 OK
{
  "url": "https://my-store.i2scd.net/resize/1024x/f/hero?exp=2026-09-24T12%3A05%3A00.000Z&kid=kid_…&sig=…",
  "expires_at": "2026-09-24T12:05:00.000Z",
  "kid": "kid_…",
  "kind": "derived",
  "transform_chain": "resize/1024x",
  "public_url": "https://my-store.i2scd.net/resize/1024x/f/hero"
}

With @img2site/sdk-server

import { createClient } from "@img2site/sdk-server";

const img2site = createClient({ apiKey: process.env.IMG2SITE_API_KEY!, project: "my-store" });

const { url } = await img2site.signedUrl({
  file_slug: "hero",
  chain: "resize/1024x/format/webp",
  expires_in_seconds: 600,
});

URL kinds

Signed URL kinds
kindPathDefault when
derived/{chain}/f/{slug}a transform_chain is given
originals/originals/f/{slug}no transform_chain is given
raw/download/raw/f/{slug}never; needs raw_download_enabled (else 403 raw_download_disabled)

Errors when minting

Errors of the signed-url endpoint
StatusCodeCause
400invalid_transform_chainThe chain does not parse or names an unknown preset.
400watermark_preset_not_foundThe chain names a watermark preset that does not exist in the project, or uses the default watermark when the project has none.
400transform_chain_not_allowedPublic projects only: the chain is not in allowed_transform_chains.
403project_suspendedThe project is suspended.
404file_not_foundNo such file, or it was deleted.
409file_not_readyThe file is blocked or not published yet.
409signing_key_missingThe project has no active signing key; rotate one in the dashboard.
429rate_limitedThe project exceeded its signed-URL limit (60 per minute by default), or sent ten times that many signing requests.

Canonical string

The edge verifies sig as base64url (no padding) HMAC-SHA256 over the canonical string, with the project signing key named by kid, compared in constant time. HEAD is verified as GET. {transform_chain} is the normalized chain (originals or download/raw for those kinds), {query_sorted} is every query parameter except sig, percent-encoded and sorted, and {exp} is the ISO-8601 expiry.

Any change to the chain, slug, query, expiry or key id invalidates the signature. An expired exp is rejected before the key is looked up.

{METHOD}|{transform_chain}|{file_slug}|{query_sorted}|{exp}|{kid}

GET|resize/1024x|hero|exp=2026-09-24T12%3A05%3A00.000Z&kid=kid_abc123|2026-09-24T12:05:00.000Z|kid_abc123

Rotating the signing key

Rotation creates a new active key and keeps the previous one valid for 24 hours, until previous_overlap_until. URLs signed with the previous kid keep working during the overlap; after it they answer 403 unknown_kid, so re-mint anything you still need. Rotation needs a dashboard session. GET /v1/projects/{slug}/signing-keys lists key ids and their status, never secrets.

Rotation

POST https://api.img2.site/v1/projects/my-store/signing-keys/rotate   (dashboard session)

200 OK
{
  "active_kid": "kid_new…",
  "previous_kid": "kid_old…",
  "previous_overlap_until": "2026-09-25T12:00:00.000Z"
}

See delivery URLs for the grammar and the REST reference for the full request and response schemas.