aov-post-purchase-upsell
🔌 Store API
Resources
Offers

Offers

Five endpoints covering what offers exist, how they are ordered, how they are set up, and what they sell.

All paths are relative to https://aov-post-purchase.firebaseapp.com/public-api/v1.

The offer row

Every list endpoint returns rows in this shape:

FieldTypeNotes
idstringThe offer id
namestringAs named in the app
statusstringactive, scheduled, or expired — there is no other state
typestringThe placement, by its admin label
abTestbooleanStated on every row, true or false
urlstringOpens the offer in your Shopify admin. Absent on a deleted offer
revenuenumberAll-time revenue, on rows that carry it
prioritynumberDisplay order, 1 shown first — only from by-priority
createdAtstringISO, on your store's clock
claims, acceptanceRatenumberOnly from the revenue ranking
isDeletedbooleanPresent only when true

type is always one of: Post-purchase upsell, Thank you page upsell, Coupon offer, Order status page upsell.

Filters accept either form — ?type=Coupon%20offer and ?type=thank-you-page-coupon both work. Responses always use the label.

List offers

GET /data/offers
ParameterTypeNotes
querystringCase-insensitive substring match on the offer name
statusstringactive, scheduled, expired
typestringOne placement. Omit for all four
pageinteger20 per page, 1-based, capped at 50. Anything below 1 or unparseable falls back to page 1; anything above 50 is clamped to 50
sortstringnewest (default), oldest, revenue, revenue_low
abTestbooleantrue returns only offers running an A/B test

Response

FieldTypeNotes
offersarrayOffer rows
totalintegerThis page plus everything still unshown. On page 1 that is the full match count; from page 2 on it counts from that page onward, so pages already walked are not in it
hasMorebooleanWhether another page exists — this is the field to page on
currencystringYour store's currency
appliedFilterobjectscope is shop or filtered, plus whichever of name, placement, status, abTest was applied
shopDomainstringThe store these rows belong to
curl -H "Authorization: Bearer $AOV_TOKEN" \
  "https://aov-post-purchase.firebaseapp.com/public-api/v1/data/offers?status=active&sort=revenue"
{
  "success": true,
  "data": {
    "offers": [
      {
        "id": "8Kd2mQ",
        "name": "Spring Sale add-on",
        "status": "active",
        "type": "Post-purchase upsell",
        "revenue": 4820.5,
        "abTest": false,
        "createdAt": "2026-03-02T09:14:00+07:00",
        "url": "https://admin.shopify.com/store/acme/apps/..."
      }
    ],
    "total": 12,
    "hasMore": false,
    "currency": "USD",
    "appliedFilter": { "scope": "filtered", "status": "active" },
    "shopDomain": "acme.myshopify.com"
  }
}
⚠️
Always read appliedFilter.scope. A filtered list of three offers and a store with three offers look identical without it.

sort=revenue_low is the only way to reach offers that have never earned. They are missing from the revenue ranking entirely — see Analytics.

Offers by priority

GET /data/offers/by-priority

One placement's offers in display order, 1 shown first.

ParameterTypeNotes
placementstringRequired
pageinteger20 per page, 1-based, capped at 50

Response is the same shape as /data/offers, with priority on every row.

⚠️
placement really is required. Priority is a rank inside one placement, so there is no shop-wide order to return — and calling without it is refused with 422 and "placement is required — priority is ranked within one placement", never answered with an empty list. Naming the placement is the fix; retrying is not.

Count offers

GET /data/offers/count
ParameterTypeNotes
typestringOne placement. Omit for all four plus a grand total

Response

FieldTypeNotes
totalintegerAcross the placements counted
byTypearrayOne row per placement: type, count, and byStatus
byStatusobjectactive, expired, scheduled
{
  "success": true,
  "data": {
    "total": 12,
    "byType": [
      {
        "type": "Post-purchase upsell",
        "count": 7,
        "byStatus": { "active": 4, "expired": 2, "scheduled": 1 }
      }
    ],
    "shopDomain": "acme.myshopify.com"
  }
}

This is the cheapest call in the API — useful as a token health check.

Get one offer

GET /data/offer/{id}

The offer's setup: who sees it, and when.

FieldTypeNotes
id, name, offerNamestringofferName is the same value, kept for clients that read it
status, typestringAs on an offer row
startDate, endDatestringISO, on your store's clock. endDate only when an end is switched on
triggerCriteriastringno-condition or set-conditions
triggerConditionsarrayEach condition, with the products, collections or tags it names
discountType, discountValueThe offer-level discount, present only when no product carries its own
revenue, claims, acceptanceRate, priority, createdAt, abTestThe offer's own stats
currencystringWhat the money above is in
urlstringOpens the offer in your admin
⚠️
Trigger conditions must all be met together, and what they name is the trigger — never what is being upsold. A trigger resource deleted from Shopify is marked as such rather than dropped, so a condition that can no longer match is visible instead of silently missing.

An id that is not yours, or no longer exists, returns 200 with data: null. See Errors.

What the offer sells is not here — use the next endpoint.

Get offer stages

GET /data/offer/{id}/stages

What the offer offers, stage by stage.

FieldTypeNotes
offerNamestring
stagesarrayOne entry per stage
currencystring

Each stage carries whichever apply:

FieldNotes
productsEach with its own discountType and discountValue, stated even when there is none
bundleDealtype, discountType, and tiers — present only when a bundle is live
abTestWhen a stage is being tested, this replaces that stage's products and bundle
stageThe stage's label, and which stage it follows — Post-purchase upsell only
Only Post-purchase upsell runs a multi-stage funnel, where each stage names the one before it and whether that one was accepted or declined. The other three placements always return exactly one stage. And a discount is not a price — these payloads never carry final prices.

Related

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