Identify
Enrich a merchant's Ranksy CRM record: send a Shopify access token and Ranksy pulls store metadata from the Admin API, or supply the shop data yourself on the data path.
/identify is ingestion — a write, not a read — so it never counts against your plan's request quota and keeps working even after your monthly read quota is spent.
Requires a Ranksy API key with the crm:write scope (see Authentication). The :app in the URL identifies which of your team's apps this merchant belongs to. To record merchant activity or usage events after identifying, use Track events.
Identify a merchant
POST /api/v1/apps/:app/identify enriches a merchant's CRM record. It works two ways: send a Shopify access token and Ranksy pulls store metadata (plan, address, timezone) from the Admin API on your behalf, or supply the data yourself if you'd rather not share a token. Either way, it also stores the contacts, tags, and custom fields you provide, and sets last_seen_at.
Choosing a path
- Token path. Send
shopify_access_token. Ranksy calls the Shopify Admin API for you and stores whatever it gets back. - Data path. Send
shop_idplus ashopobject you already hold. Ranksy stores it directly — no outbound call is made. Use this if you're not comfortable handing Ranksy an access token.
If both are sent, the token wins. A live fetch is authoritative, so Ranksy calls Shopify and the shop object in the same request is ignored.
Request body
- Name
myshopify_domain- Type
- string
- Description
The merchant's
.myshopify.comdomain. Required on every call — the routing key for both paths.
- Name
shopify_access_token- Type
- string
- Description
A valid Shopify access token (
shpat_...orshpca_...). Required unlessshop_idis supplied. Used once to call the Shopify Admin API — never stored. If present, this path always runs, even when ashopobject is also sent.
- Name
shop_id- Type
- integer or string
- Description
The shop's numeric Shopify id (e.g.
68134502401) or a Shop GID string (e.g."gid://shopify/Shop/68134502401"). Required unlessshopify_access_tokenis supplied. Must resolve to a positive integer —0and unparseable values are rejected with a 422 (0is a legacy "unknown" sentinel, not a real id).
- Name
shop- Type
- object
- Description
Store metadata you already hold — see the field table below. Data-path only: ignored whenever
shopify_access_tokenis also present. Optional even on the data path — omit it to store contacts/tags/custom fields with no enrichment.
- Name
tags- Type
- string[]
- Description
Up to 50 tags. Merged into your existing Ranksy tag system. Subsequent calls replace the full set.
- Name
contacts- Type
- object[]
- Description
Up to 100 contacts. Each has
name,email(required),phone(optional),label(optional:primary,technical,billing,owner,user), and the optional device signalsipanduser_agent(see Device signals for ad attribution). Upserted by email — unlisted contacts preserved. Email matching is case-insensitive: re-sending an address with different capitalization updates the existing contact instead of creating a second one. The address itself is stored lowercased, so if you send[email protected]you'll see[email protected]when you read it back.
- Name
custom_fields- Type
- object
- Description
Up to 50 key-value pairs. Each value must be a string, up to 500 chars — a non-string value (e.g. a number or boolean) is rejected with a 422. Keys merged in place — existing keys preserved unless overwritten.
The shop object
Flat, snake_case keys using Ranksy's own field names — not Shopify's nested, camelCase GraphQL shape. Every key is optional. On the data path, only the keys you send are written; omitted keys leave the previously stored value untouched, so a partial payload can never null out data a previous call established. On the token path, this table also documents where each column's value comes from in the Admin API response.
shop.* key | Type | Shopify Admin API source |
|---|---|---|
plan_display_name | string | plan.publicDisplayName |
is_shopify_plus | boolean | plan.shopifyPlus |
is_partner_dev_store | boolean | plan.partnerDevelopment |
shop_owner_name | string | shopOwnerName |
primary_domain_url | string, max 500 | primaryDomain.url |
currency_code | string, size 3 | currencyCode |
iana_timezone | string, valid timezone | ianaTimezone |
weight_unit | string, max 20 | weightUnit |
customer_accounts_setting | string, max 100 | customerAccounts |
taxes_included | boolean | taxesIncluded |
store_created_at | date | createdAt |
address1 | string | shopAddress.address1 |
address2 | string | shopAddress.address2 |
city | string | shopAddress.city |
province | string | shopAddress.province |
province_code | string, max 10 | shopAddress.provinceCode |
country | string | shopAddress.country |
admin_country_code | string, size 2 | shopAddress.countryCodeV2 |
zip | string | shopAddress.zip |
phone | string | shopAddress.phone |
company | string | shopAddress.company |
ships_to_countries | array of string | shipsToCountries |
shop_id accepts either form — a bare integer or a gid://shopify/Shop/… string.
Device signals for ad attribution
Two optional per-contact fields — ip and user_agent — let Ranksy attribute a merchant's install back to the ad they clicked. Attach them to the contact that represents the installing merchant (usually the owner).
Send the merchant's own IP and User-Agent as your server observed them at install — for example, from the OAuth callback request that installed your app. These are body data you supply, not request headers (the caller here is your server, so the request's own IP/UA are yours, not the merchant's). Both are optional and validated: ip must be a valid IPv4 or IPv6 address, user_agent a string up to 1024 characters. They're stored write-once per contact — the first non-empty value sticks.
| Field | Type | Notes |
|---|---|---|
contacts[].ip | string, IPv4/IPv6 | The merchant's IP at install. |
contacts[].user_agent | string, max 1024 | The merchant's User-Agent at install. |
Omit them and /identify behaves exactly as before — they're purely additive and change nothing for callers that don't send them.
Request
curl -X POST https://ranksyapp.com/api/v1/apps/:app/identify \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-d '{
"myshopify_domain": "store.myshopify.com",
"shopify_access_token": "shpat_...",
"contacts": [{ "name": "Sarah Chen", "email": "[email protected]", "label": "owner", "ip": "203.0.113.9", "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1" }],
"tags": ["vip"],
"custom_fields": { "plan_tier": "growth" }
}'
200 — Success
{
"identified": true,
"shop_id": 68134502401,
"plan": "Advanced Shopify",
"is_shopify_plus": false,
"admin_country": "US",
"timezone": "America/Los_Angeles",
"currency": "USD",
"contacts_count": 1,
"tags_count": 1,
"custom_fields_count": 1
}
Response
A successful call returns identified: true with the resolved shop fields and the counts stored (see the 200 — Success example above). The token path can also degrade or fail; the data path never reaches these states, because there's no outbound call to fail.
When the Shopify fetch fails (token path only).
If Ranksy already knows this shop — it holds a shop record for this domain with a real shop_id, whether from an earlier /identify call on either path or from Ranksy's own Partner API sync picking up the shop independently — a failing token-path fetch degrades instead of failing outright. The response is HTTP 422 with identified: false and an error object, but the contacts / tags / custom_fields you sent in the same request are still stored:
422 — Degraded: shop already known, Shopify fetch failed
{
"identified": false,
"shop_id": null,
"plan": null,
"is_shopify_plus": null,
"admin_country": null,
"timezone": null,
"currency": null,
"contacts_count": 1,
"tags_count": 1,
"custom_fields_count": 1,
"error": {
"type": "shopify_error",
"code": "shopify_token_invalid",
"message": "Shopify access token is invalid or expired. Generate a new token and retry."
}
}
If Ranksy has no shop_id on record for this domain yet — a brand-new domain it has never resolved an identity for, whether via /identify or otherwise — there's nothing to attach the CRM data to, so the failure is returned as a hard error instead — nothing is stored. The HTTP status matches the underlying failure: 422 when the token itself is rejected, 502 when the Shopify Admin API can't be reached, returns a server error, or responds without a usable shop id:
422 — Error: brand-new domain, token invalid
{
"error": {
"type": "shopify_error",
"code": "shopify_token_invalid",
"message": "Shopify access token is invalid or expired. Generate a new token and retry.",
"doc_url": "https://api.ranksyapp.com/errors#shopify_token_invalid"
}
}
502 — Error: brand-new domain, Shopify unavailable
{
"error": {
"type": "shopify_error",
"code": "shopify_api_error",
"message": "Shopify API unavailable. Retry later.",
"doc_url": "https://api.ranksyapp.com/errors#shopify_api_error"
}
}
Retry with a working token, or switch to the data path — it never depends on Shopify being reachable.
What happens under the hood
Shop-admin data — the shop.* fields (plan, address, timezone, and the rest) — is stored per installation, scoped to the app that sent it. /identify itself only ever writes the shop's identity — shop_id and myshopify_domain — onto the shared record every app installed on that merchant has in common. That same shared record also carries basic merchant details (name, contact info, a profile image) that other Ranksy features keep up to date independently of /identify, and those ARE visible to every app installed on that merchant, not just yours. If two of your apps (or another partner's app) identify the same merchant, each keeps its own independent copy of the shop.* fields — your calls only ever read and write your own installation's data, never another app's — and neither shop_id nor myshopify_domain can be repointed to relabel a shop once it's set.
Token path:
- Resolves the shop's identity by calling the Shopify Admin API
shopquery with your access token — the response's GID is the source of truth forshop_id - Looks up (or creates) the shared shop identity record — matched by
shop_idfirst, falling back tomyshopify_domainonly when noshop_idmatch exists - Upserts your app's installation for this shop — creating a placeholder if Partner API hasn't synced yet — and sets
last_seen_atto now - Stores contacts, tags, and custom fields per-application
- Persists the fetched shop metadata to your installation (token discarded immediately) — a successful fetch is a complete, authoritative snapshot and replaces every stored
shop.*column on your installation, including back to null
Data path:
- Resolves the shop's identity from the
shop_idyou supplied - Looks up (or creates) the shared shop identity record — matched by
shop_idfirst, falling back tomyshopify_domainonly when noshop_idmatch exists - Upserts your app's installation for this shop — creating a placeholder if Partner API hasn't synced yet — and sets
last_seen_atto now - Stores contacts, tags, and custom fields per-application
- If a
shopobject was sent, patches only the keys present in it on your installation — no outbound call is made, and omitted keys keep their previously stored value
Error codes
- Name
422 — validation_failed- Type
- invalid_request_error
- Description
Neither
shopify_access_tokennorshop_idwas supplied, orshop_idisn't a positive integer or a parseable Shop GID.
- Name
422 — shopify_token_invalid- Type
- shopify_error
- Description
(token path) The Shopify access token is invalid or expired. Degrades to
identified: false— with contacts/tags/custom fields still stored — if the shop is already known; otherwise returned as a hard error with nothing stored.
- Name
502 or 422 — shopify_api_error- Type
- shopify_error
- Description
(token path) The Shopify Admin API returned a server error, timed out, was rate-limited, or the response had no resolvable shop id. Returns 502 with nothing stored when Ranksy has no
shop_idon record for this domain yet; once the shop is already known — via an earlier/identifycall or Ranksy's own Partner API sync — the same failure returns 422 and degrades instead, with contacts/tags/custom fields still stored.
Rate limits
This endpoint uses the standard rate limits: 120 req/min, 50,000 req/day per key.
Notes
- No token storage. Shopify tokens are never persisted. The data path never sees one, and a token-path fetch uses its token once to call the Admin API, then discards it.
- Idempotent. Repeated calls produce the same state. Contacts upsert by email, custom fields merge by key, tags replace.
- Contact preservation. Contacts omitted from a subsequent
/identifycall are not deleted. - Data path patches, token path replaces. A
shopobject on the data path only overwrites the keys you send. A token-path fetch is a full authoritative snapshot and replaces everyshop.*column on each call, including back to null. shop_idis the identity key on the data path. Ranksy can't durably attach CRM data (contacts, tags, custom fields) to a shop it can't identify, which is whyshop_idis required whenever you're not sending a token.