Customers
A customer is a merchant that installed your app — one Installation. Two endpoints: GET /api/v1/apps/:app/customers returns a paginated object: "list" of lean merchant rows (identity, plan, status, MRR, install dates, tags), and GET /api/v1/apps/:app/customers/:customer returns a single merchant's full profile — contacts, custom fields, plan and subscription, revenue, health score, tags, shop admin data, uninstall reasons, and their App Store review. Both require the crm:read scope.
Customers come 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), and every merchant lookup returns 404. Fields that haven't been captured for a merchant (email, country, shop_admin_data, review) come back as null rather than being dropped.
List customers
Returns the app's merchants as a true-paginated object: "list", newest install first by default. total_count is the count after filters and has_more tells you when to fetch the next page. The envelope's metadata.summary is computed over the filtered set (total_customers, active, paying, on_trial), and metadata.filters echoes back the parameters that were applied.
Rows are lean by design — identity, plan, status, MRR, dates, and tags. For a merchant's contacts, custom fields, revenue, and review, retrieve the single customer below.
Path parameter
- Name
app- Type
- string
- Description
The team app's ULID, e.g.
01JN4HKQX0000000000000000.
Query parameters
- Name
status- Type
- string
- Description
Filter by install status:
active,uninstalled,frozen,closed, orall. Any other value returns a422.
- Name
plan- Type
- string
- Description
Filter by current plan name (exact match on the current subscription's name).
- Name
tag- Type
- string
- Description
Filter to merchants carrying this CRM tag (exact match on tag name).
- Name
country- Type
- string
- Description
Two-letter country code of the merchant's shop, e.g.
US. Case-insensitive.
- Name
include_test- Type
- boolean
- Description
Include test / development-store merchants. Defaults to
false(they are excluded). Acceptstrue/false/1/0/yes/no.
- Name
installed_after- Type
- string
- Description
YYYY-MM-DD. Only merchants installed on or after this date.
- Name
installed_before- Type
- string
- Description
YYYY-MM-DD. Only merchants installed on or before this date. Must be greater than or equal toinstalled_after.
- Name
sort- Type
- string
- Description
Sort column:
installed_at(default),last_seen_at, ormrr. Any other value returns a422.
- Name
order- Type
- string
- Description
Sort direction:
desc(default) orasc.
- Name
page- Type
- integer
- Description
The page to return. Default
1.
- Name
per_page- Type
- integer
- Description
Merchants per page. Default
25, maximum100.
- Name
cursor- Type
- string
- Description
Opt-in cursor pagination (opaque, max 64 chars). When set, the response returns a
next_cursorand reportspage/total_countasnull. Recommended for exporting the full list — see Pagination.
Request
curl -G https://ranksyapp.com/api/v1/apps/01JN4HKQX0000000000000000/customers \
-H "Authorization: Bearer rk_live_..." \
-d status=active \
-d sort=mrr \
-d order=desc \
-d per_page=25
Response
{
"object": "list",
"url": "/api/v1/apps/01JN4HKQX0000000000000000/customers",
"page": 1,
"per_page": 25,
"total_count": 342,
"has_more": true,
"metadata": {
"summary": {
"total_customers": 342,
"active": 318,
"paying": 96,
"on_trial": 12
},
"filters": {
"status": "active",
"plan": null,
"tag": null,
"country": null,
"include_test": false,
"installed_after": null,
"installed_before": null,
"sort": "mrr",
"order": "desc"
}
},
"data": [
{
"id": 528913,
"shop_id": 5271523,
"name": "Acme Store",
"myshopify_domain": "acme-store.myshopify.com",
"email": "[email protected]",
"country": "US",
"install_status": "active",
"is_test": false,
"is_on_trial": false,
"plan_name": "Growth",
"subscription_status": "active",
"billing_interval": "EVERY_30_DAYS",
"current_mrr": 49.00,
"installed_at": "2026-05-14T09:15:00+00:00",
"last_seen_at": "2026-08-09T11:02:00+00:00",
"trial_ends_at": null,
"tags": [
{ "name": "vip", "color": "#7C3AED" }
]
}
]
}
Retrieve a customer
Returns a single merchant's full profile — everything the CRM holds for that shop. The :customer id is the numeric id from a list row. A merchant that doesn't belong to :app (including one on another of your apps) returns 404 — a key can only read its own app's merchants.
Path parameters
- Name
app- Type
- string
- Description
The team app's ULID, e.g.
01JN4HKQX0000000000000000.
- Name
customer- Type
- integer
- Description
The merchant's numeric id (the
idfield from a list row).
Request
curl https://ranksyapp.com/api/v1/apps/01JN4HKQX0000000000000000/customers/528913 \
-H "Authorization: Bearer rk_live_..."
Response
{
"data": {
"id": 528913,
"shop_id": 5271523,
"name": "Acme Store",
"myshopify_domain": "acme-store.myshopify.com",
"email": "[email protected]",
"country": "US",
"install_status": "active",
"is_test": false,
"is_on_trial": false,
"trial_ends_at": null,
"first_installed_at": "2026-05-14T09:15:00+00:00",
"last_uninstalled_at": "2026-08-12T16:40:00+00:00",
"uninstall_reasons": [
{
"uninstalled_at": "2026-08-12T16:40:00+00:00",
"reasons": [
{ "code": "not_using_now", "label": "Not using the app right now" },
{ "code": "hard_to_set_up", "label": "Hard to set up or use" }
],
"detail": "Too many steps in onboarding"
},
{
"uninstalled_at": "2026-06-30T08:05:00+00:00",
"reasons": [
{ "code": "not_using_now", "label": "Not using the app right now" }
],
"detail": null
}
],
"last_seen_at": "2026-08-09T11:02:00+00:00",
"health_score": 82.5,
"plan": {
"name": "Growth",
"subscription_status": "active",
"billing_interval": "EVERY_30_DAYS",
"amount": 49.00,
"current_mrr": 49.00,
"activated_at": "2026-05-21T09:15:00+00:00",
"cancelled_at": null,
"current_period_end": "2026-08-21T09:15:00+00:00"
},
"revenue": {
"currency": "USD",
"lifetime_value": 441.00,
"total_revenue": 441.00,
"usage_revenue": 0.00,
"transaction_count": 9
},
"custom_fields": {
"plan_tier": "gold",
"seats": 5
},
"tags": [
{ "name": "vip", "color": "#7C3AED" }
],
"contacts": [
{
"name": "Jane Merchant",
"email": "[email protected]",
"phone": "+15551234567",
"label": "Owner"
}
],
"shop_admin_data": {
"plan_display_name": "Shopify",
"is_shopify_plus": false,
"is_partner_dev_store": false,
"shop_owner_name": "Jane Merchant",
"primary_domain_url": "https://acme-store.com",
"currency_code": "USD",
"iana_timezone": "America/New_York",
"country_code": "US",
"city": "Brooklyn",
"province": "New York",
"zip": "11201",
"phone": "+15551234567"
},
"review": {
"id": 123456789,
"rating": 5,
"sentiment": "positive",
"date": "2026-06-02",
"body": "Works exactly as described and support is fast.",
"has_reply": true,
"reply": "Thanks for the kind words!",
"reply_date": "2026-06-03"
}
}
}
Response shapes
The list is the shared catalog object: "list" envelope; each item in data is a customer row. The retrieve endpoint returns a single customer object under data — a superset of the row with the nested plan, revenue, contacts, custom_fields, shop_admin_data, uninstall_reasons, and review blocks.
List envelope (object: "list")
- Name
object- Type
- string
- Description
Always
"list".
- Name
url- Type
- string
- Description
The endpoint path for this resource.
- Name
page- Type
- integer
- Description
The current page number (1-based).
- Name
per_page- Type
- integer
- Description
Merchants per page.
- Name
total_count- Type
- integer
- Description
Total merchants across all pages, after filters.
- Name
has_more- Type
- boolean
- Description
Whether further pages exist.
- Name
metadata- Type
- object
- Description
Carries
summary(total_customers,active,paying,on_trial, all over the filtered set) andfilters(the parameters that were applied).
- Name
data- Type
- array
- Description
The customer rows for this page.
Customer row
- Name
id- Type
- integer
- Description
The merchant's numeric id — use it to retrieve the full profile.
- Name
shop_id- Type
- integer
- Description
The Shopify shop id, or
nullwhen unknown.
- Name
name- Type
- string
- Description
Shop name, or
nullwhen unknown.
- Name
myshopify_domain- Type
- string
- Description
The
*.myshopify.comdomain, ornullwhen unknown.
- Name
email- Type
- string
- Description
Shop contact email, or
nullwhen unknown.
- Name
country- Type
- string
- Description
Two-letter country code of the shop, or
nullwhen unknown.
- Name
install_status- Type
- string
- Description
One of
active,uninstalled,frozen,closed.
- Name
is_test- Type
- boolean
- Description
Whether this is a test / development store.
- Name
is_on_trial- Type
- boolean
- Description
Whether the merchant is actively on a trial.
current_mrris reported as0while on trial.
- Name
plan_name- Type
- string
- Description
The current plan name, or
Free Plan/No Planwhen there is no active subscription.
- Name
subscription_status- Type
- string
- Description
The current subscription's status, or
nonewhen there is none.
- Name
billing_interval- Type
- string
- Description
e.g.
EVERY_30_DAYSorANNUAL, ornullwhen there is no subscription.
- Name
current_mrr- Type
- number
- Description
Monthly recurring revenue in the plan currency;
0while on trial or unpaid.
- Name
installed_at- Type
- string
- Description
ISO-8601 install timestamp, or
null.
- Name
last_seen_at- Type
- string
- Description
ISO-8601 timestamp of the merchant's last activity, or
null.
- Name
trial_ends_at- Type
- string
- Description
ISO-8601 trial end timestamp, or
null.
- Name
tags- Type
- array
- Description
CRM tags on the merchant, each
{ "name", "color" }.
Customer object (retrieve)
Includes every field from the row (except the flattened plan_name / subscription_status / billing_interval / current_mrr, which move into plan), plus:
- Name
first_installed_at- Type
- string
- Description
ISO-8601 timestamp of the first install, or
null.
- Name
last_uninstalled_at- Type
- string
- Description
ISO-8601 timestamp of the most recent uninstall, or
null.
- Name
uninstall_reasons- Type
- array
- Description
Why the merchant left, from Shopify's uninstall survey — one entry per uninstall (a reinstall cycle produces several), newest uninstall first. Each entry is
{ "uninstalled_at", "reasons", "detail" }, and the array is[]when the merchant never uninstalled or never answered the survey.uninstalled_atis the ISO-8601 timestamp of that uninstall.reasonsholds the answers as{ "code", "label" }— the survey is multi-select, so one uninstall can carry several. Match oncode, the stable canonical identifier (not_using_now,hard_to_set_up,expensive_or_unexpected_cost,not_worth_cost,limited_or_missing_features,found_better_app,not_satisfied_with_support,store_closing_or_pausing,scheduled_cancellation,other, …). Shopify's raw answer is localized across ~15 languages, solabelis always the canonical English wording, never what the merchant saw; an answer not yet mapped to the taxonomy comes through as the raw Shopify string withcode: null.detailis what the merchant typed under "Other (please specify)", whitespace-collapsed and capped at 300 characters, ornull. Shopify's canned description for ascheduled_cancellation(the merchant closed or paused their store) is suppressed — it's a system string, not merchant feedback.
- Name
health_score- Type
- number
- Description
A 0–100 engagement/retention score for the merchant.
- Name
plan- Type
- object
- Description
name,subscription_status,billing_interval,amount,current_mrr,activated_at,cancelled_at,current_period_end. Values arenullwhen the merchant has no subscription.
- Name
revenue- Type
- object
- Description
currency(alwaysUSD),lifetime_value,total_revenue,usage_revenue, andtransaction_count.
- Name
custom_fields- Type
- object
- Description
Free-form key/value metadata you've set on the merchant (the CRM's metafields).
{}when none.
- Name
contacts- Type
- array
- Description
Merchant contacts, each
{ "name", "email", "phone", "label" }, oldest first.
- Name
shop_admin_data- Type
- object
- Description
Shop admin details (
plan_display_name,is_shopify_plus,is_partner_dev_store,shop_owner_name,primary_domain_url,currency_code,iana_timezone,country_code,city,province,zip,phone) when they've been fetched, otherwisenull.
- Name
review- Type
- object
- Description
The merchant's most recent App Store review (
id,rating,sentiment,date,body,has_reply,reply,reply_date,is_archived), ornullwhen they haven't reviewed. See Reviews for the full review surface.
Pagination
The list is paginated. Use page and per_page (default 25, max 100) to walk the result set, and check has_more / total_count to know when to stop. total_count and metadata.summary are computed after your filters, so paging never changes them within a single filtered query.
For exporting the full customer list — into a warehouse, say — use cursor pagination instead: pass cursor (start with an empty value), read next_cursor from the response, and keep calling with it until next_cursor is null (or has_more is false). Cursor mode walks by opaque keyset, so it stays stable while merchants install or churn mid-export and isn't subject to the page-mode row cap. When you page by cursor, page and total_count come back null.
Errors
Beyond the universal 401 / 403 / 422 / 429 (see Errors):
- The list never
404s — an app with no configured Partner API integration, or no merchants matching your filters, returns an emptydatalist. - The retrieve endpoint returns
404 resource_not_foundwhen:customerisn't a merchant of:app(including a merchant on another of your apps).
sort and status are whitelisted, so an unknown token, a bad country code, or a malformed date surfaces as 422 validation_failed rather than 400.