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

# Operations

> Manage CDN endpoints, URL signing keys, purge cached URLs, replace files, and understand the security model.

## CDN endpoints and URL signing keys

A CDN endpoint is the stable delivery identity for one environment, site, or customer. Its URL namespace is safe to embed because it is not a secret:

```txt theme={"system"}
https://cdn.convertly.sh/{endpointNamespace}/{fileId}?w=1200&format=auto
https://cdn.convertly.sh/{endpointNamespace}/o/{originSlug}/path/image.jpg?w=1200
```

Endpoints, URL signing keys, origins, presets, custom domains, and Transform Lab file selection belong to the active workspace. Switching workspaces changes the complete Image CDN configuration shown in the dashboard. A team workspace never inherits the owner's personal endpoints or credentials.

URL signing keys are revocable credentials attached to an endpoint. Use them when your backend generates protected or expiring URLs. Rotating or deleting a key does not change the endpoint URL.

<Warning>
  URL signing keys (`cvly_cdn_sk_...`) and regular API keys (`cvly_...`) are secrets. Do not put them behind `NEXT_PUBLIC_*` or anywhere they ship to the browser. Public HTML should use the endpoint URL, not a secret key.
</Warning>

### Manage endpoints and signing keys

| Method   | Endpoint                                   | Use                                                                                                                                                     |
| -------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST`   | `/api/cdn-delivery-sources`                | Create an endpoint. Body: `{ "name": "Production", "namespace": "marketing", "mode": "public" }`. Mode is `public` or `signed`.                         |
| `PATCH`  | `/api/cdn-delivery-sources/{id}`           | Rename an endpoint, change its URL namespace, set it as default, or change its access mode.                                                             |
| `GET`    | `/api/delivery-keys`                       | List active endpoints and their URL signing keys.                                                                                                       |
| `POST`   | `/api/delivery-keys`                       | Create a URL signing key for an existing endpoint. Body: `{ "name": "Production signer", "namespace": "marketing" }`. The full secret is returned once. |
| `PATCH`  | `/api/delivery-keys/{id}`                  | Rotate an active URL signing key. The previous secret stops working immediately and the replacement is returned once.                                   |
| `GET`    | `/api/delivery-keys/alias?alias=marketing` | Check URL namespace availability. This compatibility route retains `alias` in its path.                                                                 |
| `DELETE` | `/api/delivery-keys/{id}`                  | Revoke a URL signing key. Endpoint URLs keep the same public path. URLs signed with that key stop working.                                              |

```bash theme={"system"}
curl -X POST "https://convertly.sh/api/delivery-keys" \
  -H "Authorization: Bearer $CONVERTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Production signer", "namespace": "marketing" }'

