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:
| Field | Type | Notes |
|---|---|---|
id | string | The offer id |
name | string | As named in the app |
status | string | active, scheduled, or expired — there is no other state |
type | string | The placement, by its admin label |
abTest | boolean | Stated on every row, true or false |
url | string | Opens the offer in your Shopify admin. Absent on a deleted offer |
revenue | number | All-time revenue, on rows that carry it |
priority | number | Display order, 1 shown first — only from by-priority |
createdAt | string | ISO, on your store's clock |
claims, acceptanceRate | number | Only from the revenue ranking |
isDeleted | boolean | Present only when true |
type is always one of: Post-purchase upsell, Thank you page upsell, Coupon offer, Order status page upsell.
?type=Coupon%20offer and ?type=thank-you-page-coupon both work. Responses always use the label.List offers
GET /data/offers| Parameter | Type | Notes |
|---|---|---|
query | string | Case-insensitive substring match on the offer name |
status | string | active, scheduled, expired |
type | string | One placement. Omit for all four |
page | integer | 20 per page, 1-based, capped at 50. Anything below 1 or unparseable falls back to page 1; anything above 50 is clamped to 50 |
sort | string | newest (default), oldest, revenue, revenue_low |
abTest | boolean | true returns only offers running an A/B test |
Response
| Field | Type | Notes |
|---|---|---|
offers | array | Offer rows |
total | integer | This 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 |
hasMore | boolean | Whether another page exists — this is the field to page on |
currency | string | Your store's currency |
appliedFilter | object | scope is shop or filtered, plus whichever of name, placement, status, abTest was applied |
shopDomain | string | The 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"
}
}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-priorityOne placement's offers in display order, 1 shown first.
| Parameter | Type | Notes |
|---|---|---|
placement | string | Required |
page | integer | 20 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| Parameter | Type | Notes |
|---|---|---|
type | string | One placement. Omit for all four plus a grand total |
Response
| Field | Type | Notes |
|---|---|---|
total | integer | Across the placements counted |
byType | array | One row per placement: type, count, and byStatus |
byStatus | object | active, 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.
| Field | Type | Notes |
|---|---|---|
id, name, offerName | string | offerName is the same value, kept for clients that read it |
status, type | string | As on an offer row |
startDate, endDate | string | ISO, on your store's clock. endDate only when an end is switched on |
triggerCriteria | string | no-condition or set-conditions |
triggerConditions | array | Each condition, with the products, collections or tags it names |
discountType, discountValue | The offer-level discount, present only when no product carries its own | |
revenue, claims, acceptanceRate, priority, createdAt, abTest | The offer's own stats | |
currency | string | What the money above is in |
url | string | Opens the offer in your admin |
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}/stagesWhat the offer offers, stage by stage.
| Field | Type | Notes |
|---|---|---|
offerName | string | |
stages | array | One entry per stage |
currency | string |
Each stage carries whichever apply:
| Field | Notes |
|---|---|
products | Each with its own discountType and discountValue, stated even when there is none |
bundleDeal | type, discountType, and tiers — present only when a bundle is live |
abTest | When a stage is being tested, this replaces that stage's products and bundle |
stage | The stage's label, and which stage it follows — Post-purchase upsell only |
Related
- Analytics
- MCP tool reference — the same payloads, for an AI client