Skip to content
Linra Omni Developer Docs

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.

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.

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.

The six endpoints split into two families with genuinely different window semantics — read this before you build against either one.

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.

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.

Your own top-line figures for the window: order volume, gross revenue, what came back via returns, and the net.

Terminal window
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:

  • revenue is 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.
  • revenue is gross-as-originally-sold, never rewritten by a later return. A partially or fully returned order still counts at its full original value in revenue — “how did we do in January” is a historical fact a February return shouldn’t silently edit. The return itself shows up as returnsValue, 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’s returnsValue, not December’s.
  • Only Confirmed orders count — a still-pending or a cancelled order never contributed a charge, so neither counts as a sale.
  • averageOrderValue is revenue / orderCount, rounded to 4 decimal places; 0 when orderCount is 0.

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.

Terminal window
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.

The same orders, broken down per variant, ordered by revenue descending.

Terminal window
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.

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.

Terminal window
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.

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.

Terminal window
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:

  • abandonmentRate is cartsExpiredOrAbandoned / (cartsCheckedOut + cartsExpiredOrAbandoned) — deliberately not divided by cartsCreated. 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.
  • abandonedCartsValue is 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. abandonedCartsValueIsTruncated flips to true only under extreme volume, when the underlying reconstruction can’t materialize every abandoned cart in one pass; under normal volume it’s always false.

Per variant: how many of your own carts added it within the window, and of those, how many eventually checked out.

Terminal window
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.