> ## Documentation Index
> Fetch the complete documentation index at: https://docs.screenframed.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Signed URLs

Generate short-lived capture URLs without exposing an API key to a browser or client.

Signed URLs authenticate `GET /v1/capture` with a project signing secret instead of an API key. They are useful when a client should trigger a render directly but must not see your API key.

## When to use signed URLs

**Browser-triggered renders**

Let a frontend request a pre-approved render URL without embedding a secret in the bundle.
  **Embeddable templates**

Create short-lived render links for customer portals, CMS tools, or dynamic images.

## Requirements

1. Create or choose a project.
2. Read the project signing secret from the dashboard or API.
3. Mint a URL on your backend.
4. Send only the signed URL to the browser.

The signing secret belongs on your server. Treat it like an API key.

## Query parameters

Signed capture URLs include normal `GET /v1/capture` query parameters plus:

| Parameter | Description |
|---|---|
| `project_id` | Project id that owns the signing secret. |
| `expires` | Unix timestamp in seconds. |
| `signature` | Hex HMAC-SHA256 signature over the canonical payload. |

## Canonical payload

Build the signature from:

```txt
METHOD
PATH
sorted=urlencoded&query=params&excluding=signature
```

For `GET /v1/capture`, the first two lines are:

```txt
GET
/v1/capture
```

Sort all query parameter pairs by their encoded `key=value` string and exclude `signature`.

## JavaScript example

```javascript
import crypto from "node:crypto";

function signCaptureUrl({ secret, params }) {
  const url = new URL("https://screenframed.com/v1/capture");
  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, String(value));
  }

  const pairs = [];
  for (const [key, value] of url.searchParams.entries()) {
    if (key === "signature") continue;
    pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
  }
  pairs.sort();

  const canonical = ["GET", url.pathname, pairs.join("&")].join("\n");
  const signature = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
  url.searchParams.set("signature", signature);
  return url.toString();
}

const signedUrl = signCaptureUrl({
  secret: process.env.SCREENFRAMED_PROJECT_SIGNING_SECRET,
  params: {
    project_id: "prj_01K...",
    expires: Math.floor(Date.now() / 1000) + 300,
    site: "https://example.com/pricing",
    device: "browser-macos",
    bg: "midnight",
    shadow: "soft",
    aspect_ratio: "16:9",
  },
});
```

## Payload query parameter

For complex payloads, encode the JSON body as a base64url `payload` query param. Explicit query parameters still override values inside `payload`.

```json
{
  "url": "https://example.com/pricing",
  "device": "browser-macos",
  "background_preset": "midnight",
  "shadow": "float"
}
```

## Rotation

Rotate the project signing secret if it is exposed or no longer trusted:

```bash
curl -X POST "https://screenframed.com/v1/projects/prj_01K.../signing-secret/rotate" \
  -H "Authorization: Bearer $SCREENFRAMED_API_KEY"
```