Reports
The back-in-stock reports resource mirrors the preorder reports machinery: every report has a JSON read and a sibling async CSV export. Reads run against read replicas.
Path prefix: /back_in_stock/reports
Report manifest
GET /back_in_stock/reportsReturns the catalog of available reports — { "reports": [{ "name", "description", "aliases", "url" }] } — generated from the live registry so it can't drift from the actual report actions.
Shared filters
All reads accept the same filter set:
| Param | Type | Notes |
|---|---|---|
from, to | ISO 8601 date or timestamp | Window — defaults to the last 30 days. A bare date is that whole day in the shop's time zone |
range | all_time | The shop's whole history, instead of from/to |
variant_id | integer | Single Shopify variant id |
product_id | integer | Single Shopify product id |
channel | email / sms / push | |
market_id | numeric or gid://shopify/Market/… | Single Shopify market |
granularity | day / week / month | Time-series reports only; default day |
Every response echoes the resolved window at the top: "window": { "from": "...", "to": "..." }, or null for range=all_time.
What each number counts
Each number counts what its name says:
- signups: signups created in the window.
- pending: signups waiting now. It ignores the window. With double opt-in on, an unconfirmed signup is not waiting yet.
- notified: signups notified in the window, by when they were notified.
- conversions: orders attributed to a back-in-stock alert at medium or high confidence, placed in the window.
Where the numbers come from
A request with no variant_id, product_id, channel or market_id reads the same sources as the Back in Stock Overview dashboard, so the numbers match it:
range=all_time: the shop's lifetime counters (signups, orders, revenue).- A window: the daily rollups, plus today counted live.
A scoped request counts the rows live, because the dashboard's sources have no product, variant, channel or market dimension. Every report returns source: lifetime totals, daily rollups including today, or live. Figures with no dashboard source (notified, pending, distinct customers) are always counted live.
Reads
| Method | Path | Description |
|---|---|---|
GET | /back_in_stock/reports/summary | Totals for a window. |
GET | /back_in_stock/reports/signups | Time-series of signups created (day/week/month). |
GET | /back_in_stock/reports/notifications | Time-series of notifications sent, split by channel, with the emails and delivered SMS behind them. |
GET | /back_in_stock/reports/conversions | Time-series of orders attributed to alerts, the revenue recovered and the buyers. |
GET | /back_in_stock/reports/products_in_demand | Variants or products ranked by waitlist demand. |
Summary
{
"window": { "from": "...", "to": "..." },
"currency": "USD",
"source": "daily rollups including today",
"totals": {
"signups": 420,
"unique_customers": 390,
"pending": 661,
"pending_by_channel": { "email": 600, "sms": 61, "push": 0 },
"notified": 461,
"notified_customers": 323,
"last_notified_at": "2026-09-26T14:02:11Z",
"conversions": 96,
"conversion_rate": 0.2972,
"recovered_revenue": "4812.50"
}
}unique_customers: distinct people among the signups created in the window.notified_customers: distinct people among the signups notified in the window.conversion_rate:conversions / notified_customers, so a person notified for several variants counts once.last_notified_at: the latest send for the scope, whatever the window.
Signups
Per bucket: signups created in it, how many of those are still waiting, and signups notified in it.
{
"window": { "...": "..." },
"granularity": "day",
"source": "daily rollups including today",
"series": [
{ "date": "2026-06-01", "signups": 24, "pending": 10, "notified": 14 }
]
}date is the bucket's first day in the shop's time zone. Unscoped, signups comes from the daily rollups.
Notifications
Per bucket: notifications sent (by when they were sent) split by channel, blocked sends (by when they were attempted), and the messages behind the sends.
{
"window": { "...": "..." },
"granularity": "day",
"source": "daily rollups including today",
"series": [
{ "date": "2026-06-01", "sent": 120, "blocked": 4, "email": 100, "sms": 15, "push": 5,
"emails_sent": 101, "sms_delivered": 14 }
]
}emails_sent counts back-in-stock emails created and sms_delivered counts SMS messages delivered. Unscoped, those two come from the daily rollups, as the Overview chart does.
This is the aggregate view. For the row-level send log — who got each notification, and why a blocked send was stopped — use the read-only notifications resource at GET /back_in_stock/notifications.
Conversions
{
"window": { "...": "..." },
"granularity": "day",
"currency": "USD",
"source": "daily rollups including today",
"series": [
{ "date": "2026-06-01", "orders": 8, "recovered_revenue": "401.00", "unique_customers": 7 }
]
}unique_customers counts the distinct buyers in the bucket. From the daily rollups it is summed per day, so a buyer on two days of a week counts twice in that week.
Products in demand
Variants (or products) ranked by demand — the answer to "what should I restock first?". pending is signups waiting now, whatever the window. total is signups created in the window. A row appears when it has either.
| Param | Notes |
|---|---|
group_by | variant (default) / product |
sort_by | pending (default) / total / last_requested_at |
direction | desc (default) / asc |
page, per_page | per_page default 50, max 500 |
{
"window": { "...": "..." },
"group_by": "variant",
"sort_by": "pending",
"direction": "desc",
"rows": [
{
"shopify_variant_id": 123,
"shopify_product_id": 456,
"variant_title": "Small / Blue",
"product_title": "Classic Tee",
"pending": 42,
"total": 65,
"last_requested_at": "2026-06-09T08:30:00Z"
}
],
"meta": { "total_count": 1, "page": 1, "per_page": 50, "total_pages": 1 }
}With group_by=product, rows have shopify_product_id and product_title and no variant fields. last_requested_at is the latest signup in the window.
CSV exports
Every read has a sibling */export action:
| Method | Path |
|---|---|
POST | /back_in_stock/reports/summary/export |
POST | /back_in_stock/reports/signups/export |
POST | /back_in_stock/reports/notifications/export |
POST | /back_in_stock/reports/conversions/export |
POST | /back_in_stock/reports/products_in_demand/export |
Exports accept the same filters as the corresponding read (plus sort_by, direction, page, per_page where the read supports them). They submit a job and return 202 Accepted:
{
"job_id": "exp_1a2b3c4d5e6f7a8b",
"status_url": "/api/v2/external/back_in_stock/reports/exports/exp_1a2b3c4d5e6f7a8b"
}Polling
GET /back_in_stock/reports/exports/:job_idstatus is one of running, ready, failed, cancelled. When ready, the response includes the download URL:
{
"job_id": "exp_1a2b3c4d5e6f7a8b",
"status": "ready",
"download_url": "https://stoq-exports.s3.amazonaws.com/...",
"file_name": "back_in_stock_signups_2026-06-10.csv",
"row_count": 420,
"progress": 100
}An unknown job_id returns 404. The polling pattern is the same one used across v2 — see Bulk & Async Jobs.
Example: signups by week, then export
curl 'https://app.stoqapp.com/api/v2/external/back_in_stock/reports/signups?from=2026-03-01&to=2026-06-01&granularity=week' \
-H "X-Auth-Token: $STOQ_API_KEY"curl -X POST 'https://app.stoqapp.com/api/v2/external/back_in_stock/reports/signups/export' \
-H "X-Auth-Token: $STOQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"2026-03-01","to":"2026-06-01","granularity":"week"}'# Poll until status: "ready"
curl 'https://app.stoqapp.com/api/v2/external/back_in_stock/reports/exports/exp_1a2b3c4d5e6f7a8b' \
-H "X-Auth-Token: $STOQ_API_KEY"
