Analytics & reports
Stats, daily numbers, the notification list and detail, and CSV export.
Stats
GET /api/v1/stats — totals for the website, plus its 20 most recent sends (newest first):
{
"subscribers": 1280,
"subscriberDevices": { "desktop": 830, "android": 390, "ios": 60 },
"totals": { "sent": 5400, "clicks": 310, "ctr": 5.7, "conversions": 12, "revenue": 940.5 },
"notifications": [
{ "id": "5b0c2f7e-1d7a-4c3e-9a51-2f8e6b1d0c42", "title": "Flash sale 🔥", "body": "…", "url": "…",
"status": "sent", "recipients": 1275, "sent": 1262, "failed": 3, "pruned": 10, "received": 1190,
"clicks": 74, "conversions": 4, "revenue": 316,
"devices": { "desktop": 820, "android": 384, "ios": 58 },
"ab": null, "abStats": null, "error": null,
"scheduled_at": null, "created_at": "2026-10-14T09:30:00.000Z" }
],
"promptAb": null,
"emailVerified": true,
"sendingBlocked": false
}| Field | Meaning |
|---|---|
subscribers | Browsers currently subscribed (one person on two devices counts twice). subscriberDevices splits them by device: desktop, android, ios, mobile, other. |
totals | All sends ever: sent (pushes the browsers' push services accepted), clicks, ctr (clicks ÷ sent, as a percentage with one decimal), conversions and revenue credited to a notification. |
status | queued, scheduled, held_for_review, sending, sent, failed (reason in error) or canceled. |
recipients, sent, failed, pruned | Browsers targeted; pushes accepted by the push service; pushes that failed; expired subscriptions found and removed during this send. |
received | Pushes the browser confirmed it showed. Usually lower than sent (for example a device that stays offline until the push expires). |
devices | sent split by device type. |
ab, abStats | For an A/B send: the ab settings it was sent with, and { a: { delivered, clicks }, b: { delivered, clicks } }. Otherwise null. |
promptAb | Opt-in prompt A/B results, { a: { shown, subscribed }, b: { shown, subscribed } }, or null if you haven't run one. |
emailVerified, sendingBlocked | Whether the account's email is verified, and whether sends are refused until it is (403 email_not_verified). |
Also: GET /api/v1/stats/growth?days=30 → { days, series: [{ date, subscribed, churned }], totals: { subscribed, churned, net } } (1–365 days), and GET /api/v1/stats/health → { active, reachable, unsubscribed, new30, lost30, dormant, churnRate }.
Reports & export
The numbers mean the same thing in every report: sent, pushes the browsers' push services accepted; delivered, pushes a device confirmed it received (what /stats calls received); clicked; ctr, clicked ÷ sent as a percentage with one decimal (0 when nothing was sent); conversions and revenue credited to a notification. from and to are calendar days (YYYY-MM-DD, both included) in tz: an IANA time zone such as Europe/London. Left out, tz is your website's time zone (the one its quiet hours use), or UTC.
Daily numbers
GET /api/v1/stats/timeseries?from=2026-10-01&to=2026-10-14&tz=Asia/Kolkata — one point per day (days without sends are zeros), the totals, and the same totals for the period of the same length just before. Each notification counts on the day it went out: its scheduled time, or the time it was made for a send that started straight away. Without from and to it covers the last 30 days. A range can be at most 366 days (400 invalid_range).
{
"from": "2026-10-01", "to": "2026-10-14", "tz": "Asia/Kolkata", "interval": "day",
"points": [
{ "date": "2026-10-01", "sent": 1262, "delivered": 1190, "clicked": 74, "conversions": 4, "revenue": 316 }
],
"totals": { "sent": 15400, "delivered": 14120, "clicked": 820, "ctr": 5.3, "conversions": 41, "revenue": 3120.5 },
"previous": { "from": "2026-09-17", "to": "2026-09-30", "sent": 13900, "delivered": 12800, "clicked": 690, "ctr": 5, "conversions": 30, "revenue": 2210 },
"currency": "USD"
}currency is the revenue currency set for the website, or null.
List of notifications
GET /api/v1/notifications?from&to&status&q&limit&cursor — newest first, 25 per page (limit up to 100). status is one or more of draft, scheduled, queued, held_for_review, sending, sent, failed and canceled, separated by commas; without it, everything except drafts is listed. q searches titles and texts, in any case. from and to filter on the day a notification was made. For the next page, send the nextCursor you got as cursor, with the same filters; it is null on the last page. Pages don't shift when new sends arrive.
{
"items": [
{ "id": "5b0c2f7e-1d7a-4c3e-9a51-2f8e6b1d0c42", "title": "Flash sale 🔥", "body": "…", "status": "sent",
"created_at": "2026-10-14T09:30:00.000Z", "scheduled_at": null,
"recipients": 1275, "sent": 1262, "delivered": 1190, "failed": 3, "pruned": 10,
"clicked": 74, "ctr": 5.9, "conversions": 4, "revenue": 316,
"ab": false, "targetSummary": "VIP buyers · Topic: Deals", "automation": false, "error": null }
],
"nextCursor": "WyIyMDI2LTEwLTE0VDA5OjMwOjAwLjAwMDAwMFoiLCI1YjBj…"
}| Field | Meaning |
|---|---|
scheduled_at | When a scheduled send goes (or went) out; null for one that started straight away. |
recipients, failed, pruned | Browsers targeted; pushes that failed; expired subscriptions found and removed during the send. |
targetSummary | Who it went to, in words: All subscribers, a segment's name, Topic: News, Specific users, Custom filter, One subscriber, Back-in-stock alert, … |
ab, automation | Whether it was an A/B test; whether an automation, a journey or a product alert made it. |
error | Why a failed send never went out (email_not_verified, suspended, segment_missing, review_rejected or dispatch_error), otherwise null. |
A bad filter answers 400 with invalid_status, invalid_range, invalid_tz or invalid_cursor.
One notification
GET /api/v1/notifications/:id — everything stored about one notification, and how it did. An id that isn't one of this website's answers 404 not_found.
{
"notification": { "id": "5b0c2f7e-…", "title": "Flash sale 🔥", "status": "sent", "target": { "segmentId": "…" },
"targetSummary": "VIP buyers", "ab": null, "ab_test": null, "translations": null, "actions": null,
"image": null, "url": "https://shop.example/sale", "ttl": null, "scheduled_at": null, "stats": { … },
"sent": 1262, "delivered": 1190, "clicked": 74, "ctr": 5.9, … },
"funnel": { "recipients": 1275, "sent": 1262, "delivered": 1190, "clicked": 74, "converted": 4, "failed": 3, "expired": 10 },
"devices": [ { "device": "desktop", "sent": 820, "clicked": 51 } ],
"variants": [],
"languages": [ { "language": "en", "sent": 900, "clicked": 55 } ],
"errors": [ { "code": "expired", "count": 10 }, { "code": "timeout", "count": 3 } ],
"revenue": 316, "currency": "USD",
"sendBody": { "title": "Flash sale 🔥", "url": "https://shop.example/sale", "segmentId": "…" }
}| Field | Meaning |
|---|---|
devices, languages | sent and clicked by device type and by the subscriber's language (two letters, or unknown). They come from the send's event log, so they are empty once your data retention has removed it; the funnel stays. |
variants | For an A/B send, { variant, sent, clicked, ctr } for versions a and b; otherwise []. |
errors | Why pushes didn't arrive: what the push service answered (an HTTP status, timeout, …), and expired for subscriptions found dead and removed. |
sendBody | The POST /api/v1/notifications body that makes this send again, or a draft's saved fields. null when the people were picked at the time (automation, journey and product-alert sends). |
Export to CSV
GET /api/v1/notifications/export.csv — the same filters as the list (no cursor), the same rows in the same order, up to 10,000 of them. Columns: id, created_at, scheduled_at, status, title, target, recipients, sent, failed, clicked, ctr, conversions, revenue, where target is the targetSummary. A title that starts with =, +, - or @ gets a leading ', so a spreadsheet doesn't run it as a formula.
curl "https://relaybell.com/api/v1/notifications/export.csv?from=2026-10-01&to=2026-10-14&status=sent" \
-H "Authorization: Bearer rb_live_xxxxx" -o notifications.csv