# {
#   "key": { "id": "...", "name": "Production signer", "key_prefix": "cvly_cdn_sk_...", "namespace": "marketing" },
#   "token": "cvly_cdn_sk_..."
# }
```

You can manage CDN endpoints, access policies, and URL signing keys visually from **Image CDN -> Delivery**.

These endpoints accept either an authenticated dashboard session or a standard server-side
`cvly_...` API key. WordPress site keys cannot create, list, or revoke workspace URL signing keys.
The full signing key secret is returned only when a key is created.

### What is secure, and what is public

| Part                                            | Secret? | Purpose                                                                                                                                |
| ----------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| CDN endpoint and URL namespace                  | No      | Stable routing identity. Safe in HTML, CSS, SDK configuration, and browser bundles.                                                    |
| File UUID or CDN slug                           | No      | Identifies the requested asset. A UUID is difficult to guess, but it is not an authorization mechanism.                                |
| URL signing key (`cvly_cdn_sk_...`)             | **Yes** | Authorizes your backend to mint signed URLs for one endpoint. Store it only in server-side environment variables or a secrets manager. |
| Signature (`s`), key id (`kid`), expiry (`exp`) | No      | A scoped proof embedded in a URL. The CDN validates it at the edge.                                                                    |

For a signed request, Convertly canonicalizes the endpoint, asset identifier, and every transform parameter, then verifies an HMAC signature using the active key identified by `kid`. Changing the file, width, quality, expiry, or any other signed parameter invalidates the URL. If `exp` is present, the edge rejects the URL after that Unix timestamp.

Signing-key secrets are shown once and stored as hashes. The edge receives active key IDs, not plaintext customer keys. Revoking a key removes it from the endpoint's active key set after the short authorization-cache window. New signed URLs include `kid`, so old and new keys can overlap safely during rotation.

<Warning>
  A **public** endpoint is intentionally readable without a signature. Its URL path does not prevent copying, hotlinking, or downloading an asset. Use a **signed** endpoint for member-only, paid, private, or time-limited media.
</Warning>

### Endpoint URLs

The URL namespace is a short prefix (`marketing`, `site`, `products`) in the endpoint URL:

```txt theme={"system"}
https://cdn.convertly.sh/marketing/{fileId}?w=1200&format=auto
```

URL namespaces are:

* Set when creating a CDN endpoint (3-63 chars, lowercase, `[a-z0-9_-]`, globally unique)
* Checked with `GET /api/delivery-keys/alias?alias=...` before you commit. The endpoint name is kept for backward compatibility.
* Compatible with [custom domains](/docs/image-cdn/custom-domain), [signed URLs](/docs/image-cdn/presets-and-signing#signed-urls), and [CDN slugs](/docs/image-cdn/url-structure#cdn-slugs)

See [URL structure](/docs/image-cdn/url-structure) for how endpoints compose with hostnames and file identifiers.

### Custom domains

When you use a custom CDN hostname, the hostname maps to one CDN endpoint, so its URL namespace does not need to appear in the URL:

```txt theme={"system"}
https://img.example.com/{fileId}?w=1200&format=auto
```

Internally, Convertly resolves that hostname to the mapped endpoint and applies the same access policy.

### Rotating a signing key

URL signing keys can be rotated without downtime. Create a new key for the same endpoint, update your backend signing configuration, then revoke the old one. Newly signed URLs include `kid`, allowing old and new keys to overlap safely while the endpoint URL remains stable.

## Delivery access modes

Each CDN endpoint has an **access policy**. The CDN enforces it on every request.

| Mode                   | Unsigned URLs               | Typical use                                                                |
| ---------------------- | --------------------------- | -------------------------------------------------------------------------- |
| **`public`** (default) | Allowed                     | Marketing sites, product catalogs, blog heroes                             |
| **`signed`**           | **403** without valid `?s=` | Gated downloads, time-limited assets, premium transforms                   |
| **`private`**          | **403** without valid `?s=` | Same enforcement as `signed` — use when every render must be server-minted |

When mode is `signed` or `private`, embed URLs only after your backend signs them with a [URL signing key](/docs/image-cdn/presets-and-signing#signed-urls). Unsigned requests return:

```json theme={"system"}
{ "error": "This CDN endpoint requires a signed URL." }
```

**Public marketing pages** should use a `public` endpoint. **Member-only or paid assets** should use `signed`/`private` plus [`POST /api/delivery-keys/{id}/sign`](/docs/image-cdn/presets-and-signing#signed-urls) or your SDK signing helper.

Forma AI transforms (`upscale=ai`, `bgReplace`, etc.) already require signed URLs on every endpoint when those parameters are present. See [AI & ML transforms](/docs/image-cdn/transforms/ai-ml).

## Origin sources

**Required before origin-backed CDN URLs work.** Any URL containing `/o/{slug}/` tells Convertly to fetch from a source you registered here. The SDK and Next.js loader only rewrite paths to that shape - they do not create origins for you.

Typical setups:

| Use case                       | Slug example | Base URL example                              |
| ------------------------------ | ------------ | --------------------------------------------- |
| Deployed Next.js / static site | `site`       | `https://example.com`                         |
| Public asset bucket or CDN     | `products`   | `https://assets.example.com`                  |
| WordPress uploads folder       | `wp-uploads` | `https://blog.example.com/wp-content/uploads` |

Each external host must be registered separately. Convertly does not offer open per-request remote URL fetch (unlike some competitors); origins are workspace-scoped for security.

Add sources from **Image CDN -> Sources** in the dashboard (or `POST /api/cdn-origins`).

| Mode               | When to use                                  | What Convertly stores                        |
| ------------------ | -------------------------------------------- | -------------------------------------------- |
| **Public HTTPS**   | Bucket or site already has a public read URL | Origin URL + optional path prefix            |
| **Private bucket** | Object storage without public access         | Encrypted read credentials + bucket metadata |

Private credentials are encrypted at rest and never returned by the API. Rotate credentials by updating the origin source with new keys.

