Tracking
Conversions
Measure post-install events — signups, deposits, purchases, shares — and attribute them back to the link that drove them. Conversions are backend-only: every event comes from a server-side webhook. No client SDK calls, no ad-blocker loss, no spoofable events. If your backend didn't see it, Rift won't count it — and that's the point.
How it works
Conversions flow through a source — a webhook receiver with a unique URL. Every tenant gets an auto-provisioned default custom source on first request, so you can start POSTing events in under a minute.
1. Source
A webhook receiver with a unique URL. Your backend fires HTTP POSTs to it whenever a user does something worth counting.
2. Attribution
Rift looks up the user_id in the event, finds the originating link via your existing attribution chain, and records the conversion.
3. Stats
Counts and sums per link, per conversion type, surface on the link stats endpoint. Raw events fan out to any registered webhook.
Quick start
Get your webhook URL
Your tenant's default custom source is auto-provisioned on first request. List your sources to get the URL:
curl https://api.riftl.ink/v1/sources \
-H "Authorization: Bearer rl_live_YOUR_KEY"{
"sources": [
{
"id": "66a1b2c3d4e5f6a7b8c9d0e1",
"name": "default",
"source_type": "custom",
"webhook_url": "https://api.riftl.ink/w/a1b2c3d4e5f6...",
"created_at": "2026-04-10T12:00:00Z"
}
]
}The webhook_url is the opaque, unguessable URL your backend POSTs events to. Treat it like a secret. To rotate it, delete the source and create a new one.
Bind a user to a link (prerequisite)
Before you can attribute conversions, each user needs an attribution record linking them back to the install. If you already have the mobile SDK wired up, this happens via PUT /v1/attribution/link after signup — see the Attribution doc for details.
curl -X PUT https://api.riftl.ink/v1/attribution/link \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"install_id": "device-uuid-here",
"user_id": "usr_abc123"
}'user_id that has no matching attribution are silently dropped (the request still returns 200, but the event is not counted). Make sure your signup flow calls this endpoint before you start firing conversions.Fire a conversion
POST to the source's webhook URL whenever a user does something worth counting. The only required fields are user_id and type. Amount, currency, idempotency key, and metadata are optional.
curl -X POST https://api.riftl.ink/w/a1b2c3d4e5f6... \
-H "Content-Type: application/json" \
-d '{
"user_id": "usr_abc123",
"type": "deposit",
"amount_cents": 10000,
"currency": "usd",
"idempotency_key": "tx_550e8400-e29b",
"metadata": { "tx_hash": "0xabc..." }
}'The response tells you what Rift did with the batch:
{
"accepted": 1,
"deduped": 0,
"unattributed": 0,
"failed": 0
}Check your stats
The link stats endpoint now returns a conversions array with counts and sums grouped by type:
curl https://api.riftl.ink/v1/links/summer-sale/stats \
-H "Authorization: Bearer rl_live_YOUR_KEY"{
"link_id": "summer-sale",
"click_count": 1420,
"install_count": 340,
"conversion_rate": 0.239,
"conversions": [
{ "type": "deposit", "count": 19, "sum_cents": 24700000, "currency": "usd" },
{ "type": "signup", "count": 91 }
]
}The event payload
Every custom-source event follows this shape:
{
"user_id": "usr_abc123", // required
"type": "deposit", // required — free-form, up to 64 chars
"amount_cents": 10000, // optional, i64 — see below
"currency": "usd", // required IF amount_cents is set
"idempotency_key": "tx_abc", // optional, <=256 chars
"metadata": { "tx_hash": "0x..." } // optional, <=1KB, stored verbatim
}Why amount_cents is a signed integer
- Integer math — no float rounding errors. Summing $0.10 + $0.20 in floats gives you
0.30000000000000004, which is not what you want when tallying real money. - Currency's smallest unit — cents for USD, yen for JPY (JPY has no subunit so send whole units as
amount_cents). Interpret with thecurrencyfield. - Signed — refunds, chargebacks, and dispute reversals are legitimately negative. Send
amount_cents: -5000for a $50 refund. - Matches the Stripe / RevenueCat convention so you can pass amounts through directly from upstream webhooks.
Idempotency key contract
- Scoped per tenant — two tenants can use the same key without collision.
- Unique within a 30-day window — after TTL expiry, keys may be safely reused.
- Opaque to Rift — any string up to 256 characters, not parsed or validated.
- Collision behavior — Rift silently drops duplicates and returns 200, so your retry logic stays happy. The event is not double-counted.
- Typical values — Stripe invoice ID, RevenueCat event ID, on-chain transaction hash, your DB transaction UUID.
- Optional — if you omit it, every call counts. That's fine for events where double-counting doesn't matter (e.g. content views), but dangerous for revenue.
Managing sources
The default custom source handles the common case — one pipe from your backend to Rift. If you want to segment events by origin (e.g. “backend-deposits” vs “admin-overrides”), create additional custom sources explicitly.
Create a source
curl -X POST https://api.riftl.ink/v1/sources \
-H "Authorization: Bearer rl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "backend-deposits",
"source_type": "custom"
}'Get one source
curl https://api.riftl.ink/v1/sources/SOURCE_ID \
-H "Authorization: Bearer rl_live_YOUR_KEY"Delete a source
Historical events for the deleted source remain queryable via the link stats endpoint — they still carry the source_id reference even after the source document is gone. There is no rotate endpoint; to rotate a webhook URL, delete the source and create a new one.
curl -X DELETE https://api.riftl.ink/v1/sources/SOURCE_ID \
-H "Authorization: Bearer rl_live_YOUR_KEY"Outbound webhook delivery
Every conversion fires an outbound conversion webhook to any registered webhook subscribed to that event type. The payload includes a stable event_id — use it as an idempotency key in your handler to safely dedupe delivery retries. See the Webhooks doc for the full payload shape and signature verification.
GET /v1/links/{link_id}/stats on a schedule — events are the durable source of truth inside Rift's store. The webhook is a push notification for convenience, not the canonical data path.What Rift answers
Rift's conversion API is deliberately bounded. It answers one class of question well and refuses the rest. If a question starts with “which link”, it's in scope. If it starts with “which user”, that's your warehouse's job — pipe events via webhook.
In scope
- Total count and sum per link, per conversion type
- Revenue attribution tied to the originating link
- Idempotent event ingestion with at-least-once delivery
- Outbound webhooks for streaming events to your warehouse
Out of scope
- User-level queries (cohorts, funnels, retention)
- Filtering or grouping by metadata fields
- Multi-event behavioral sequences
- Per-event drill-down from the API
Metadata is stored verbatim and forwarded on the outbound webhook, but it's never indexed or queried inside Rift. Use it for your own debugging and warehouse joins.