# Dynamic Document API: Signed links

> Image URLs rendered straight from a template.

> Part of the Dynamic Document API documentation. Index of all pages for agents: [llms.txt](https://dynamicdocumentapi.com/llms.txt), every page in one file: [llms-full.txt](https://dynamicdocumentapi.com/llms-full.txt).

7 endpoints of the [API reference](https://dynamicdocumentapi.com/docs/api-reference/index.md), version 1.0.0. Interactive reference: https://dynamicdocumentapi.com/docs/api-reference#tag/signed-links. Machine-readable OpenAPI document: https://dynamicdocumentapi.com/openapi/openapi-public.json

## Servers

- https://api-eu.dynamicdocumentapi.com — EU (Germany)

## Endpoints

- `GET /l/{link_id}/{filename}` — Render a signed link
- `GET /v1/signed-links` — List signed links
- `POST /v1/signed-links` — Create a signed link
- `GET /v1/signed-links/{link_id}` — Retrieve a signed link
- `PATCH /v1/signed-links/{link_id}` — Update a signed link
- `DELETE /v1/signed-links/{link_id}` — Delete a signed link
- `POST /v1/signed-links/{link_id}/sign` — Sign a link URL

## GET /l/{link_id}/{filename}

**Render a signed link**

The public image URL of a signed link, served from `https://img.dynamicdocumentapi.com` without an API key. Checks, in order: unknown, disabled or deleted link, or a format the link does not enable → 404 `not_found`; expired link or a signed `exp` in the past → 410 `link_expired`; `access: signed` needs `sig` = base64url(HMAC-SHA256(secret, "GET\n/l/{link_id}/{name}.{ext}\n" + canonical query without `sig`)) → 403 `invalid_signature` (open links accept only allowed parameters that have `param_limits`); unknown parameters or a value over `max_length` → 400 `validation_error`; a `Referer` whose host is not in `allowed_referrers` → 403 `referrer_not_allowed`; the per-IP hourly limit → 429 `rate_limited`; the plan feature → 402 `plan_feature_unavailable`; the link quota → 429 `quota_exceeded`.

 A cached result is returned with `X-Billed-Renders: 0` and creates no render; a cache miss renders the template's live version (`X-Billed-Renders: 1`, `X-Render-Id`). Responses carry `ETag` (304 on `If-None-Match`), `Cache-Control: public, max-age=…` (`private` for a link with `allowed_referrers` and in zero-retention workspaces: no shared cache keeps the file) and `Access-Control-Allow-Origin: *`. `HEAD` runs the same checks and never renders. When the render limit is reached, a link with a fallback image returns it instead of 402 `render_limit_reached`.

 Cloudflare's cache in front of the URL keeps `public` responses until their `max-age`, so a repeated URL can come from it (`CF-Cache-Status: HIT`, `Age`) with the headers of the response that filled it: `X-Billed-Renders` and `X-Render-Id` then describe that first response, and the hit is never billed. Changing what a link serves or deleting it, a change to its template's live version or settings, and suspending or deleting the workspace or turning on zero retention remove the copies from Cloudflare within seconds of the change reaching the region. Browsers keep theirs until `max-age`.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `If-None-Match` | header | string | A previous `ETag`. |
| `exp` | query | integer | Expiry (Unix seconds), signed. |
| `filename` (required) | path | string | `{name}.{ext}`: any name of 1 to 80 characters `[A-Za-z0-9_-]` and a format enabled for the link (`png`, `jpeg` or `jpg`, `webp`, `pdf`). The name only sets the download filename; both are signed. |
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |
| `params` | query | object | The template parameters (`allowed_params`), e.g. `title.text=Summer%20Sale`. |
| `sig` | query | string | base64url HMAC-SHA256 signature (`access: signed`). |

Responses:

- `200` The rendered file (PNG, JPEG, WebP or PDF per the extension).
- `304` Not modified (`If-None-Match`).
- `400` Parameter not allowed, over `max_length`, malformed or over 8 KB of query.
- `402` `plan_feature_unavailable` or `render_limit_reached` (no fallback image).
- `403` `invalid_signature` or `referrer_not_allowed`.
- `404` Unknown, disabled or deleted link, or a format the link does not enable.
- `410` `link_expired` (or the template was deleted).
- `422` The render failed (template error).
- `429` `rate_limited` (per IP and hour, `Retry-After`) or `quota_exceeded`.

## GET /v1/signed-links

**List signed links**

Image links that render a template straight from a URL. Secrets are never returned by a read.

Responses:

- `200` Every object in the workspace (a single page today).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/signed-links

**Create a signed link**

Returns `base_url` and the HMAC `secret` **once**; SDKs sign URLs locally with it. Use `access: "open"` for tools that cannot compute an HMAC.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |

Request body (`application/json`, required): SignedLinkWrite

| Field | Type | Description |
|---|---|---|
| `access` | "signed" \| "open" | `open` accepts unsigned requests within `allowed_params` and `param_limits`. * `signed` - signed * `open` - open |
| `allowed_params` | array of string | Dynamic properties callers may set, e.g. `title.text`. |
| `allowed_referrers` | array of string |  |
| `cache_ttl_seconds` | integer \| null |  |
| `defaults` | object |  |
| `enabled` | boolean |  |
| `expires_at` | string \| null |  |
| `fallback_image_asset_id` | string \| null | Ready image asset (`ast_…`) returned instead of 402 `render_limit_reached`; null clears. |
| `formats` (required) | array of "png" \| "jpeg" \| "webp" \| "pdf" |  |
| `name` (required) | string |  |
| `param_limits` | object |  |
| `quota_total` | integer \| null |  |
| `rate_limit_per_ip_per_hour` | integer \| null |  |
| `template_id` (required) | string |  |

Responses:

- `201` Created; `secret` is shown once.
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Template not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## GET /v1/signed-links/{link_id}

**Retrieve a signed link**

Common base: API key or OAuth only, never a dashboard region token.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Responses:

- `200` 
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## PATCH /v1/signed-links/{link_id}

**Update a signed link**

Partial update. `template_id` is immutable and the secret is unchanged.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Request body (`application/json`): PatchedSignedLinkWrite

| Field | Type | Description |
|---|---|---|
| `access` | "signed" \| "open" | `open` accepts unsigned requests within `allowed_params` and `param_limits`. * `signed` - signed * `open` - open |
| `allowed_params` | array of string | Dynamic properties callers may set, e.g. `title.text`. |
| `allowed_referrers` | array of string |  |
| `cache_ttl_seconds` | integer \| null |  |
| `defaults` | object |  |
| `enabled` | boolean |  |
| `expires_at` | string \| null |  |
| `fallback_image_asset_id` | string \| null | Ready image asset (`ast_…`) returned instead of 402 `render_limit_reached`; null clears. |
| `formats` | array of "png" \| "jpeg" \| "webp" \| "pdf" |  |
| `name` | string |  |
| `param_limits` | object |  |
| `quota_total` | integer \| null |  |
| `rate_limit_per_ip_per_hour` | integer \| null |  |
| `template_id` | string |  |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## DELETE /v1/signed-links/{link_id}

**Delete a signed link**

Existing URLs stop working immediately (cached CDN copies expire on their own).

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Responses:

- `204` Deleted.
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).

## POST /v1/signed-links/{link_id}/sign

**Sign a link URL**

Convenience for languages without an SDK: signs `params` with the link secret. Signing locally avoids the round trip.

Parameters:

| Name | In | Type | Description |
|---|---|---|---|
| `Idempotency-Key` | header | string | Replays the stored response for 24 h; a different body with the same key → 422. |
| `link_id` (required) | path | string | Signed link id (`lnk_…`). |

Request body (`application/json`, required): SignUrl

| Field | Type | Description |
|---|---|---|
| `expires_at` | integer | Unix seconds; omit for a link that does not expire. |
| `format` (required) | "png" \| "jpeg" \| "webp" \| "pdf" | * `png` - png * `jpeg` - jpeg * `webp` - webp * `pdf` - pdf |
| `name` | string | File name in the URL (cosmetic, but signed). |
| `params` | object |  |

Responses:

- `200` 
- `400` Invalid request body (`errors[]` with JSON pointers).
- `401` Missing, invalid, expired or revoked credentials.
- `403` Insufficient scope, IP not allowed, workspace suspended or scheduled for deletion, or email not verified.
- `404` Signed link not found.
- `429` Rate limited (`Retry-After`).
- `503` The control plane is unreachable (`Retry-After`).
