Skip to content

Errors and rate limits

The error shape every API v2 action returns, what each status code means, and the 40-points-per-minute rate limit with the 429 response.

Last updated

Every failed request returns a JSON body with an errors array of plain sentences, and a status code that says what kind of failure it was:

{
"errors": ["access.download_limit_count must be an integer of at least 0"],
"field_errors": {
"access.download_limit_count": ["must be an integer of at least 0"]
}
}

Some errors add machine-readable keys so your code does not have to read the sentence.

Status Meaning What to do
401 The API key is missing, invalid, expired or revoked (code says which). Check the X-Auth-Token header; an expired or revoked key needs a new one. See Authentication.
404 The record does not exist for this store, or the path is not an action. Check the id and the path.
409 The request conflicts with the current state, or it needs a confirmation. Read the message; do not retry blindly. A code of confirmation_required means a preview.
422 Validation failed. Read field_errors (dotted field path to messages), then fix the request.
429 Rate limited. Nothing was done. Wait retry_after seconds and send the same request again.
502 Shopify or the email service did not finish the request (code: "shopify_error"). Nothing was changed. Try again shortly.

Each action accepts exactly the fields in its schema. If you send one it does not take, the request is rejected before anything runs, and the response tells you what it does accept:

{
"errors": ["Unknown parameter for GET /orders: state. It accepts: page, per_page, search, financial_status."],
"code": "unknown_parameters",
"unknown": ["state"],
"accepted": ["page", "per_page", "search", "financial_status"]
}

Fix the call; retrying it unchanged will fail the same way.

Requests are limited per store, shared by all of its API keys, with a points budget:

Budget 40 points per 60 seconds
A read (GET) 1 point
A write (POST, PUT, PATCH, DELETE) 2 points

The budget belongs to the key, so everything using it shares it: your scripts, your integrations and any AI assistant connected with the key. It is also shared with API v1.

Every response carries three headers so you can see where you are:

Header Meaning
X-RateLimit-Limit Points per window (40).
X-RateLimit-Remaining Points left in the current window.
X-RateLimit-Reset Unix time, in seconds, when the window ends.

A 429 means the request was not processed. The response has a Retry-After header and the same information in the body:

{
"errors": [
"You have exceeded the rate limit of 40 points per 60 seconds. Read requests cost 1 point(s) and write requests cost 2 point(s). Please retry after 12 second(s)."
],
"code": "rate_limited",
"retry_after": 12,
"error": "Rate limit exceeded",
"message": "You have exceeded the rate limit of 40 points per 60 seconds. Read requests cost 1 point(s) and write requests cost 2 point(s). Please retry after 12 second(s)."
}

Wait retry_after seconds, then send the same request again. It is safe, because nothing happened the first time.

async function call(url, options, attempts = 5) {
const response = await fetch(url, options);
if (response.status !== 429 || attempts === 0) return response;
const wait = Number(response.headers.get('Retry-After') || 5);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
return call(url, options, attempts - 1);
}
Was this page helpful?

Still stuck? Ask Quinn or chat with support