# GET /v1/content/:containerId/assets/:assetId (/docs/api/reference/content/get-asset)



<Endpoint method="GET" path="/v1/content/:containerId/assets/:assetId" scope="content:read" phase="1" />

Returns the asset descriptor for a single media file on the container: the public CDN URL, kind, and any per-asset metadata the workflow stamped on it (`width`, `height`, `durationMs`, `mimeType`, `sizeBytes`).

URLs are durable — they're stable R2 paths, not short-lived signatures, so it's safe to embed them in your own UI for the lifetime of the container. They DO go away when the asset is replaced (via [`regenerate`](/docs/api/reference/content/regenerate-content)) or the container is deleted, so always serve from the current container state rather than caching the URL itself.

The `assets` array on [`GET /v1/content/:containerId`](/docs/api/reference/content/get-container) carries the same shape as the body below. This endpoint exists for direct asset addressing (e.g. you have an `assetId` from `primaryAsset` and don't want to re-pull the whole container).

## Path parameters [#path-parameters]

<Parameters
  rows="[
  { name: 'containerId', type: 'string', required: true, description: 'The container id (UUID; both raw and `cnt_<id>` accepted).' },
  { name: 'assetId', type: 'string', required: true, description: 'The asset id, taken from `container.assets[].assetId` or `primaryAsset.assetId`. Opaque — derived from the asset URL on the current implementation.' },
]"
/>

## Request [#request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```sh title="terminal"
    curl https://api.layers.com/v1/content/{containerId}/assets/{assetId} \
      -H "X-Api-Key: $LAYERS_API_KEY"
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts title="get-asset.ts"
    const res = await fetch(
      `https://api.layers.com/v1/content/${containerId}/assets/${assetId}`,
      { headers: { 'X-Api-Key': process.env.LAYERS_API_KEY! } },
    );
    const { url, mimeType, expiresAt } = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```py title="get_asset.py"
    import os, httpx

    r = httpx.get(
        f"https://api.layers.com/v1/content/{container_id}/assets/{asset_id}",
        headers={"X-Api-Key": os.environ["LAYERS_API_KEY"]},
    )
    asset = r.json()
    ```
  </Tab>
</Tabs>

## Responses [#responses]

<Response status="200" description="Asset descriptor with public CDN URL.">
  ```json
  {
    "containerId": "cnt_01HXZ9...",
    "assetId": "video-9c855048-b470-42e7-8f1b-032324d48555",
    "kind": "video",
    "url": "https://media.layers.com/public-media/content-container/.../media/video-9c855048-b470-42e7-8f1b-032324d48555.mp4",
    "thumbnailUrl": null
  }
  ```

  Optional fields populate when the workflow records them — typically present on the newest containers, sometimes absent on legacy data:

  ```json
  {
    "mimeType": "video/mp4",
    "sizeBytes": 1843200,
    "durationMs": 9200,
    "width": 1080,
    "height": 1920,
    "checksumSha256": "e3b0c44298fc1c...",
    "expiresAt": "2026-04-18T10:45:00Z"
  }
  ```
</Response>

<Response status="404" description="Asset not found on this container.">
  ```json
  {
    "error": {
      "code": "NOT_FOUND",
      "message": "Asset not on this container.",
      "requestId": "req_..."
    }
  }
  ```
</Response>

## Notes [#notes]

<Callout type="info">
  URLs are durable, not pre-signed. They're stable R2/CDN paths — fine to
  embed in your UI or pass to a video player without `expiresAt` bookkeeping.
  When the workflow records `expiresAt` (rare, typically only for partner-
  uploaded sources with a TTL), the field is included; otherwise treat the
  URL as valid until the asset is replaced or the container is deleted.
</Callout>

* **`assetId` is derived from the URL.** The current implementation uses the URL filename without extension (e.g. `video-<UUID>` for videos, `slide-3` for slideshows) so it round-trips between [list](/docs/api/reference/content/list-containers), [get](/docs/api/reference/content/get-container), and this endpoint. Treat it as opaque on your side.
* **`checksumSha256` is stable across re-fetches when present.** Use it to dedupe downloads or verify integrity after upload to a third-party CDN. Not all rows carry it — older containers may omit the field.
* **Regenerated containers replace assets.** After [`regenerate`](/docs/api/reference/content/regenerate-content), old asset ids stop resolving once R2 lifecycle rules sweep the bytes (typically within minutes). Always fetch the current container state and use the asset ids it returns rather than caching ids long-term.

## Errors [#errors]

| Code              | When                                                          |
| ----------------- | ------------------------------------------------------------- |
| `NOT_FOUND`       | Asset id not on this container, or container not in this org. |
| `FORBIDDEN_SCOPE` | Key lacks `content:read`.                                     |

## See also [#see-also]

* [Read the container](/docs/api/reference/content/get-container)
* [Regenerate to replace assets](/docs/api/reference/content/regenerate-content)