For **public** origins, Convertly does not fetch `localhost`, `127.0.0.1`, private IP ranges, or non-HTTPS URLs. That keeps the CDN from becoming a private-network proxy.

```bash theme={"system"}
curl -X POST "https://convertly.sh/api/cdn-origins" \
  -H "Authorization: Bearer $CONVERTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Product images",
    "slug": "products",
    "sourceType": "r2",
    "authMode": "private",
    "baseUrl": "https://my-bucket.example.r2.cloudflarestorage.com",
    "pathPrefix": "catalog",
    "credentials": {
      "kind": "s3",
      "accessKeyId": "...",
      "secretAccessKey": "...",
      "bucket": "my-bucket",
      "endpoint": "https://...r2.cloudflarestorage.com"
    }
  }'
```

### Private Google Cloud Storage

```bash theme={"system"}
curl -X POST "https://convertly.sh/api/cdn-origins" \
  -H "Authorization: Bearer $CONVERTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "GCS catalog",
    "slug": "gcs-products",
    "sourceType": "gcs",
    "authMode": "private",
    "baseUrl": "https://storage.googleapis.com/my-gcs-bucket",
    "pathPrefix": "catalog",
    "credentials": {
      "kind": "gcs",
      "bucket": "my-gcs-bucket",
      "serviceAccountJson": "{ \"type\": \"service_account\", ... }"
    }
  }'
```

Paste the full service-account JSON from Google Cloud IAM (read-only on the bucket is enough). Convertly stores it encrypted and never returns it.

### Private Azure Blob Storage

```bash theme={"system"}
curl -X POST "https://convertly.sh/api/cdn-origins" \
  -H "Authorization: Bearer $CONVERTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Azure assets",
    "slug": "azure-assets",
    "sourceType": "azure",
    "authMode": "private",
    "baseUrl": "https://myaccount.blob.core.windows.net/my-container",
    "pathPrefix": "",
    "credentials": {
      "kind": "azure",
      "accountName": "myaccount",
      "accountKey": "...",
      "container": "my-container"
    }
  }'
```

