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.


POST/api/v1/apps/:app/track

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 a type and optional meta, feeds the merchant's activity timeline and last_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 numeric attributes.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 an app_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.com domain.

  • 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 an event_handle is 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 an event_handle is present.

  • Name
    event_handle
    Type
    string
    Description

    Send this and the call is a usage event (the type/meta fields are ignored). A Shopify App Event handle. A standard handle from the standard event registry starts with shopify. 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 the appevents:write scope.

  • 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 value key 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_key for dedupe.

Shopify App Events scope

  • Name
    appevents:write
    Type
    required
    Description

    Write access to the Shopify App Events proxy. Without it, event_handle is ignored and the response tells you why. See Authentication.

  • Name
    crm:write
    Type
    required
    Description

    Required for all /track requests, with or without the proxy.

Request

POST
/api/v1/apps/:app/track
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 its analytics_app_events configuration. 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.


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

  1. Finds the shop by myshopify_domain
  2. Sets last_seen_at = now()
  3. No event_handle → activity. Records an activity event with the given type and meta (defaults to "app_opened" with no metadata) in the activity stream, then stops. Nothing is forwarded to Shopify.
  4. event_handle present → usage (needs the appevents:write scope). 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 numeric attributes.value stored so usage analytics can sum it b. Writes a row to the shopify_app_events table (a TimescaleDB hypertable). Status is pending when you sent an app_events_token, otherwise local c. With a token, dispatches an async ForwardShopifyAppEvent job to the data queue; Shopify resolves the shop GID from the stored shop_id, so you only send myshopify_domain d. Without a token, the event stops here — recorded in Ranksy, never sent to Shopify. Re-forward it later by calling /track again 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_key and timestamp already exists; the existing one is returned.

  • Name
    invalid
    Type
    standard
    Description

    A standard event failed registry validation. See errors for the missing or mistyped attributes.

  • Name
    skipped
    Type
    any
    Description

    Nothing recorded. Your API key is missing the appevents:write scope.


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.

Was this page helpful?