Retain HTTP API — integrate anything

The JavaScript SDK is a convenience. Underneath it are two HTTPS endpoints, and anything that can make a web request can use Retain: Swift, Kotlin, Flutter, Unity, a game server, a CLI tool, a backend cron job.

What you get via raw API: full tracking, retention curve, funnel, drift data, revenue attribution, and the live dashboard. What stays SDK-side for now: the automatic plays/hooks engine (it runs client-side in JS). Server-side plays are on the roadmap; until then, native apps measure everything and can render their own nudges from their own logic.


Auth — two public values

Every request needs your Supabase project URL and publishable (anon) key (safe to embed in clients, write-only by design), plus your app key (rk_live_...) in the body.

Headers on every call:

Content-Type: application/json
apikey: <ANON_KEY>

If your anon key is the legacy JWT kind (starts eyJ), also send:

Authorization: Bearer <ANON_KEY>

1. Send events

POST https://<PROJECT>.supabase.co/rest/v1/rpc/ingest

Body:

{
  "p_app_key": "rk_live_xxxxxxxxxxxx",
  "p_batch": [
    { "uid": "3f1c9d2e-8a4b-4c6d-9e0f-1a2b3c4d5e6f",
      "t": "session_start", "ts": 1789000000000 },
    { "uid": "3f1c9d2e-8a4b-4c6d-9e0f-1a2b3c4d5e6f",
      "t": "core_action", "ts": 1789000012345 }
  ]
}

Field rules, these matter:

Field Rule
uid UUID v4, one per user, generated by you, persisted by you (Keychain / SharedPreferences / your DB). Same user must always send the same uid — it's the entire identity model. Anonymous by design: never derive it from email or name.
t Event type. The ones the dashboard understands: session_start, session_end, core_action (the thing that means "user got value"), onboarded, installed. Anything else is stored as a custom event.
ts Unix epoch milliseconds.
sid Optional session UUID if you track sessions.
anything else Extra keys ride along as payload. Never put user content in them.

Returns the number of events written, e.g. 2. Batch up to a few hundred at a time; send session_start when the app foregrounds, core_action on your core loop, session_end on background.

2. Send revenue

POST https://<PROJECT>.supabase.co/rest/v1/rpc/ingest_revenue
{
  "p_app_key": "rk_live_xxxxxxxxxxxx",
  "p_batch": [
    { "uid": "3f1c9d2e-...", "cents": 999, "currency": "USD",
      "kind": "subscription", "external_id": "txn_abc123",
      "ts": 1789000020000 }
  ]
}

external_id = your payment provider's transaction id (dedupe key, safe to retry). Any currency string; amounts always in minor units (cents).

3. Read your numbers

Your dashboard already does this, but it's the same API if you want it raw:

POST https://<PROJECT>.supabase.co/rest/v1/rpc/app_stats
{ "p_token": "<READ_TOKEN>" }

The read_token is private, server-side or your own eyes only, never in a shipped client.


Copy-paste starters

curl (smoke test):

curl -X POST "https://<PROJECT>.supabase.co/rest/v1/rpc/ingest" \
  -H "Content-Type: application/json" -H "apikey: <ANON_KEY>" \
  -d '{"p_app_key":"rk_live_xxx","p_batch":[{"uid":"3f1c9d2e-8a4b-4c6d-9e0f-1a2b3c4d5e6f","t":"session_start","ts":1789000000000}]}'

Swift:

func retainTrack(_ type: String) {
    let uid = retainUID() // UUID v4, stored in Keychain on first launch
    var req = URLRequest(url: URL(string: "https://<PROJECT>.supabase.co/rest/v1/rpc/ingest")!)
    req.httpMethod = "POST"
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.setValue("<ANON_KEY>", forHTTPHeaderField: "apikey")
    let body: [String: Any] = ["p_app_key": "rk_live_xxx", "p_batch": [
        ["uid": uid, "t": type, "ts": Int(Date().timeIntervalSince1970 * 1000)]]]
    req.httpBody = try? JSONSerialization.data(withJSONObject: body)
    URLSession.shared.dataTask(with: req).resume()
}

Kotlin: same two lines of JSON with OkHttp/Ktor, headers apikey, body p_app_key + p_batch.

That's the whole integration: pick a core action, send three event types, optionally send revenue. Your dashboard fills in from there.

Already using a subscription platform?

Point its webhook at us and revenue attribution starts working. No code on your side.

We accept the RevenueCat event taxonomy natively, which is the shape most of this category emits: INITIAL_PURCHASE, RENEWAL, PRODUCT_CHANGE, CANCELLATION, EXPIRATION, BILLING_ISSUE. Adapty, Qonversion, Apphud, Chargebee and Stripe all send a compatible set.

PlatformWhere to point it
RevenueCatIntegrations → Webhooks → custom HTTP
AdaptyIntegrations → Webhook
QonversionIntegrations → Webhooks
ApphudIntegrations → server-to-server webhook
StripeDevelopers → Webhooks → add endpoint

Send it to /rest/v1/rpc/ingest_revenue with your app key. Events are deduplicated on transaction ID, so retries and duplicate deliveries are safe. Sandbox and test events are ignored rather than counted.

What this gets you. Your retention curve and your revenue land in the same place, so the dashboard can show which retained customers actually paid, and which paying customers have gone quiet before they cancel. Your payment platform sees the cancellation. We see it coming.

Switching payment vendors later

Your event history and your customer records stay here regardless of who processes the money. We are deliberately not in the payments business on the mobile side, so migrating from one subscription platform to another does not cost you your retention history. Point the new webhook at the same endpoint and carry on.

Your data

Every event you send is yours. Export the raw table any time as CSV or JSON from the dashboard, or query it directly through the read endpoint above. No lock-in and no export fee. If you leave, one call erases everything we hold.

Need a hand integrating?

Send the app URL and what it does in a sentence. We will get it wired up and send the dashboard link.

Request access