GET /v1/content/:containerId/assets/:assetId
Read one asset descriptor — public R2 URL plus per-asset metadata.
/v1/content/:containerId/assets/:assetId- Auth
- Bearer
- Scope
- content:read
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) 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 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
containerIdstringrequiredThe container id (UUID; both raw and `cnt_<id>` accepted).assetIdstringrequiredThe asset id, taken from `container.assets[].assetId` or `primaryAsset.assetId`. Opaque — derived from the asset URL on the current implementation.
Request
curl https://api.layers.com/v1/content/{containerId}/assets/{assetId} \
-H "X-Api-Key: $LAYERS_API_KEY"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();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()Responses
{
"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:
{
"mimeType": "video/mp4",
"sizeBytes": 1843200,
"durationMs": 9200,
"width": 1080,
"height": 1920,
"checksumSha256": "e3b0c44298fc1c...",
"expiresAt": "2026-04-18T10:45:00Z"
}{
"error": {
"code": "NOT_FOUND",
"message": "Asset not on this container.",
"requestId": "req_..."
}
}Notes
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.
assetIdis derived from the URL. The current implementation uses the URL filename without extension (e.g.video-<UUID>for videos,slide-3for slideshows) so it round-trips between list, get, and this endpoint. Treat it as opaque on your side.checksumSha256is 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, 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
| Code | When |
|---|---|
NOT_FOUND | Asset id not on this container, or container not in this org. |
FORBIDDEN_SCOPE | Key lacks content:read. |