Your sales & cart analytics
Self-serve reporting over your own orders and cart activity — sales figures and cart-behavior insights.
Six read-only endpoints give you your own sales figures and cart-behavior analytics — nothing about any other partner, and nothing about Linra’s own commercial position.
Self-only, by construction
Section titled “Self-only, by construction”Every endpoint on this surface is derived exclusively from your authenticated partner identity —
there is no partnerId parameter anywhere on this API, so a cross-partner read isn’t something you
could accidentally (or deliberately) construct. You will only ever see your own orders, your own
cart events, and your own carts.
What the reports contain
Section titled “What the reports contain”Every figure here (order count, revenue, returns, quantities, conversion rates) is a fact about your own business and is reported in full. Linra’s own commercial figures are not part of this API.
Windows: two shapes, two behaviours
Section titled “Windows: two shapes, two behaviours”The six endpoints split into two families with genuinely different window semantics — read this before you build against either one.
Sales endpoints (fromUtc / toUtc)
Section titled “Sales endpoints (fromUtc / toUtc)”GET .../sales/summary, GET .../sales/time-series, and GET .../sales/by-product all take
fromUtc (inclusive) and toUtc (exclusive). Omit either or both and the window defaults to the
last 30 days: toUtc defaults to now, fromUtc to 30 days before whatever toUtc resolved to.
These three endpoints validate the resolved window and reject a nonsensical one:
| Code | HTTP | When |
|---|---|---|
VALIDATION_INVALID_PERIOD |
400 | fromUtc is not strictly before toUtc. |
VALIDATION_REPORT_WINDOW_TOO_LARGE |
400 | The window spans more than 400 days. |
Cart-insights endpoints (from / to)
Section titled “Cart-insights endpoints (from / to)”GET .../cart-insights/lost-demand, GET .../cart-insights/abandonment, and
GET .../cart-insights/conversion take from (inclusive) and to (exclusive) — same half-open
shape, different parameter names, because they read from the cart-event ledger rather than the
order ledger. They default to the last 30 days identically.
Unlike the sales endpoints, these three do not validate the window at all — there’s no
VALIDATION_INVALID_PERIOD and no maximum-window check. An inverted window (from after to)
simply matches no events and returns an empty page (or all-zero figures for abandonment), rather
than a 400. Don’t rely on a 400 to catch a swapped from/to on these three — check your own inputs.
1. Sales summary
Section titled “1. Sales summary”Your own top-line figures for the window: order volume, gross revenue, what came back via returns, and the net.
GET /api/v1/scent/reports/sales/summary?fromUtc=2026-01-01T00:00:00Z&toUtc=2026-02-01T00:00:00Z{ "state": "SUCCESS", "payload": { "fromUtc": "2026-01-01T00:00:00Z", "toUtc": "2026-02-01T00:00:00Z", "orderCount": 42, "revenue": 8500.00, "returnsValue": 300.00, "netRevenue": 8200.00, "averageOrderValue": 202.3810 }}A few conventions worth knowing:
revenueis GOODS-only — it excludes the order-level shipping fee, at every level of this report (summary, time-series, by-product). A fee is charged once per order and has no natural per-product home, so folding it in here would make this figure structurally unable to match the by-product breakdown’s own totals.revenueis gross-as-originally-sold, never rewritten by a later return. A partially or fully returned order still counts at its full original value inrevenue— “how did we do in January” is a historical fact a February return shouldn’t silently edit. The return itself shows up asreturnsValue, dated by when the RETURN happened, not when the original order was placed — so a return of a December order that lands in January reduces January’sreturnsValue, not December’s.- Only
Confirmedorders count — a still-pending or a cancelled order never contributed a charge, so neither counts as a sale. averageOrderValueisrevenue / orderCount, rounded to 4 decimal places;0whenorderCountis0.
2. Sales time series
Section titled “2. Sales time series”The same underlying orders, bucketed one point per Riyadh calendar day the window spans — including days with zero orders, so a chart built from this never has a silent gap.
GET /api/v1/scent/reports/sales/time-series?fromUtc=2026-01-01T00:00:00Z&toUtc=2026-01-03T00:00:00Z{ "state": "SUCCESS", "payload": [ { "dateUtc": "2026-01-01T00:00:00Z", "orderCount": 3, "revenue": 620.00 }, { "dateUtc": "2026-01-02T00:00:00Z", "orderCount": 0, "revenue": 0 } ]}The response is a bare array (not a paged result) — there’s exactly one row per calendar day in the window, so paging would only add overhead for a bounded, predictable result size.
3. Sales by product
Section titled “3. Sales by product”The same orders, broken down per variant, ordered by revenue descending.
GET /api/v1/scent/reports/sales/by-product?fromUtc=2026-01-01T00:00:00Z&toUtc=2026-02-01T00:00:00Z&page=1&pageSize=20{ "state": "SUCCESS", "payload": { "items": [ { "variantId": "11111111-1111-1111-1111-111111111111", "sku": "DIOR-SAUVAGE-100", "scentName": "Sauvage", "brandName": "Dior", "quantitySold": 12, "revenue": 3480.00 } ], "pagination": { "page": 1, "pageSize": 20, "totalCount": 1, "totalPages": 1 } }}The summary endpoint’s totals are computed independently over the full window — never derive your own total revenue by summing across this endpoint’s pages; fetch every page if you need an exact sum, or just use the summary endpoint.
4. Lost demand
Section titled “4. Lost demand”Per-variant counts of times a shopper tried to add this variant to their cart and was blocked — split by why (the variant was unavailable, or the requested quantity exceeded what was actually sellable). Only variants with at least one blocked event in the window appear, ordered by total blocked count descending.
GET /api/v1/scent/reports/cart-insights/lost-demand?from=2026-01-01T00:00:00Z&to=2026-02-01T00:00:00Z&page=1&pageSize=20{ "state": "SUCCESS", "payload": { "items": [ { "variantId": "11111111-1111-1111-1111-111111111111", "sku": "DIOR-SAUVAGE-100", "scentName": "Sauvage", "brandName": "Dior", "blockedUnavailableCount": 4, "blockedQuantityCount": 1, "totalBlockedCount": 5 } ], "pagination": { "page": 1, "pageSize": 20, "totalCount": 1, "totalPages": 1 } }}Linra’s own internal version of this report additionally shows how many partners in total (including your competitors) are waiting on a restock notification for a given variant — that’s a fleet-wide, cross-partner signal, so it’s withheld here. This endpoint only ever tells you about demand from your own shoppers.
5. Cart abandonment
Section titled “5. Cart abandonment”A single summary object (not paged) — how many carts were created, checked out, or reached a terminal non-purchase outcome in the window, plus the abandonment rate and the value currently parked in your own live abandoned carts.
GET /api/v1/scent/reports/cart-insights/abandonment?from=2026-01-01T00:00:00Z&to=2026-02-01T00:00:00Z{ "state": "SUCCESS", "payload": { "from": "2026-01-01T00:00:00Z", "to": "2026-02-01T00:00:00Z", "cartsCreated": 60, "cartsCheckedOut": 42, "cartsExpiredOrAbandoned": 10, "abandonmentRate": 0.1923, "abandonedCartsValue": 1450.75, "abandonedCartsValueIsTruncated": false }}Two things worth knowing:
abandonmentRateiscartsExpiredOrAbandoned / (cartsCheckedOut + cartsExpiredOrAbandoned)— deliberately not divided bycartsCreated. A cart that’s still open (neither bought nor abandoned yet) hasn’t reached any outcome yet, so counting it in the denominator would understate the rate with carts that simply haven’t resolved.abandonedCartsValueis a live snapshot, not scoped to[from, to). It answers “how much is sitting in my abandoned carts right now,” regardless of when those carts were abandoned — a cart abandoned last month still counts today if it’s still live.abandonedCartsValueIsTruncatedflips totrueonly under extreme volume, when the underlying reconstruction can’t materialize every abandoned cart in one pass; under normal volume it’s alwaysfalse.
6. Add-to-cart conversion
Section titled “6. Add-to-cart conversion”Per variant: how many of your own carts added it within the window, and of those, how many eventually checked out.
GET /api/v1/scent/reports/cart-insights/conversion?from=2026-01-01T00:00:00Z&to=2026-02-01T00:00:00Z&page=1&pageSize=20{ "state": "SUCCESS", "payload": { "items": [ { "variantId": "11111111-1111-1111-1111-111111111111", "sku": "DIOR-SAUVAGE-100", "scentName": "Sauvage", "brandName": "Dior", "itemAddedEventCount": 30, "cartsThatAdded": 25, "cartsThatCheckedOut": 18, "conversionRate": 0.72 } ], "pagination": { "page": 1, "pageSize": 20, "totalCount": 1, "totalPages": 1 } }}cartsThatCheckedOut is not re-windowed against [from, to) — a cart that added the variant right
before the window closes still counts as converted if it checks out slightly after. conversionRate
is cartsThatCheckedOut / cartsThatAdded; 0 when cartsThatAdded is 0.

