Scorecard & competitors
Two endpoints that frame an app against its rivals. GET /apps/:app/scorecard returns a single object: "scorecard" — a weighted overall health score, four sub-scores each paired with their breakdown, and prioritised improvement opportunities. GET /apps/:app/competitors returns the team's tracked (followed) competitors as a paginated object: "list", each row enriched with category and keyword overlap against your own app. Both read the scraped catalog, so neither is gated on the Shopify Partner integration.
Both endpoints 404 with resource_not_found when the addressed app does not exist or is not tracked by your team. When the team follows no competitors yet, both fall back to a provisional comparison set (embedding-similar → top-category apps) rather than erroring — the scorecard flags this with is_provisional: true and the competitor list with metadata.is_provisional.
Scorecard
The app's composite competitive scorecard — the same computation the dashboard renders. Returns a weighted overall_score (0–100) plus four sub-scores: listing_quality, keyword_performance, reviews_reputation, and visibility. Each sub-score is a { score, breakdown } pair where breakdown carries the per-factor inputs (max-score weights, ratios, the outranked/unranked keyword lists, head-to-head review distribution, ranked-category positions, and so on). improvement_opportunities is a priority-ordered action list and competitor_comparison is the head-to-head against each followed competitor.
The scorecard is cached — the stored row is returned unless it is stale. Pass refresh=true to force a recompute.
is_provisional is true when the score was computed against an embedding-similar → top-category fallback set because the team follows no competitors yet; computed_at is the last-computed ISO-8601 timestamp, or null if never computed.
Path parameter
- Name
app- Type
- string
- Description
The team app's ULID, e.g.
01JN4HKQX0000000000000000.
Query parameters
- Name
refresh- Type
- boolean
- Description
Force a fresh recompute instead of returning the cached (non-stale) scorecard. Accepts the natural query forms (
true/false/1/0/yes/no). Defaultfalse.
Request
curl https://ranksyapp.com/api/v1/apps/01JN4HKQX0000000000000000/scorecard \
-H "Authorization: Bearer rk_live_..."
Response
{
"object": "scorecard",
"overall_score": 72,
"listing_quality": {
"score": 68,
"breakdown": {
"characterUtilization": {
"score": 28,
"maxScore": 40,
"averageUtilization": 71.2,
"fields": {
"name": { "length": 28, "limit": 30, "utilization": 93.3 },
"subtitle": { "length": 60, "limit": 62, "utilization": 96.8 },
"introduction": { "length": 90, "limit": 100, "utilization": 90 },
"details": { "length": 320, "limit": 500, "utilization": 64 }
},
"featureCount": 3
},
"antiPatterns": {
"score": 25,
"maxScore": 30,
"count": 1,
"details": { "details": ["Avoid superlatives like \"best\"."] }
},
"keywordPresence": {
"score": 15,
"maxScore": 30,
"found": ["inventory sync"],
"missing": ["stock alerts"],
"source": "traffic"
}
}
},
"keyword_performance": {
"score": 74,
"breakdown": {
"keywordBreadth": { "score": 16, "maxScore": 20, "count": 41 },
"outrankingRatio": { "score": 22, "maxScore": 35, "ratio": 0.63, "trackedWeightApplied": true },
"topTenDensity": { "score": 21, "maxScore": 30, "count": 12, "total": 41, "ratio": 0.293, "trackedWeightApplied": true },
"keywordCoverage": { "score": 11, "maxScore": 15, "ratio": 0.74 },
"outrankedKeywords": [
{
"keyword_id": 5012,
"keyword": "stock alerts",
"yourRank": 14,
"bestCompetitorRank": 3,
"bestCompetitorAppName": "Stocky",
"tracked": true,
"installAttributed": false,
"tier": 2,
"gap": 11
}
],
"unrankedTrackedKeywords": [
{ "keyword_id": 5099, "keyword": "low stock", "installAttributed": true }
],
"outrankedTrackedCount": 4,
"unrankedTrackedCount": 2
}
},
"reviews_reputation": {
"score": 81,
"breakdown": {
"rating": { "score": 30, "maxScore": 35, "value": 4.7, "competitorAvg": 4.5 },
"volume": { "score": 17, "maxScore": 20, "count": 312, "competitorAvg": 540 },
"sentiment": {
"score": 22,
"maxScore": 25,
"positiveRatio": 0.91,
"distribution": { "positive": 284, "neutral": 12, "negative": 16 }
},
"recency": { "score": 12, "maxScore": 20 },
"competitorAdjustment": 1
}
},
"visibility": {
"score": 65,
"breakdown": {
"categoriesRankedIn": {
"score": 20,
"maxScore": 25,
"count": 4,
"categories": [
{ "name": "Inventory management", "rank": 7 },
{ "name": "Stock control", "rank": 18 }
]
},
"bestPosition": { "score": 21, "maxScore": 30, "position": 7 },
"avgPosition": { "score": 13, "maxScore": 25, "position": 12.5 },
"categoryGaps": { "score": 11, "maxScore": 20, "gaps": 3 }
}
},
"is_provisional": false,
"computed_at": "2026-05-30T08:15:00+00:00",
"improvement_opportunities": [
{
"type": "keyword_performance",
"message": "Competitors outrank you on 4 keywords you track: stock alerts, low stock. Improve your listing relevance for these.",
"priority": 1,
"current_value": "37% win rate",
"target_value": "50%+",
"link_to": "/competitors",
"keywords": [
{ "keyword": "stock alerts", "tracked": true, "installAttributed": false }
]
},
{
"type": "listing_quality",
"message": "Your app details field is significantly underutilized. This is the most impactful field for conversions.",
"priority": 1,
"current_value": "320 chars",
"target_value": "500 chars",
"link_to": "/suggestions/listings"
}
],
"competitor_comparison": [
{
"id": 4471,
"name": "Stocky",
"slug": "stocky",
"logo_url": "https://cdn.shopify.com/app-store/listing/stocky/icon.png",
"metrics": {
"rating": { "yours": 4.7, "theirs": 4.5, "winner": "you" },
"reviews": { "yours": 312, "theirs": 540, "winner": "them" },
"keywords": { "yours": 41, "theirs": 38, "winner": "you" },
"categories": { "yours": 4, "theirs": 3, "winner": "you" },
"estInstalls": { "yours": 5200, "theirs": 9100, "winner": "them" }
},
"winsCount": 3
}
]
}
Scorecard fields
- Name
object- Type
- string
- Description
Always
"scorecard".
- Name
overall_score- Type
- integer
- Description
Weighted overall competitive health score, 0–100.
- Name
listing_quality- Type
- object
- Description
Listing-quality sub-score as
{ score, breakdown }.breakdowncarriescharacterUtilization(per-field length/limit/utilization +featureCount),antiPatterns(count + per-field messages), andkeywordPresence(found/missinglists +source).
- Name
keyword_performance- Type
- object
- Description
Keyword-performance sub-score as
{ score, breakdown }.breakdowncarrieskeywordBreadth,outrankingRatio,topTenDensity,keywordCoverage, theoutrankedKeywordsandunrankedTrackedKeywordslists, and theoutrankedTrackedCount/unrankedTrackedCounttotals.
- Name
reviews_reputation- Type
- object
- Description
Reviews & reputation sub-score as
{ score, breakdown }.breakdowncarriesrating,volume,sentiment(with positive/neutral/negativedistribution),recency, and thecompetitorAdjustmentinteger (−5..+5).
- Name
visibility- Type
- object
- Description
Visibility sub-score as
{ score, breakdown }.breakdowncarriescategoriesRankedIn(with per-categoryrank),bestPosition,avgPosition, andcategoryGaps.
- Name
is_provisional- Type
- boolean
- Description
truewhen computed against a provisional comparison set (embedding-similar → top-category apps) because the team follows no competitors yet.
- Name
computed_at- Type
- string
- Description
When the scorecard was last computed (ISO-8601), or
nullif never.
- Name
improvement_opportunities- Type
- array
- Description
Prioritised opportunities (priority 1 = highest first). Each item:
type(one of the four sub-scores),message,priority,current_value/target_value(string/int/null),link_to(string/null), and optionallykeywordschips ({ keyword, tracked, installAttributed }) on keyword items.
- Name
competitor_comparison- Type
- array
- Description
Head-to-head against each followed competitor (empty when none are followed). Each item:
id,name,slug,logo_url, ametricsmap (rating/reviews/keywords/categories/estInstalls→{ yours, theirs, winner }wherewinneris"you"/"them"/"tie"), andwinsCount(0–5).
Competitors
The team's tracked (followed) competitors for this app, paginated as an object: "list". This is the followed set — the same one the dashboard and scorecard use — not catalog-similar discovery. Each row is a compact app card (id, slug, name, rating, reviews_count, est_installs, logo_url) plus five overlap stats against your own app (shared_categories, shared_keywords, relevance_score, category_avg_rank, keyword_avg_rank). The overlap fields are null when that tracked competitor is not also a catalog-competitor of your app; rows with a null relevance_score sort last on relevance.
When the team follows no competitors, a provisional set (embedding-similar → top-category apps) is returned and metadata.is_provisional is true. The endpoint reads the scraped catalog only, so it always returns a list — there is no Partner / BigQuery gate.
Path parameter
- Name
app- Type
- string
- Description
The team app's ULID, e.g.
01JN4HKQX0000000000000000.
Query parameters
- Name
sort- Type
- string
- Description
One of
relevance(default),reviews,rating. Whitelisted — any other value returns422.
- Name
order- Type
- string
- Description
ascordesc(defaultdesc).
- Name
page- Type
- integer
- Description
The page to return (≥ 1). Default
1.
- Name
per_page- Type
- integer
- Description
Rows per page (1–100). Default
25.
Request
curl -G https://ranksyapp.com/api/v1/apps/01JN4HKQX0000000000000000/competitors \
-H "Authorization: Bearer rk_live_..." \
-d sort=reviews \
-d order=desc \
-d per_page=25
Response
{
"object": "list",
"url": "/api/v1/apps/01JN4HKQX0000000000000000/competitors",
"page": 1,
"per_page": 25,
"total_count": 6,
"has_more": false,
"metadata": { "is_provisional": false },
"data": [
{
"id": 4471,
"slug": "stocky",
"name": "Stocky",
"rating": 4.5,
"reviews_count": 540,
"est_installs": 9100,
"logo_url": "https://cdn.shopify.com/app-store/listing/stocky/icon.png",
"shared_categories": 3,
"shared_keywords": 27,
"relevance_score": 84,
"category_avg_rank": 6.4,
"keyword_avg_rank": 9.1
}
]
}
List envelope
The list envelope is shared across catalog collections. Competitor lists are not date-windowed, so the coverage block is omitted.
- Name
object- Type
- string
- Description
Always
"list".
- Name
url- Type
- string
- Description
Path of this list resource.
- Name
page- Type
- integer
- Description
Current page number (1-based).
- Name
per_page- Type
- integer
- Description
Rows per page.
- Name
total_count- Type
- integer
- Description
Total rows across every page.
- Name
has_more- Type
- boolean
- Description
Whether more pages follow this one.
- Name
metadata- Type
- object
- Description
Endpoint-specific metadata; carries the
is_provisionalflag for this endpoint.
- Name
data- Type
- array
- Description
The competitor rows.
Competitor row
- Name
id- Type
- integer
- Description
Internal ShopifyApp id of the competitor.
- Name
slug- Type
- string
- Description
App Store slug (handle) of the competitor.
- Name
name- Type
- string
- Description
Competitor app name.
- Name
rating- Type
- number
- Description
Average star rating (rounded to 1 dp), or
nullwhen unrated.
- Name
reviews_count- Type
- integer
- Description
Total number of reviews, or
null.
- Name
est_installs- Type
- integer
- Description
Estimated active installs, or
nullwhen not estimated.
- Name
logo_url- Type
- string
- Description
Competitor app icon URL, or
nullwhen not captured.
- Name
shared_categories- Type
- integer
- Description
Catalog categories shared with your app;
nullwhen the competitor is not one of your app's catalog-competitors.
- Name
shared_keywords- Type
- integer
- Description
Keywords both apps rank for;
nullwhen not a catalog-competitor.
- Name
relevance_score- Type
- integer
- Description
Catalog relevance score (higher = more similar);
nullwhen not a catalog-competitor. Null-score rows sort last onrelevance.
- Name
category_avg_rank- Type
- number
- Description
Competitor's average rank across the shared categories (1 dp);
nullwhen not a catalog-competitor.
- Name
keyword_avg_rank- Type
- number
- Description
Competitor's average rank across the shared keywords (1 dp);
nullwhen not a catalog-competitor.
Errors
Beyond the universal 401 / 403 / 422 / 429 (see Errors), both endpoints return:
404 resource_not_found— the addressed app does not exist or is not tracked by your team.
Neither endpoint reads the Shopify Partner integration or BigQuery — they serve from the scraped catalog — so neither returns the 409 partner errors documented for the metrics endpoints. A bad query parameter (an unknown sort token, a non-integer page, per_page over 100) surfaces as 422 validation_failed.