Errors
Every failure comes back in the same envelope:
{"success": false, "message": "Invalid token"}Branch on the HTTP status and success, never on message. This surface has no stable
machine-readable error codes — the message is written for a person reading a log and may be
reworded at any time. If you have integration code from another AOV.ai app that switches on
error.code, it will not work here.
Every refusal
| Message | HTTP | Cause | What to do |
|---|---|---|---|
| Missing credentials | 401 | No Authorization header arrived | Send Authorization: Bearer aovmcp_…. Do not revoke your token — it is not the problem |
| Invalid token | 401 | Unknown token, revoked token, or the app is uninstalled | See below before creating a new one |
| Unauthorized | 401 | The token could not be checked | Retry once; if it persists, treat as Invalid token |
| This token is read-only | 403 | The request was not a GET | Use GET. No token can be granted more |
| Token is not authorized for this shop | 403 | The domain in the URL is not this token's store | Fix the base URL — see below |
| This endpoint requires an internal API key | 403 | An internal support route | Not available to merchants |
| Missing shop domain | 400 | The URL has no /shop/<domain>/ segment | You are calling the prefix, not an endpoint |
| Rate limit exceeded | 429 | More than 60 requests in a minute | Back off; read Retry-After |
| (varies) | 500 | Something failed on the server | Safe to retry; the cause is in our logs, not the response |
Which are worth retrying
| Retry after a wait | 429, 500 |
| Never retry unchanged | 403 — neither the method nor the token's permissions will change |
| Stop and alert a human | Missing credentials, Invalid token, Token is not authorized for this shop |
A retry loop that treats every failure the same will hammer the API for an hour on a wrong method and hide the one error that needed someone's attention.
Missing credentials and Invalid token are deliberately separate
Missing credentials means no header arrived. The token is fine; the request is not — usually a
header dropped by a proxy, an HTTP client that passes headers differently, or a missing Bearer
prefix and its single space.
Invalid token means a token arrived and is not usable.
Revoking is not reversible. Reading Missing credentials as "bad token" and revoking destroys a
working credential while leaving the real problem — the missing header — exactly where it was.
An unknown token and a revoked one both return Invalid token, on purpose. Distinguishing them
would confirm to whoever holds a token that it was once real.
Invalid token also covers an uninstalled app. Before creating a replacement, check in this
order:
- Is the header present, with
Bearerand one space? - Is AOV.ai Bundle Upsell still installed on the store? Uninstalling revokes every token, and reinstalling does not restore them.
- Was the connection revoked in Settings → Integrations?
Token is not authorized for this shop
The token is valid; the domain in your base URL is not the store it belongs to. A token carries its own store, and the URL may only confirm that store, never select a different one — which is why editing the domain cannot be used to read someone else's data.
In practice this is a copy-paste error: a base URL kept from another store, or a custom domain used
where the .myshopify.com one belongs. Copy the Public API base URL from Settings →
Integrations, which already has the right domain in it.
The comparison ignores case, so MyStore.myshopify.com and mystore.myshopify.com both work. If
you are seeing this error, the domain is genuinely a different store.
This token is read-only
The request used a method other than GET. Every write on this API needs an internal service key,
and there is no merchant-facing scope that grants one.
This is checked on the method, before the route. So a POST to a path that does not exist
answers 403 This token is read-only rather than a 404 — the message is about your method, not
about the path being wrong.
Rate limit exceeded
Sixty requests per minute, counted per token, with Retry-After giving the seconds to wait.
Every response — including successful ones — carries X-RateLimit-Limit and
X-RateLimit-Remaining. Read X-RateLimit-Remaining and slow down before you hit zero, rather
than discovering the limit by tripping it.
Requests are counted before the token is resolved, so a burst of bad tokens is throttled the same way as a burst of good ones.
Successes that are not what they look like
Three cases return 200 and still do not mean what a naive client will assume:
| Request | Answer | Why it matters |
|---|---|---|
GET /offer/:id with an unknown id | success: true, no data | Not a 404. Check data exists |
GET /offers/count after an internal failure | success: false, data: {total: 0} | A client reading data.total without checking success renders a confident zero |
GET /settings on a store that never saved any | success: true, data: {} | An empty object is the real answer, not an error |
Check success before reading data, on every call. The /offers/count case is the one that
bites: it is the only endpoint that ships a plausible-looking payload alongside success: false.
Empty results are not errors
An endpoint answering "there is nothing here" — no offers matching a filter, no published languages
beyond the primary one, no active discounts — returns 200 with an empty list. Treat that as an
answer.
Reporting a problem
Include the endpoint and full query string, the HTTP status, the message, the connection's
name from Settings → Integrations, and roughly when it happened.
Never send the token itself. Support cannot read it back either — the server holds only a fingerprint. If a token has appeared in a ticket, a screenshot, a shared log, or any browser bundle, revoke it and create a new one.
Related
- Overview
- Endpoints
- MCP troubleshooting — the same token, seen from the AI side