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.


POST/api/v1/apps/:app/identify

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_id plus a shop object 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.com domain. Required on every call — the routing key for both paths.

  • Name
    shopify_access_token
    Type
    string
    Description

    A valid Shopify access token (shpat_... or shpca_...). Required unless shop_id is supplied. Used once to call the Shopify Admin API — never stored. If present, this path always runs, even when a shop object 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 unless shopify_access_token is supplied. Must resolve to a positive integer — 0 and unparseable values are rejected with a 422 (0 is 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_token is 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 signals ip and user_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.* keyTypeShopify Admin API source
plan_display_namestringplan.publicDisplayName
is_shopify_plusbooleanplan.shopifyPlus
is_partner_dev_storebooleanplan.partnerDevelopment
shop_owner_namestringshopOwnerName
primary_domain_urlstring, max 500primaryDomain.url
currency_codestring, size 3currencyCode
iana_timezonestring, valid timezoneianaTimezone
weight_unitstring, max 20weightUnit
customer_accounts_settingstring, max 100customerAccounts
taxes_includedbooleantaxesIncluded
store_created_atdatecreatedAt
address1stringshopAddress.address1
address2stringshopAddress.address2
citystringshopAddress.city
provincestringshopAddress.province
province_codestring, max 10shopAddress.provinceCode
countrystringshopAddress.country
admin_country_codestring, size 2shopAddress.countryCodeV2
zipstringshopAddress.zip
phonestringshopAddress.phone
companystringshopAddress.company
ships_to_countriesarray of stringshipsToCountries

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.

FieldTypeNotes
contacts[].ipstring, IPv4/IPv6The merchant's IP at install.
contacts[].user_agentstring, max 1024The 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

POST
/api/v1/apps/:app/identify
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:

  1. Resolves the shop's identity by calling the Shopify Admin API shop query with your access token — the response's GID is the source of truth for shop_id
  2. Looks up (or creates) the shared shop identity record — matched by shop_id first, falling back to myshopify_domain only when no shop_id match exists
  3. Upserts your app's installation for this shop — creating a placeholder if Partner API hasn't synced yet — and sets last_seen_at to now
  4. Stores contacts, tags, and custom fields per-application
  5. 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:

  1. Resolves the shop's identity from the shop_id you supplied
  2. Looks up (or creates) the shared shop identity record — matched by shop_id first, falling back to myshopify_domain only when no shop_id match exists
  3. Upserts your app's installation for this shop — creating a placeholder if Partner API hasn't synced yet — and sets last_seen_at to now
  4. Stores contacts, tags, and custom fields per-application
  5. If a shop object 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_token nor shop_id was supplied, or shop_id isn'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_id on record for this domain yet; once the shop is already known — via an earlier /identify call 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 /identify call are not deleted.
  • Data path patches, token path replaces. A shop object on the data path only overwrites the keys you send. A token-path fetch is a full authoritative snapshot and replaces every shop.* column on each call, including back to null.
  • shop_id is 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 why shop_id is required whenever you're not sending a token.

Was this page helpful?