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 human
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 plan | Upgrade, then create new keys — see below |
APP_UNINSTALLED | 403 | Free Gift 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 the path 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 of these 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 actually needed someone's attention.
NO_PLAN also revokes your keys
When a request arrives from a store that is no longer on a paid plan, the response is NO_PLAN
and every Store API key on that store is revoked. Restoring the plan does not restore the keys.
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 will move from NO_PLAN to INVALID_KEY and stay there. Treat NO_PLAN as an
alert, not as a backoff.
Nothing changes on the /mcp side: connections there are refused while the plan is gone and resume
working when it returns.
MISSING_KEY and INVALID_KEY are deliberately separate
They look similar and their fixes are opposites.
MISSING_KEY means no header arrived. The key is fine; the request is not. Usually a header that
was dropped by a proxy, a client library that needs headers passed 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 is holding it that a key was once real.
WRONG_SURFACE
The key is valid — for the other surface. Free Gift issues aov_sk_ keys for this API and
aov_mcp_ keys for MCP, and each is refused by the other.
This has its own code rather than being folded into INVALID_KEY because the fix is different: mint
the right kind of key, rather than go looking for a typo in a key that has none.
INVALID_INPUT
A parameter the endpoint cannot use. The message names the parameter and the problem. Retrying an identical request cannot help — fix the parameter.
| Request | Why |
|---|---|
/products with no handles | At least one product handle is required |
/products?handles= | Same — nothing usable in the value |
/products?handles=,,, | No handle survives parsing |
Other parameters are not currently rejected. An out-of-range ?days, an unrecognised
?status, or a timezone that does not exist are passed through rather than refused, so the response
may be an empty result or a window you did not intend. Validate inputs on your side, and check
period in the analytics response against what you asked for.
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.
If you are hitting this legitimately, batch instead: /products takes up to 50 handles in one call,
and /campaigns returns every campaign in one response rather than one per id.
Empty results are not errors
Several endpoints answer "there is nothing here" with HTTP 200 and found: false:
/campaigns/{id}— no campaign with that id on this store/widgets/{type}— this store never configured that widget type/translations— nothing has been customised; the built-in wording is in use/settings— no configuration saved yet
Treat found: false as an answer, not a failure. In particular, /translations returning
found: false means the store uses default wording — not that the widget has no text.
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 log you shared, or any browser bundle, revoke it and create a new one.