Skip to main content

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

StatusMeaningRetry?
400Malformed request — a parameter is out of range or invalidNo; fix the request
401Missing, expired, or wrongly-scoped tokenOnce, after obtaining a new token — naming the scopes explicitly if it recurs
402Payment required (licensed download)No
403Your subscription does not cover thisNo; contact your account manager
404No such itemNo
410The item existed but has been withdrawnNo; stop requesting it
500Server errorYes 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

errorCause
invalid_clientThe 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_scopeThe scope string is not recognised. Scopes are full URIs — search is rejected, https://api.alamy.com/v3/scopes/search is accepted.
invalid_grantThe 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:

  1. No Authorization header, or a malformed one.
  2. The token has expired. Tokens last 24 hours.
  3. 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 scope carries 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.

Do not automatically 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 no X-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.