Asset URLs
How thumbnail and download URLs arrive in results, and how to resolve and fetch them.
Using the client library?
download() in the client library performs this entire resolution automatically. This page matters on the HTTP path, or when you need to understand what the library does underneath.
You do not request a thumbnail or a download by asset ID. Each asset in a folder or search result already carries the URLs you need: thumbnailUrl for a preview image and downloadUrl for the full file. Conversions carry their own URLs too. To show a preview or download a file, take the URL from the result, run it through the resolution steps below, then fetch it.
Resolution process
Run these steps, in order, on any thumbnailUrl, downloadUrl, or conversion URL before you fetch it:
- Strip the CI HUB signature. Keep
cihubSigonly on/api/v1/assets/downloadURLs; remove it from every other URL. - Resolve placeholders. Process any
$...$placeholder in the URL (see below). At most one authentication placeholder appears per URL. - Add CI HUB auth for CI HUB endpoints. If the URL points at
/api/v1/assets/downloador/api/v1/assets/thumbnail, send the two CI HUB headers:Authorization: Bearer <access_token>andprovider-authorization: Bearer <DAM connection token>. Keep the URL's own signature parameter (cihubSigon a/api/v1/assets/downloadURL,sigon a/api/v1/assets/thumbnailURL); a modified CI HUB URL fails its signature check with HTTP 400.
For a /api/v1/assets/download URL you can append noRedirect=true to receive { "downloadUrl": "..." } as JSON instead of the file bytes or an HTTP 302 to the provider host. Use that when you want a ready URL to hand to an <img> tag or a browser download.
Unlike the JSON endpoints, these media URLs reply with a plain HTTP status, and at most a short text body, rather than the structured error envelope when they fail: a 400 when the URL signature (cihubSig or sig) is missing or modified, a 404 when the proxy cannot resolve the asset. Branch on the HTTP status code here rather than parsing an error object.
Worked example
A downloadUrl from a result points at the CI HUB proxy and carries a cihubSig:
https://stage.ci-hub.com/api/v1/assets/download/asset-456?cihubSig=abc123It has no $...$ placeholder, so resolution is: keep cihubSig (step 1), nothing to replace (step 2), and add the two CI HUB headers because it is a /api/v1/assets/download URL (step 3). Then fetch:
const res = await fetch(asset.downloadUrl, {
headers: {
Authorization: `Bearer ${ciHubAccessToken}`,
'provider-authorization': `Bearer ${damProviderToken}`,
},
})
const bytes = await res.arrayBuffer()The payload
Several placeholders inject a value called the payload. It is the payload claim inside the DAM connection token (the provider's own credential), not the whole token. Decode the connection token and read that claim:
import { jwtDecode } from 'jwt-decode'
const { payload } = jwtDecode(damProviderToken)Placeholders
| Placeholder | Resolve by |
|---|---|
$AUTH_PAYLOAD$ | Replace with encodeURIComponent(payload), as a query value. |
$AUTH_PAYLOAD_BEARER$ | Replace with encodeURIComponent('Bearer ' + payload), as a query value. |
$AUTH_HEADER_PAYLOAD_BEARER$ | Remove the placeholder and send Authorization: Bearer {payload} as a request header. |
$AUTH_HEADER_PAYLOAD_APITOKEN$ | Remove the placeholder and send Authorization: apiToken {payload} as a request header. |
$NO_AUTH$ | Remove the placeholder. Fetch without authentication, unless the URL resolves to /api/v1/assets/download or /api/v1/assets/thumbnail, in which case add those endpoints' CI HUB headers. |
$HEADER_VALUE_STATIC_ENCODED_{name}={value}$ | Remove the placeholder and send header {name}: {value}. URL-decode both name and value first. |
$FORCE_DOWNLOAD$ | Remove the placeholder. Its presence signals the client should force a download rather than render inline. |
$NO_CACHE$ | Remove the placeholder and send Cache-Control: no-cache. |
$REQUEST_HEADERS$
Remove the placeholder and read two query parameters from the URL:
headers: a URL-encoded JSON object of header key-value pairs.type: how to merge them with the request's existing headers.merge(default),replace, oroverwrite.
type | Result |
|---|---|
merge | Existing headers plus the provided headers; provided headers win on a conflict. |
replace | Only the provided headers; existing headers are dropped. |
overwrite | Existing headers plus the provided headers; existing headers win on a conflict. |
Original:
https://api.unsplash.com/photos/abc123/download$REQUEST_HEADERS$?headers=%7B%22X-API-Key%22%3A%22key-123%22%7D&type=merge
Existing headers: { "Accept": "*/*", "User-Agent": "CI-HUB-Client/1.0" }
Result:
URL: https://api.unsplash.com/photos/abc123/download
Headers: { "Accept": "*/*", "User-Agent": "CI-HUB-Client/1.0", "X-API-Key": "key-123" }$AUTH_PAYLOAD$, $AUTH_PAYLOAD_BEARER$, and the header placeholders put the provider credential into the request, and the query-value forms put it in the URL. Resolve these in your backend or in trusted client code, and keep resolved URLs out of logs, analytics, and referrer headers.
Resolving a payload placeholder
A provider-direct URL can embed the credential as a query value. Replace $AUTH_PAYLOAD$ with the URL-encoded payload claim, then fetch:
const { payload } = jwtDecode(damProviderToken)
const resolved = downloadUrl.replace('$AUTH_PAYLOAD$', encodeURIComponent(payload))
// https://api.example-dam.com/assets/123/download?access_token=$AUTH_PAYLOAD$
// becomes
// https://api.example-dam.com/assets/123/download?access_token=<encoded-credential>
const res = await fetch(resolved)Conversions
An asset's conversions array holds alternate renditions: a smaller size, a different format, a low-resolution copy. Each entry carries its own URL in the same forms described above. Resolve and fetch a conversion URL exactly as you would the asset's own downloadUrl.
Verifying a download
When an asset carries a content hash field, you can verify the bytes you received against it after download. The hash field and algorithm depend on the provider. Treat it as optional integrity, not a required step.