🔌 Store API
Endpoints

Endpoints

Ten endpoints, all GET, all relative to https://upsell.avada.io/store-api/v1.

Each one runs the same handler as the MCP tool beside it, so the two surfaces cannot drift apart. For what the returned fields mean, the MCP tool guide is the longer explanation; this page is the HTTP detail.

EndpointQueryAnswers
/setup-healthCan the gift widget appear on this store?
/campaignsstatusWhat offers are running?
/campaigns/{id}How is one campaign configured?
/analyticsdays includeToday ianaTimezoneRevenue, orders, conversion, AOV
/productshandlesCan these products be gifted?
/widgetsWhich widget types exist here?
/widgets/{type}One widget's settings
/translationslocaleCustomised storefront wording
/settingsGlobal app configuration
/planPlan and usage

GET /store-api/v1 itself returns this list, generated from the same table that builds the routes.


/setup-health

Whether the free gift widget can actually appear on the storefront.

Query: none

Returns: the live theme and whether it supports app blocks, whether the app embed is on, campaign counts by status, the active campaigns, and issues — the conclusion.

{
  "success": true,
  "data": {
    "domain": "example.myshopify.com",
    "theme": {
      "liveThemeId": 139204001866,
      "liveThemeName": "Horizon",
      "isSupportThemeOS": true,
      "embedEnabled": false,
      "themes": []
    },
    "campaigns": {
      "total": 37,
      "active": 1,
      "scheduled": 0,
      "expired": 36,
      "activeCampaigns": []
    },
    "issues": [
      {
        "code": "APP_EMBED_DISABLED",
        "message": "App embed is disabled on the live theme, so the widget cannot render."
      }
    ],
    "errors": []
  },
  "error": null
}

Issue codes: APP_EMBED_DISABLED, THEME_NOT_OS2, NO_CAMPAIGN, NO_ACTIVE_CAMPAIGN.

Sections fail independently. If Shopify is unreachable, campaign counts still come back and the failure is recorded in errors — so errors being non-empty does not mean the rest of the payload is untrustworthy, only that part of it is missing.


/campaigns

Query:

NameValuesNotes
statusactive · scheduled · expiredOmit for everything

Returns: total, and a summary of each campaign — id, name, status, type, targetPage, startDate, endDate, code.

Omit status and you get the campaigns the merchant can see — deleted ones are filtered out in the query, so a capped list of 100 means 100 visible campaigns rather than 100 rows with some missing.

⚠️

Pass only active, scheduled or expired. Any other value is currently forwarded to the query as-is: an unrecognised status returns an empty list rather than an error, and ?status=deleted returns soft-deleted campaigns the app treats as gone. Do not build on either behaviour.


/campaigns/{id}

Path parameter: campaignId, from /campaigns.

Returns: found, and the campaign's complete configuration — trigger conditions, gift products, discount settings, schedule.

An id that does not exist on this store returns HTTP 200 with found: false, not a 404. An id belonging to a different store returns the same, so this endpoint cannot be used to probe whether a campaign exists elsewhere.


/analytics

Query:

NameTypeNotes
days1–365Window length, ending yesterday. Default 30
includeTodaybooleanExtend to the store's current day
ianaTimezonestringOverride the timezone. Omit it

includeToday accepts true, 1, false, 0 — and bare ?includeToday with no value, which counts as yes.

Returns:

{
  "success": true,
  "data": {
    "domain": "example.myshopify.com",
    "period": {
      "start": "August 19, 2026",
      "end": "August 25, 2026",
      "days": 7,
      "timezone": "America/New_York",
      "includesToday": false
    },
    "currency": "USD",
    "cached": false,
    "metrics": {
      "orders": {"value": 0, "previous": 0, "changePercent": null},
      "revenue": {"value": 0, "previous": 0, "changePercent": null},
      "conversionRate": {"value": 0, "previous": 0, "unit": "percent", "changePercent": null},
      "aovByFG": {"value": 0, "previous": 0, "changePercent": null},
      "aovStore": {"value": 0, "previous": 0, "changePercent": null}
    }
  },
  "error": null
}

Three things worth reading rather than assuming:

  • period.timezone — windows are built in the store's own timezone, which may not be yours.
  • currency — so you never have to guess the symbol.
  • conversionRate.unit — it is a percentage, not a fraction.

changePercent is null when the previous window was zero; there is no meaningful change from nothing. includeToday makes the result uncached and partial — the store's day is still running.


/products

Query:

NameRequiredNotes
handlesYesOne handle, or several comma-separated. Max 50

A handle is the part of the product URL after /products/, not the title.

Returns: requested, found, notFound, and products — each with status, totalInventory, tracksInventory, publishedOnOnlineStore, variants, variantCount, giftableVariantCount, canBeGifted and issues. Each variant carries stockStatus and giftable.

⚠️

Use giftable, stockStatus and issues for the verdict. Do not compute it from inventoryQuantity and inventoryPolicy: a variant with stockStatus: "not_tracked" has inventory tracking off and is always available, and precisely because nothing is tracked, those two fields come back null. Treating null as zero produces a confident wrong answer.


/widgets

Query: none

Returns: total, and for each type its internal type name, display title, isPremium and isLocked.

{
  "success": true,
  "data": {
    "total": 11,
    "types": [
      {"type": "progress-bar", "title": "Milestone bar", "isPremium": false, "isLocked": false},
      {"type": "deal-of-the-day", "title": "Deal of the day", "isPremium": false, "isLocked": true}
    ]
  },
  "error": null
}

Call this before /widgets/{type} to get a valid name.


/widgets/{type}

Path parameter: type, for example progress-bar.

Returns: found and the saved settings. A type this store never configured returns 200 with found: false and a pointer back to /widgets.


/translations

Query:

NameNotes
localeOne language code, e.g. vi. Omit for all

Returns: activeLanguages, totalStrings, and two lists — widgets and campaigns. Each entry carries two layers:

  • text — wording typed on the Design screen. What shoppers see, and the fallback for any language without its own translation.
  • translations — per-language overrides.

Only customised strings appear. A string that is absent is using the app's built-in wording, which is not the same as being empty. A store on a plain install returns found: false with an explanation — the defaults are in use, nothing is broken.

Pass locale on multi-language stores; the unfiltered response is the one most likely to hit the size limit.


/settings

Query: none

Returns: found and the store-wide configuration — the switches that apply across every campaign. Per-campaign rules live in /campaigns/{id}.


/plan

Query: none

Returns: the plan and trial state, campaign allowance against campaigns live, revenue used against the cap and when it resets, and issues for limits already exceeded.

{
  "success": true,
  "data": {
    "domain": "example.myshopify.com",
    "plan": {"plan": "basic_26", "shopifyPlan": "basic", "isPaid": true, "trialEndsAt": null},
    "campaigns": {"activeOrScheduled": 1, "limit": null, "unlimited": true, "exceeded": false},
    "revenue": {"used": 0, "limit": null, "unlimited": true, "resetAt": "2026-09-25T05:30:14.769Z"},
    "issues": []
  },
  "error": null
}

limit: null with unlimited: true means no cap on this plan — not a missing value.

This endpoint reports your own store's situation. It does not list plans or quote prices.


Related

Product
Install AppWebsiteAvada Apps
Resources
DocumentationFAQPrivacy Policy
Company
Avada GroupContact
© 2026 Avada Group. All rights reserved.