Skip to content

A daily digest of the whole store

Goal: one Slack message every morning with yesterday’s numbers across every quiz, the quizzes that earned the most, and the products the quizzes sold most, so the team sees the day without opening the dashboard.

Uses: Analytics: the store-wide GET /v1/analytics/overview and GET /v1/analytics/quizzes, then GET /v1/quizzes/{quiz_id}/analytics/products for the quizzes that earned. Scope: analytics:read. For one quiz’s digest see the completion alert; this recipe covers the store.

The flow

  1. Pick one timezone (TZ) and compute yesterday in it; send it as tz on every request so all three routes describe the same day.
  2. GET /v1/analytics/overview?from=yesterday&to=yesterday: totals holds the store’s views, starts, completions, orders and revenue.
  3. GET /v1/analytics/quizzes?sort=revenue&dir=desc&limit=5 for the same day: the quizzes that earned, with their completion_rate.
  4. For each of those quizzes, GET /v1/quizzes/{quiz_id}/analytics/products?sort=units&limit=20, then add the rows up by product_id: one product can be recommended by several quizzes.
  5. Post the lines to a Slack incoming webhook.
import datetime as dt
import os
from decimal import Decimal
from zoneinfo import ZoneInfo
import httpx
API = "https://api.octaneai.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['OCTANE_API_KEY']}"}
SLACK = os.environ["SLACK_WEBHOOK_URL"]
TZ = "America/New_York"
def get(path, **params):
r = httpx.get(f"{API}{path}", params={**params, "tz": TZ}, headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json()
yesterday = (dt.datetime.now(ZoneInfo(TZ)).date() - dt.timedelta(days=1)).isoformat()
day = {"from": yesterday, "to": yesterday}
t = get("/analytics/overview", **day)["totals"]
quizzes = get("/analytics/quizzes", sort="revenue", dir="desc", limit=5, **day)["data"]
products = {} # product_id -> {title, units, revenue}
for quiz in quizzes:
if quiz["revenue"] is None or Decimal(quiz["revenue"]["amount"]) == 0:
continue
for row in get(f"/quizzes/{quiz['quiz_id']}/analytics/products", sort="units", limit=20, **day)["rows"]:
p = products.setdefault(row["product_id"], {"title": row["title"] or row["product_id"], "units": 0, "revenue": Decimal(0)})
p["units"] += row["units"]
p["revenue"] += Decimal(row["line_revenue"]["amount"])
revenue = f"{t['revenue']['amount']} {t['revenue']['currency']}" if t["revenue"] else "no revenue" # money can be null
lines = [f"*{yesterday}*: {t['views']} views, {t['starts']} starts, {t['completions']} completions, {t['orders']} orders, {revenue}"]
for q in quizzes:
if q["starts"] == 0:
continue # every quiz is listed, drafts and quiet ones with zeros
rate = f"{q['completion_rate']:.0f}%" if q["completion_rate"] is not None else "no starts"
lines.append(f"- {q['name']}: {q['starts']} starts, {rate} completed, {q['orders']} orders, {q['revenue']['amount']} {q['revenue']['currency']}")
for p in sorted(products.values(), key=lambda p: p["units"], reverse=True)[:5]:
lines.append(f" - {p['title']}: {p['units']} units, {p['revenue']}")
print("\n".join(lines))
httpx.post(SLACK, json={"text": "\n".join(lines)}, timeout=30).raise_for_status()

The message reads:

*2026-09-12*: 18853 views, 16979 starts, 12009 completions, 253 orders, 13991.00 USD
- Find your routine: 9840 starts, 70% completed, 141 orders, 7812.00 USD
- Gift finder: 7139 starts, 69% completed, 112 orders, 6179.00 USD
- Hydrating Serum: 96 units, 3360.00
- Night Cream: 71 units, 2911.00

Errors to handle

StatusWhyWhat to do
403 insufficient_scopeThe key lacks analytics:read.Mint a key with the scope.
422 validation_errorAn unknown tz, or to before from.Fix the request; nothing to retry.
429 rate_limitedThe allowance is spent (a digest over many quizzes).Sleep for Retry-After seconds and continue; every call here is idempotent.
503 limiter_unavailableLimits could not be checked.Retry after Retry-After.

Things to know

  • Money is a decimal string. {"amount": "12.50", "currency": "USD"}; add it up with a decimal type (Decimal, big.Rat, integer cents), never a float. A quiz with no orders reports "0"; revenue is null only when the store’s currency is not known yet.
  • One product, several quizzes. The products route is per quiz, so the same product appears once per quiz that showed it; the digest adds the rows up by product_id. units and line_revenue count the product’s lines on attributed orders; times_shown is how often a result page showed it.
  • Yesterday is closed. freshness.closed_days_through is yesterday in the request’s timezone by the time a morning job runs, so the numbers will not change; the same request tomorrow answers a 304 if you send the ETag back.
  • Cost. Each call that answers a body costs 3 units: 2 + one per quiz that earned, against a Plus allowance of 72 refilled at 12 a second. A store with hundreds of quizzes should keep limit=5 on the quizzes call, as here.
  • Every quiz is a row. GET /v1/analytics/quizzes lists every quiz of the store, drafts included, with zeros on a quiet day; status says which are published. A deleted quiz is not listed.