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 on the Developer Tools tab in Settings?
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 the Developer Tools tab in Settings, 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, 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