Errors
Every failure comes back in the same envelope, with a stable code:
{"success": false, "data": null, "error": {"code": "INVALID_KEY", "message": "…"}}Branch on error.code or on the HTTP status. Both are stable; the message is written for a person
reading a log and may be reworded.
Every code
| Code | HTTP | Cause | What to do |
|---|---|---|---|
MISSING_KEY | 401 | No Authorization header | Send Authorization: Bearer aov_sk_…. Do not revoke your key — it is not the problem |
INVALID_KEY | 401 | Unknown, revoked, or expired key | Create a new key in Settings → Store API |
WRONG_SURFACE | 401 | An aov_mcp_ key was used here | Mint a Store API key; MCP keys work only at /mcp |
NO_PLAN | 403 | Store is not on a paid subscription | Renew, then create new keys — see below |
APP_UNINSTALLED | 403 | Cart Drawer was uninstalled from the store | Reinstall. New keys will not help |
RATE_LIMIT | 429 | More than 60 requests in a minute | Back off about a minute |
INVALID_INPUT | 400 | A parameter is wrong | Fix the parameter. Retrying unchanged will never succeed |
NOT_FOUND | 404 | No such path | Check it against GET /store-api/v1 |
INTERNAL_ERROR | 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 | RATE_LIMIT, INTERNAL_ERROR |
| Never retry unchanged | INVALID_INPUT, NOT_FOUND, WRONG_SURFACE |
| Stop and alert a human | MISSING_KEY, INVALID_KEY, NO_PLAN, APP_UNINSTALLED |
A retry loop that treats every failure the same will hammer the API for an hour on a typo and hide the one error that needed someone's attention.
This surface reports MISSING_KEY and APP_UNINSTALLED separately, which the
MCP endpoint does not — there, all of those arrive as one "revoked"
message. If you are diagnosing a key that works in neither place, the Store API will tell you more
about why.
NO_PLAN also revokes your keys
When a request arrives from a store that is no longer on a paid subscription, the response is
NO_PLAN and every Store API key on that store is revoked. Renewing does not restore them.
Someone has to create new ones and update the integration.
Every other failure here is recoverable with the same key. This one is not, and an integration that
retries through it moves from NO_PLAN to INVALID_KEY and stays there.
MISSING_KEY and INVALID_KEY are deliberately separate
MISSING_KEY means no header arrived. The key is fine; the request is not — usually a header
dropped by a proxy, a client library that passes headers differently, or a missing Bearer
prefix.
INVALID_KEY means a key arrived and is not usable.
Revoking is not reversible. Reading MISSING_KEY as "bad key" and revoking destroys a working
credential while leaving the real problem — the missing header — exactly where it was.
An unknown key and a revoked key both return INVALID_KEY, on purpose. Distinguishing them would
confirm to whoever holds it that a key was once real.
INVALID_INPUT
Only /analytics can produce it today, and only for two cross-field rules:
| Request | Why |
|---|---|
?start=2026-08-31&end=2026-08-01 | The window runs backwards |
| A range longer than 400 days | Beyond the analytics window cap |
The date format itself is not checked on this surface. ?start=2026-02-31, or a value that is
not a date at all, is passed through rather than refused — you get a window you did not ask for
rather than an error. Validate on your side, and always compare dateRange in the response
against what you sent.
The equivalent request over MCP is refused, with a message naming the bad date. The two surfaces share their handlers but not, today, their input checking.
RATE_LIMIT
60 requests per minute, counted per key. Wait roughly a minute.
Requests are counted before the key is resolved, so a burst of bad keys is throttled the same way as a burst of good ones. The budget is per key, so one integration cannot spend another's.
Empty results are not errors
Some endpoints answer "there is nothing here" with HTTP 200 and a found-style flag or an
explanatory message rather than a failure — a store with no saved translations, for instance, is told
so in words. Treat that as an answer, not an error.
Reporting a problem
Include the endpoint and full query string, the error.code, the HTTP status, the key's name
from the connections list, and roughly when it happened.
Never send the key itself. Support cannot read it back either — the server holds only a fingerprint. If a key has appeared in a ticket, a screenshot, a shared log, or any browser bundle, revoke it and create a new one.