Store API
A read-only REST API over this store's free gift data, for your own scripts, dashboards and
integrations. Every endpoint is a GET, every response has the same shape, and each key belongs to
one store.
If what you want is an AI assistant that can answer questions about the store, you want MCP instead. This surface is for code you write.
| Base URL | https://upsell.avada.io/store-api/v1 |
| Methods | GET only |
| Credential | A key beginning aov_sk_ |
| Sent as | Authorization: Bearer aov_sk_… |
| Rate limit | 60 requests per minute, per key |
| Requires | A paid plan, re-checked on every request |
Create a key
- In AOV.ai Free Gift, go to Settings, then the Store API section.
- Copy the Base URL.
- Give the key a name — the integration it is for. 1–40 characters, and it has to be distinct from your other active keys.
- Click Create key.
The key is shown once, with a ready-to-run curl and Node example underneath.
The key appears only at this moment. The server stores a fingerprint, not the key, so it cannot be shown again — not by you and not by support. Copy it into your secret store before closing the banner. If you lose it, revoke that key and create another.
Your first request
curl "https://upsell.avada.io/store-api/v1/campaigns" \
-H "Authorization: Bearer aov_sk_YOUR_KEY"const res = await fetch('https://upsell.avada.io/store-api/v1/analytics?days=7', {
headers: {Authorization: 'Bearer aov_sk_YOUR_KEY'}
});
const {success, data, error} = await res.json();Every response has the same shape
{"success": true, "data": { }, "error": null}On failure, success is false, data is null, and error carries a stable code and a
message meant to be read by a person:
{"success": false, "data": null, "error": {"code": "INVALID_INPUT", "message": "…"}}Branch on error.code, not on the message. Codes are stable; wording is not. The HTTP status
carries the same meaning, so a client that only checks the status is also correct. Both are listed
in Errors.
This envelope holds even for failures that happen outside the router — a bad path, an unexpected server error. You will never get an HTML error page back, so your parser does not need a special case for one.
The API describes itself
GET /store-api/v1 returns the endpoint list, with each path, its summary, and the query parameters
it accepts. It is generated from the same table that builds the routes, so it cannot fall out of date
with what actually exists.
It requires a key like everything else under the prefix.
Never call this from a browser
The API sends permissive CORS headers, so a request from browser JavaScript will succeed. That is there for server-side tools and local development, not for your storefront.
A key in browser code is a key you have published. Anything running in a shopper's browser is readable by that shopper — bundling, minifying, or obfuscating changes nothing. Call this API from your server, and let the browser talk to your server.
If a key has ever been shipped to a browser, revoke it in the app and create a new one. Removing the code does not un-publish what was already served.
Keys are per surface
Free Gift issues two kinds of key and they are not interchangeable:
| Prefix | Surface | For |
|---|---|---|
aov_sk_ | /store-api/v1 | This API |
aov_mcp_ | /mcp | AI clients — see MCP |
Present an aov_mcp_ key here and the response says so by name — WRONG_SURFACE — rather than
reporting it invalid, because the fix is to mint the right kind of key, not to hunt for a typo.
Revoking one surface never affects the other.
Losing the plan revokes your keys
Every other refusal on this API is temporary: fix the cause and the same key resumes working. Loss of the paid plan is not.
When a request arrives from a store that is no longer on a paid plan, the response is NO_PLAN
and that store's Store API keys are revoked. Upgrading does not bring them back — you create new
keys and update your integration with them.
This is deliberate. Billing changes do not all pass through this app, so the first refused request is the reliable moment to act on one; and keys left dormant would otherwise all wake up together on the day a store is reinstated, with nobody having decided that they should.
Limits
| Requests | 60 per minute, per key |
| Result size | About 16,000 characters per response |
| Campaign list | 100 campaigns |
| Product lookup | 50 handles per request |
| Long strings | Trimmed at 160 characters |
A rate limit is counted per key, so one integration cannot exhaust another's budget. Requests are counted before the key is even resolved, which means a flood of bad keys is throttled too.
What is never returned
- No customer data. No names, emails, addresses, or individual orders. Analytics figures are aggregates over a date range.
- No credentials or internal ids. The store's Shopify access token, its internal record id, and the key's own fingerprint are stripped before anything leaves the server.
- No write access. There is no
POST,PUTorDELETEon this surface. Nothing you call can change a campaign, a widget, a setting or a plan.
Where next
- Endpoints — all ten, with their parameters
- Errors — every code, what causes it, what to do
- MCP tool guide — the meaning of the fields each endpoint returns