Track events
POST /api/v1/apps/:app/track records one event and bumps last_seen_at: it logs an activity ping or a usage event, and event_handle is the switch between them.
/track is ingestion and fires on every merchant page view, so it is never metered — it keeps recording events even after your monthly read quota is spent.
Requires a Ranksy API key with the crm:write scope; usage events (an event_handle) also need appevents:write (see Authentication). The :app in the URL identifies which of your team's apps this merchant belongs to. Call /identify first to enrich the merchant record.
Track an event
POST /api/v1/apps/:app/track records one event and bumps the merchant's last_seen_at. Lightweight — no Shopify token, no Admin API call.
It records two different kinds of event, and event_handle is the switch between them:
- Activity — no
event_handle. How a merchant uses your app: opened it, viewed a page, changed a setting. Carries atypeand optionalmeta, feeds the merchant's activity timeline andlast_seen_at, and never leaves Ranksy. Call it on every page load after/identify. Think of it as your product-analytics stream. - Usage — an
event_handle, usually with a numericattributes.value. What your app did that's worth counting or billing: messages sent, orders synced, an email campaign fired. It's stored in the usage store, aggregated by the usage-metrics endpoint, and — when you add anapp_events_token— forwarded to Shopify App Events.
One call is one or the other. Send an event_handle and it's a usage event — the type and meta fields are ignored. Leave event_handle out and it's an activity ping. A usage event is never also logged as activity, so a single tracked action never double-counts across the two streams.
Request body
- Name
myshopify_domain- Type
- string
- Description
The merchant's
.myshopify.comdomain.
- Name
type- Type
- string
- Description
Activity events only (no
event_handle). Event type, max 50 chars. Defaults to"app_opened"when omitted. Use any string that makes sense for your app:"dashboard_viewed","checkout_opened","settings_changed", etc. Ignored when anevent_handleis present.
- Name
meta- Type
- object
- Description
Activity events only. Arbitrary JSON metadata attached to the event, e.g.
{"page": "/dashboard", "plan": "growth"}. Stored as-is, no schema enforced. Ignored when anevent_handleis present.
- Name
event_handle- Type
- string
- Description
Send this and the call is a usage event (the
type/metafields are ignored). A Shopify App Event handle. A standard handle from the standard event registry starts withshopify.and is validated against that registry before forwarding, e.g."shopify.marketing.email_sent". Any other handle is recorded as a custom usage event with no schema check. Either way, your API key needs theappevents:writescope.
- Name
app_events_token- Type
- string
- Description
An app-level Shopify App Events token — a JWT you mint from your Dev Dashboard client credentials (see Getting an app_events_token). Ranksy forwards the event to Shopify with it and never stores it. Omit it and the event is recorded but not forwarded. The token is app-scoped and lives 60 minutes. Mint it once per app, cache it, and refresh it rather than minting a fresh one for every shop. An already-expired token is rejected up front with
422 app_events_token_expired— nothing is recorded or forwarded, so refresh and resend.
- Name
attributes- Type
- object
- Description
Event attributes. For a standard event, Ranksy checks keys and value types against the registry before forwarding; max 15 keys, values must be strings, numbers, or booleans. For a custom event, any keys are accepted, and a numeric
valuekey is stored as the event's value so it can be summed in usage analytics.
- Name
idempotency_key- Type
- string
- Description
Custom events only. Ranksy dedupes on this key plus
timestamp— resend the same pair and you get the existing event back instead of a new one. Omit it and Ranksy generates one.
- Name
timestamp- Type
- string
- Description
Custom events only. ISO 8601 time the event happened; defaults to now. Pairs with
idempotency_keyfor dedupe.
Don't put personal data in attributes — no names, email addresses, phone numbers, customer IDs, or addresses. Shopify's App Events API terms prohibit it, and Ranksy forwards your attributes to Shopify as-is. Use non-identifying values instead, like campaign, workflow, plan, or aggregate counts.
Shopify App Events scope
- Name
appevents:write- Type
- required
- Description
Write access to the Shopify App Events proxy. Without it,
event_handleis ignored and the response tells you why. See Authentication.
- Name
crm:write- Type
- required
- Description
Required for all
/trackrequests, with or without the proxy.
Request
curl -X POST https://ranksyapp.com/api/v1/apps/:app/track \
-H "Authorization: Bearer rk_live_..." \
-H "Content-Type: application/json" \
-d '{ "myshopify_domain": "store.myshopify.com" }'
200 — Basic track, no event_handle
{ "tracked": true }
Where forwarded events land
Forwarding an event (sending an app_events_token) always logs it in your app's Dev Dashboard. What happens beyond that depends on the handle:
- Custom handle — e.g.
messages_sent. Reaches the Dev Dashboard and can feed a usage-based billing meter you've set up in your app. No preview and no analytics config needed. This is the path for usage and billing events, and it's what most usage-based apps want. - Standard handle — e.g.
shopify.marketing.email_sent. Does everything a custom event does, and can additionally surface in the merchant-facing Shopify Analytics once your app declares the same handle in itsanalytics_app_eventsconfiguration. Ranksy can't set that up for you — it's config on your app's extension, not on the API call.
Either way, Ranksy also records the event locally, so it counts toward the usage metrics endpoint and the in-app Usage panel whether or not you forward it.
The developer preview applies only to that last step — standard events showing in Shopify Analytics. It's open to approved apps. Custom events, the Dev Dashboard, and usage billing don't need it. Even with the handle declared, a standard event surfaces in Analytics only once your app has preview access; until then it still lands in the Dev Dashboard, and Ranksy still records it. If a forwarded event reaches the Dev Dashboard but not Analytics, check that your declared handle exactly matches the one you send.
Getting an app_events_token
Shopify's App Events API takes one kind of credential: an app-level JWT you mint with your Dev Dashboard client credentials. The merchant's Admin API token does not work here — Shopify rejects it with a 401.
Mint the JWT with the client_credentials grant:
Mint an App Events token
curl -X POST https://api.shopify.com/auth/access_token \
-H "Content-Type: application/json" \
-d '{
"client_id": "your-app-client-id",
"client_secret": "your-app-client-secret",
"grant_type": "client_credentials"
}'
The response's access_token is the JWT, good for 60 minutes. One token covers every shop your app is installed on, so mint it once, cache it, and refresh when it expires. Pass it as app_events_token on any /track call you want forwarded. If its exp has already passed, /track stops before recording or forwarding and returns 422 app_events_token_expired.
Response
A basic activity ping returns { "tracked": true }. A usage event (an event_handle) adds a shopify_event object whose status reports what happened. One case fails outright instead of returning 200: a supplied app_events_token that has already expired is rejected before anything is recorded or forwarded.
200 — Standard event, token supplied, queued to forward
{
"tracked": true,
"shopify_event": {
"status": "pending",
"event_handle": "shopify.marketing.email_sent",
"category": "marketing",
"idempotency_key": "rk_evt_a1b2c3d4e5f6...",
"forwarded": true
}
}
200 — Standard event, no token, recorded but not forwarded
{
"tracked": true,
"shopify_event": {
"status": "local",
"event_handle": "shopify.marketing.email_sent",
"category": "marketing",
"idempotency_key": "rk_evt_a1b2c3d4e5f6...",
"forwarded": false,
"reason": "no app_events_token supplied; recorded locally but not forwarded to Shopify Analytics"
}
}
200 — Custom usage event recorded
{
"tracked": true,
"shopify_event": {
"status": "recorded",
"event_handle": "orders_synced",
"value": 128,
"forwarded": false,
"idempotency_key": "sync-2026-08-12-store"
}
}
200 — Custom event, duplicate idempotency_key
{
"tracked": true,
"shopify_event": {
"status": "duplicate",
"event_handle": "orders_synced",
"value": 128,
"forwarded": false,
"idempotency_key": "sync-2026-08-12-store"
}
}
200 — Skipped: missing scope
{
"tracked": true,
"shopify_event": {
"status": "skipped",
"reason": "API key lacks appevents:write scope"
}
}
200 — Invalid attributes (missing required fields)
{
"tracked": true,
"shopify_event": {
"status": "invalid",
"errors": [
"Missing required attribute 'member_id' (int)",
"Missing required attribute 'points_delta' (int)"
]
}
}
422 — app_events_token already expired
{
"error": {
"type": "invalid_request_error",
"code": "app_events_token_expired",
"message": "The app_events_token has expired. Mint a new 60-minute token from your Dev Dashboard client credentials (POST https://api.shopify.com/auth/access_token) before sending events.",
"param": "app_events_token",
"doc_url": "https://api.ranksyapp.com/errors#app_events_token_expired"
}
}
Unlike the 200 responses above — which all mean the call was accepted, whether or not the event reaches Shopify — a 422 app_events_token_expired means nothing happened: no event row was written and nothing was queued. Mint a fresh token and resend the same call.
What happens under the hood
- Finds the shop by
myshopify_domain - Sets
last_seen_at = now() - No
event_handle→ activity. Records an activity event with the giventypeandmeta(defaults to"app_opened"with no metadata) in the activity stream, then stops. Nothing is forwarded to Shopify. event_handlepresent → usage (needs theappevents:writescope). No activity row is written — a usage event is never also logged as activity: a. A standard handle is checked against the 81-event registry; anything else is treated as a custom usage event, with its numericattributes.valuestored so usage analytics can sum it b. Writes a row to theshopify_app_eventstable (a TimescaleDB hypertable). Status ispendingwhen you sent anapp_events_token, otherwiselocalc. With a token, dispatches an asyncForwardShopifyAppEventjob to thedataqueue; Shopify resolves the shop GID from the storedshop_id, so you only sendmyshopify_domaind. Without a token, the event stops here — recorded in Ranksy, never sent to Shopify. Re-forward it later by calling/trackagain with the same event and a token
The shopify_event.status values you can get back:
- Name
pending- Type
- standard
- Description
Recorded and queued to forward to Shopify (you supplied an
app_events_token).
- Name
local- Type
- standard
- Description
Recorded in Ranksy only — no
app_events_token, so nothing was forwarded.
- Name
recorded- Type
- custom
- Description
Custom usage event stored. Forwarded too if you supplied a token.
- Name
duplicate- Type
- custom
- Description
A custom event with this
idempotency_keyandtimestampalready exists; the existing one is returned.
- Name
invalid- Type
- standard
- Description
A standard event failed registry validation. See
errorsfor the missing or mistyped attributes.
- Name
skipped- Type
- any
- Description
Nothing recorded. Your API key is missing the
appevents:writescope.
last_seen_at
Every /track call sets last_seen_at on the installation record (so does /identify). This powers:
- Customer detail sidebar — "Last seen · 2h ago"
- CRM table — sortable "Last seen" column
- CRM filters — filter by recency (Today, 7d, 30d, 90d, Never)
Rate limits
This endpoint uses the standard rate limits: 120 req/min, 50,000 req/day per key.