Customer LTV
Your paying shops ranked by estimated lifetime value, each row carrying current MRR, tenure, plan, and first-touch acquisition channel. GET /api/v1/apps/:app/ltv/customers returns a paginated object: "list", sorted by ltv descending by default, and requires the metrics:read scope.
This is the per-customer view of lifetime value — one row per paying shop. For aggregate cohort retention and blended LTV curves by install month, see LTV cohorts instead. Data here comes from your Shopify Partner API integration; if the integration was never configured for the app, the list is simply empty (a 200, not an error).
Customer LTV
Returns the app's paying shops as a paginated object: "list", highest estimated lifetime value first by default. Each row pairs the shop's identity with its current_mrr, installed_at, lifetime_months, ltv, plan_name, and first_touch_channel, so you can see who your most valuable merchants are and how they were acquired.
total_count is the total number of paying shops and has_more tells you when to fetch the next page. Amounts are expressed in the envelope's top-level currency.
Path parameter
- Name
app- Type
- string
- Description
The team app's ULID, e.g.
01JN4HKQX0000000000000000.
Query parameters
- Name
sort_by- Type
- string
- Description
Sort column:
ltv(default),current_mrr, orinstalled_at. Any other value returns a422.
- Name
cursor- Type
- string
- Description
Opt-in cursor for cursor pagination. Opaque, maximum 64 characters. See Pagination.
- Name
page- Type
- integer
- Description
The page to return. Minimum
1.
- Name
per_page- Type
- integer
- Description
Rows per page,
1–100.
Request
curl -G https://ranksyapp.com/api/v1/apps/01JN4HKQX0000000000000000/ltv/customers \
-H "Authorization: Bearer rk_live_..." \
-d sort_by=ltv \
-d per_page=25
Response
{
"object": "list",
"url": "/api/v1/apps/01JN4HKQX0000000000000000/ltv/customers",
"currency": "USD",
"page": 1,
"per_page": 25,
"total_count": 96,
"has_more": true,
"metadata": {},
"data": [
{
"shop_domain": "skinnupweightloss.myshopify.com",
"shop_name": "Skinny Up!",
"plan_name": "Advanced",
"current_mrr": 9.99,
"installed_at": "2023-08-15T10:00:00+00:00",
"lifetime_months": 34,
"ltv": 339.66,
"first_touch_channel": "Organic Search"
}
]
}
Response fields
The list is the shared object: "list" envelope; each item in data is a customer LTV row.
- Name
object- Type
- string
- Description
Always
"list".
- Name
url- Type
- string
- Description
The endpoint path for this resource.
- Name
currency- Type
- string
- Description
The currency all amounts in the response are expressed in, e.g.
USD.
- Name
page- Type
- integer
- Description
The current page number (1-based).
nullin cursor mode.
- Name
per_page- Type
- integer
- Description
Rows per page.
- Name
total_count- Type
- integer
- Description
Total paying shops across all pages.
nullin cursor mode.
- Name
has_more- Type
- boolean
- Description
Whether further pages exist.
- Name
next_cursor- Type
- string
- Description
Cursor mode only — the cursor to pass on the next request, or
nullwhen the list is exhausted.
- Name
metadata- Type
- object
- Description
Reserved metadata object.
- Name
data- Type
- array
- Description
The customer LTV rows for this page. Each row has:
- Name
shop_domain- Type
- string
- Description
The shop's
*.myshopify.comdomain, ornullwhen unknown.
- Name
shop_name- Type
- string
- Description
Shop name, or
nullwhen unknown.
- Name
plan_name- Type
- string
- Description
The shop's current plan name, or
nullwhen unknown.
- Name
current_mrr- Type
- number
- Description
Monthly recurring revenue in the plan currency, or
nullwhen unknown.
- Name
installed_at- Type
- string
- Description
ISO-8601 install timestamp, or
null.
- Name
lifetime_months- Type
- integer
- Description
Months the shop has been a customer, or
nullwhen unknown.
- Name
ltv- Type
- number
- Description
Estimated lifetime value, or
nullwhen unknown.
- Name
first_touch_channel- Type
- string
- Description
The acquisition channel of the shop's first touch, or
nullwhen unknown.
Pagination
By default the list uses offset pagination: walk it with page and per_page (1–100), and check has_more / total_count to know when to stop.
You can opt into cursor pagination by passing the cursor parameter (start with it empty). Each response then returns a next_cursor; pass it on the next request and repeat until next_cursor is null (equivalently, has_more is false). In cursor mode page and total_count come back null.
Cursor mode is stable under concurrent inserts and is not subject to the page-mode row cap, so it is the recommended way to export the full list into a warehouse (e.g. BigQuery).
Errors
Beyond the universal 401 / 403 / 422 / 429 (see Errors):
- The list never
404s — an app with no configured Partner API integration, or no paying customers, returns an emptydatalist (a200). - An unknown
sort_bysurfaces as422 validation_failed.