Supported private credential kinds: **`s3`** (AWS S3, R2, Wasabi, Linode, any S3-compatible endpoint), **`gcs`**, and **`azure`**. See [Origin deliverability](/guides/cdn-performance#origin-deliverability) if fetches fail or time out.

## Purging the edge cache

When you change a file's underlying bytes in place (rare — see [Replacing a file](#replacing-a-file) for the recommended pattern) and need cached variants evicted immediately, call the purge API. You can purge **exact URLs** or **cache tags**.

### Purge by URL

```bash theme={"system"}
curl -X POST "https://convertly.sh/api/cdn-purge" \
  -H "Authorization: Bearer $CONVERTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://cdn.convertly.sh/marketing/{fileId}?w=1200",
      "https://cdn.convertly.sh/marketing/o/products/hero.jpg?w=600"
    ]
  }'

# { "ok": true, "purged": [ "...", "..." ], "purgedTags": [] }
```

### Purge by cache tag

CDN responses include a `Cache-Tag` header with tags Convertly assigns automatically. Purge every cached variant for an asset or origin without listing every query string:

```bash theme={"system"}
curl -X POST "https://convertly.sh/api/cdn-purge" \
  -H "Authorization: Bearer $CONVERTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tags": [
      "asset:{fileId}",
      "origin:products"
    ]
  }'

# { "ok": true, "purged": [], "purgedTags": [ "asset:...", "origin:products" ] }
```

| Tag prefix                | Invalidates                                               |
| ------------------------- | --------------------------------------------------------- |
| `workspace:{workspaceId}` | All CDN variants for the active workspace (use sparingly) |
| `asset:{fileId}`          | Every transform variant of one storage file               |
| `origin:{slug}`           | Every variant fetched through one origin source slug      |

You can pass **URLs and tags in the same request** (up to 30 of each). Tag values must match the `Cache-Tag` header on CDN responses exactly.

### Purge notes

* Use your server-side `cvly_...` API key, not a CDN signing key. Purging is a workspace mutation.
* **URL purge** is exact-match: `?w=1200` and `?w=600` are separate entries.
* **Tag purge** evicts all width/format/gravity variants that share the tag — best when you overwrite a file in place or bulk-refresh an origin.
* Up to 30 URLs or 30 tags per request. Send multiple requests for more.
* Purges complete in seconds globally.

If the purge API returns `503`, retry later or use the file-id versioning pattern below instead.

## Analytics

The dashboard includes **Image CDN -> Analytics** for month-to-date delivery monitoring. It shows:

* Origin requests used against your plan allowance
* Total edge deliveries, including cache hits, against the safety ceiling
* Data delivered through the image CDN edge against the safety ceiling
* Daily request trends for the current billing month
* Recent 24-hour activity
* Active CDN endpoints, origins, and custom domains

The origin meter counts requests that reach Convertly rendering or origin fetch. The separate edge meters count every response and its delivered bytes, including cache hits. Use the origin chart for billing usage and render pressure, then use the edge meters to identify hotlinking, automated redistribution, or unexpectedly popular assets.

For full-fidelity audience analytics, pair CDN delivery with your site analytics. For operational spikes, use the Image CDN analytics tab to spot whether traffic is reaching Convertly instead of being absorbed by cache.

You can also fetch the same month-to-date meter from the API with a standard workspace API key:

| Method | Endpoint             | Use                                                                                                                                                                       |
| ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET`  | `/api/cdn/analytics` | Return Image CDN origin-request usage, edge-delivery safety usage, delivered data, quota progress, daily buckets, recent hourly buckets, and CDN configuration inventory. |

```bash theme={"system"}
curl "https://convertly.sh/api/cdn/analytics" \
  -H "Authorization: Bearer $CONVERTLY_API_KEY"

# {
#   "plan": "pro",
#   "totals": {
#     "originRequests": 18420,
#     "limit": 250000,
#     "usedPercent": 7.36,
#     "remaining": 231580,
#     "projectedMonth": 55260,
#     "edgeRequests": 310420,
#     "edgeRequestLimit": 50000000,
#     "edgeBytes": 48318382080,
#     "edgeByteLimit": 5497558138880
#   },
#   "daily": [{ "date": "2026-07-01", "requests": 842 }],
#   "recentHours": [{ "hour": "2026-07-09T09:00:00.000Z", "requests": 42 }]
# }
```

This endpoint is available on every plan so teams can monitor usage before they hit plan or safety limits. Advanced traffic analytics, such as geography, referrers, top URLs, and cache-hit ratio, may require edge-log analytics.

## Replacing a file

URLs are immutable for a year by design. That enables high cache hit ratios. Practical implications when you need to update an image:

* **Upload the new version as a separate file** (which gets its own `fileId`), then update your markup to point at the new id. The old URL keeps serving the old bytes from CDN cache until it eventually expires, and the new one is fresh from day one.
* **Overwriting a file in place will not refresh cached URLs.** Caches that already have the old bytes will keep serving them until edge eviction or the year-long TTL expires. Use the [purge API](#purging-the-edge-cache) if you need immediate invalidation.

For most workflows, treating uploaded files as immutable assets and rotating the file id for updates is simpler and aligns with how every URL-transform CDN expects to be used.

## Security model

* CDN endpoint URL namespaces are globally unique public routing identifiers assigned to one endpoint. They are not secrets.
* **Access modes** (`public`, `signed`, `private`) control whether unsigned CDN URLs are accepted — see [Delivery access modes](#delivery-access-modes).
* URL signing keys are revocable credentials. Revoking one stops URLs signed with it after the short edge authorization cache expires.
* Storage-backed CDN URLs can use generated filename slugs or UUIDs. UUID URLs remain valid, and slugs are public identifiers. Anyone who sees a URL can request that asset at allowed transform sizes on a `public` endpoint.
* For gated content, use [Signed URLs](/docs/image-cdn/presets-and-signing#signed-urls) on a `signed` or `private` endpoint. A signed URL covers the endpoint path, file ID, and every parameter. Any tampering invalidates the signature and returns `403`.
* The endpoint never reveals file lists, account identifiers, or any internal storage path.
* URL signing keys can overlap during rotation without changing endpoint URLs.

## Local development

**Public** origin sources must be reachable over public HTTPS. Convertly does not fetch `localhost`, `127.0.0.1`, or private IP ranges through the public-URL path.

**Private** origin sources do not require a public bucket URL. Credentials are used server-side only.

For framework loaders, use a local passthrough option while running your app locally so public-folder assets render from your dev server. Those local requests are not transformed by Convertly. To test the real optimized path before production, use a public preview deployment as the origin source, or temporarily expose your local server through an HTTPS tunnel and point the origin source at that tunnel URL.
