Retrieve one media asset and a signed URL
The asset plus a short-lived signed url for its bytes. The URL is unauthenticated once minted, so treat it as a secret and let it expire — expires_in is capped, deliberately, so a scoped read cannot be turned into a permanent public link.
download=true signs it with a download disposition (the browser saves the file) instead of an inline one.
An unknown slug is 404 ASSET_NOT_FOUND. So is a slug that exists but belongs to inbound customer media when the key lacks conversations:read — identical response either way, so probing slugs reveals nothing about what a customer sent in.
Required scope: media:read
Authorizations
Project API key issued in Settings → API keys. Send it as Authorization: Bearer pk_live_….
Headers
Target project id, for an MCP OAuth bearer (mcp_at_…) attached to more than one project — get ids from GET /v1/projects. Matched case-insensitively.
Omit it and a READ falls back to the connection's default project; a mutation (any non-GET) on a connection with 2+ projects is rejected with 400 project_required — a write is never defaulted to a guessed project. A connection with exactly one project never needs the header.
Scopes are checked against the SELECTED project only, never a union across the connection.
For a pk_ API key the header selects nothing — one key is one project's context — but it IS validated: omit it and the key's own project is used, send it and it must name that project, otherwise the call is rejected with 403 project_not_attached (a blank value is 400 invalid_project_header, as above).
Path Parameters
The asset's bot-facing slug — the key the runtime getMediaId / sendMedia helpers look it up by.
^[a-z0-9][a-z0-9-]{0,119}$Query Parameters
Sign the URL with a download disposition instead of an inline one.
true, false, 1, 0, Signed-URL lifetime in seconds (30-3600, default 300).
30 <= x <= 3600