Analytics Module — Vendor surface
Vendor-facing HTTP surface for precomputed dashboard snapshots (sales, earnings, fulfilment, inventory KPIs for today / 7d / 30d / all-time) and a live SSE paid-order feed.
Vendor-facing HTTP surface for precomputed dashboard snapshots (sales, earnings, fulfilment, inventory KPIs) and a live SSE order feed. The dashboard is computed every 10 minutes by a background worker and served directly from a snapshot table — it is never computed on request. The SSE stream is a long-lived connection that emits a baseline count on connect and increments as new paid orders arrive.
Source:
api-modules/vendor-analytics/src/controllers/vendor-analytics.controller.ts,api-modules/vendor-analytics/src/controllers/vendor-live-orders.controller.ts.
Conventions
Authentication
Both endpoints require a Better-Auth bearer session with an active vendor.
Authorization: Bearer <session-token>The active vendor is resolved via resolveActiveVendorId(session) — prefers session.session.activeOrganizationId (better-auth canonical) and falls back to activeVendorId. Sessions missing an active vendor are rejected with 403 Forbidden (FORBIDDEN).
There is no platform RBAC permission guard on this surface — vendor-session scoping is the only access gate.
Tenant scoping
Every read scopes to the active vendor's id. Cross-vendor data is never exposed.
Response envelope (dashboard)
{
"data": { /* dashboard payload */ },
"message": "Success",
"statusCode": 200
}SSE stream (live/orders)
The SSE endpoint is not wrapped by the standard response envelope. It is a raw text/event-stream connection; each frame is a data: line containing a JSON object.
Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN (no active vendor) |
| 500 | INTERNAL_SERVER_ERROR |
Money + dates
All monetary fields are integer subunits (paise / cents / eurocents — single currency per tenant). Date/time fields are ISO 8601 strings (computedAt, at, bucket).
Endpoints
GET /vendor/analytics/dashboard
Returns the vendor's precomputed analytics snapshot for the selected time window.
The snapshot is computed every 10 minutes by a background BullMQ worker (vendor-analytics-refresh job). It is never recalculated on request. Before the worker has run for the first time — or for a brand-new vendor with no data — every metric is zeroed and computedAt is null.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
window | "today" | "7d" | "30d" | "all_time" | "30d" | The analytics time window to serve |
Response fields (data object)
| Field | Type | Description |
|---|---|---|
window | string | The window requested |
computedAt | string | null | ISO datetime of the last snapshot computation, or null if never computed |
grossSales | int (subunits) | Total order revenue in the window |
netEarnings | int (subunits) | Revenue after commission deduction |
commissionPaid | int (subunits) | Commission withheld by the platform |
refundsAmount | int (subunits) | Total value of refunds processed |
ordersCount | int | Total orders (paid) in the window |
unitsSold | int | Total line-item units sold |
aov | int (subunits) | Average order value (grossSales / ordersCount, 0 when ordersCount = 0) |
deliveredOrders | int | Sub-orders in delivered status |
cancelledOrders | int | Sub-orders in cancelled status |
avgFulfillSeconds | int | Average seconds from order placed to fulfilled |
avgDeliverSeconds | int | Average seconds from fulfilled to delivered |
productsActive | int | Active product variants |
productsDraft | int | Draft product variants |
productsArchived | int | Archived product variants |
skusLowStock | int | SKUs below their low-stock threshold |
skusOutOfStock | int | SKUs with zero on-hand inventory |
inventoryUnitsOnHand | int | Total on-hand units across all SKUs |
pendingActionCount | int | Sub-orders awaiting vendor action (pending status) |
salesTrend | TrendBucket[] | Revenue + order count over time (see below) |
statusBreakdown | StatusBreakdown | Sub-order counts by status (see below) |
topProductsByRevenue | TopProduct[] (max 5) | Top 5 SKUs ranked by revenue |
topProductsByUnits | TopProduct[] (max 5) | Top 5 SKUs ranked by units sold |
lowStockList | LowStockItem[] (max 20) | Up to 20 SKUs closest to or past their low-stock threshold |
deltas | Deltas | null | Period-over-period changes; null for all_time (no previous period) |
TrendBucket
Bucket granularity depends on the window:
today→ hourly buckets7d,30d→ daily bucketsall_time→ monthly buckets
{
bucket: string; // ISO datetime (start of the bucket)
revenue: number; // integer subunits
orders: number;
}StatusBreakdown
{
pending: number;
fulfilled: number;
delivered: number;
cancelled: number;
returned: number;
}TopProduct
{
variantId: string;
productId: string | null;
name: string;
sku: string;
units: number;
revenue: number; // integer subunits
}LowStockItem
{
variantId: string;
sku: string;
name: string;
onHand: number;
threshold: number | null;
}Deltas
Holds period-over-period comparison for the four headline KPIs. Each delta has an absolute value (in the same unit as the metric) and a pct (percentage change, or null if the previous period had a zero baseline so percentage is undefined). The deltas field itself is null for the all_time window because there is no defined previous period.
{
grossSales: { absolute: number; pct: number | null };
netEarnings: { absolute: number; pct: number | null };
ordersCount: { absolute: number; pct: number | null };
unitsSold: { absolute: number; pct: number | null };
}Example response
// GET /vendor/analytics/dashboard?window=7d
{
"data": {
"window": "7d",
"computedAt": "2026-06-08T09:40:00.000Z",
"grossSales": 18750000,
"netEarnings": 15937500,
"commissionPaid": 2812500,
"refundsAmount": 350000,
"ordersCount": 124,
"unitsSold": 198,
"aov": 151209,
"deliveredOrders": 98,
"cancelledOrders": 7,
"avgFulfillSeconds": 14400,
"avgDeliverSeconds": 172800,
"productsActive": 42,
"productsDraft": 5,
"productsArchived": 3,
"skusLowStock": 4,
"skusOutOfStock": 1,
"inventoryUnitsOnHand": 831,
"pendingActionCount": 19,
"salesTrend": [
{ "bucket": "2026-06-02T00:00:00.000Z", "revenue": 2100000, "orders": 14 },
{ "bucket": "2026-06-03T00:00:00.000Z", "revenue": 2450000, "orders": 16 },
{ "bucket": "2026-06-04T00:00:00.000Z", "revenue": 3200000, "orders": 21 },
{ "bucket": "2026-06-05T00:00:00.000Z", "revenue": 2800000, "orders": 18 },
{ "bucket": "2026-06-06T00:00:00.000Z", "revenue": 3100000, "orders": 20 },
{ "bucket": "2026-06-07T00:00:00.000Z", "revenue": 2750000, "orders": 18 },
{ "bucket": "2026-06-08T00:00:00.000Z", "revenue": 350000, "orders": 17 }
],
"statusBreakdown": {
"pending": 19,
"fulfilled": 0,
"delivered": 98,
"cancelled": 7,
"returned": 0
},
"topProductsByRevenue": [
{ "variantId": "var_01", "productId": "prod_01", "name": "Vitamin C Serum 30ml", "sku": "VCS-30", "units": 28, "revenue": 4200000 },
{ "variantId": "var_02", "productId": "prod_02", "name": "Retinol Night Cream", "sku": "RNC-50", "units": 17, "revenue": 3400000 },
{ "variantId": "var_03", "productId": "prod_01", "name": "Vitamin C Serum 15ml", "sku": "VCS-15", "units": 31, "revenue": 2170000 },
{ "variantId": "var_04", "productId": "prod_03", "name": "Hyaluronic Toner", "sku": "HYA-T", "units": 12, "revenue": 1800000 },
{ "variantId": "var_05", "productId": "prod_04", "name": "SPF 50 Sunscreen", "sku": "SPF-50", "units": 22, "revenue": 1540000 }
],
"topProductsByUnits": [
{ "variantId": "var_03", "productId": "prod_01", "name": "Vitamin C Serum 15ml", "sku": "VCS-15", "units": 31, "revenue": 2170000 },
{ "variantId": "var_01", "productId": "prod_01", "name": "Vitamin C Serum 30ml", "sku": "VCS-30", "units": 28, "revenue": 4200000 },
{ "variantId": "var_05", "productId": "prod_04", "name": "SPF 50 Sunscreen", "sku": "SPF-50", "units": 22, "revenue": 1540000 },
{ "variantId": "var_02", "productId": "prod_02", "name": "Retinol Night Cream", "sku": "RNC-50", "units": 17, "revenue": 3400000 },
{ "variantId": "var_06", "productId": "prod_05", "name": "Peptide Eye Cream", "sku": "PEC-15", "units": 13, "revenue": 1430000 }
],
"lowStockList": [
{ "variantId": "var_07", "sku": "MZ-CLN-100", "name": "Micellar Cleanser 100ml", "onHand": 3, "threshold": 10 },
{ "variantId": "var_08", "sku": "RET-EYE", "name": "Retinol Eye Serum", "onHand": 0, "threshold": 5 },
{ "variantId": "var_09", "sku": "GLW-MASK", "name": "Glow Sheet Mask", "onHand": 4, "threshold": 8 },
{ "variantId": "var_10", "sku": "NIAC-SER", "name": "Niacinamide Serum 30ml", "onHand": 2, "threshold": 10 }
],
"deltas": {
"grossSales": { "absolute": 1250000, "pct": 7.14 },
"netEarnings": { "absolute": 1062500, "pct": 7.14 },
"ordersCount": { "absolute": 11, "pct": 9.74 },
"unitsSold": { "absolute": 17, "pct": 9.39 }
}
},
"message": "Success",
"statusCode": 200
}GET /vendor/analytics/live/orders (Server-Sent Events)
Opens a long-lived SSE connection (text/event-stream) that streams real-time paid-order activity to the vendor.
This endpoint is not wrapped in the standard response envelope — the response body is a raw event stream.
Behaviour
| Event | When emitted | Payload |
|---|---|---|
| Immediately on (re)connect | baseline snapshot of today's paid-order count | snapshot frame |
| On each newly-paid order containing this vendor's line items | incremented live count | order.paid frame |
| Every 30 seconds | keep-alive | heartbeat frame |
Self-healing counter: on every connect (including EventSource auto-reconnects after a dropped connection), the controller re-baselines the count from a fresh DB query. This means a missed event corrects itself within one reconnect cycle — the count never permanently drifts.
Frame shapes
All frames arrive as data: <JSON>\n\n lines.
snapshot — emitted once immediately on connect:
{
"type": "snapshot",
"paidOrdersToday": 12,
"at": "2026-06-08T09:41:05.123Z"
}order.paid — emitted for each newly-paid order that includes this vendor's items:
{
"type": "order.paid",
"paidOrdersToday": 13,
"orderNumber": "ORD-2026-00892",
"vendorTotal": 249900,
"at": "2026-06-08T09:53:28.447Z"
}vendorTotal is the vendor's slice of that order in integer subunits.
heartbeat — emitted every 30 seconds:
{
"type": "heartbeat",
"at": "2026-06-08T10:00:05.001Z"
}Example: curl
curl -N \
-H "Authorization: Bearer <session-token>" \
-H "Accept: text/event-stream" \
https://api.example.com/vendor/analytics/live/ordersExample stream output:
data: {"type":"snapshot","paidOrdersToday":12,"at":"2026-06-08T09:41:05.123Z"}
data: {"type":"heartbeat","at":"2026-06-08T09:41:35.001Z"}
data: {"type":"order.paid","paidOrdersToday":13,"orderNumber":"ORD-2026-00892","vendorTotal":249900,"at":"2026-06-08T09:53:28.447Z"}
data: {"type":"heartbeat","at":"2026-06-08T09:54:05.002Z"}Vendor Module — Admin
HTTP surface for the platform-admin side of vendor onboarding: list / inspect vendor applications, approve them (provisioning a Better-Auth organization + vendor profile) or…
Bulk Invoices — Vendor
HTTP surface for a vendor to batch-generate invoices for many of their own sub-orders at once, merged into a single downloadable PDF, with async status polling and retry of failed orders.