Errors
Every status code the API returns, what causes it, and whether retrying helps.
Response shapes
There are two error shapes. Endpoint errors carry a human-readable message:
{
"message": "Unauthorized"
}
Token errors follow OAuth 2.0 and carry a machine-readable error code instead:
{
"error": "invalid_client"
}
There is no error code on endpoint errors — only the status and the message string. Branch on
the status code, not on the message text, which is prose and may be reworded.
Correlation
Every response carries x-correlation-id and x-amzn-requestid headers. Quote them when
raising anything with support, and log them alongside failures.
At a glance
| Status | Meaning | Retry? |
|---|---|---|
400 | Malformed request — a parameter is out of range or invalid | No; fix the request |
401 | Missing, expired, or wrongly-scoped token | Once, after obtaining a new token — naming the scopes explicitly if it recurs |
402 | Payment required (licensed download) | No |
403 | Your subscription does not cover this | No; contact your account manager |
404 | No such item | No |
410 | The item existed but has been withdrawn | No; stop requesting it |
500 | Server error | Yes with backoff — except downloads, see below |
Retrying safely
The whole policy in one function: refresh the token once on 401, back off on 5xx, and treat
everything else as permanent. It applies to every endpoint except download — see
the warning below.
async function call(path, { token, refreshToken, maxAttempts = 4 }) {
for (let attempt = 1; ; attempt += 1) {
const response = await fetch(`https://api.alamy.com/v3${path}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) return response.json();
// 401: the token may simply have expired. Refresh once and retry - a second
// 401 means the scope is wrong, and retrying will never fix that.
if (response.status === 401 && attempt === 1) {
token = await refreshToken();
continue;
}
// 5xx: transient. Exponential backoff with jitter, because the rate limit is
// per source IP and a fleet retrying in lockstep will trip it.
if (response.status >= 500 && attempt < maxAttempts) {
const delay = 2 ** attempt * 250 * (0.5 + Math.random());
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
// 400, 402, 403, 404, 410: permanent. Fix the request or stop asking.
throw new Error(
`${path}: ${response.status} (x-correlation-id: ${response.headers.get('x-correlation-id')})`,
);
}
}
There is no Retry-After header to read, and requests over the rate limit are blocked rather than
answered with a documented status — so the delay above has to come from your own policy, not from
the response. See Rate limiting.
400 Bad Request
Token errors
error | Cause |
|---|---|
invalid_client | The key or secret is wrong, or they were sent in the wrong order. The key is the Basic username, the secret is the password. |
invalid_scope | The scope string is not recognised. Scopes are full URIs — search is rejected, https://api.alamy.com/v3/scopes/search is accepted. |
invalid_grant | The scope is recognised but your credentials are not granted it. Contact your account manager. |
The distinction between the last two is useful: invalid_scope means you got the string wrong,
invalid_grant means you got it right but lack the entitlement.
Parameter errors
Exceeding the maximum limit on search:
{
"message": "Invalid limit: 101, must be less than 100"
}
The cap is 100 and is rejected rather than silently clamped, so a client that assumes clamping will see an error rather than a smaller page.
Paging beyond the 100,000-item ceiling (see Pagination):
{
"message": "Offset and limit exceeded the maximum"
}
401 Unauthorized
{
"message": "Unauthorized"
}
Three distinct causes, indistinguishable from the body:
- No
Authorizationheader, or a malformed one. - The token has expired. Tokens last 24 hours.
- The token does not carry the scope this endpoint requires. This is the easiest one to miss,
because the token itself is valid. A token requested with an explicit
scopecarries only that scope — see Scopes.
Retry once with a fresh token. If it recurs, decode the token's scope claim and check it covers
the endpoint you are calling.
402 Payment Required
Declared on POST /download/{id}, the
licensed download operation. Not retryable.
403 Forbidden
An entitlement boundary — the request was understood and your token was valid, but your
subscription does not cover it. The message says which:
{
"message": "Your subscription does not allow listing of group orders. Please contact your account manager for details."
}
Others follow the same pattern for items, downloads, the feed and orders. Retrying will not help; these change only when your subscription does.
404 Not Found
{
"message": "Product with ID [1] not found"
}
Returned for an id that has never existed, or one outside the content your subscription can see.
410 Gone
Declared on the item and download operations for content that existed but has been withdrawn. Treat it as permanent: stop requesting the item, and remove it from your platform if you have stored it.
If you consume the feed, you will normally learn about withdrawal there first —
a notification with pubstatus: withheld — rather than by receiving a 410.
500 and other server errors
Retry with exponential backoff, with one exception: do not blind-retry a download.
Calling /download/{id} is logged as a download
against your account. It is not itself an immediate charge — usage is declared later if you go on
to use the item — but it is not established whether a 500 from a download recorded the download
before failing, so a blind retry may create a duplicate download record that is later counted as
usage. Treat a failed download as needing a human decision rather than retrying automatically.
GET /orders will not settle this either way: it lists
confirmed purchases (after the usage is declared or the pack is decremented), not raw download
records.
Rate limiting
The API enforces a limit of 15 requests a second per source IP across all endpoints, and requests above it are blocked.
Two things to know if you are writing an automated client:
- No rate-limit headers are returned. There is no
Retry-After, and noX-RateLimit-*headers, so you cannot observe your remaining budget — you have to track your own request rate. - The limit is per source IP. Anything sharing an egress address shares the budget: containers behind one NAT gateway, several Lambdas in a VPC, or a CI fleet. Concurrency that is safe from one host may not be from a cluster.
Build in client-side rate limiting rather than relying on the API to push back, and back off on any
5xx regardless.
Note that a held long-polling request occupies a connection for up to five seconds. Whether that consumes budget for its duration or only at request time is not documented.