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.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
since | ISO 8601 datetime | no | Lower bound on paid_at for the count. Clients send the viewer's local start of day (new Date().setHours(0,0,0,0)), so "today" follows the device's timezone. Defaults to the server's start of day. The bound is fixed for the life of the connection — the count does not reset at midnight. |
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/orders?since=2026-09-18T18:30:00.000Z'Example 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"}Webhooks Module — Admin
Archive of every inbound provider webhook delivery — the request we received and the exact response we sent back, including deliveries we rejected and ones rate-limited before routing. Stored in ClickHouse with a 90-day TTL, joined to the idempotency ledger, and served as a filterable, paginated admin read API with an operator resend that recomputes the provider credential rather than storing it.
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.