diff --git a/api-reference/event-capture.mdx b/api-reference/event-capture.mdx
index f775258a..b9011cf1 100644
--- a/api-reference/event-capture.mdx
+++ b/api-reference/event-capture.mdx
@@ -14,13 +14,13 @@ icon: "shuffle"
See [Linkrunner Agent Skills](/ai-agent-skills).
-This documentation is for tracking custom events from your backend only! For tracking events from your app please go through the [Flutter](https://docs.linkrunner.io/sdk/flutter/usage#tracking-custom-events) or [React Native](https://docs.linkrunner.io/sdk/react-native/usage#tracking-custom-events) documentation.
+Use this API to send custom events and payments from your backend. To track them from your app, use `trackEvent` in your platform's SDK, for example [Android](/sdk/android), [iOS](/sdk/ios), [React Native](/sdk/react-native), or [Flutter](/sdk/flutter).
- Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Each request must identify the user with `user_id` or `install_instance_id`. If Linkrunner has not seen the `user_id` yet (your app has not called `signup` for that user), the event is held and shows up once that user signs up. Held events are kept for up to 30 days. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. For capturing revenue, it is recommended to use the [capture-payment](/api-reference/revenue-tracking) API instead of capture-event.
+ Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Each request must identify the user with `user_id` or `install_instance_id`. If Linkrunner has not seen the `user_id` yet (your app has not called `signup` for that user), the event is held and shows up once that user signs up. Held events are kept for up to 30 days. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. To record revenue, send the [payment fields](#payment-fields) with the event.
-Use [Test Custom Events and Payments](/testing/event-and-revenue-testing) to decide whether an action belongs in this API or the Revenue Tracking API.
+Use [Test Custom Events and Payments](/testing/event-and-revenue-testing) to decide whether an action is a custom event or a payment.
## Base URL
@@ -71,6 +71,41 @@ POST: /capture-event
| event_id | string | **Optional**. Your own unique identifier for the event, useful for deduplication and correlating with your backend |
| client_ip_address | string | **Optional**. Public IP of the user's device when the event happened. See [End-user IP address](#end-user-ip-address) |
| source | string | **Optional**. `app` (default) or `web`. Send `web` for a user of your website. See [Website events](#website-events) |
+| amount | number | **Optional**. Payment amount. Send with `payment_id` and `currency` to record a payment. See [Payment fields](#payment-fields) |
+| payment_id | string | **Optional**. Your unique transaction ID. Send with `amount` and `currency` |
+| currency | string | **Optional**. 3-letter ISO 4217 code of `amount`, for example `USD` or `INR`. Send with `amount` and `payment_id` |
+| status | string | **Optional**. Payment status. Used only when the payment fields are sent. Defaults to `PAYMENT_COMPLETED` |
+
+### Payment fields
+
+Send `amount`, `payment_id`, and `currency` at the top level of the request, next to `event_name`, when the event is a payment. Linkrunner then stores the event as revenue, and `event_name` becomes the payment type you see in reports. Without these fields, the request is a custom event.
+
+`event_name` is stored exactly as you send it. To keep existing event mappings, reports, and affiliate templates working, send an old payment type in uppercase, exactly as before, such as `FIRST_PAYMENT`, `SUBSCRIPTION_RENEWED`, or `DEFAULT`. A different spelling such as `first_payment` is a new payment type. Any other name is also stored exactly as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks. See [Old payment types](/sdk/migrate-to-track-event#old-payment-types) for the full list.
+
+```json
+{
+ "event_name": "purchase_completed",
+ "amount": 149.99,
+ "payment_id": "ORD-12345",
+ "currency": "USD",
+ "event_data": {
+ "product_id": "ABC123"
+ },
+ "user_id": "user_12345"
+}
+```
+
+- **Send all three or none.** If you send only one or two of `amount`, `payment_id`, and `currency`, the request fails with `400` and the message `amount, payment_id and currency must be sent together`.
+- **`amount`** must be a number of zero or more. A numeric string such as `"149.99"` is also accepted.
+- **`payment_id`** must be a non-empty string. Use a unique value for each transaction. Retrying a request with the same `payment_id`, `event_name`, and `status` does not create a second payment, and the API still returns `200`.
+- **`currency`** must be a 3-letter ISO 4217 code. Lowercase is accepted and stored in uppercase.
+- **`status`** accepts `PAYMENT_INITIATED`, `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. If you leave it out, the payment is `PAYMENT_COMPLETED`.
+- **`event_data`**, if sent, must be a JSON object.
+- **`user_id`** links the payment to your user. The SDKs always send it with a payment, so send it from your backend too. A payment identified only by `install_instance_id` is still accepted.
+
+Only `PAYMENT_COMPLETED` payments are sent to ad networks as revenue. An `amount` inside `event_data` does not make the event a payment.
+
+Payment fields apply to app events. For a payment from a user of your website, use [capture-payment with `"source": "web"`](/api-reference/revenue-tracking#website-payments).
### End-user IP address
@@ -101,8 +136,8 @@ If the event comes from a user of your website rather than your app, send `"sour
### Responses
-1. **200** Event captured successfully
-2. **400** Missing required parameters, neither `user_id` nor `install_instance_id` was sent, `source` is not `app` or `web`, or `source` is `web` and `user_id` is missing or website tracking is not enabled for the project
+1. **200** Event or payment captured successfully
+2. **400** Missing required parameters, neither `user_id` nor `install_instance_id` was sent, the payment fields are incomplete or invalid, `source` is not `app` or `web`, or `source` is `web` and `user_id` is missing or website tracking is not enabled for the project
3. **401** Invalid server key
#### Sample Response
@@ -141,23 +176,14 @@ Here are some common event names you might want to track:
## Revenue Sharing with Ad Networks
-To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:
+Send revenue as a payment with the [payment fields](#payment-fields). Meta and Google receive the value and currency from the top-level `amount` and `currency`, so send the order total as `amount`. Linkrunner does not add up item prices.
-```json
-{
- "event_name": "purchase_completed",
- "event_data": {
- "product_id": "ABC123",
- "category": "electronics",
- "amount": 149.99
- },
- "user_id": "user_12345"
-}
-```
+Put product details in `event_data`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`. Google receives `content_ids` as item IDs.
+
+An `amount` inside `event_data` is not sent to ad networks as revenue. Revenue is shared only when **Send Revenue** is turned on for the event's mapping in the dashboard. See [Meta Ads](/ad-networks/meta-ads) and [Google Ads](/ad-networks/google-ads).
- For revenue sharing with ad networks to work properly, ensure the `amount`
- parameter is passed as a number, not as a string.
+ `POST /capture-payment` is deprecated. It keeps working for existing integrations, but new integrations should send payments through `/capture-event`. See [Migrate payments to in-app events](/sdk/migrate-to-track-event).
## Best Practices
@@ -169,7 +195,7 @@ To enable revenue sharing with ad networks like Google Ads and Meta, include an
## Example
-### Tracking a Purchase Event
+### Tracking a Purchase
```javascript
// Using fetch API
@@ -181,11 +207,11 @@ fetch("https://api.linkrunner.io/api/v1/capture-event", {
},
body: JSON.stringify({
event_name: "purchase_completed",
+ amount: 125.99,
+ payment_id: "ORD-12345",
+ currency: "USD",
event_data: {
- order_id: "ORD-12345",
product_ids: ["P-001", "P-002"],
- total_amount: 125.99,
- currency: "USD",
payment_method: "credit_card",
},
user_id: "user_12345",
@@ -205,8 +231,6 @@ The API will return appropriate HTTP status codes along with error messages when
- **429 Too Many Requests**: You've exceeded the rate limit, please try again later
- **500 Internal Server Error**: Contact support if this persists
-For any help please reach out to [support@linkrunner.io](mailto:support@linkrunner.io)
-
## Meta Ecommerce Events
If you are tracking Ecommerce events (like `add_to_cart` or `view_content`) to sync with Meta Catalog Sales, **you must first map your custom event with the standard commerce event in the Linkrunner Dashboard** before sending the event.
@@ -245,4 +269,8 @@ For comprehensive details on each field requirement, refer to our [Meta Commerce
}
```
+For a purchase, send the same `event_data` together with the [payment fields](#payment-fields), and map the event name with **Purchase**.
+
To verify your events are being correctly received by Meta, please follow our [Testing Ecommerce Events](/ecommerce-manager/meta-commerce-manager#testing-ecommerce-events) guide.
+
+**Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io)
diff --git a/api-reference/revenue-tracking.mdx b/api-reference/revenue-tracking.mdx
index 1a6a7c7e..7860c1e0 100644
--- a/api-reference/revenue-tracking.mdx
+++ b/api-reference/revenue-tracking.mdx
@@ -10,6 +10,8 @@ icon: "money-bill"
Use [Test Custom Events and Payments](/testing/event-and-revenue-testing) to verify event classification, payment fields, and deduplication.
+To track payments from your app, and for how each status affects revenue and ad networks, see [Track payments](/sdk/track-payments).
+
## Base URL
```
@@ -26,43 +28,43 @@ In the request header add the below attribute:
linkrunner-key: YOUR-SERVER-KEY
```
-## Capture payment
+## Capture a payment
+
+Send the payment to the [capture-event](/api-reference/event-capture) endpoint with `amount`, `payment_id`, and `currency` at the top level. `event_name` becomes the payment type you see in reports, so use a name that describes the payment, such as `purchase_completed` or `SUBSCRIPTION_RENEWED`.
+
+`event_name` is stored exactly as you send it. To keep existing event mappings, reports, and affiliate templates working, send an old payment type in uppercase, exactly as before, such as `FIRST_PAYMENT`, `SUBSCRIPTION_RENEWED`, or `DEFAULT`. A different spelling such as `first_payment` is a new payment type. Any other name is also stored exactly as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks. See [Old payment types](/sdk/migrate-to-track-event#old-payment-types) for the full list.
```
-POST: /capture-payment
+POST: /capture-event
```
### Request Body
```json
{
- "user_id": "666",
- "payment_id": "ABC", // required, must be unique for each transaction
- "amount": 25096, // Send amount in one currency only
- "type": "FIRST_PAYMENT", // optional
- // "type": "SECOND_PAYMENT", // optional
- "status": "PAYMENT_COMPLETED", // optional
- "source": "app" // optional: "app" (default) or "web"
+ "event_name": "purchase_completed",
+ "amount": 250.96,
+ "payment_id": "ORD-12345",
+ "currency": "USD",
+ "user_id": "user_12345"
}
```
-**NOTE:** If you accept payments in multiple currencies convert them to one currency before calling the API.
+| Parameter | Type | Description |
+| ---------- | ------ | ----------- |
+| event_name | string | **Required**. Name of the payment, stored as the payment type |
+| amount | number | **Required** for a payment. Zero or more. A numeric string is also accepted |
+| payment_id | string | **Required** for a payment. Your unique transaction ID |
+| currency | string | **Required** for a payment. 3-letter ISO 4217 code, for example `USD` or `INR` |
+| status | string | **Optional**. Defaults to `PAYMENT_COMPLETED` |
+| user_id | string | **Required** unless `install_instance_id` is sent. Links the payment to your user. The SDKs always send it with a payment, so send it whenever you have it |
+| install_instance_id | string | **Required** unless `user_id` is sent. Linkrunner's ID for the install |
+| event_data | object | **Optional**. Additional data, such as ecommerce fields |
+| client_ip_address | string | **Optional**. See [End-user IP address](#end-user-ip-address) |
-**Deduplication:** We perform idempotent deduplication using a combination of `type` and `payment_id`. For each unique combination of payment type and payment ID, only one record will be created. If you send multiple requests with the same `type` and `payment_id` combination, only the first will be recorded and subsequent ones will be ignored. To ensure proper deduplication, always provide a unique `payment_id` for each transaction of a given type.
+Send `amount`, `payment_id`, and `currency` together. If you send only one or two of them, the request fails with `400` and the message `amount, payment_id and currency must be sent together`. If you send none of them, the request is stored as a custom event, not a payment. An `amount` inside `event_data` does not make the event a payment.
-### Payment types
-
-- **FIRST_PAYMENT**: User's first payment
-- **SECOND_PAYMENT**: User's second payment
-- **WALLET_TOPUP**: Adding funds to a wallet
-- **FUNDS_WITHDRAWAL**: Withdrawing funds
-- **SUBSCRIPTION_CREATED**: Subscription started
-- **SUBSCRIPTION_RENEWED**: Subscription renewed
-- **ONE_TIME**: One-time payment
-- **RECURRING**: Recurring payment
-- **DEFAULT**: Generic/unspecified payment type
-
-If `type` is omitted, it defaults to **DEFAULT**.
+**Deduplication:** Use a unique `payment_id` for each transaction. Retrying a request with the same `payment_id`, `event_name`, and `status` does not create a second payment, and the API still returns `200`.
### Payment status
@@ -71,7 +73,7 @@ If `type` is omitted, it defaults to **DEFAULT**.
- **PAYMENT_FAILED**: Payment attempt failed
- **PAYMENT_CANCELLED**: Payment was cancelled
-If `status` is omitted, it defaults to **PAYMENT_COMPLETED**.
+If `status` is omitted, it defaults to **PAYMENT_COMPLETED**. Only `PAYMENT_COMPLETED` payments are sent to ad networks. See [Payment status](/sdk/track-payments#payment-status) for what each status does in reports and how to track a payment lifecycle.
### End-user IP address
@@ -80,9 +82,23 @@ These calls come from your server, so Linkrunner never sends your server's IP to
- Send only the device's public IP. Private, loopback and link-local addresses are ignored.
- Leave it out if you don't have it. Linkrunner then uses the install-time device IP.
-### Website payments
+### Responses
+
+1. **200** Payment captured. The response is the same as for a [custom event](/api-reference/event-capture#sample-response), including `processing_id`
+2. **400** Missing or invalid fields, or the payment fields were not sent together
+3. **401** Invalid server key
+
+
+ `POST /capture-payment` is deprecated for app payments. It keeps working for existing integrations, but new integrations should send app payments through `/capture-event`. Website payments still use `/capture-payment`, see [Website payments](#website-payments). See [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
+
+## Website payments
-If the payment comes from a user of your website rather than your app, send `"source": "web"`. Your website must be tracked with the [Web SDK](/sdk/web), and the payment then shows up on the [Web Events](https://dashboard.linkrunner.io/dashboard/web-events) page instead of the app dashboards. Leave `source` out, or send `app`, for app users. Existing integrations do not need to change.
+If the payment comes from a user of your website rather than your app, send it to `POST /capture-payment` with `"source": "web"`. The [capture-event](/api-reference/event-capture) payment fields apply to app users only. Your website must be tracked with the [Web SDK](/sdk/web), and the payment then shows up on the [Web Events](https://dashboard.linkrunner.io/dashboard/web-events) page instead of the app dashboards.
+
+```
+POST: /capture-payment
+```
```json
{
@@ -103,55 +119,11 @@ If the payment comes from a user of your website rather than your app, send `"so
- **No deduplication.** Unlike app payments, every website payment request is stored, including retries with the same `type` and `payment_id`. Avoid resending a payment that was already accepted.
- **Send each purchase from one place.** If your website also calls `lr.track('purchase', …)` for the same order, the purchase is counted twice. Track it either in the browser or from your backend, not both.
-### Responses
-
-1. **201** Payment captured. For app payments, sending the same `type` and `payment_id` again also returns `201`, and Linkrunner counts that payment once.
-2. **400** `source` is not `app` or `web`, or `source` is `web` and `user_id` is missing or website tracking is not enabled for the project
-3. **401** Invalid server key
-
-#### Sample Response
-
-```json
-{
- "msg": "Payment capture request received!",
- "status": 201,
- "processing_id": "0b7e4d2c-81a5-4f3e-9c6d-5a2f1e8b3d47",
- "data": {
- "message": "Payment capture request received!"
- }
-}
-```
-
-`processing_id` identifies this request. Log it on your side and share it with Linkrunner support if a payment does not show up.
-
-## Remove captured payment
-
-```
-POST: /remove-payment
-```
-
-### Request Body
-
-```json
-{
- "user_id": "666",
- "payment_id": "ABC"
-}
-```
-
-**NOTE:** `user_id` or `payment_id` is required in order to remove a payment entry. If you pass `user_id`, all the payments attributed to that user will be removed!
-
-### Responses
-
-1. **200** Payment entry/entries deleted
-2. **400** No payment found with the given payment id or user id
-3. **401** Invalid server key
-
## Meta Ecommerce Events
If you are tracking `Purchase` events to sync with Meta Catalog Sales, **you must first map the purchase event with the standard commerce event in the Linkrunner Dashboard** before sending it.
-_Note: Any Payment event you send should be mapped with **Purchase** for Commerce Event Manager. For example, map **FIRST_PAYMENT** with **Purchase** or **DEFAULT** with **Purchase**._
+_Note: Any payment you send should be mapped with **Purchase** for Commerce Event Manager. For example, map **purchase_completed** with **Purchase**._
While you can include any custom attributes in the `event_data` object, Meta requires specific fields for ecommerce events to correctly attribute catalog sales. Your `event_data` object should include the following fields:
@@ -159,11 +131,11 @@ For comprehensive details on each field requirement, refer to our [Meta Commerce
```json
{
- "user_id": "666",
- "payment_id": "ABC",
- "amount": 25096,
- "type": "DEFAULT", // Event is mapped with the Purchase in the Linkrunner Meta Integration Settings
- "status": "PAYMENT_COMPLETED",
+ "event_name": "purchase_completed",
+ "user_id": "user_12345",
+ "payment_id": "ORD-12345",
+ "amount": 2000.0,
+ "currency": "INR",
"event_data": {
"content_ids": ["whshct4mwc"],
"contents": [
@@ -182,6 +154,8 @@ For comprehensive details on each field requirement, refer to our [Meta Commerce
}
```
-_Note: In the example above, the `type` field is omitted from the request body, which means it defaults to **DEFAULT**. Ensure this default payment type is properly mapped with **Purchase** in your Linkrunner dashboard._
+_Note: In the example above, map **purchase_completed** with **Purchase** in your Linkrunner dashboard before sending it._
To verify your events are being correctly received by Meta, please follow our [Testing Ecommerce Events](/ecommerce-manager/meta-commerce-manager#testing-ecommerce-events) guide.
+
+**Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io)
diff --git a/docs.json b/docs.json
index 3596c369..cb94bf06 100644
--- a/docs.json
+++ b/docs.json
@@ -65,7 +65,9 @@
"sdk/appbrew",
"sdk/web",
"sdk/unity",
- "sdk/shopify"
+ "sdk/shopify",
+ "sdk/track-payments",
+ "sdk/migrate-to-track-event"
]
},
{
diff --git a/ecommerce-manager/google-commerce-manager.mdx b/ecommerce-manager/google-commerce-manager.mdx
index 24dd7e69..a528a834 100644
--- a/ecommerce-manager/google-commerce-manager.mdx
+++ b/ecommerce-manager/google-commerce-manager.mdx
@@ -83,13 +83,13 @@ There are **3 essential events** to map:
- Your view item event (e.g., `view_item`, `item_viewed`) → maps to **`view_item`**
- Your add to cart event (e.g., `add_to_cart`) → maps to **`add_to_cart`**
-- Your purchase event (e.g., `FIRST_PAYMENT`, `DEFAULT`) → maps to **`ecommerce_purchase`**
+- Your purchase event (e.g., `purchase_completed`) → maps to **`ecommerce_purchase`**
_In the Linkrunner Dashboard, go to your Google Ads integration settings and map each of your custom event names to the corresponding standard Google event._
## Understanding `event_data` for Google
-When sending events via the [Capture Event API](/api-reference/event-capture), [Capture Payment API](/api-reference/revenue-tracking), or the Linkrunner SDK (`trackEvent` and `capturePayment` methods), you need to include specific fields in `event_data` so Google can correctly attribute conversions and match products in your Merchant Center catalog.
+When sending events via the [Capture Event API](/api-reference/event-capture) or the Linkrunner SDK `trackEvent` method, you need to include specific fields in `event_data` so Google can correctly attribute conversions and match products in your Merchant Center catalog.
### Required Fields
@@ -169,36 +169,34 @@ await linkrunner.trackEvent(
### ecommerce_purchase
-Fired when a user completes a purchase.
+Fired when a user completes a purchase. Send it as a payment: put `amount`, `paymentId`, `currency`, and `userId` in the event data so Linkrunner records the revenue.
```javascript SDK
-await linkrunner.capturePayment({
- amount: 2298.00,
- userId: "user_456",
- paymentId: "order_98765",
- type: "FIRST_PAYMENT", // Map this payment type to "ecommerce_purchase" in the Linkrunner Dashboard
- status: "PAYMENT_COMPLETED",
- eventData: {
- content_ids: ["sku_blue_tshirt", "sku_red_shoes"], // Must match Merchant Center ID (offer_id)
- value: 2298, // Required
+await linkrunner.trackEvent(
+ "purchase_completed", // Map this event to "ecommerce_purchase" in the Linkrunner Dashboard
+ {
+ amount: 2298.0, // Required: order total, sent as the value
+ paymentId: "order_98765", // Required for a payment
currency: "INR", // Required
+ userId: "user_456", // Required for a payment
+ content_ids: ["sku_blue_tshirt", "sku_red_shoes"], // Must match Merchant Center ID (offer_id)
num_items: 2, // Required
order_id: "order_98765", // Required for purchase events
},
-});
+);
```
```json API
-// POST /capture-payment
+// POST /capture-event
{
"token": "your_project_token",
+ "event_name": "purchase_completed",
"payment_id": "order_98765",
"user_id": "user_456",
"amount": 2298.00,
- "type": "FIRST_PAYMENT",
- "status": "PAYMENT_COMPLETED",
+ "currency": "INR",
"install_instance_id": "abc-123",
"event_data": {
"content_ids": ["sku_blue_tshirt", "sku_red_shoes"], // Must match Merchant Center ID (offer_id)
@@ -219,7 +217,7 @@ The `content_ids` you send must exactly match the **ID (`offer_id`)** of the pro
-The `amount` field in `capturePayment` and the `value` field in `event_data` should be consistent. Both represent the total order value. Google reads `value` from `event_data` for conversion reporting.
+The payment `amount` and the `value` field in `event_data` should be consistent. Both represent the total order value. For a purchase, Google receives the conversion value and currency from the payment `amount` and `currency`.
## Verifying Events in Google Ads
@@ -235,4 +233,6 @@ It can take up to **24 hours** for conversion data to appear after your first ev
---
-> **Note:** The `event_data` fields above can be sent via the Linkrunner SDK (`trackEvent` and `capturePayment` methods) or directly via the APIs. For full request schemas, refer to the [Capture Event API](/api-reference/event-capture) and [Capture Payment API](/api-reference/revenue-tracking) documentation.
+> **Note:** The `event_data` fields above can be sent via the Linkrunner SDK `trackEvent` method or directly via the API. For full request schemas, refer to the [Capture Event API](/api-reference/event-capture) and [Revenue Tracking API](/api-reference/revenue-tracking) documentation.
+
+**Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io)
diff --git a/ecommerce-manager/meta-commerce-manager.mdx b/ecommerce-manager/meta-commerce-manager.mdx
index b82efd2e..60fd05c3 100644
--- a/ecommerce-manager/meta-commerce-manager.mdx
+++ b/ecommerce-manager/meta-commerce-manager.mdx
@@ -79,7 +79,7 @@ View the dataset ID and check the events list. There are **3 essential events**
- `Purchase` (processed via the [Capture Payment API](/api-reference/revenue-tracking) or SDK)
- `AddToCart` and `ViewContent` (processed via the [Event Capture API](/api-reference/event-capture) or SDK)
-_Note: Any event you want to send for these actions should be mapped with their respective standard commerce manager events in the Linkrunner Dashboard. For example, map your custom add to cart event (e.g., **add_to_cart**) with **AddToCart**, your view event (e.g., **item_viewed** or **view_content**) with **ViewContent**, and your payment event (e.g., **FIRST_PAYMENT** or **DEFAULT**) with **Purchase**._
+_Note: Any event you want to send for these actions should be mapped with their respective standard commerce manager events in the Linkrunner Dashboard. For example, map your custom add to cart event (e.g., **add_to_cart**) with **AddToCart**, your view event (e.g., **item_viewed** or **view_content**) with **ViewContent**, and your payment event (the event name you send with the payment, e.g., **purchase**) with **Purchase**._
Make sure you have integrated the appropriate APIs in your application to send these events.
diff --git a/features/events-tab.mdx b/features/events-tab.mdx
index 39639ee8..451f9d62 100644
--- a/features/events-tab.mdx
+++ b/features/events-tab.mdx
@@ -152,8 +152,8 @@ If this sub-tab is empty, nothing has arrived yet. The empty state links to the
empties every step below it.
- The amount, currency, and payment ID come from the `capturePayment` call in your app. Confirm your app is
- passing them. See [Revenue Tracking](/sdk/react-native#revenue-tracking).
+ The amount, currency, and payment ID come from the payment keys your app sends in `trackEvent` event data. Confirm your app
+ sends all three. See your platform's SDK guide, for example [React Native](/sdk/react-native).
diff --git a/features/skadnetwork-integration.mdx b/features/skadnetwork-integration.mdx
index 35cc0222..83752ebb 100644
--- a/features/skadnetwork-integration.mdx
+++ b/features/skadnetwork-integration.mdx
@@ -54,7 +54,7 @@ Make sure your app is using at least these SDK versions before enabling SKAN. Li
### How the SDK updates conversion values
-You don't need to call any SKAN APIs yourself. On first launch the SDK registers an initial conversion value (0/low), and every `trackEvent`, `capturePayment`, and `signup` call returns the updated conversion value from Linkrunner, which the SDK applies automatically. On iOS 17.4+ the SDK uses Apple's AdAttributionKit; on iOS 14.0–17.3 it falls back to SKAdNetwork.
+You don't need to call any SKAN APIs yourself. On first launch the SDK registers an initial conversion value (0/low), and every `trackEvent` (including payments) and `signup` call returns the updated conversion value from Linkrunner, which the SDK applies automatically. On iOS 17.4+ the SDK uses Apple's AdAttributionKit; on iOS 14.0–17.3 it falls back to SKAdNetwork.
### Add Ad Network Identifiers
diff --git a/features/web-to-app-journeys.mdx b/features/web-to-app-journeys.mdx
index 98a7fc9a..725cb1e2 100644
--- a/features/web-to-app-journeys.mdx
+++ b/features/web-to-app-journeys.mdx
@@ -18,7 +18,7 @@ One thing does the bridging: a **Linkrunner link on your custom subdomain**, pla
- **If the app is installed**, the link opens the app directly. Your app reports the open with [`handleDeeplink`](/sdk/react-native#handle-deeplink), which records a re-engagement against the campaign.
- **If the app is not installed**, the link routes the user to the store. After they install and open the app, [`getAttributionData`](/sdk/react-native#getting-attribution-data) returns the attributed campaign and the deferred deep link so you can drop them into the right screen.
-Everything after the open is the mobile SDK's job: `init` records the app open, [`signup`](/sdk/react-native#user-identification-required) registers the user, [`trackEvent`](/sdk/react-native#tracking-custom-events) captures actions, and [`capturePayment`](/sdk/react-native#revenue-tracking) captures revenue.
+Everything after the open is the mobile SDK's job: `init` records the app open, [`signup`](/sdk/react-native#user-identification-required) registers the user, [`trackEvent`](/sdk/react-native#tracking-custom-events) captures actions, and [`trackEvent` with a payment](/sdk/react-native) captures revenue.
For Meta campaigns, use the dedicated handoff instead of building the link yourself. Linkrunner auto-creates and attributes the campaign for you. See [Meta Web to App](/ad-networks/meta-web-to-app).
@@ -43,7 +43,7 @@ These journeys open the app directly through the Linkrunner link. You track the
| --- | --- | --- | --- |
| **Website → App Open** | Link click + app re-engagement via `handleDeeplink` | Campaigns → **Clicks**; switch **Install Attribution** to **Retargeting** | Deep linking configured; `handleDeeplink` called on every launch; "Open App" button uses a Linkrunner link |
| **Web content → specific screen** | Same, with a deep link **path** that routes in-app (e.g. `/product/123`) | Same as above; in-screen actions show in event columns | App routes match the link's paths; encode the path in the link |
-| **Web CTA → in-app conversion** | Re-engagement + in-app [`trackEvent`](/sdk/react-native#tracking-custom-events) or [`capturePayment`](/sdk/react-native#revenue-tracking) | Custom **event columns**; **Revenue**, **ROAS**, **Paying Users** | Call `signup` first; instrument the conversion event or payment |
+| **Web CTA → in-app conversion** | Re-engagement + in-app [`trackEvent`](/sdk/react-native#tracking-custom-events), with a payment for purchases | Custom **event columns**; **Revenue**, **ROAS**, **Paying Users** | Call `signup` first; instrument the conversion event or payment |
| **CRM → app** (email, SMS, WhatsApp, push) | One tagged link per channel + downstream events | One **campaign per channel**: Clicks, Reengagements, and event columns | A Linkrunner link per channel; for push, carry the link in the notification payload |
@@ -62,7 +62,7 @@ These journeys go through the store. Deferred deep linking carries the original
| --- | --- | --- | --- |
| **Website → Install** | Click → install, with the attributed campaign from `getAttributionData` | **Overview** stat cards (All / Attributed / Organic Installs); Campaigns → **Installs** | Linkrunner link as the store target; `init` + `getAttributionData` after first open |
| **Website → Install → Signup** | Install + user registration | **Sign-ups** and **Conversion** (sign-ups / installs) columns; user list in the campaign drawer | Call [`signup`](/sdk/react-native#user-identification-required) with `user_data.id` |
-| **Website → Install → Purchase** | Install + revenue | **Revenue**, **ROAS**, **Paying Users**, **Daywise Revenue** columns | [`capturePayment`](/sdk/react-native#revenue-tracking); convert to one currency; map the event for Meta/Google ROAS |
+| **Website → Install → Purchase** | Install + revenue | **Revenue**, **ROAS**, **Paying Users**, **Daywise Revenue** columns | [`trackEvent` with a payment](/sdk/react-native) (amount, payment ID, currency); map the event for Meta/Google ROAS |
| **Website → Install → Onboarding** | Install + activation event | Custom **event column**; [Events](https://dashboard.linkrunner.io/dashboard/events) | Define the activation event and fire it with [`trackEvent`](/sdk/react-native#tracking-custom-events) |
| **Specific page → Install → Same context** | Deferred deep link carrying the page context through the install | Campaign **Installs**; the link resolves via `getAttributionData().deeplink` | Enable deferred deep linking on the campaign; pass the path into the link; parse `deeplink` in-app |
| **Referral link → Install** | Install + referral conversion | Campaign per referral; event column | [Referral tracking](/features/referral-codes); add `?referrer=` to the link; handle it in-app |
diff --git a/sdk-less/android-minimal.mdx b/sdk-less/android-minimal.mdx
index eb4cb140..f0f8c650 100644
--- a/sdk-less/android-minimal.mdx
+++ b/sdk-less/android-minimal.mdx
@@ -132,16 +132,23 @@ object LinkrunnerMinimal {
.put("data", JSONObject().put("device_data", deviceData(context)))
)
- suspend fun trackEvent(context: Context, name: String, data: JSONObject? = null) =
+ // Send amount, paymentId, and currency together to record a payment; leave them out for a custom event
+ suspend fun trackEvent(
+ context: Context,
+ name: String,
+ data: JSONObject? = null,
+ amount: Double? = null,
+ paymentId: String? = null,
+ currency: String? = null
+ ) =
post(
"/api/client/capture-event",
- base(context).put("event_name", name).apply { if (data != null) put("event_data", data) }
- )
-
- suspend fun capturePayment(context: Context, userId: String, amount: Double, paymentId: String) =
- post(
- "/api/client/capture-payment",
- base(context).put("user_id", userId).put("amount", amount).put("payment_id", paymentId).put("status", "PAYMENT_COMPLETED")
+ base(context).put("event_name", name).apply {
+ if (data != null) put("event_data", data)
+ if (amount != null && paymentId != null && currency != null) {
+ put("amount", amount).put("payment_id", paymentId).put("currency", currency)
+ }
+ }
)
}
```
@@ -185,9 +192,9 @@ CoroutineScope(Dispatchers.IO).launch {
LinkrunnerMinimal.trackEvent(context, "purchase_initiated", JSONObject().put("amount", 99.99))
}
-// 5. Capture a payment
+// 5. Track a payment
CoroutineScope(Dispatchers.IO).launch {
- LinkrunnerMinimal.capturePayment(context, userId = "user_123", amount = 99.99, paymentId = "payment_123")
+ LinkrunnerMinimal.trackEvent(context, "purchase_completed", amount = 99.99, paymentId = "payment_123", currency = "USD")
}
```
diff --git a/sdk-less/android-quickstart.mdx b/sdk-less/android-quickstart.mdx
index 7b21ed1f..66a06f8d 100644
--- a/sdk-less/android-quickstart.mdx
+++ b/sdk-less/android-quickstart.mdx
@@ -284,6 +284,14 @@ import java.io.IOException
import java.util.UUID
import java.util.concurrent.TimeUnit
+/** Send with trackEvent to record the event as a payment. status defaults to PAYMENT_COMPLETED on the server. */
+data class LinkrunnerPayment(
+ val amount: Double,
+ val paymentId: String,
+ val currency: String,
+ val status: String? = null,
+)
+
/**
* Linkrunner REST client. Mirrors the SDK's API over plain HTTP.
* Call configure() once, then use the suspend methods from a background coroutine.
@@ -354,38 +362,20 @@ object LinkrunnerClient {
suspend fun setUserData(userData: Map): JSONObject =
post("/api/client/set-user-data", base().put("user_data", jsonOf(userData)))
- suspend fun trackEvent(eventName: String, eventData: Map? = null): JSONObject {
- val body = base().put("event_name", eventName)
- if (eventData != null) body.put("event_data", jsonOf(eventData))
- return post("/api/client/capture-event", body)
- }
-
- suspend fun capturePayment(
- amount: Double,
- paymentId: String,
- userId: String? = null,
- type: String = "DEFAULT",
- status: String = "PAYMENT_COMPLETED",
+ suspend fun trackEvent(
+ eventName: String,
eventData: Map? = null,
+ payment: LinkrunnerPayment? = null,
): JSONObject {
- val body = base().apply {
- put("amount", amount)
- put("type", type)
- put("status", status)
- put("payment_id", paymentId)
- if (userId != null) put("user_id", userId)
- if (eventData != null) put("event_data", jsonOf(eventData))
- }
- return post("/api/client/capture-payment", body)
- }
-
- suspend fun removePayment(paymentId: String? = null, userId: String? = null): JSONObject {
- require(paymentId != null || userId != null) { "Provide paymentId or userId" }
- val body = base().apply {
- if (paymentId != null) put("payment_id", paymentId)
- if (userId != null) put("user_id", userId)
+ val body = base().put("event_name", eventName)
+ if (eventData != null) body.put("event_data", jsonOf(eventData))
+ if (payment != null) {
+ body.put("amount", payment.amount)
+ body.put("payment_id", payment.paymentId)
+ body.put("currency", payment.currency)
+ if (payment.status != null) body.put("status", payment.status)
}
- return post("/api/client/remove-captured-payment", body)
+ return post("/api/client/capture-event", body)
}
suspend fun setPushToken(pushToken: String): JSONObject =
@@ -496,25 +486,28 @@ CoroutineScope(Dispatchers.IO).launch {
## 7. Track events and payments
+Use `trackEvent` for both. Pass a `LinkrunnerPayment` when the event is a payment, so Linkrunner records it as revenue. The event name becomes the payment type in reports.
+
```kotlin
// Custom event
LinkrunnerClient.trackEvent(
"purchase_initiated",
- mapOf("product_id" to "12345", "amount" to 99.99)
+ mapOf("product_id" to "12345")
)
// Payment
-LinkrunnerClient.capturePayment(
- amount = 99.99,
- userId = "user_123",
- paymentId = "payment_123",
- type = "FIRST_PAYMENT"
+LinkrunnerClient.trackEvent(
+ "purchase_completed",
+ mapOf("product_id" to "12345"),
+ payment = LinkrunnerPayment(
+ amount = 99.99,
+ paymentId = "payment_123",
+ currency = "USD"
+ )
)
```
-
- Add `amount` as a number in event or payment data to enable revenue sharing with Google and Meta.
-
+`currency` is a 3-letter ISO 4217 code. Only completed payments are sent to ad networks. See [capture-event](/sdk-less/api-reference#capture-event) for the field rules and `status` values.
## Other endpoints
@@ -525,7 +518,6 @@ LinkrunnerClient.setUserData(mapOf("id" to "user_123", "phone" to "9876543210"))
LinkrunnerClient.setPushToken(fcmToken)
LinkrunnerClient.setIntegrationData(clevertapId = "YOUR_CLEVERTAP_ID")
LinkrunnerClient.handleDeeplink("https://app.example.com/product/123")
-LinkrunnerClient.removePayment(paymentId = "payment_123")
```
## Retries and idempotency
@@ -537,11 +529,11 @@ A retry can send the same request twice if the first attempt reached the server
| Endpoint | Safe to retry? | Why |
| --- | --- | --- |
| `init` | Yes | The server deduplicates on `install_instance_id`. |
-| `capture-payment` | Yes, with `payment_id` | The server deduplicates on `payment_id`. Always send one. |
-| `capture-event` | Not deduplicated | A retried event can be counted twice. |
+| `capture-event` with a payment | Yes | The server deduplicates on `payment_id` for the same event name and status. |
+| `capture-event` without a payment | Not deduplicated | A retried event can be counted twice. |
- `payment_id` is required for `capturePayment`, and a unique value ensures retries cannot double count revenue. Events have no server-side deduplication, so if exact event counts matter, disable retries for those calls.
+ A unique `paymentId` for each transaction ensures retries cannot double count revenue. Custom events have no server-side deduplication, so if exact event counts matter, disable retries for those calls.
## Troubleshooting
diff --git a/sdk-less/api-reference.mdx b/sdk-less/api-reference.mdx
index 5d01a341..98d80a84 100644
--- a/sdk-less/api-reference.mdx
+++ b/sdk-less/api-reference.mdx
@@ -31,7 +31,7 @@ These fields appear on most requests. Send them on every call once your install
- **Call from the device** so Linkrunner sees the device IP.
- This reference uses Android examples. On iOS, send `"platform": "IOS"` and a different `device_data` shape (IDFA, IDFV, and `adservices_attribution_token` instead of GAID and the install referrer). The `init`, `trigger`, `capture-event`, and `capture-payment` responses may also include SKAdNetwork values (`fine_conversion_value`, `coarse_conversion_value`, `lock_postback`). See [iOS device data](/sdk-less/ios-device-data).
+ This reference uses Android examples. On iOS, send `"platform": "IOS"` and a different `device_data` shape (IDFA, IDFV, and `adservices_attribution_token` instead of GAID and the install referrer). The `init`, `trigger`, and `capture-event` responses may also include SKAdNetwork values (`fine_conversion_value`, `coarse_conversion_value`, `lock_postback`). See [iOS device data](/sdk-less/ios-device-data).
---
@@ -190,64 +190,45 @@ When `user_data` carries a tool's ID (`mixpanel_distinct_id`, `amplitude_device_
POST /api/client/capture-event
```
-Tracks a custom event. Events are stored for all users, including organic ones. Identify the user with `user_id` or `install_instance_id`.
+Tracks a custom event or a payment. Events are stored for all users, including organic ones. Identify the user with `user_id` or `install_instance_id`.
| Field | Type | Description |
| --- | --- | --- |
| `token` | string | **Required**. |
-| `event_name` | string | **Required**. |
-| `event_data` | object | Optional. Custom attributes. Add `amount` as a number to share revenue with ad networks. |
+| `event_name` | string | **Required**. For a payment, this is the payment type shown in reports. |
+| `event_data` | object | Optional. Custom attributes. Must be a JSON object. An `amount` here is not sent to ad networks as revenue. Send the payment fields instead. |
| `install_instance_id` | string | The persisted UUID. |
| `user_id` | string | Optional. |
| `platform` | string | `"ANDROID"`. |
+| `amount` | number | Payment only. Zero or greater. |
+| `payment_id` | string | Payment only. Your unique transaction ID. |
+| `currency` | string | Payment only. 3-letter ISO 4217 code, for example `"USD"`. |
+| `status` | string | Optional, payment only. Defaults to `PAYMENT_COMPLETED`. |
-```json
-{ "msg": "Event capture request received!", "status": 200, "data": { "message": "Event capture request received!" } }
-```
-
-## capture-payment
+To record a payment, send `amount`, `payment_id`, and `currency` together at the top level. Sending only one or two of them returns `400` with `amount, payment_id and currency must be sent together`. Without them, the request is a custom event. An `amount` inside `event_data` does not make the request a payment.
-```
-POST /api/client/capture-payment
-```
-
-Tracks revenue. Like events, the user must be attributed via `signup`.
-
-| Field | Type | Description |
-| --- | --- | --- |
-| `token` | string | **Required**. |
-| `amount` | number | Payment amount. Must be zero or greater. |
-| `user_id` | string | The paying user. |
-| `payment_id` | string | **Required**. Used to deduplicate. Sending the same `payment_id` twice is ignored. |
-| `type` | string | Optional. Defaults to `DEFAULT`. |
-| `status` | string | Optional. Defaults to `PAYMENT_COMPLETED`. |
-| `event_data` | object | Optional. Ecommerce attributes. |
-| `install_instance_id` | string | The persisted UUID. |
-| `platform` | string | `"ANDROID"`. |
-
-`type` values: `FIRST_PAYMENT`, `SECOND_PAYMENT`, `ONE_TIME`, `RECURRING`, `WALLET_TOPUP`, `FUNDS_WITHDRAWAL`, `SUBSCRIPTION_CREATED`, `SUBSCRIPTION_RENEWED`, `SUBSCRIPTION_CANCELLED`, `SUBSCRIPTION_REFUNDED`, `SUBSCRIPTION_BILLING_ISSUE`, `DEFAULT`.
+Retrying with the same `payment_id`, `event_name`, and `status` does not create a second payment and still returns `200`. Only `PAYMENT_COMPLETED` payments are sent to ad networks.
`status` values: `PAYMENT_INITIATED`, `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, `PAYMENT_CANCELLED`.
```json
-{ "msg": "Payment capture request received!", "status": 201, "data": { "message": "Payment capture request received!" } }
+{
+ "token": "YOUR_PROJECT_TOKEN",
+ "platform": "ANDROID",
+ "install_instance_id": "f1e2...c9",
+ "user_id": "user_123",
+ "event_name": "purchase_completed",
+ "amount": 99.99,
+ "payment_id": "payment_123",
+ "currency": "USD"
+}
```
-## remove-captured-payment
+The response is the same for a custom event and a payment:
+```json
+{ "msg": "Event capture request received!", "status": 200, "data": { "message": "Event capture request received!" } }
```
-POST /api/client/remove-captured-payment
-```
-
-Removes a captured payment, for a refund. Provide `payment_id` or `user_id`.
-
-| Field | Type | Description |
-| --- | --- | --- |
-| `token` | string | **Required**. |
-| `payment_id` | string | The payment to remove. |
-| `user_id` | string | Remove payments for this user. |
-| `install_instance_id` | string | The persisted UUID. |
-| `platform` | string | `"ANDROID"`. |
## update-push-token
@@ -304,7 +285,7 @@ Reports a deeplink open for remarketing and reattribution. Call when your app op
| Code | Meaning |
| --- | --- |
-| `400` | Missing required field or invalid token. |
+| `400` | Missing required field, invalid token, or incomplete payment fields. |
| `401` | Invalid token (on `attribution-data`). |
| `429` | Rate limit exceeded (25 req/s per token). |
| `500` | Server error. Retry with backoff. |
diff --git a/sdk-less/ios-device-data.mdx b/sdk-less/ios-device-data.mdx
index 49f231a2..9cf363ad 100644
--- a/sdk-less/ios-device-data.mdx
+++ b/sdk-less/ios-device-data.mdx
@@ -88,7 +88,7 @@ let idfv = await UIDevice.current.identifierForVendor?.uuidString
## SKAdNetwork
-iOS measures ad-driven installs through SKAdNetwork. After you call `init`, `signup`, `capture-event`, or `capture-payment`, the response can include a conversion value:
+iOS measures ad-driven installs through SKAdNetwork. After you call `init`, `signup`, or `capture-event`, the response can include a conversion value:
```json
{
diff --git a/sdk-less/ios-minimal.mdx b/sdk-less/ios-minimal.mdx
index 8e500f6b..7c063d8d 100644
--- a/sdk-less/ios-minimal.mdx
+++ b/sdk-less/ios-minimal.mdx
@@ -90,23 +90,20 @@ enum LinkrunnerMinimal {
return try await post("/api/client/trigger", body)
}
+ // Send amount, paymentId, and currency together to record a payment; leave them out for a custom event
@discardableResult
- static func trackEvent(_ name: String, data: [String: Any]? = nil) async throws -> [String: Any] {
+ static func trackEvent(_ name: String, data: [String: Any]? = nil,
+ amount: Double? = nil, paymentId: String? = nil, currency: String? = nil) async throws -> [String: Any] {
var body = base()
body["event_name"] = name
if let data { body["event_data"] = data }
+ if let amount, let paymentId, let currency {
+ body["amount"] = amount
+ body["payment_id"] = paymentId
+ body["currency"] = currency
+ }
return try await post("/api/client/capture-event", body)
}
-
- @discardableResult
- static func capturePayment(userId: String, amount: Double, paymentId: String) async throws -> [String: Any] {
- var body = base()
- body["user_id"] = userId
- body["amount"] = amount
- body["payment_id"] = paymentId
- body["status"] = "PAYMENT_COMPLETED"
- return try await post("/api/client/capture-payment", body)
- }
}
```
@@ -151,8 +148,8 @@ Task { try? await LinkrunnerMinimal.signup(userId: "user_123") }
// 4. Track an event
Task { try? await LinkrunnerMinimal.trackEvent("purchase_initiated", data: ["amount": 99.99]) }
-// 5. Capture a payment
-Task { try? await LinkrunnerMinimal.capturePayment(userId: "user_123", amount: 99.99, paymentId: "payment_123") }
+// 5. Track a payment
+Task { try? await LinkrunnerMinimal.trackEvent("purchase_completed", amount: 99.99, paymentId: "payment_123", currency: "USD") }
```
diff --git a/sdk-less/ios-quickstart.mdx b/sdk-less/ios-quickstart.mdx
index aa964e65..a58f5c85 100644
--- a/sdk-less/ios-quickstart.mdx
+++ b/sdk-less/ios-quickstart.mdx
@@ -109,11 +109,25 @@ final class LinkrunnerClient {
return try await post("/api/client/set-user-data", body)
}
+ /// Pass a payment to record the event as revenue. A nil status defaults to PAYMENT_COMPLETED on the server.
+ struct Payment {
+ let amount: Double
+ let paymentId: String
+ let currency: String
+ var status: String? = nil
+ }
+
@discardableResult
- func trackEvent(_ name: String, data eventData: [String: Any]? = nil) async throws -> [String: Any] {
+ func trackEvent(_ name: String, data eventData: [String: Any]? = nil, payment: Payment? = nil) async throws -> [String: Any] {
var body = base()
body["event_name"] = name
if let eventData { body["event_data"] = eventData }
+ if let payment {
+ body["amount"] = payment.amount
+ body["payment_id"] = payment.paymentId
+ body["currency"] = payment.currency
+ if let status = payment.status { body["status"] = status }
+ }
body["device_data"] = await deviceData()
body["time_since_app_install"] = timeSinceInstall()
let res = try await post("/api/client/capture-event", body)
@@ -121,38 +135,6 @@ final class LinkrunnerClient {
return res
}
- @discardableResult
- func capturePayment(
- amount: Double,
- userId: String,
- paymentId: String,
- type: String = "DEFAULT",
- status: String = "PAYMENT_COMPLETED",
- eventData: [String: Any]? = nil
- ) async throws -> [String: Any] {
- var body = base()
- body["amount"] = amount
- body["user_id"] = userId
- body["type"] = type
- body["status"] = status
- body["payment_id"] = paymentId
- if let eventData { body["event_data"] = eventData }
- body["time_since_app_install"] = timeSinceInstall()
- body["data"] = ["device_data": await deviceData()]
- let res = try await post("/api/client/capture-payment", body)
- applySKAN(res)
- return res
- }
-
- @discardableResult
- func removePayment(userId: String, paymentId: String? = nil) async throws -> [String: Any] {
- var body = base()
- body["user_id"] = userId
- if let paymentId { body["payment_id"] = paymentId }
- body["data"] = ["device_data": await deviceData()]
- return try await post("/api/client/remove-captured-payment", body)
- }
-
@discardableResult
func setPushToken(_ pushToken: String) async throws -> [String: Any] {
var body = base()
@@ -358,29 +340,28 @@ Task {
## 5. Track events and payments
+Use `trackEvent` for both. Pass a `Payment` when the event is a payment, so Linkrunner records it as revenue. The event name becomes the payment type in reports.
+
```swift
// Custom event
Task {
try? await LinkrunnerClient.shared.trackEvent(
"purchase_initiated",
- data: ["product_id": "12345", "amount": 99.99]
+ data: ["product_id": "12345"]
)
}
// Payment
Task {
- try? await LinkrunnerClient.shared.capturePayment(
- amount: 99.99,
- userId: "user_123",
- paymentId: "payment_123",
- type: "FIRST_PAYMENT"
+ try? await LinkrunnerClient.shared.trackEvent(
+ "purchase_completed",
+ data: ["product_id": "12345"],
+ payment: .init(amount: 99.99, paymentId: "payment_123", currency: "USD")
)
}
```
-
- Add `amount` as a number in event or payment data to enable revenue sharing with Google and Meta.
-
+`currency` is a 3-letter ISO 4217 code. Only completed payments are sent to ad networks. See [capture-event](/sdk-less/api-reference#capture-event) for the field rules and `status` values.
## Other endpoints
@@ -389,12 +370,11 @@ try? await LinkrunnerClient.shared.setUserData(["id": "user_123", "phone": "9876
try? await LinkrunnerClient.shared.setPushToken(apnsOrFcmToken)
try? await LinkrunnerClient.shared.setIntegrationData(clevertapId: "YOUR_CLEVERTAP_ID")
try? await LinkrunnerClient.shared.handleDeeplink("https://app.example.com/product/123")
-try? await LinkrunnerClient.shared.removePayment(userId: "user_123", paymentId: "payment_123")
```
## SKAdNetwork
-`init`, `signup`, `capture-event`, and `capture-payment` responses can include a conversion value (`fine_conversion_value`, `coarse_conversion_value`, `lock_postback`). The client applies it automatically through `applySKAN`, which calls `SKAdNetwork.updatePostbackConversionValue`. You do not need to manage conversion values yourself. See [Collecting device data](/sdk-less/ios-device-data#skadnetwork) for how this fits with Apple's attribution.
+`init`, `signup`, and `capture-event` responses can include a conversion value (`fine_conversion_value`, `coarse_conversion_value`, `lock_postback`). The client applies it automatically through `applySKAN`, which calls `SKAdNetwork.updatePostbackConversionValue`. You do not need to manage conversion values yourself. See [Collecting device data](/sdk-less/ios-device-data#skadnetwork) for how this fits with Apple's attribution.
## Retries and idempotency
@@ -405,11 +385,11 @@ A retry can send the same request twice if the first attempt reached the server
| Endpoint | Safe to retry? | Why |
| --- | --- | --- |
| `init` | Yes | The server deduplicates on `install_instance_id`. |
-| `capture-payment` | Yes, with `payment_id` | The server deduplicates on `payment_id`. Always send one. |
-| `capture-event` | Not deduplicated | A retried event can be counted twice. |
+| `capture-event` with a payment | Yes | The server deduplicates on `payment_id` for the same event name and status. |
+| `capture-event` without a payment | Not deduplicated | A retried event can be counted twice. |
- `paymentId` is required for `capturePayment`, and a unique value ensures retries cannot double count revenue. Events have no server-side deduplication, so if exact event counts matter, disable retries for those calls.
+ A unique `paymentId` for each transaction ensures retries cannot double count revenue. Custom events have no server-side deduplication, so if exact event counts matter, disable retries for those calls.
## Troubleshooting
diff --git a/sdk/android.mdx b/sdk/android.mdx
index b526796f..cc55d0ef 100644
--- a/sdk/android.mdx
+++ b/sdk/android.mdx
@@ -28,7 +28,7 @@ Add the Linkrunner SDK to your app's `build.gradle` file:
```gradle
dependencies {
- implementation 'io.linkrunner:android-sdk:4.4.0'
+ implementation 'io.linkrunner:android-sdk:5.0.0'
}
```
@@ -486,105 +486,92 @@ private fun setIntegrationData() {
## Revenue Tracking
- Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so payments are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
+ Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Pass your user ID as `userId` so each payment is linked to the user who paid. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
-### Capturing Payments
+### Tracking Payments
-Track payment information with the following details:
+A payment is an in-app event with revenue. Send it with `trackEvent`, and put `amount`, `paymentId`, and `currency` in `eventData`. The event name becomes the payment type in reports and mappings, so you can use any name, such as `purchase` or `SUBSCRIPTION_RENEWED`. Requires Android SDK 5.0.0 or later. For examples for every case and how payments reach ad networks, see the full [Track payments](/sdk/track-payments) guide.
+
+This example sends every payment key. `amount`, `paymentId`, `currency`, and `userId` are required for a payment.
```kotlin
-private fun capturePayment() {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- val paymentData = CapturePaymentRequest(
- paymentId = "payment_123", // Required: Unique payment identifier
- userId = "user123", // Required: User identifier
- amount = 99.99, // Required: Payment amount
- type = PaymentType.FIRST_PAYMENT, // Optional: Defaults to DEFAULT
- // type = PaymentType.SECOND_PAYMENT, // Optional: Defaults to DEFAULT
- status = PaymentStatus.PAYMENT_COMPLETED, // Optional: Defaults to PAYMENT_COMPLETED
- eventData = mapOf( // Optional: Ecommerce/custom event data
- "content_ids" to listOf("product_123"),
- "content_type" to "product",
- "currency" to "USD",
- "value" to 99.99,
- "num_items" to 1,
- "order_id" to "order_12345",
- "contents" to listOf(
- mapOf(
- "id" to "product_123",
- "quantity" to 1,
- "item_price" to 99.99
- )
- )
- )
- )
+import io.linkrunner.sdk.LinkRunner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
- val result = LinkRunner.getInstance().capturePayment(paymentData)
+private fun trackPurchase() {
+ CoroutineScope(Dispatchers.IO).launch {
+ val result = LinkRunner.getInstance().trackEvent(
+ eventName = "purchase", // Payment name, stored as the payment type in reports and mappings
+ eventData = mapOf(
+ "amount" to 1499.0, // Required for a payment: total the user paid, a finite number >= 0
+ "paymentId" to "order_123", // Required for a payment: your order or transaction ID, used to deduplicate
+ "currency" to "INR", // Required for a payment: 3-letter ISO 4217 code
+ "userId" to "user_123", // Required for a payment: your user ID. "" uses the user saved by signup or setUserData
+ "status" to "PAYMENT_COMPLETED", // Optional: defaults to PAYMENT_COMPLETED (see statuses below)
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ "content_ids" to listOf("sku_1", "sku_2"),
+ "contents" to listOf(
+ mapOf("id" to "sku_1", "quantity" to 1, "item_price" to 999),
+ mapOf("id" to "sku_2", "quantity" to 1, "item_price" to 500)
+ ),
+ "content_type" to "product",
+ "num_items" to 2,
+ "order_id" to "order_123"
+ ),
+ eventId = "evt_order_123" // Optional: your own ID for this event
+ )
- result.onSuccess {
- println("Payment captured successfully")
- }.onFailure { error ->
- println("Error capturing payment: ${error.message}")
- }
- } catch (e: Exception) {
- println("Exception capturing payment: ${e.message}")
+ result.onSuccess {
+ println("Payment event queued")
+ }.onFailure { error ->
+ // An invalid payment returns IllegalArgumentException here, and nothing is sent
+ println("Error tracking payment: ${error.message}")
}
}
}
```
-#### Available Payment Types
+#### Payment keys in `eventData`
-| Type | Description |
-| ---------------------- | ------------------------ |
-| `FIRST_PAYMENT` | User's first payment |
-| `SECOND_PAYMENT` | User's second payment |
-| `WALLET_TOPUP` | Adding funds to wallet |
-| `FUNDS_WITHDRAWAL` | Withdrawing funds |
-| `SUBSCRIPTION_CREATED` | New subscription created |
-| `SUBSCRIPTION_RENEWED` | Subscription renewal |
-| `ONE_TIME` | One-time payment |
-| `RECURRING` | Recurring payment |
-| `DEFAULT` | Default payment type |
+| Key | Type | Required | Description |
+| --- | ---- | -------- | ----------- |
+| `amount` | `Double` or `Int` | Yes | Total the user paid. Must be a finite number, 0 or greater. This is the revenue value. |
+| `paymentId` | `String` | Yes | Your order or transaction ID. Must not be blank. Used to recognize the same payment when it arrives again. |
+| `currency` | `String` | Yes | 3-letter ISO 4217 currency code, such as `INR` or `USD`. Case-insensitive, sent in uppercase. |
+| `userId` | `String` | Yes | Your ID for the user who paid. If you pass an empty string, the SDK uses the user saved by `signup` or `setUserData`. |
+| `status` | `String` | No | `PAYMENT_INITIATED`, `PAYMENT_COMPLETED` (default), `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. |
+| Any other key | Any | No | Stays in the event data, such as product details for [ecommerce events](#ecommerce-events). |
-#### Available Payment Statuses
+How `trackEvent` decides:
-| Status | Description |
-| ------------------- | ------------------------------ |
-| `PAYMENT_INITIATED` | Payment process started |
-| `PAYMENT_COMPLETED` | Payment successfully completed |
-| `PAYMENT_FAILED` | Payment failed |
-| `PAYMENT_CANCELLED` | Payment was cancelled |
+- **`amount`, `paymentId`, and `currency` are all in `eventData`:** the event is a payment. The SDK checks the payment keys and sends them as payment fields.
+- **None of them, or `amount` or `currency` without `paymentId`:** the event is a normal custom event, exactly as before. An `amount` or `currency` key on its own is ordinary event data.
+- **`paymentId` with `amount` or `currency` missing:** `trackEvent` returns a failed `Result` with an `IllegalArgumentException` with `amount, paymentId and currency must be sent together`, and nothing is sent.
+- **A negative or non-numeric `amount`, a blank `paymentId`, a currency that is not 3 letters, or an unknown `status`:** `trackEvent` returns a failed `Result` with an `IllegalArgumentException`, and nothing is sent.
-### Removing Payments
+#### Payment status
-To remove or refund a payment:
+`status` is a string. `io.linkrunner.sdk.models.PaymentStatus` lists the same four values if you prefer an enum.
-```kotlin
-private fun removePayment() {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- // Either paymentId or userId must be provided
- val removeRequest = RemovePaymentRequest(
- paymentId = "payment_123", // Optional: Payment ID to remove
- userId = "user123" // Optional: User ID to remove payments for
- )
+| Status | Use it when | Effect |
+| ------ | ----------- | ------ |
+| `PAYMENT_INITIATED` | The user started paying, such as opening the checkout or payment sheet. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded. | Added to revenue and paying users. Sent to ad networks when the event name is mapped. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_CANCELLED` | The user backed out before paying, or you refunded it. Send it with the same `paymentId` as the completed payment. | Stored for reports and analytics. The payment no longer counts as revenue and nothing is sent to ad networks. |
- val result = LinkRunner.getInstance().removePayment(removeRequest)
+Linkrunner keeps one payment per `paymentId`, event name, and status, so a retry with the same values is recorded once. See [Payment status](/sdk/track-payments#payment-status) for a full lifecycle example.
- result.onSuccess {
- println("Payment removed successfully")
- }.onFailure { error ->
- println("Error removing payment: ${error.message}")
- }
- } catch (e: Exception) {
- println("Exception removing payment: ${e.message}")
- }
- }
-}
-```
+
+ The event name is stored exactly as you send it. If you used one of the old payment types, pass it in uppercase exactly as before, such as `FIRST_PAYMENT` or `DEFAULT`, so your existing mappings and reports keep working. A different spelling such as `first_payment` is a new payment type. Any other name, such as `purchase`, is also stored as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks.
+
+
+
+ `capturePayment` is deprecated. Apps on Android SDK 4.x keep using it and continue to work unchanged. To move to 5.0.0, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
## Tracking Custom Events
@@ -593,7 +580,7 @@ private fun removePayment() {
- Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. For capturing revenue, it is recommended to use the [`.capturePayment`](#revenue-tracking) method instead of `.trackEvent`.
+ Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. To record revenue, add the [payment keys](#tracking-payments) to `eventData`.
Track custom events in your app:
@@ -606,8 +593,7 @@ private fun trackEvent() {
eventName = "purchase_initiated", // Event name
eventData = mapOf( // Optional: Event data
"product_id" to "12345",
- "category" to "electronics",
- "amount" to 99.99 // Include amount as a number for revenue sharing with ad networks like Google and Meta
+ "category" to "electronics"
),
eventId = "order_12345" // Optional: Your own unique event identifier
)
@@ -627,42 +613,37 @@ private fun trackEvent() {
#### Parameters for `LinkRunner.getInstance().trackEvent`
- `eventName`: `String` (required) - Name of the event to track
-- `eventData`: `Map` (optional) - Key-value pairs for additional event data, including Meta ecommerce properties
+- `eventData`: `Map` (optional) - Key-value pairs for additional event data, including Meta ecommerce properties. Add `amount`, `paymentId`, `currency`, and `userId` to make it a payment. See [Tracking Payments](#tracking-payments).
- `eventId`: `String` (optional) - Your own unique identifier for the event, useful for deduplication and correlating with your backend
### Revenue Sharing with Ad Networks
-To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:
+Send revenue to ad networks such as Google Ads and Meta as a [payment](#tracking-payments). Meta and Google receive the value and currency from the payment `amount` and `currency`, so send the order total as `amount`. Linkrunner does not add up item prices.
+
+Put the payment keys and the product details in `eventData`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`. Google receives `content_ids` as item IDs.
```kotlin
-private fun trackPurchaseEvent() {
+private fun trackPurchase() {
CoroutineScope(Dispatchers.IO).launch {
- try {
- val result = LinkRunner.getInstance().trackEvent(
- eventName = "purchase_completed",
- eventData = mapOf(
- "product_id" to "12345",
- "category" to "electronics",
- "amount" to 149.99 // Revenue amount as a number
- )
+ LinkRunner.getInstance().trackEvent(
+ eventName = "purchase",
+ eventData = mapOf(
+ "amount" to 149.99, // Order total, sent to ad networks as the value
+ "paymentId" to "order_12345",
+ "currency" to "USD",
+ "userId" to "user_123",
+ "content_ids" to listOf("product_123"),
+ "num_items" to 1,
+ "order_id" to "order_12345"
)
-
- result.onSuccess {
- println("Purchase event with revenue tracked successfully")
- }.onFailure { error ->
- println("Error tracking purchase event: ${error.message}")
- }
- } catch (e: Exception) {
- println("Exception tracking purchase event: ${e.message}")
+ ).onFailure { error ->
+ println("Invalid payment: ${error.message}")
}
}
}
```
-
- For revenue sharing with ad networks to work properly, ensure the `amount` parameter is passed as a number (Double
- or Int), not as a string.
-
+An `amount` in `eventData` is revenue only when `paymentId` and `currency` are there too. On its own it is ordinary event data. Revenue is shared only when **Send Revenue** is turned on for the event's mapping in the dashboard. See [Meta Ads](/ad-networks/meta-ads) and [Google Ads](/ad-networks/google-ads).
## Ecommerce Events
@@ -730,37 +711,27 @@ private fun trackViewContent() {
### Payment / Purchase Example
-Use the `capturePayment` method to send a `Purchase` event containing the ecommerce payload:
+Use `trackEvent` with the [payment keys](#tracking-payments) and the ecommerce fields in `eventData` to send a `Purchase` event:
```kotlin
-private fun capturePurchase() {
+private fun trackPurchase() {
CoroutineScope(Dispatchers.IO).launch {
- try {
- val paymentData = CapturePaymentRequest(
- paymentId = "payment_456",
- userId = "user123",
- amount = 49.99,
- type = PaymentType.FIRST_PAYMENT, // Map this payment type to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard. See: /ecommerce-manager/meta-commerce-manager, /ecommerce-manager/google-commerce-manager
- status = PaymentStatus.PAYMENT_COMPLETED,
- eventData = mapOf(
- "content_ids" to listOf("product_123"),
- "contents" to listOf(
- mapOf(
- "id" to "product_123", // Matches content_ids
- "quantity" to 1,
- "item_price" to 49.99
- )
- ),
- "content_type" to "product",
- "currency" to "USD",
- "value" to 49.99,
- "num_items" to 1,
- "order_id" to "order_abc123" // Required for Purchase events
- )
+ LinkRunner.getInstance().trackEvent(
+ eventName = "purchase", // Map this event to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard
+ eventData = mapOf(
+ "amount" to 49.99, // Order total, sent to ad networks as the value
+ "paymentId" to "payment_456",
+ "currency" to "USD",
+ "userId" to "user_123",
+ "status" to "PAYMENT_COMPLETED", // Optional, the default
+ "content_ids" to listOf("product_123"),
+ "contents" to listOf(mapOf("id" to "product_123", "quantity" to 1, "item_price" to 49.99)),
+ "content_type" to "product",
+ "num_items" to 1,
+ "order_id" to "order_abc123" // Required for Purchase events
)
- LinkRunner.getInstance().capturePayment(paymentData)
- } catch (e: Exception) {
- println("Exception capturing purchase: ${e.message}")
+ ).onFailure { error ->
+ println("Invalid payment: ${error.message}")
}
}
}
@@ -1008,8 +979,7 @@ Connect Firebase Cloud Messaging (FCM) with Linkrunner
| [`LinkRunner.getInstance().setAdditionalData`](/sdk/android#setting-clevertap-id) | Optional | Integration code | When third-party integration IDs are available |
| [`LinkRunner.getInstance().setUserData`](/sdk/android#setting-user-data) | Optional | App start, authentication or profile update logic | Right after `init` with your analytics IDs, and when user details become available |
| [`LinkRunner.getInstance().trackEvent`](/sdk/android#tracking-custom-events) | Optional | Throughout app | When specific user actions occur, including [ecommerce events](/sdk/android#ecommerce-events) |
-| [`LinkRunner.getInstance().capturePayment`](/sdk/android#capturing-payments) | Optional | Payment processing | When user makes a payment, including [ecommerce purchases](/sdk/android#ecommerce-events) |
-| [`LinkRunner.getInstance().removePayment`](/sdk/android#removing-payments) | Optional | Refund flow | When payment needs to be removed |
+| [`LinkRunner.getInstance().trackEvent` with payment keys](/sdk/android#tracking-payments) | Optional | Payment processing | When user makes a payment, including [ecommerce purchases](/sdk/android#ecommerce-events) |
| [`LinkRunner.getInstance().setPushToken`](/sdk/android#uninstall-tracking) | Optional | Push notification setup | When FCM token is available |
| [`LinkRunner.getInstance().setConsent`](/sdk/android#google-ads-consent) | Optional | App initialization or consent flow | Before `init`, and again when consent changes |
| [`LinkRunner.getInstance().enablePIIHashing`](/sdk/android#enhanced-privacy-controls) | Optional | Privacy settings | When you want to enable PII hashing |
diff --git a/sdk/capacitor.mdx b/sdk/capacitor.mdx
index 98a1fc1b..00e4dced 100644
--- a/sdk/capacitor.mdx
+++ b/sdk/capacitor.mdx
@@ -377,69 +377,85 @@ This method allows you to connect user identities across different analytics and
## Revenue Tracking
- Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so payments are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
+ Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Pass your user ID as `userId` so each payment is linked to the user who paid. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
-### Capture Payment
+### Tracking Payments
-Use this method to capture payment information:
+A payment is an in-app event with revenue. Send it with `trackEvent`, and put `amount`, `paymentId`, and `currency` in `eventData`. The event name becomes the payment type in reports and mappings, so you can use any name, such as `purchase` or `SUBSCRIPTION_RENEWED`. Requires `capacitor-linkrunner` 3.0.0 or later. For examples for every case and how payments reach ad networks, see the full [Track payments](/sdk/track-payments) guide.
-```typescript
-import linkrunner from "capacitor-linkrunner";
+This example sends every payment key. `amount`, `paymentId`, `currency`, and `userId` are required for a payment.
-const capturePayment = async () => {
- await linkrunner.capturePayment({
- amount: 100, // Payment amount
- userId: "user123", // User identifier
- paymentId: "payment456", // Required: Unique payment identifier
- type: "FIRST_PAYMENT", // Optional: Payment type
- // type: "SECOND_PAYMENT", // Optional: Payment type
- status: "PAYMENT_COMPLETED", // Optional: Payment status
- });
+```javascript
+import linkrunner, { PaymentStatus } from "capacitor-linkrunner";
+
+const trackPurchase = async () => {
+ try {
+ await linkrunner.trackEvent(
+ "purchase", // Payment name, stored as the payment type in reports and mappings
+ {
+ amount: 1499, // Required for a payment: total the user paid, a finite number >= 0
+ paymentId: "order_123", // Required for a payment: your order or transaction ID, used to deduplicate
+ currency: "INR", // Required for a payment: 3-letter ISO 4217 code
+ userId: "user_123", // Required for a payment: your user ID. "" uses the user saved by signup or setUserData
+ status: PaymentStatus.PAYMENT_COMPLETED, // Optional: defaults to PAYMENT_COMPLETED (see statuses below)
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ content_ids: ["sku_1", "sku_2"],
+ contents: [
+ { id: "sku_1", quantity: 1, item_price: 999 },
+ { id: "sku_2", quantity: 1, item_price: 500 },
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: "order_123",
+ },
+ "evt_order_123" // Optional: your own ID for this event
+ );
+ } catch (error) {
+ // On Android an invalid payment rejects here, and nothing is sent
+ console.error("Error tracking payment:", error);
+ }
};
```
-#### Parameters for `linkrunner.capturePayment`
-
-- `amount`: number (required) - The payment amount
-- `userId`: string (required) - Identifier for the user making the payment
-- `paymentId`: string (required) - Unique identifier for the payment, used to deduplicate transactions
-- `type`: string (optional) - Type of payment. Available options:
- - `FIRST_PAYMENT` - First payment made by the user
- - `WALLET_TOPUP` - Adding funds to a wallet
- - `FUNDS_WITHDRAWAL` - Withdrawing funds
- - `SUBSCRIPTION_CREATED` - New subscription created
- - `SUBSCRIPTION_RENEWED` - Subscription renewal
- - `ONE_TIME` - One-time payment
- - `RECURRING` - Recurring payment
- - `DEFAULT` - Default type (used if not specified)
-- `status`: string (optional) - Status of the payment. Available options:
- - `PAYMENT_INITIATED` - Payment has been initiated
- - `PAYMENT_COMPLETED` - Payment completed successfully (default if not specified)
- - `PAYMENT_FAILED` - Payment attempt failed
- - `PAYMENT_CANCELLED` - Payment was cancelled
-
-### Removing Payments
-
-Remove payment records (for refunds or cancellations):
+#### Payment keys in `eventData`
-```typescript
-import linkrunner from "capacitor-linkrunner";
+| Key | Type | Required | Description |
+| --- | ---- | -------- | ----------- |
+| `amount` | `number` | Yes | Total the user paid. Must be a finite number, 0 or greater. This is the revenue value. |
+| `paymentId` | `string` | Yes | Your order or transaction ID. Must not be blank. Used to recognize the same payment when it arrives again. |
+| `currency` | `string` | Yes | 3-letter ISO 4217 currency code, such as `INR` or `USD`. Case-insensitive, sent in uppercase. |
+| `userId` | `string` | Yes | Your ID for the user who paid. If you pass an empty string, the SDK uses the user saved by `signup` or `setUserData`. |
+| `status` | `string` | No | `PAYMENT_INITIATED`, `PAYMENT_COMPLETED` (default), `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. |
+| Any other key | Any | No | Stays in the event data, such as product details for [Meta Commerce Manager](/ecommerce-manager/meta-commerce-manager). |
-const removePayment = async () => {
- await linkrunner.removePayment({
- userId: "user123", // User identifier
- paymentId: "payment456", // Optional: Unique payment identifier
- });
-};
-```
+How `trackEvent` decides:
+
+- **`amount`, `paymentId`, and `currency` are all in `eventData`:** the event is a payment. The SDK checks the payment keys and sends them as payment fields.
+- **None of them, or `amount` or `currency` without `paymentId`:** the event is a normal custom event, exactly as before. An `amount` or `currency` key on its own is ordinary event data.
+- **`paymentId` with `amount` or `currency` missing:** The promise returned by `trackEvent` rejects with `amount, paymentId and currency must be sent together`, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
+- **A negative or non-numeric `amount`, a blank `paymentId`, a currency that is not 3 letters, or an unknown `status`:** The promise returned by `trackEvent` rejects, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
+
+#### Payment status
-#### Parameters for `linkrunner.removePayment`
+`capacitor-linkrunner` exports `PaymentStatus` as an object of string values. Plain strings such as `"PAYMENT_COMPLETED"` also work.
-- `userId`: string (optional) - Identifier for the user whose payment is being removed. If only `userId` is provided, all payments for that user will be removed.
-- `paymentId`: string (optional) - Unique identifier for the payment to be removed
+| Status | Use it when | Effect |
+| ------ | ----------- | ------ |
+| `PAYMENT_INITIATED` | The user started paying, such as opening the checkout or payment sheet. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded. | Added to revenue and paying users. Sent to ad networks when the event name is mapped. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_CANCELLED` | The user backed out before paying, or you refunded it. Send it with the same `paymentId` as the completed payment. | Stored for reports and analytics. The payment no longer counts as revenue and nothing is sent to ad networks. |
-Note: At least one of `paymentId` or `userId` must be provided when calling `removePayment`.
+Linkrunner keeps one payment per `paymentId`, event name, and status, so a retry with the same values is recorded once. See [Payment status](/sdk/track-payments#payment-status) for a full lifecycle example.
+
+
+ The event name is stored exactly as you send it. If you used one of the old payment types, pass it in uppercase exactly as before, such as `FIRST_PAYMENT` or `DEFAULT`, so your existing mappings and reports keep working. A different spelling such as `first_payment` is a new payment type. Any other name, such as `purchase`, is also stored as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks.
+
+
+
+ `capturePayment` is deprecated. Apps on `capacitor-linkrunner` 2.x keep using it and continue to work unchanged. To move to 3.0.0, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
## Tracking Custom Events
@@ -448,7 +464,7 @@ Note: At least one of `paymentId` or `userId` must be provided when calling `rem
- Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. For capturing revenue, it is recommended to use the [`.capturePayment`](#revenue-tracking) method instead of `.trackEvent`.
+ Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-identification-required) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. To record revenue, add the [payment keys](#tracking-payments) to `eventData`.
Track custom events in your app:
@@ -459,7 +475,7 @@ import linkrunner from "capacitor-linkrunner";
const trackEvent = async () => {
await linkrunner.trackEvent(
"purchase_initiated", // Event name
- { product_id: "12345", category: "electronics", amount: 99.99 }, // Optional: Event data, include amount as a number for revenue sharing with ad networks like Google and Meta
+ { product_id: "12345", category: "electronics" }, // Optional: Event data
"order_12345" // Optional: Your own unique event identifier (string or number)
);
};
@@ -468,27 +484,34 @@ const trackEvent = async () => {
#### Parameters for `linkrunner.trackEvent`
- `eventName`: string (required) - Name of the event to track
-- `eventData`: object (optional) - Key-value pairs for additional event data, including Meta ecommerce properties
+- `eventData`: object (optional) - Key-value pairs for additional event data, including Meta ecommerce properties. Add `amount`, `paymentId`, `currency`, and `userId` to make it a payment. See [Tracking Payments](#tracking-payments).
- `eventId`: string | number (optional) - Your own unique identifier for the event, useful for deduplication and correlating with your backend
### Revenue Sharing with Ad Networks
-To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:
+Send revenue to ad networks such as Google Ads and Meta as a [payment](#tracking-payments). Meta and Google receive the value and currency from the payment `amount` and `currency`, so send the order total as `amount`. Linkrunner does not add up item prices.
+
+Put the payment keys and the product details in `eventData`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`. Google receives `content_ids` as item IDs.
```typescript
-const trackPurchaseEvent = async () => {
- await linkrunner.trackEvent("purchase_completed", {
- product_id: "12345",
- category: "electronics",
- amount: 149.99, // Revenue amount as a number
- });
+const trackPurchase = async () => {
+ try {
+ await linkrunner.trackEvent("purchase", {
+ amount: 149.99, // Order total, sent to ad networks as the value
+ paymentId: "order_12345",
+ currency: "USD",
+ userId: "user_123",
+ content_ids: ["product_123"],
+ num_items: 1,
+ order_id: "order_12345",
+ });
+ } catch (error) {
+ console.error("Error tracking payment:", error);
+ }
};
```
-
- For revenue sharing with ad networks to work properly, ensure the `amount` parameter is passed as a number, not as a
- string.
-
+An `amount` in `eventData` is revenue only when `paymentId` and `currency` are there too. On its own it is ordinary event data. Revenue is shared only when **Send Revenue** is turned on for the event's mapping in the dashboard. See [Meta Ads](/ad-networks/meta-ads) and [Google Ads](/ad-networks/google-ads).
## Enhanced Privacy Controls
@@ -672,8 +695,7 @@ Connect APNs with Linkrunner
| [`linkrunner.setAdditionalData`](/sdk/capacitor#setting-clevertap-id) | Optional | Integration code | When third-party integration IDs are available |
| [`linkrunner.setUserData`](/sdk/capacitor#setting-user-data) | Optional | App start, authentication or profile update logic | Right after `init` with your analytics IDs, and when user details become available |
| [`linkrunner.trackEvent`](/sdk/capacitor#tracking-custom-events) | Optional | Throughout app | When specific user actions occur |
-| [`linkrunner.capturePayment`](/sdk/capacitor#capture-payment) | Optional | Payment processing | When user makes a payment |
-| [`linkrunner.removePayment`](/sdk/capacitor#removing-payments) | Optional | Refund flow | When payment needs to be removed |
+| [`linkrunner.trackEvent` with payment keys](/sdk/capacitor#tracking-payments) | Optional | Payment processing | When user makes a payment |
| [`linkrunner.setPushToken`](/sdk/capacitor#uninstall-tracking) | Optional | Push notification setup | When push token is available |
| [`linkrunner.enablePIIHashing`](/sdk/capacitor#enhanced-privacy-controls) | Optional | Privacy configuration | When enabling PII hashing |
diff --git a/sdk/cordova.mdx b/sdk/cordova.mdx
index 3df18ffa..c4060fa0 100644
--- a/sdk/cordova.mdx
+++ b/sdk/cordova.mdx
@@ -372,81 +372,80 @@ This method allows you to connect user identities across different analytics and
## Revenue Tracking
- Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so payments are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
+ Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Pass your user ID as `userId` so each payment is linked to the user who paid. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
-### Capture Payment
+### Tracking Payments
-Use this method to capture payment information:
+A payment is an in-app event with revenue. Send it with `trackEvent`, and put `amount`, `paymentId`, and `currency` in `eventData`. The event name becomes the payment type in reports and mappings, so you can use any name, such as `purchase` or `SUBSCRIPTION_RENEWED`. Requires `cordova-linkrunner` 2.0.0 or later. For examples for every case and how payments reach ad networks, see the full [Track payments](/sdk/track-payments) guide.
+
+This example sends every payment key. `amount`, `paymentId`, `currency`, and `userId` are required for a payment.
```javascript
-linkrunner.capturePayment({
- amount: 100, // Payment amount
- userId: "user123", // User identifier
- paymentId: "payment456", // Required: Unique payment identifier
- type: "FIRST_PAYMENT", // Optional: Payment type
- status: "PAYMENT_COMPLETED", // Optional: Payment status
- eventData: { // Optional: Ecommerce/custom event data
- content_ids: ["product_123"],
- content_type: "product",
- currency: "USD",
- value: 99.99,
- num_items: 1,
- order_id: "order_12345",
+// linkrunner is a global after the deviceready event, so no import is needed
+linkrunner.trackEvent(
+ "purchase", // Payment name, stored as the payment type in reports and mappings
+ {
+ amount: 1499, // Required for a payment: total the user paid, a finite number >= 0
+ paymentId: "order_123", // Required for a payment: your order or transaction ID, used to deduplicate
+ currency: "INR", // Required for a payment: 3-letter ISO 4217 code
+ userId: "user_123", // Required for a payment: your user ID. "" uses the user saved by signup or setUserData
+ status: linkrunner.PaymentStatus.PAYMENT_COMPLETED, // Optional: defaults to PAYMENT_COMPLETED (see statuses below)
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ content_ids: ["sku_1", "sku_2"],
contents: [
- {
- id: "product_123",
- quantity: 1,
- item_price: 99.99
- }
- ]
- }
-}).then(function () {
- console.log("Payment captured");
+ { id: "sku_1", quantity: 1, item_price: 999 },
+ { id: "sku_2", quantity: 1, item_price: 500 },
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: "order_123",
+ },
+ "evt_order_123" // Optional: your own ID for this event
+).catch(function (error) {
+ // On Android an invalid payment rejects here, and nothing is sent
+ console.error("Error tracking payment:", error);
});
```
-#### Parameters for `linkrunner.capturePayment`
-
-- `amount`: number (required) - The payment amount
-- `userId`: string (required) - Identifier for the user making the payment
-- `paymentId`: string (required) - Unique identifier for the payment, used to deduplicate transactions
-- `type`: string (optional) - Type of payment. Available options:
- - `FIRST_PAYMENT` - First payment made by the user
- - `SECOND_PAYMENT` - Second payment made by the user
- - `WALLET_TOPUP` - Adding funds to a wallet
- - `FUNDS_WITHDRAWAL` - Withdrawing funds
- - `SUBSCRIPTION_CREATED` - New subscription created
- - `SUBSCRIPTION_RENEWED` - Subscription renewal
- - `ONE_TIME` - One-time payment
- - `RECURRING` - Recurring payment
- - `DEFAULT` - Default type (used if not specified)
-- `status`: string (optional) - Status of the payment. Available options:
- - `PAYMENT_INITIATED` - Payment has been initiated
- - `PAYMENT_COMPLETED` - Payment completed successfully (default if not specified)
- - `PAYMENT_FAILED` - Payment attempt failed
- - `PAYMENT_CANCELLED` - Payment was cancelled
-- `eventData`: object (optional) - Key-value pairs for additional event data, including Meta ecommerce properties.
-
-### Removing Payments
-
-Remove payment records (for refunds or cancellations):
+#### Payment keys in `eventData`
-```javascript
-linkrunner.removePayment({
- userId: "user123", // User identifier
- paymentId: "payment456" // Optional: Unique payment identifier
-}).then(function () {
- console.log("Payment removed");
-});
-```
+| Key | Type | Required | Description |
+| --- | ---- | -------- | ----------- |
+| `amount` | `number` | Yes | Total the user paid. Must be a finite number, 0 or greater. This is the revenue value. |
+| `paymentId` | `string` | Yes | Your order or transaction ID. Must not be blank. Used to recognize the same payment when it arrives again. |
+| `currency` | `string` | Yes | 3-letter ISO 4217 currency code, such as `INR` or `USD`. Case-insensitive, sent in uppercase. |
+| `userId` | `string` | Yes | Your ID for the user who paid. If you pass an empty string, the SDK uses the user saved by `signup` or `setUserData`. |
+| `status` | `string` | No | `PAYMENT_INITIATED`, `PAYMENT_COMPLETED` (default), `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. |
+| Any other key | Any | No | Stays in the event data, such as product details for [ecommerce events](#ecommerce-events). |
+
+How `trackEvent` decides:
+
+- **`amount`, `paymentId`, and `currency` are all in `eventData`:** the event is a payment. The SDK checks the payment keys and sends them as payment fields.
+- **None of them, or `amount` or `currency` without `paymentId`:** the event is a normal custom event, exactly as before. An `amount` or `currency` key on its own is ordinary event data.
+- **`paymentId` with `amount` or `currency` missing:** The promise returned by `trackEvent` rejects with `amount, paymentId and currency must be sent together`, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
+- **A negative or non-numeric `amount`, a blank `paymentId`, a currency that is not 3 letters, or an unknown `status`:** The promise returned by `trackEvent` rejects, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
+
+#### Payment status
+
+The plugin exposes `PaymentStatus` on the `linkrunner` global as an object of string values, such as `linkrunner.PaymentStatus.PAYMENT_CANCELLED`. Plain strings also work.
-#### Parameters for `linkrunner.removePayment`
+| Status | Use it when | Effect |
+| ------ | ----------- | ------ |
+| `PAYMENT_INITIATED` | The user started paying, such as opening the checkout or payment sheet. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded. | Added to revenue and paying users. Sent to ad networks when the event name is mapped. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_CANCELLED` | The user backed out before paying, or you refunded it. Send it with the same `paymentId` as the completed payment. | Stored for reports and analytics. The payment no longer counts as revenue and nothing is sent to ad networks. |
-- `userId`: string (optional) - Identifier for the user whose payment is being removed. If only `userId` is provided, all payments for that user will be removed.
-- `paymentId`: string (optional) - Unique identifier for the payment to be removed
+Linkrunner keeps one payment per `paymentId`, event name, and status, so a retry with the same values is recorded once. See [Payment status](/sdk/track-payments#payment-status) for a full lifecycle example.
-Note: At least one of `paymentId` or `userId` must be provided when calling `removePayment`.
+
+ The event name is stored exactly as you send it. If you used one of the old payment types, pass it in uppercase exactly as before, such as `FIRST_PAYMENT` or `DEFAULT`, so your existing mappings and reports keep working. A different spelling such as `first_payment` is a new payment type. Any other name, such as `purchase`, is also stored as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks.
+
+
+
+ `capturePayment` is deprecated. Apps on `cordova-linkrunner` 1.x keep using it and continue to work unchanged. To move to 2.0.0, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
## Ecommerce Events
@@ -512,34 +511,22 @@ linkrunner.trackEvent(
### Payment / Purchase Example
-Use the `capturePayment` method to send a `Purchase` event containing the ecommerce payload:
+Use `trackEvent` with the [payment keys](#tracking-payments) and the ecommerce fields in `eventData` to send a `Purchase` event:
```javascript
-linkrunner.capturePayment({
- amount: 49.99,
- userId: "user123",
+linkrunner.trackEvent("purchase", { // Map this event to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard
+ amount: 49.99, // Order total, sent to ad networks as the value
paymentId: "payment_456",
- type: "FIRST_PAYMENT", // Map this payment type to "Purchase" in the Linkrunner Dashboard
- status: "PAYMENT_COMPLETED",
- eventData: {
- content_ids: ["product_123"],
- contents: [
- {
- id: "product_123",
- quantity: 1,
- item_price: 49.99
- }
- ],
- content_type: "product",
- currency: "USD",
- value: 49.99,
- num_items: 1,
- order_id: "order_abc123" // Required for Purchase events
- }
-}).then(function () {
- console.log("Purchase captured successfully");
+ currency: "USD",
+ userId: "user_123",
+ status: "PAYMENT_COMPLETED", // Optional, the default
+ content_ids: ["product_123"],
+ contents: [{ id: "product_123", quantity: 1, item_price: 49.99 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: "order_abc123", // Required for Purchase events
}).catch(function (error) {
- console.error("Error capturing purchase:", error);
+ console.error("Error tracking payment:", error);
});
```
@@ -552,7 +539,7 @@ linkrunner.capturePayment({
- Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. For capturing revenue, it is recommended to use the [`.capturePayment`](#revenue-tracking) method instead of `.trackEvent`.
+ Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-identification-required) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. To record revenue, add the [payment keys](#tracking-payments) to `eventData`.
Track custom events in your app:
@@ -560,7 +547,7 @@ Track custom events in your app:
```javascript
linkrunner.trackEvent(
"purchase_initiated", // Event name
- { product_id: "12345", category: "electronics", amount: 99.99 }, // Optional: Event data, include amount as a number for revenue sharing with ad networks like Google and Meta
+ { product_id: "12345", category: "electronics" }, // Optional: Event data
"order_12345" // Optional: Your own unique event identifier (string or number)
).then(function () {
console.log("Event tracked");
@@ -570,27 +557,30 @@ linkrunner.trackEvent(
#### Parameters for `linkrunner.trackEvent`
- `eventName`: string (required) - Name of the event to track
-- `eventData`: object (optional) - Key-value pairs for additional event data, including Meta ecommerce properties
+- `eventData`: object (optional) - Key-value pairs for additional event data, including Meta ecommerce properties. Add `amount`, `paymentId`, `currency`, and `userId` to make it a payment. See [Tracking Payments](#tracking-payments).
- `eventId`: string | number (optional) - Your own unique identifier for the event, useful for deduplication and correlating with your backend
### Revenue Sharing with Ad Networks
-To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:
+Send revenue to ad networks such as Google Ads and Meta as a [payment](#tracking-payments). Meta and Google receive the value and currency from the payment `amount` and `currency`, so send the order total as `amount`. Linkrunner does not add up item prices.
+
+Put the payment keys and the product details in `eventData`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`. Google receives `content_ids` as item IDs.
```javascript
-linkrunner.trackEvent("purchase_completed", {
- product_id: "12345",
- category: "electronics",
- amount: 149.99 // Revenue amount as a number
-}).then(function () {
- console.log("Purchase event tracked");
+linkrunner.trackEvent("purchase", {
+ amount: 149.99, // Order total, sent to ad networks as the value
+ paymentId: "order_12345",
+ currency: "USD",
+ userId: "user_123",
+ content_ids: ["product_123"],
+ num_items: 1,
+ order_id: "order_12345",
+}).catch(function (error) {
+ console.error("Error tracking payment:", error);
});
```
-
- For revenue sharing with ad networks to work properly, ensure the `amount` parameter is passed as a number, not as a
- string.
-
+An `amount` in `eventData` is revenue only when `paymentId` and `currency` are there too. On its own it is ordinary event data. Revenue is shared only when **Send Revenue** is turned on for the event's mapping in the dashboard. See [Meta Ads](/ad-networks/meta-ads) and [Google Ads](/ad-networks/google-ads).
## Enhanced Privacy Controls
@@ -777,8 +767,7 @@ Connect APNs with Linkrunner
| [`linkrunner.setAdditionalData`](/sdk/cordova#setting-clevertap-id) | Optional | Integration code | When third-party integration IDs are available |
| [`linkrunner.setUserData`](/sdk/cordova#setting-user-data) | Optional | App start, authentication or profile update logic | Right after `init` with your analytics IDs, and when user details become available |
| [`linkrunner.trackEvent`](/sdk/cordova#tracking-custom-events) | Optional | Throughout app, including ecommerce flows | When specific user actions occur, including AddToCart or ViewContent events |
-| [`linkrunner.capturePayment`](/sdk/cordova#capture-payment) | Optional | Payment processing, including ecommerce purchases | When user makes a payment, including Purchase events with ecommerce data |
-| [`linkrunner.removePayment`](/sdk/cordova#removing-payments) | Optional | Refund flow | When payment needs to be removed |
+| [`linkrunner.trackEvent` with payment keys](/sdk/cordova#tracking-payments) | Optional | Payment processing, including ecommerce purchases | When user makes a payment, including Purchase events with ecommerce data |
| [`linkrunner.setPushToken`](/sdk/cordova#uninstall-tracking) | Optional | Push notification setup | When push token is available |
| [`linkrunner.enablePIIHashing`](/sdk/cordova#enhanced-privacy-controls) | Optional | Privacy configuration | When enabling PII hashing |
diff --git a/sdk/expo.mdx b/sdk/expo.mdx
index ee78d620..0be8d941 100644
--- a/sdk/expo.mdx
+++ b/sdk/expo.mdx
@@ -26,6 +26,7 @@ This guide will help you install, configure, and use the Linkrunner SDK in your
- Expo SDK 52.0 or higher
- Node.js 18.0 or higher
- React Native 0.70 or higher
+- `rn-linkrunner` 4.0.0 or higher when you use `expo-linkrunner` 4.0.0 or higher
## Installation
@@ -181,6 +182,47 @@ Since `expo-linkrunner` is a config plugin that prepares your project, the actua
- Ecommerce Events
- Enhanced privacy controls
+
+ `capturePayment` and `removePayment` are deprecated and removed in `rn-linkrunner` 4.0.0. With `expo-linkrunner` 4.0.0, a payment is an in-app event: call `trackEvent` with the old payment type as the event name, and put `amount`, `paymentId`, `currency`, and `userId` in the event data. Keep the old type spelling exactly, such as `FIRST_PAYMENT`, so your existing mappings keep working. Any other name needs a new mapping in the dashboard. See [Migrate payments to in-app events](/sdk/migrate-to-track-event). For status, examples, and how payments reach ad networks, see [Track payments](/sdk/track-payments).
+
+
+### Tracking Payments
+
+Expo apps call `trackEvent` from `rn-linkrunner`. When the event data has `amount`, `paymentId`, and `currency`, the event is a payment. For the full key table and status values, see [Tracking Payments](/sdk/react-native#tracking-payments) in the React Native guide.
+
+```javascript
+import linkrunner, { PaymentStatus } from "rn-linkrunner";
+
+const trackPurchase = async () => {
+ try {
+ await linkrunner.trackEvent(
+ "purchase", // Payment name, stored as the payment type in reports and mappings
+ {
+ amount: 1499, // Required for a payment: total the user paid, a finite number >= 0
+ paymentId: "order_123", // Required for a payment: your order or transaction ID, used to deduplicate
+ currency: "INR", // Required for a payment: 3-letter ISO 4217 code
+ userId: "user_123", // Required for a payment: your user ID. "" uses the user saved by signup or setUserData
+ status: PaymentStatus.PAYMENT_COMPLETED, // Optional: PAYMENT_INITIATED, PAYMENT_COMPLETED (default), PAYMENT_FAILED, or PAYMENT_CANCELLED
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ content_ids: ["sku_1", "sku_2"],
+ contents: [
+ { id: "sku_1", quantity: 1, item_price: 999 },
+ { id: "sku_2", quantity: 1, item_price: 500 },
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: "order_123",
+ },
+ );
+ } catch (error) {
+ // On Android an invalid payment throws here, and nothing is sent
+ console.error("Error tracking payment:", error);
+ }
+};
+```
+
+Only `PAYMENT_COMPLETED` counts as revenue and is sent to ad networks. Plain strings such as `"PAYMENT_COMPLETED"` also work for `status`.
+
## Function Placement Guide
Expo uses the `rn-linkrunner` runtime. The function links below open the implementation details in the React Native guide.
@@ -195,8 +237,7 @@ Expo uses the `rn-linkrunner` runtime. The function links below open the impleme
| [`linkrunner.setAdditionalData`](/sdk/react-native#setting-clevertap-id) | Optional | Integration code | When third-party integration IDs are available |
| [`linkrunner.setUserData`](/sdk/react-native#setting-user-data) | Optional | App start, authentication or profile update logic | Right after `init` with your analytics IDs, and when user details become available |
| [`linkrunner.trackEvent`](/sdk/react-native#tracking-custom-events) | Optional | Throughout app, including ecommerce flows | When specific user actions occur, including AddToCart or ViewContent events |
-| [`linkrunner.capturePayment`](/sdk/react-native#capture-payment) | Optional | Payment processing, including ecommerce purchases | When user makes a payment, including Purchase events with ecommerce data |
-| [`linkrunner.removePayment`](/sdk/react-native#removing-payments) | Optional | Refund flow | When payment needs to be removed |
+| [`linkrunner.trackEvent` with payment keys](/sdk/react-native#tracking-payments) | Optional | Payment processing, including ecommerce purchases | When user makes a payment, including Purchase events with ecommerce data |
| [`linkrunner.setPushToken`](/sdk/react-native#uninstall-tracking) | Optional | Push notification setup | When FCM/APNs token is available |
| [`linkrunner.setConsent`](/sdk/react-native#google-integrated-conversion-measurement-optional) | Optional | App initialization or consent flow | Before `init`, and again when consent changes |
| [`linkrunner.enablePIIHashing`](/sdk/react-native#enhanced-privacy-controls) | Optional | Privacy configuration | When enabling PII hashing |
diff --git a/sdk/flutter.mdx b/sdk/flutter.mdx
index 1d0ac791..4d149307 100644
--- a/sdk/flutter.mdx
+++ b/sdk/flutter.mdx
@@ -357,7 +357,7 @@ If the platform's identifier function is not called, you must provide a user ide
- posthogDistinctId for PostHog
- The model classes (`LRUserData`, `LRCapturePayment`, `LRRemovePayment`, `LRConsent`) are not re-exported by `linkrunner.dart`. Import each one directly, for example `import 'package:linkrunner/models/lr_user_data.dart';`.
+ The model classes (`LRUserData`, `LRConsent`, `PaymentStatus`) are not re-exported by `linkrunner.dart`. Import `package:linkrunner/main.dart`, which exports `linkrunner.dart` and all models, or import each model directly, for example `import 'package:linkrunner/models/lr_user_data.dart';`.
```dart
@@ -594,94 +594,88 @@ This method allows you to connect user identities across different analytics and
## Revenue Tracking
- Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so payments are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
+ Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Pass your user ID as `userId` so each payment is linked to the user who paid. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
-### Capturing Payments
+### Tracking Payments
-Track payment information:
+A payment is an in-app event with revenue. Send it with `trackEvent`, and put `amount`, `paymentId`, and `currency` in `eventData`. The event name becomes the payment type in reports and mappings, so you can use any name, such as `purchase` or `SUBSCRIPTION_RENEWED`. Requires `linkrunner` 5.0.0 or later. For examples for every case and how payments reach ad networks, see the full [Track payments](/sdk/track-payments) guide.
+
+This example sends every payment key. `amount`, `paymentId`, `currency`, and `userId` are required for a payment.
```dart
-Future capturePayment() async {
+import 'package:flutter/services.dart';
+import 'package:linkrunner/main.dart';
+
+Future trackPurchase() async {
try {
- await LinkRunner().capturePayment(
- capturePayment: LRCapturePayment(
- amount: 99.99, // Required: Payment amount
- userId: 'user123', // Required: User identifier
- paymentId: 'payment456', // Required: Unique payment identifier
- type: PaymentType.FIRST_PAYMENT, // Optional: Payment type
- status: PaymentStatus.PAYMENT_COMPLETED, // Optional: Payment status
- eventData: { // Optional: Ecommerce/custom event data
- 'content_ids': ['product_123'],
- 'content_type': 'product',
- 'currency': 'USD',
- 'value': 99.99,
- 'num_items': 1,
- 'order_id': 'order_12345',
- 'contents': [
- {
- 'id': 'product_123',
- 'quantity': 1,
- 'item_price': 99.99
- }
- ]
- },
- ),
+ await LinkRunner().trackEvent(
+ eventName: 'purchase', // Payment name, stored as the payment type in reports and mappings
+ eventData: {
+ 'amount': 1499.0, // Required for a payment: total the user paid, a finite number >= 0
+ 'paymentId': 'order_123', // Required for a payment: your order or transaction ID, used to deduplicate
+ 'currency': 'INR', // Required for a payment: 3-letter ISO 4217 code
+ 'userId': 'user_123', // Required for a payment: your user ID. '' uses the user saved by signup or setUserData
+ 'status': 'PAYMENT_COMPLETED', // Optional: defaults to PAYMENT_COMPLETED (see statuses below)
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ 'content_ids': ['sku_1', 'sku_2'],
+ 'contents': [
+ {'id': 'sku_1', 'quantity': 1, 'item_price': 999},
+ {'id': 'sku_2', 'quantity': 1, 'item_price': 500},
+ ],
+ 'content_type': 'product',
+ 'num_items': 2,
+ 'order_id': 'order_123',
+ },
+ eventId: 'evt_order_123', // Optional: your own ID for this event
);
- print('Payment captured successfully');
+ } on PlatformException catch (e) {
+ // On Android an invalid payment throws here (code TRACK_EVENT_FAILED), and nothing is sent
+ print('Invalid payment: ${e.message}');
} catch (e) {
- print('Error capturing payment: $e');
+ print('Error tracking payment: $e');
}
}
```
-#### Parameters for `LRCapturePayment`
-
-- `amount`: double (required) - The payment amount
-- `userId`: String (required) - Identifier for the user making the payment
-- `paymentId`: String (required) - Unique identifier for the payment, used to deduplicate transactions
-- `type`: PaymentType (optional) - Type of payment. Available options:
- - `PaymentType.FIRST_PAYMENT` - First payment made by the user
- - `PaymentType.WALLET_TOPUP` - Adding funds to a wallet
- - `PaymentType.FUNDS_WITHDRAWAL` - Withdrawing funds
- - `PaymentType.SUBSCRIPTION_CREATED` - New subscription created
- - `PaymentType.SUBSCRIPTION_RENEWED` - Subscription renewal
- - `PaymentType.ONE_TIME` - One-time payment
- - `PaymentType.RECURRING` - Recurring payment
- - `PaymentType.DEFAULT_PAYMENT` - Default type (used if not specified)
-- `status`: PaymentStatus (optional) - Status of the payment. Available options:
- - `PaymentStatus.PAYMENT_INITIATED` - Payment has been initiated
- - `PaymentStatus.PAYMENT_COMPLETED` - Payment completed successfully (default if not specified)
- - `PaymentStatus.PAYMENT_FAILED` - Payment attempt failed
- - `PaymentStatus.PAYMENT_CANCELLED` - Payment was cancelled
-- `eventData`: `Map` (optional) - Key-value pairs for additional event data, including ecommerce properties for Meta and Google.
-
-### Removing Payments
-
-Remove payment records (for refunds or cancellations):
+#### Payment keys in `eventData`
-```dart
-Future removePayment() async {
- try {
- await LinkRunner().removePayment(
- removePayment: LRRemovePayment(
- userId: 'user123', // Either userId or paymentId must be provided
- paymentId: 'payment456', // Optional: Unique payment identifier
- ),
- );
- print('Payment removed successfully');
- } catch (e) {
- print('Error removing payment: $e');
- }
-}
-```
+| Key | Type | Required | Description |
+| --- | ---- | -------- | ----------- |
+| `amount` | `double` or `int` | Yes | Total the user paid. Must be a finite number, 0 or greater. This is the revenue value. |
+| `paymentId` | `String` | Yes | Your order or transaction ID. Must not be blank. Used to recognize the same payment when it arrives again. |
+| `currency` | `String` | Yes | 3-letter ISO 4217 currency code, such as `INR` or `USD`. Case-insensitive, sent in uppercase. |
+| `userId` | `String` | Yes | Your ID for the user who paid. If you pass an empty string, the SDK uses the user saved by `signup` or `setUserData`. |
+| `status` | `String` | No | `PAYMENT_INITIATED`, `PAYMENT_COMPLETED` (default), `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. |
+| Any other key | Any | No | Stays in the event data, such as product details for [ecommerce events](#ecommerce-events). |
+
+How `trackEvent` decides:
+
+- **`amount`, `paymentId`, and `currency` are all in `eventData`:** the event is a payment. The SDK checks the payment keys and sends them as payment fields.
+- **None of them, or `amount` or `currency` without `paymentId`:** the event is a normal custom event, exactly as before. An `amount` or `currency` key on its own is ordinary event data.
+- **`paymentId` with `amount` or `currency` missing:** on Android `trackEvent` throws `PlatformException` (code `TRACK_EVENT_FAILED`) with `amount, paymentId and currency must be sent together`; on iOS the payment is dropped and debug builds log the reason. Nothing is sent.
+- **A negative or non-numeric `amount`, a blank `paymentId`, a currency that is not 3 letters, or an unknown `status`:** on Android `trackEvent` throws `PlatformException` with the reason; on iOS the payment is dropped and debug builds log it. Nothing is sent.
-#### Parameters for `LRRemovePayment`
+#### Payment status
-- `userId`: String (required) - Identifier for the user whose payment is being removed
-- `paymentId`: String (optional) - Unique identifier for the payment to be removed
+`status` is a string. The `PaymentStatus` enum exported by `package:linkrunner/main.dart` has the same values, so `PaymentStatus.PAYMENT_CANCELLED.name` also works.
-Note: Either `paymentId` or `userId` must be provided when calling `removePayment`. If only `userId` is provided, all payments for that user will be removed.
+| Status | Use it when | Effect |
+| ------ | ----------- | ------ |
+| `PAYMENT_INITIATED` | The user started paying, such as opening the checkout or payment sheet. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded. | Added to revenue and paying users. Sent to ad networks when the event name is mapped. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_CANCELLED` | The user backed out before paying, or you refunded it. Send it with the same `paymentId` as the completed payment. | Stored for reports and analytics. The payment no longer counts as revenue and nothing is sent to ad networks. |
+
+Linkrunner keeps one payment per `paymentId`, event name, and status, so a retry with the same values is recorded once. See [Payment status](/sdk/track-payments#payment-status) for a full lifecycle example.
+
+
+ The event name is stored exactly as you send it. If you used one of the old payment types, pass it in uppercase exactly as before, such as `FIRST_PAYMENT` or `DEFAULT`, so your existing mappings and reports keep working. A different spelling such as `first_payment` is a new payment type. Any other name, such as `purchase`, is also stored as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks.
+
+
+
+ `capturePayment` is deprecated. Apps on `linkrunner` 4.x keep using it and continue to work unchanged. To move to 5.0.0, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
## Ecommerce Events
@@ -755,38 +749,28 @@ Future trackViewContent() async {
### Payment / Purchase Example
-Use the `capturePayment` method to send a `Purchase` event containing the ecommerce payload:
+Use `trackEvent` with the [payment keys](#tracking-payments) and the ecommerce fields in `eventData` to send a `Purchase` event:
```dart
-Future capturePurchase() async {
+Future trackPurchase() async {
try {
- await LinkRunner().capturePayment(
- capturePayment: LRCapturePayment(
- amount: 49.99,
- userId: 'user123',
- paymentId: 'payment_456',
- type: PaymentType.FIRST_PAYMENT, // Map this payment type to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard. See: /ecommerce-manager/meta-commerce-manager, /ecommerce-manager/google-commerce-manager
- status: PaymentStatus.PAYMENT_COMPLETED,
- eventData: {
- 'content_ids': ['product_123'],
- 'contents': [
- {
- 'id': 'product_123', // Matches content_ids
- 'quantity': 1,
- 'item_price': 49.99
- }
- ],
- 'content_type': 'product',
- 'currency': 'USD',
- 'value': 49.99,
- 'num_items': 1,
- 'order_id': 'order_abc123' // Required for Purchase events
- },
- ),
+ await LinkRunner().trackEvent(
+ eventName: 'purchase', // Map this event to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard
+ eventData: {
+ 'amount': 49.99, // Order total, sent to ad networks as the value
+ 'paymentId': 'payment_456',
+ 'currency': 'USD',
+ 'userId': 'user_123',
+ 'status': 'PAYMENT_COMPLETED', // Optional, the default
+ 'content_ids': ['product_123'],
+ 'contents': [{'id': 'product_123', 'quantity': 1, 'item_price': 49.99}],
+ 'content_type': 'product',
+ 'num_items': 1,
+ 'order_id': 'order_abc123', // Required for Purchase events
+ },
);
- print('Purchase captured successfully');
} catch (e) {
- print('Error capturing purchase: $e');
+ print('Error tracking payment: $e');
}
}
```
@@ -800,7 +784,7 @@ Future capturePurchase() async {
- Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. For capturing revenue, it is recommended to use the [`.capturePayment`](#revenue-tracking) method instead of `.trackEvent`.
+ Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-identification-required) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. To record revenue, add the [payment keys](#tracking-payments) to `eventData`.
Track custom events in your app:
@@ -813,7 +797,6 @@ Future trackEvent() async {
eventData: { // Optional: Event data
'product_id': '12345',
'category': 'electronics',
- 'amount': 99.99, // Include amount as a number for revenue sharing with ad networks like Google and Meta
},
eventId: 'order_12345', // Optional: Your own unique event identifier (String or num)
);
@@ -827,35 +810,37 @@ Future trackEvent() async {
#### Parameters for `LinkRunner().trackEvent`
- `eventName`: `String` (required) - Name of the event to track
-- `eventData`: `Map` (optional) - Key-value pairs for additional event data, including Meta ecommerce properties
+- `eventData`: `Map` (optional) - Key-value pairs for additional event data, including Meta ecommerce properties. Add `amount`, `paymentId`, `currency`, and `userId` to make it a payment. See [Tracking Payments](#tracking-payments).
- `eventId`: `Object` (optional) - Your own unique identifier for the event (`String` or `num`), useful for deduplication and correlating with your backend
### Revenue Sharing with Ad Networks
-To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:
+Send revenue to ad networks such as Google Ads and Meta as a [payment](#tracking-payments). Meta and Google receive the value and currency from the payment `amount` and `currency`, so send the order total as `amount`. Linkrunner does not add up item prices.
+
+Put the payment keys and the product details in `eventData`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`. Google receives `content_ids` as item IDs.
```dart
-Future trackPurchaseEvent() async {
+Future trackPurchase() async {
try {
await LinkRunner().trackEvent(
- eventName: 'purchase_completed',
+ eventName: 'purchase',
eventData: {
- 'product_id': '12345',
- 'category': 'electronics',
- 'amount': 149.99, // Revenue amount as a number
+ 'amount': 149.99, // Order total, sent to ad networks as the value
+ 'paymentId': 'order_12345',
+ 'currency': 'USD',
+ 'userId': 'user_123',
+ 'content_ids': ['product_123'],
+ 'num_items': 1,
+ 'order_id': 'order_12345',
},
);
- print('Purchase event with revenue tracked successfully');
} catch (e) {
- print('Error tracking purchase event: $e');
+ print('Error tracking payment: $e');
}
}
```
-
- For revenue sharing with ad networks to work properly, ensure the `amount` parameter is passed as a number (double
- or int), not as a string.
-
+An `amount` in `eventData` is revenue only when `paymentId` and `currency` are there too. On its own it is ordinary event data. Revenue is shared only when **Send Revenue** is turned on for the event's mapping in the dashboard. See [Meta Ads](/ad-networks/meta-ads) and [Google Ads](/ad-networks/google-ads).
## Enhanced Privacy Controls
@@ -1077,8 +1062,7 @@ Connect APNs with Linkrunner
| [`LinkRunner().setAdditionalData`](/sdk/flutter#setting-clevertap-id) | Optional | Integration code | When third-party integration IDs are available |
| [`LinkRunner().setUserData`](/sdk/flutter#setting-user-data) | Optional | App start, authentication or profile update logic | Right after `init` with your analytics IDs, and when user details become available |
| [`LinkRunner().trackEvent`](/sdk/flutter#tracking-custom-events) | Optional | Throughout app | When specific user actions occur, including [ecommerce events](/sdk/flutter#ecommerce-events) |
-| [`LinkRunner().capturePayment`](/sdk/flutter#capturing-payments) | Optional | Payment processing | When user makes a payment, including [ecommerce purchases](/sdk/flutter#ecommerce-events) |
-| [`LinkRunner().removePayment`](/sdk/flutter#removing-payments) | Optional | Refund flow | When payment needs to be removed |
+| [`LinkRunner().trackEvent` with payment keys](/sdk/flutter#tracking-payments) | Optional | Payment processing | When user makes a payment, including [ecommerce purchases](/sdk/flutter#ecommerce-events) |
| [`LinkRunner().setPushToken`](/sdk/flutter#uninstall-tracking) | Optional | Push notification setup | When FCM (Android) or APNs (iOS) token is available |
| [`LinkRunner().setConsent`](/sdk/flutter#google-integrated-conversion-measurement-optional) | Optional | App initialization or consent flow | Before `init`, and again when consent changes |
| [`LinkRunner().enablePIIHashing`](/sdk/flutter#enhanced-privacy-controls) | Optional | Privacy settings | When you want to enable PII hashing |
diff --git a/sdk/ios.mdx b/sdk/ios.mdx
index 83fe42e0..c0e6139e 100644
--- a/sdk/ios.mdx
+++ b/sdk/ios.mdx
@@ -39,7 +39,7 @@ Alternatively, you can add the package dependency to your `Package.swift` file:
```swift
dependencies: [
- .package(url: "https://github.com/linkrunner-labs/linkrunner-ios.git", from: "4.3.1")
+ .package(url: "https://github.com/linkrunner-labs/linkrunner-ios.git", from: "5.0.0")
]
```
@@ -542,81 +542,80 @@ func setAdditionalData() async {
## Revenue Tracking
- Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so payments are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
+ Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Pass your user ID as `userId` so each payment is linked to the user who paid. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
-### Capturing Payments
+### Tracking Payments
-Track payment information:
+A payment is an in-app event with revenue. Send it with `trackEvent`, and put `amount`, `paymentId`, and `currency` in `eventData`. The event name becomes the payment type in reports and mappings, so you can use any name, such as `purchase` or `SUBSCRIPTION_RENEWED`. Requires LinkrunnerKit 5.0.0 or later. For examples for every case and how payments reach ad networks, see the full [Track payments](/sdk/track-payments) guide.
+
+This example sends every payment key. `amount`, `paymentId`, `currency`, and `userId` are required for a payment.
```swift
-func capturePayment() async {
- await LinkrunnerSDK.shared.capturePayment(
- amount: 99.99, // Payment amount
- userId: "user123", // User identifier
- paymentId: "payment456", // Required: Unique payment identifier
- type: .firstPayment, // optional
- status: .completed, // optional
- eventData: [ // Optional: Ecommerce/custom event data
- "content_ids": ["product_123"],
- "content_type": "product",
- "currency": "USD",
- "value": 99.99,
- "num_items": 1,
- "order_id": "order_12345",
+import LinkrunnerKit
+
+func trackPurchase() async {
+ await LinkrunnerSDK.shared.trackEvent(
+ eventName: "purchase", // Payment name, stored as the payment type in reports and mappings
+ eventData: [
+ "amount": 1499.0, // Required for a payment: total the user paid, a finite number >= 0
+ "paymentId": "order_123", // Required for a payment: your order or transaction ID, used to deduplicate
+ "currency": "INR", // Required for a payment: 3-letter ISO 4217 code
+ "userId": "user_123", // Required for a payment: your user ID. "" uses the user saved by signup or setUserData
+ "status": "PAYMENT_COMPLETED", // Optional: defaults to PAYMENT_COMPLETED (see statuses below)
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ "content_ids": ["sku_1", "sku_2"],
"contents": [
- [
- "id": "product_123",
- "quantity": 1,
- "item_price": 99.99
- ]
- ]
- ]
+ ["id": "sku_1", "quantity": 1, "item_price": 999],
+ ["id": "sku_2", "quantity": 1, "item_price": 500]
+ ],
+ "content_type": "product",
+ "num_items": 2,
+ "order_id": "order_123"
+ ],
+ eventId: "evt_order_123" // Optional: your own ID for this event
)
- print("Payment captured successfully")
}
```
-#### Available Payment Types
+#### Payment keys in `eventData`
-```swift
-public enum PaymentType: String, Sendable {
- case firstPayment = "FIRST_PAYMENT"
- case secondPayment = "SECOND_PAYMENT"
- case walletTopup = "WALLET_TOPUP"
- case fundsWithdrawal = "FUNDS_WITHDRAWAL"
- case subscriptionCreated = "SUBSCRIPTION_CREATED"
- case subscriptionRenewed = "SUBSCRIPTION_RENEWED"
- case oneTime = "ONE_TIME"
- case recurring = "RECURRING"
- case `default` = "DEFAULT"
-}
-```
+| Key | Type | Required | Description |
+| --- | ---- | -------- | ----------- |
+| `amount` | `Double` or `Int` | Yes | Total the user paid. Must be a finite number, 0 or greater. This is the revenue value. |
+| `paymentId` | `String` | Yes | Your order or transaction ID. Must not be blank. Used to recognize the same payment when it arrives again. |
+| `currency` | `String` | Yes | 3-letter ISO 4217 currency code, such as `INR` or `USD`. Case-insensitive, sent in uppercase. |
+| `userId` | `String` | Yes | Your ID for the user who paid. If you pass an empty string, the SDK uses the user saved by `signup` or `setUserData`. |
+| `status` | `String` | No | `PAYMENT_INITIATED`, `PAYMENT_COMPLETED` (default), `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. |
+| Any other key | Any | No | Stays in the event data, such as product details for [ecommerce events](#ecommerce-events). |
-#### Available Payment Statuses
+How `trackEvent` decides:
-```swift
-public enum PaymentStatus: String, Sendable {
- case initiated = "PAYMENT_INITIATED"
- case completed = "PAYMENT_COMPLETED"
- case failed = "PAYMENT_FAILED"
- case cancelled = "PAYMENT_CANCELLED"
-}
-```
+- **`amount`, `paymentId`, and `currency` are all in `eventData`:** the event is a payment. The SDK checks the payment keys and sends them as payment fields.
+- **None of them, or `amount` or `currency` without `paymentId`:** the event is a normal custom event, exactly as before. An `amount` or `currency` key on its own is ordinary event data.
+- **`paymentId` with `amount` or `currency` missing:** `trackEvent` does not send the event, and debug builds print with `amount, paymentId and currency must be sent together`, and nothing is sent.
+- **A negative or non-numeric `amount`, a blank `paymentId`, a currency that is not 3 letters, or an unknown `status`:** `trackEvent` does not send the event, and debug builds print the reason to the console, and nothing is sent.
-### Removing Payments
+#### Payment status
-Remove payment records (for refunds or cancellations):
+`status` is a string. `LinkrunnerKit` also has a `PaymentStatus` enum whose `rawValue` is the same string, for example `PaymentStatus.cancelled.rawValue`.
-```swift
-func removePayment() async {
- await LinkrunnerSDK.shared.removePayment(
- userId: "user123", // User identifier
- paymentId: "payment456" // Optional: Unique payment identifier
- )
- print("Payment removed successfully")
-}
-```
+| Status | Use it when | Effect |
+| ------ | ----------- | ------ |
+| `PAYMENT_INITIATED` | The user started paying, such as opening the checkout or payment sheet. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded. | Added to revenue and paying users. Sent to ad networks when the event name is mapped. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_CANCELLED` | The user backed out before paying, or you refunded it. Send it with the same `paymentId` as the completed payment. | Stored for reports and analytics. The payment no longer counts as revenue and nothing is sent to ad networks. |
+
+Linkrunner keeps one payment per `paymentId`, event name, and status, so a retry with the same values is recorded once. See [Payment status](/sdk/track-payments#payment-status) for a full lifecycle example.
+
+
+ The event name is stored exactly as you send it. If you used one of the old payment types, pass it in uppercase exactly as before, such as `FIRST_PAYMENT` or `DEFAULT`, so your existing mappings and reports keep working. A different spelling such as `first_payment` is a new payment type. Any other name, such as `purchase`, is also stored as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks.
+
+
+
+ `capturePayment` is deprecated. Apps on LinkrunnerKit 4.x keep using it and continue to work unchanged. To move to 5.0.0, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
## Tracking Custom Events
@@ -625,7 +624,7 @@ func removePayment() async {
- Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. For capturing revenue, it is recommended to use the [`.capturePayment`](#revenue-tracking) method instead of `.trackEvent`.
+ Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. To record revenue, add the [payment keys](#tracking-payments) to `eventData`.
Track custom events in your app:
@@ -636,8 +635,7 @@ func trackEvent() async {
eventName: "purchase_initiated", // Event name
eventData: [ // Optional: Event data
"product_id": "12345",
- "category": "electronics",
- "amount": 99.99 // Include amount as a number for revenue sharing with ad networks like Google and Meta
+ "category": "electronics"
],
eventId: "order_12345" // Optional: Your own unique event identifier
)
@@ -648,31 +646,33 @@ func trackEvent() async {
#### Parameters for `LinkrunnerSDK.shared.trackEvent`
- `eventName`: `String` (required) - Name of the event to track
-- `eventData`: `[String: Any]` (optional) - Key-value pairs for additional event data, including Meta ecommerce properties
+- `eventData`: `[String: Any]` (optional) - Key-value pairs for additional event data, including Meta ecommerce properties. Add `amount`, `paymentId`, `currency`, and `userId` to make it a payment. See [Tracking Payments](#tracking-payments).
- `eventId`: `String` (optional) - Your own unique identifier for the event, useful for deduplication and correlating with your backend
### Revenue Sharing with Ad Networks
-To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:
+Send revenue to ad networks such as Google Ads and Meta as a [payment](#tracking-payments). Meta and Google receive the value and currency from the payment `amount` and `currency`, so send the order total as `amount`. Linkrunner does not add up item prices.
+
+Put the payment keys and the product details in `eventData`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`. Google receives `content_ids` as item IDs.
```swift
-func trackPurchaseEvent() async {
+func trackPurchase() async {
await LinkrunnerSDK.shared.trackEvent(
- eventName: "purchase_completed",
+ eventName: "purchase",
eventData: [
- "product_id": "12345",
- "category": "electronics",
- "amount": 149.99 // Revenue amount as a number
+ "amount": 149.99, // Order total, sent to ad networks as the value
+ "paymentId": "order_12345",
+ "currency": "USD",
+ "userId": "user_123",
+ "content_ids": ["product_123"],
+ "num_items": 1,
+ "order_id": "order_12345"
]
)
- print("Purchase event with revenue tracked successfully")
}
```
-
- For revenue sharing with ad networks to work properly, ensure the `amount` parameter is passed as a number (Double
- or Int), not as a string.
-
+An `amount` in `eventData` is revenue only when `paymentId` and `currency` are there too. On its own it is ordinary event data, and on iOS it still counts toward SKAN conversion values. Revenue is shared only when **Send Revenue** is turned on for the event's mapping in the dashboard. See [Meta Ads](/ad-networks/meta-ads) and [Google Ads](/ad-networks/google-ads).
## Ecommerce Events
@@ -738,33 +738,25 @@ func trackViewContent() async {
### Payment / Purchase Example
-Use the `capturePayment` method to send a `Purchase` event containing the ecommerce payload:
+Use `trackEvent` with the [payment keys](#tracking-payments) and the ecommerce fields in `eventData` to send a `Purchase` event:
```swift
-func capturePurchase() async {
- await LinkrunnerSDK.shared.capturePayment(
- amount: 49.99,
- userId: "user123",
- paymentId: "payment_456",
- type: .firstPayment, // Map this payment type to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard
- status: .completed,
+func trackPurchase() async {
+ await LinkrunnerSDK.shared.trackEvent(
+ eventName: "purchase", // Map this event to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard
eventData: [
+ "amount": 49.99, // Order total, sent to ad networks as the value
+ "paymentId": "payment_456",
+ "currency": "USD",
+ "userId": "user_123",
+ "status": "PAYMENT_COMPLETED", // Optional, the default
"content_ids": ["product_123"],
- "contents": [
- [
- "id": "product_123", // Matches content_ids
- "quantity": 1,
- "item_price": 49.99
- ]
- ],
+ "contents": [["id": "product_123", "quantity": 1, "item_price": 49.99]],
"content_type": "product",
- "currency": "USD",
- "value": 49.99,
"num_items": 1,
"order_id": "order_abc123" // Required for Purchase events
]
)
- print("Purchase captured successfully")
}
```
@@ -914,8 +906,7 @@ Connect APNs with Linkrunner
| [`LinkrunnerSDK.shared.setAdditionalData`](/sdk/ios#setting-clevertap-id) | Optional | Integration code | When third-party integration IDs are available |
| [`LinkrunnerSDK.shared.setUserData`](/sdk/ios#setting-user-data) | Optional | App start, authentication or profile update logic | Right after `init` with your analytics IDs, and when user details become available |
| [`LinkrunnerSDK.shared.trackEvent`](/sdk/ios#tracking-custom-events) | Optional | Throughout app | When specific user actions occur, including [ecommerce events](/sdk/ios#ecommerce-events) |
-| [`LinkrunnerSDK.shared.capturePayment`](/sdk/ios#capturing-payments) | Optional | Payment processing | When user makes a payment, including [ecommerce purchases](/sdk/ios#ecommerce-events) |
-| [`LinkrunnerSDK.shared.removePayment`](/sdk/ios#removing-payments) | Optional | Refund flow | When payment needs to be removed |
+| [`LinkrunnerSDK.shared.trackEvent` with payment keys](/sdk/ios#tracking-payments) | Optional | Payment processing | When user makes a payment, including [ecommerce purchases](/sdk/ios#ecommerce-events) |
| [`LinkrunnerSDK.shared.setPushToken`](/sdk/ios#uninstall-tracking) | Optional | Push notification setup | When APNs token is available |
| [`LinkrunnerSDK.shared.setConsent`](/sdk/ios#consent) | Optional | App initialization or consent flow | Before `initialize`, and again when consent changes |
| [`LinkrunnerSDK.shared.enablePIIHashing`](/sdk/ios#enhanced-privacy-controls) | Optional | Privacy settings | When you want to enable PII hashing |
diff --git a/sdk/migrate-to-track-event.mdx b/sdk/migrate-to-track-event.mdx
new file mode 100644
index 00000000..08519c50
--- /dev/null
+++ b/sdk/migrate-to-track-event.mdx
@@ -0,0 +1,376 @@
+---
+title: "Migrate payments to in-app events"
+sidebarTitle: "Migrate to in-app events"
+description: "Move from capturePayment to trackEvent, which sends each payment as an in-app event with revenue"
+icon: "arrow-right-arrow-left"
+---
+
+In the new SDK versions, a payment is an in-app event with revenue. You send it with `trackEvent`, the same method you use for custom events. `capturePayment` and `removePayment` are removed.
+
+Apps on older SDK versions keep working with no change. Migrate when you upgrade to the [versions below](#sdk-versions).
+
+
+ **Use the exact same event name, with the same spelling and letter case.**
+
+ Your old payment `type` becomes the event name. If you sent `FIRST_PAYMENT` before, send `"FIRST_PAYMENT"` now. Do not change it to `first_payment`, `First_Payment`, or `firstPayment`.
+
+ Linkrunner matches the name exactly. A different spelling is treated as a new in-app event, so your existing Meta and Google mappings will not match it, and those payments will not be sent to Meta or Google.
+
+
+## Before and after
+
+The old `capturePayment` call, then the same payment with `trackEvent`. The product keys are optional: Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`, and Google receives `content_ids` as item IDs. `amount` stays the order total.
+
+
+
+ Before (Android SDK 4.x):
+
+ ```kotlin
+ LinkRunner.getInstance().capturePayment(
+ CapturePaymentRequest(
+ paymentId = "bk_10231",
+ userId = "playo_u_8842",
+ amount = 1200.0,
+ type = PaymentType.FIRST_PAYMENT,
+ status = PaymentStatus.PAYMENT_COMPLETED
+ )
+ )
+ ```
+
+ After (Android SDK 5.0.0):
+
+ ```kotlin
+ // Payment: same trackEvent, payment fields in eventData
+ LinkRunner.getInstance().trackEvent(
+ eventName = "FIRST_PAYMENT", // was type, same spelling
+ eventData = mapOf(
+ "amount" to 1200.0,
+ "paymentId" to "bk_10231",
+ "currency" to "INR", // new, required
+ "userId" to "playo_u_8842",
+ "status" to "PAYMENT_COMPLETED", // optional, the default
+ // product details for Meta and Google
+ "content_ids" to listOf("court_c2_1800"),
+ "contents" to listOf(mapOf("id" to "court_c2_1800", "quantity" to 1, "item_price" to 1200.0)),
+ "content_type" to "product",
+ "num_items" to 1,
+ "order_id" to "bk_10231"
+ )
+ )
+ ```
+
+ From Java, call `LinkrunnerJava.getInstance().trackEvent("FIRST_PAYMENT", eventData, null, callback)` with the same keys. See [Tracking Payments](/sdk/android#tracking-payments).
+
+
+ Before (LinkrunnerKit 4.x):
+
+ ```swift
+ await LinkrunnerSDK.shared.capturePayment(
+ amount: 1200,
+ userId: "playo_u_8842",
+ paymentId: "bk_10231",
+ type: .firstPayment,
+ status: .completed
+ )
+ ```
+
+ After (LinkrunnerKit 5.0.0):
+
+ ```swift
+ // Payment: same trackEvent, payment fields in eventData
+ await LinkrunnerSDK.shared.trackEvent(
+ eventName: "FIRST_PAYMENT", // was type, same spelling
+ eventData: [
+ "amount": 1200,
+ "paymentId": "bk_10231",
+ "currency": "INR", // new, required
+ "userId": "playo_u_8842",
+ "status": "PAYMENT_COMPLETED", // optional, the default
+ // product details for Meta and Google
+ "content_ids": ["court_c2_1800"],
+ "contents": [["id": "court_c2_1800", "quantity": 1, "item_price": 1200]],
+ "content_type": "product",
+ "num_items": 1,
+ "order_id": "bk_10231"
+ ]
+ )
+ ```
+
+ See [Tracking Payments](/sdk/ios#tracking-payments).
+
+
+ Before (`rn-linkrunner` 3.x):
+
+ ```javascript
+ await linkrunner.capturePayment({
+ amount: 1200,
+ userId: "playo_u_8842",
+ paymentId: "bk_10231",
+ type: "FIRST_PAYMENT",
+ status: "PAYMENT_COMPLETED",
+ });
+ ```
+
+ After (`rn-linkrunner` 4.0.0):
+
+ ```javascript
+ // Payment: same trackEvent, payment fields in eventData
+ await linkrunner.trackEvent("FIRST_PAYMENT", { // was type, same spelling
+ amount: 1200,
+ paymentId: "bk_10231",
+ currency: "INR", // new, required
+ userId: "playo_u_8842",
+ status: "PAYMENT_COMPLETED", // optional, the default
+ // product details for Meta and Google
+ content_ids: ["court_c2_1800"],
+ contents: [{ id: "court_c2_1800", quantity: 1, item_price: 1200 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: "bk_10231",
+ });
+ ```
+
+ See [Tracking Payments](/sdk/react-native#tracking-payments).
+
+
+ Before (`linkrunner` 4.x):
+
+ ```dart
+ await LinkRunner().capturePayment(
+ capturePayment: LRCapturePayment(
+ amount: 1200,
+ userId: 'playo_u_8842',
+ paymentId: 'bk_10231',
+ type: PaymentType.FIRST_PAYMENT,
+ status: PaymentStatus.PAYMENT_COMPLETED,
+ ),
+ );
+ ```
+
+ After (`linkrunner` 5.0.0):
+
+ ```dart
+ // Payment: same trackEvent, payment fields in eventData
+ await LinkRunner().trackEvent(
+ eventName: 'FIRST_PAYMENT', // was type, same spelling
+ eventData: {
+ 'amount': 1200,
+ 'paymentId': 'bk_10231',
+ 'currency': 'INR', // new, required
+ 'userId': 'playo_u_8842',
+ 'status': 'PAYMENT_COMPLETED', // optional, the default
+ // product details for Meta and Google
+ 'content_ids': ['court_c2_1800'],
+ 'contents': [
+ {'id': 'court_c2_1800', 'quantity': 1, 'item_price': 1200},
+ ],
+ 'content_type': 'product',
+ 'num_items': 1,
+ 'order_id': 'bk_10231',
+ },
+ );
+ ```
+
+ See [Tracking Payments](/sdk/flutter#tracking-payments).
+
+
+ Expo apps call `rn-linkrunner`, so the code change is the same as React Native. Upgrade `expo-linkrunner` to 4.0.0 and `rn-linkrunner` to 4.0.0 or later together.
+
+ Before (`rn-linkrunner` 3.x):
+
+ ```javascript
+ await linkrunner.capturePayment({
+ amount: 1200,
+ userId: "playo_u_8842",
+ paymentId: "bk_10231",
+ type: "FIRST_PAYMENT",
+ });
+ ```
+
+ After (`rn-linkrunner` 4.0.0):
+
+ ```javascript
+ // Payment: same trackEvent, payment fields in eventData
+ await linkrunner.trackEvent("FIRST_PAYMENT", { // was type, same spelling
+ amount: 1200,
+ paymentId: "bk_10231",
+ currency: "INR", // new, required
+ userId: "playo_u_8842",
+ // product details for Meta and Google
+ content_ids: ["court_c2_1800"],
+ contents: [{ id: "court_c2_1800", quantity: 1, item_price: 1200 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: "bk_10231",
+ });
+ ```
+
+ See [Tracking Payments](/sdk/expo#tracking-payments).
+
+
+ Before (`capacitor-linkrunner` 2.x):
+
+ ```typescript
+ await linkrunner.capturePayment({
+ amount: 1200,
+ userId: "playo_u_8842",
+ paymentId: "bk_10231",
+ type: "FIRST_PAYMENT",
+ status: "PAYMENT_COMPLETED",
+ });
+ ```
+
+ After (`capacitor-linkrunner` 3.0.0):
+
+ ```typescript
+ // Payment: same trackEvent, payment fields in eventData
+ await linkrunner.trackEvent("FIRST_PAYMENT", { // was type, same spelling
+ amount: 1200,
+ paymentId: "bk_10231",
+ currency: "INR", // new, required
+ userId: "playo_u_8842",
+ status: "PAYMENT_COMPLETED", // optional, the default
+ // product details for Meta and Google
+ content_ids: ["court_c2_1800"],
+ contents: [{ id: "court_c2_1800", quantity: 1, item_price: 1200 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: "bk_10231",
+ });
+ ```
+
+ See [Tracking Payments](/sdk/capacitor#tracking-payments).
+
+
+ Before (`cordova-linkrunner` 1.x):
+
+ ```javascript
+ linkrunner.capturePayment({
+ amount: 1200,
+ userId: "playo_u_8842",
+ paymentId: "bk_10231",
+ type: "FIRST_PAYMENT",
+ status: "PAYMENT_COMPLETED"
+ });
+ ```
+
+ After (`cordova-linkrunner` 2.0.0):
+
+ ```javascript
+ // Payment: same trackEvent, payment fields in eventData
+ linkrunner.trackEvent("FIRST_PAYMENT", { // was type, same spelling
+ amount: 1200,
+ paymentId: "bk_10231",
+ currency: "INR", // new, required
+ userId: "playo_u_8842",
+ status: "PAYMENT_COMPLETED", // optional, the default
+ // product details for Meta and Google
+ content_ids: ["court_c2_1800"],
+ contents: [{ id: "court_c2_1800", quantity: 1, item_price: 1200 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: "bk_10231"
+ }).catch(function (error) {
+ console.error("Error tracking payment:", error);
+ });
+ ```
+
+ See [Tracking Payments](/sdk/cordova#tracking-payments).
+
+
+ Before (bridges built on native 4.x):
+
+ ```csharp
+ LinkrunnerSDK.CapturePayment(
+ userId: "playo_u_8842",
+ amount: 1200.0,
+ paymentId: "bk_10231",
+ type: "FIRST_PAYMENT",
+ status: "PAYMENT_COMPLETED"
+ );
+ ```
+
+ After (bridges built on native 5.0.0):
+
+ ```csharp
+ // Payment: same TrackEvent, payment fields in the event data JSON
+ string eventDataJson =
+ "{\"amount\":1200.0," +
+ "\"paymentId\":\"bk_10231\"," +
+ "\"currency\":\"INR\"," + // new, required
+ "\"userId\":\"playo_u_8842\"," +
+ "\"status\":\"PAYMENT_COMPLETED\"," + // optional, the default
+ // product details for Meta and Google
+ "\"content_ids\":[\"court_c2_1800\"]," +
+ "\"contents\":[{\"id\":\"court_c2_1800\",\"quantity\":1,\"item_price\":1200}]," +
+ "\"content_type\":\"product\",\"num_items\":1,\"order_id\":\"bk_10231\"}";
+ LinkrunnerSDK.TrackEvent("FIRST_PAYMENT", eventDataJson); // was type, same spelling
+ ```
+
+ Update your Android and iOS bridge code and C# wrapper as shown in [Track Payments](/sdk/unity#track-payments).
+
+
+
+## How to migrate
+
+For each `capturePayment` call, make three edits:
+
+1. **Rename `capturePayment` to `trackEvent`.**
+2. **Pass your old `type` as the event name,** the first argument, with exactly the same spelling.
+3. **Move the payment fields into the event data:** `amount`, `paymentId`, `userId`, and `status` keep their names. Add `currency`, which is new and required, for example `"USD"` or `"INR"`.
+
+Then remove every `removePayment` call, and test once with a debug build. The payment should show on the [Events](https://dashboard.linkrunner.io/dashboard/events) page under the same name as before.
+
+| `capturePayment` | `trackEvent` |
+| --- | --- |
+| `type` | the event name (first argument), same value and spelling |
+| `amount` | `amount` in the event data, the order total |
+| `paymentId` | `paymentId` in the event data, required |
+| `userId` | `userId` in the event data, still required. An empty string uses the user saved by `signup` or `setUserData` |
+| `status` | `status` in the event data, optional, defaults to `PAYMENT_COMPLETED` |
+| `eventData` | the other keys in the event data, such as product details |
+| Not available | `currency` in the event data, new and required |
+
+The event is a payment only when the event data has `amount`, `paymentId`, and `currency` together. If any of the three is missing, it is not a payment:
+
+- `amount` or `currency` without `paymentId` is a normal custom event, exactly as before.
+- `paymentId` with `amount` or `currency` missing is an error, and nothing is sent.
+
+Only `PAYMENT_COMPLETED` payments count as revenue and are sent to ad networks. See [Payment status](/sdk/track-payments#payment-status) for the other statuses.
+
+## Old payment types
+
+These are the old payment types. Send them exactly like this, in uppercase:
+
+`FIRST_PAYMENT`, `SECOND_PAYMENT`, `TRIAL_STARTED`, `TRIAL_CANCELLED`, `WALLET_TOPUP`, `FUNDS_WITHDRAWAL`, `SUBSCRIPTION_CREATED`, `SUBSCRIPTION_RENEWED`, `SUBSCRIPTION_CANCELLED`, `SUBSCRIPTION_REFUNDED`, `SUBSCRIPTION_BILLING_ISSUE`, `DEFAULT`, `ONE_TIME`, `RECURRING`
+
+If you did not pass a `type` before, your payments were saved as `DEFAULT`. Use `"DEFAULT"` as the event name. In Flutter, `PaymentType.DEFAULT_PAYMENT` was also saved as `DEFAULT`, so use `"DEFAULT"`, not `"DEFAULT_PAYMENT"`.
+
+## SDK versions
+
+| Platform | Package | Old versions (`capturePayment`) | New versions (`trackEvent` for payments) |
+| --- | --- | --- | --- |
+| Android | `io.linkrunner:android-sdk` | 4.x | 5.0.0 |
+| iOS | LinkrunnerKit | 4.x | 5.0.0 |
+| React Native | `rn-linkrunner` | 3.x | 4.0.0 |
+| Flutter | `linkrunner` | 4.x | 5.0.0 |
+| Expo | `expo-linkrunner` | 3.x | 4.0.0, with `rn-linkrunner` 4.0.0 or later |
+| Capacitor | `capacitor-linkrunner` | 2.x | 3.0.0 |
+| Cordova | `cordova-linkrunner` | 1.x | 2.0.0 |
+| Unity | [Unity bridges](/sdk/unity#track-payments) | Bridges on native 4.x | Bridges on native 5.0.0 |
+
+## Refunds and removePayment
+
+`removePayment` is removed and has no replacement. To record a refund or a cancelled booking, send the same event name and `paymentId` again with `status` set to `PAYMENT_CANCELLED`. That payment no longer counts as revenue, and nothing is sent to ad networks.
+
+## Using a new event name
+
+You can use a new name such as `purchase` instead of an old type. Before you release, add a mapping for that name in the dashboard for Meta, Google, and any other partner that should get it. Without a mapping, the payment is saved in Linkrunner but not sent to ad networks.
+
+Keep one name per payment. The same `paymentId` sent under two names counts as two payments.
+
+## Sending payments from your server
+
+Use `POST /api/v1/capture-event` with `amount`, `payment_id`, and `currency` next to `event_name` and `user_id`. The same naming rule applies. See [Events Capture API](/api-reference/event-capture#payment-fields). `POST /api/v1/capture-payment` is deprecated but keeps working.
+
+**Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io).
diff --git a/sdk/react-native.mdx b/sdk/react-native.mdx
index aa6fb25e..5e3951dc 100644
--- a/sdk/react-native.mdx
+++ b/sdk/react-native.mdx
@@ -457,81 +457,85 @@ This method allows you to connect user identities across different analytics and
## Revenue Tracking
- Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so payments are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
+ Revenue is stored for all users, including organic ones. Payments from users with no matching click are stored without campaign attribution. Pass your user ID as `userId` so each payment is linked to the user who paid. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page.
-### Capture Payment
+### Tracking Payments
-Use this method to capture payment information:
+A payment is an in-app event with revenue. Send it with `trackEvent`, and put `amount`, `paymentId`, and `currency` in `eventData`. The event name becomes the payment type in reports and mappings, so you can use any name, such as `purchase` or `SUBSCRIPTION_RENEWED`. Requires `rn-linkrunner` 4.0.0 or later. For examples for every case and how payments reach ad networks, see the full [Track payments](/sdk/track-payments) guide.
-```js
-const capturePayment = async () => {
- await linkrunner.capturePayment({
- amount: 100, // Payment amount
- userId: "user123", // User identifier
- paymentId: "payment456", // Required: Unique payment identifier
- type: "FIRST_PAYMENT", // Optional: Payment type
- status: "PAYMENT_COMPLETED", // Optional: Payment status
- eventData: {
- // Optional: Ecommerce/custom event data
- content_ids: ["product_123"],
- content_type: "product",
- currency: "USD",
- value: 99.99,
- num_items: 1,
- order_id: "order_12345",
- contents: [
- {
- id: "product_123",
- quantity: 1,
- item_price: 99.99,
- },
- ],
- },
- });
-};
-```
-
-#### Parameters for `linkrunner.capturePayment`
-
-- `amount`: number (required) - The payment amount
-- `userId`: string (required) - Identifier for the user making the payment
-- `paymentId`: string (required) - Unique identifier for the payment, used to deduplicate transactions
-- `type`: string (optional) - Type of payment. Available options:
- - `FIRST_PAYMENT` - First payment made by the user
- - `WALLET_TOPUP` - Adding funds to a wallet
- - `FUNDS_WITHDRAWAL` - Withdrawing funds
- - `SUBSCRIPTION_CREATED` - New subscription created
- - `SUBSCRIPTION_RENEWED` - Subscription renewal
- - `ONE_TIME` - One-time payment
- - `RECURRING` - Recurring payment
- - `DEFAULT` - Default type (used if not specified)
-- `status`: string (optional) - Status of the payment. Available options:
- - `PAYMENT_INITIATED` - Payment has been initiated
- - `PAYMENT_COMPLETED` - Payment completed successfully (default if not specified)
- - `PAYMENT_FAILED` - Payment attempt failed
- - `PAYMENT_CANCELLED` - Payment was cancelled
-- `eventData`: object (optional) - Key-value pairs for additional event data, including ecommerce properties for Meta and Google.
-
-### Removing Payments
-
-Remove payment records (for refunds or cancellations):
+This example sends every payment key. `amount`, `paymentId`, `currency`, and `userId` are required for a payment.
```javascript
-const removePayment = async () => {
- await linkrunner.removePayment({
- userId: "user123", // User identifier
- paymentId: "payment456", // Optional: Unique payment identifier
- });
+import linkrunner, { PaymentStatus } from "rn-linkrunner";
+
+const trackPurchase = async () => {
+ try {
+ await linkrunner.trackEvent(
+ "purchase", // Payment name, stored as the payment type in reports and mappings
+ {
+ amount: 1499, // Required for a payment: total the user paid, a finite number >= 0
+ paymentId: "order_123", // Required for a payment: your order or transaction ID, used to deduplicate
+ currency: "INR", // Required for a payment: 3-letter ISO 4217 code
+ userId: "user_123", // Required for a payment: your user ID. "" uses the user saved by signup or setUserData
+ status: PaymentStatus.PAYMENT_COMPLETED, // Optional: defaults to PAYMENT_COMPLETED (see statuses below)
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ content_ids: ["sku_1", "sku_2"],
+ contents: [
+ { id: "sku_1", quantity: 1, item_price: 999 },
+ { id: "sku_2", quantity: 1, item_price: 500 },
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: "order_123",
+ },
+ "evt_order_123", // Optional: your own ID for this event
+ );
+ } catch (error) {
+ // On Android an invalid payment throws here, and nothing is sent
+ console.error("Error tracking payment:", error);
+ }
};
```
-#### Parameters for `Linkrunner.removePayment`
+#### Payment keys in `eventData`
+
+| Key | Type | Required | Description |
+| --- | ---- | -------- | ----------- |
+| `amount` | `number` | Yes | Total the user paid. Must be a finite number, 0 or greater. This is the revenue value. |
+| `paymentId` | `string` | Yes | Your order or transaction ID. Must not be blank. Used to recognize the same payment when it arrives again. |
+| `currency` | `string` | Yes | 3-letter ISO 4217 currency code, such as `INR` or `USD`. Case-insensitive, sent in uppercase. |
+| `userId` | `string` | Yes | Your ID for the user who paid. If you pass an empty string, the SDK uses the user saved by `signup` or `setUserData`. |
+| `status` | `string` | No | `PAYMENT_INITIATED`, `PAYMENT_COMPLETED` (default), `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. |
+| Any other key | Any | No | Stays in the event data, such as product details for [ecommerce events](#ecommerce-events). |
+
+How `trackEvent` decides:
-- `userId`: String (required) - Identifier for the user whose payment is being removed
-- `paymentId`: String (optional) - Unique identifier for the payment to be removed
+- **`amount`, `paymentId`, and `currency` are all in `eventData`:** the event is a payment. The SDK checks the payment keys and sends them as payment fields.
+- **None of them, or `amount` or `currency` without `paymentId`:** the event is a normal custom event, exactly as before. An `amount` or `currency` key on its own is ordinary event data.
+- **`paymentId` with `amount` or `currency` missing:** `trackEvent` throws with `amount, paymentId and currency must be sent together`, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
+- **A negative or non-numeric `amount`, a blank `paymentId`, a currency that is not 3 letters, or an unknown `status`:** `trackEvent` throws, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
-Note: `userId` is always required. `paymentId` is optional, and if you omit it all payments for that user are removed.
+#### Payment status
+
+`rn-linkrunner` exports `PaymentStatus` as an object of string values. Plain strings such as `"PAYMENT_COMPLETED"` also work.
+
+| Status | Use it when | Effect |
+| ------ | ----------- | ------ |
+| `PAYMENT_INITIATED` | The user started paying, such as opening the checkout or payment sheet. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded. | Added to revenue and paying users. Sent to ad networks when the event name is mapped. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_CANCELLED` | The user backed out before paying, or you refunded it. Send it with the same `paymentId` as the completed payment. | Stored for reports and analytics. The payment no longer counts as revenue and nothing is sent to ad networks. |
+
+Linkrunner keeps one payment per `paymentId`, event name, and status, so a retry with the same values is recorded once. See [Payment status](/sdk/track-payments#payment-status) for a full lifecycle example.
+
+
+ The event name is stored exactly as you send it. If you used one of the old payment types, pass it in uppercase exactly as before, such as `FIRST_PAYMENT` or `DEFAULT`, so your existing mappings and reports keep working. A different spelling such as `first_payment` is a new payment type. Any other name, such as `purchase`, is also stored as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks.
+
+
+
+ `capturePayment` is deprecated. Apps on `rn-linkrunner` 3.x keep using it and continue to work unchanged. To move to 4.0.0, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
## Ecommerce Events
@@ -605,36 +609,25 @@ const trackViewContent = async () => {
### Payment / Purchase Example
-Use the `capturePayment` method to send a `Purchase` event containing the ecommerce payload:
+Use `trackEvent` with the [payment keys](#tracking-payments) and the ecommerce fields in `eventData` to send a `Purchase` event:
```javascript
-const capturePurchase = async () => {
+const trackPurchase = async () => {
try {
- await linkrunner.capturePayment({
- amount: 49.99,
- userId: "user123",
+ await linkrunner.trackEvent("purchase", { // Map this event to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard
+ amount: 49.99, // Order total, sent to ad networks as the value
paymentId: "payment_456",
- type: "FIRST_PAYMENT", // Map this payment type to "Purchase" (Meta) or "ecommerce_purchase" (Google) in the Linkrunner Dashboard. See: /ecommerce-manager/meta-commerce-manager, /ecommerce-manager/google-commerce-manager
- status: "PAYMENT_COMPLETED",
- eventData: {
- content_ids: ["product_123"],
- contents: [
- {
- id: "product_123", // Matches content_ids
- quantity: 1,
- item_price: 49.99,
- },
- ],
- content_type: "product",
- currency: "USD",
- value: 49.99,
- num_items: 1,
- order_id: "order_abc123", // Required for Purchase events
- },
+ currency: "USD",
+ userId: "user_123",
+ status: "PAYMENT_COMPLETED", // Optional, the default
+ content_ids: ["product_123"],
+ contents: [{ id: "product_123", quantity: 1, item_price: 49.99 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: "order_abc123", // Required for Purchase events
});
- console.log("Purchase captured successfully");
} catch (error) {
- console.error("Error capturing purchase:", error);
+ console.error("Error tracking payment:", error);
}
};
```
@@ -648,7 +641,7 @@ const capturePurchase = async () => {
- Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-registration) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. For capturing revenue, it is recommended to use the [`.capturePayment`](#revenue-tracking) method instead of `.trackEvent`.
+ Events are stored for all users, including organic ones. Events from users with no matching click are stored without campaign attribution. Call [`.signup`](#user-identification-required) so events are linked to a user. You can verify your events are being captured on the [Events](https://dashboard.linkrunner.io/dashboard/events) page. To record revenue, add the [payment keys](#tracking-payments) to `eventData`.
Track custom events in your app:
@@ -657,7 +650,7 @@ Track custom events in your app:
const trackEvent = async () => {
await linkrunner.trackEvent(
"purchase_initiated", // Event name
- { product_id: "12345", category: "electronics", amount: 99.99 }, // Optional: Event data, include amount as a number for revenue sharing with ad networks like Google and Meta
+ { product_id: "12345", category: "electronics" }, // Optional: Event data
"order_12345" // Optional: Your own unique event identifier (string or number)
);
};
@@ -666,27 +659,34 @@ const trackEvent = async () => {
#### Parameters for `linkrunner.trackEvent`
- `eventName`: string (required) - Name of the event to track
-- `eventData`: object (optional) - Key-value pairs for additional event data, including Meta ecommerce properties
+- `eventData`: object (optional) - Key-value pairs for additional event data, including Meta ecommerce properties. Add `amount`, `paymentId`, `currency`, and `userId` to make it a payment. See [Tracking Payments](#tracking-payments).
- `eventId`: string | number (optional) - Your own unique identifier for the event, useful for deduplication and correlating with your backend
### Revenue Sharing with Ad Networks
-To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:
+Send revenue to ad networks such as Google Ads and Meta as a [payment](#tracking-payments). Meta and Google receive the value and currency from the payment `amount` and `currency`, so send the order total as `amount`. Linkrunner does not add up item prices.
+
+Put the payment keys and the product details in `eventData`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id`. Google receives `content_ids` as item IDs.
```javascript
-const trackPurchaseEvent = async () => {
- await linkrunner.trackEvent("purchase_completed", {
- product_id: "12345",
- category: "electronics",
- amount: 149.99, // Revenue amount as a number
+const trackPurchase = async () => {
+ try {
+ await linkrunner.trackEvent("purchase", {
+ amount: 149.99, // Order total, sent to ad networks as the value
+ paymentId: "order_12345",
+ currency: "USD",
+ userId: "user_123",
+ content_ids: ["product_123"],
+ num_items: 1,
+ order_id: "order_12345",
});
+ } catch (error) {
+ console.error("Error tracking payment:", error);
+ }
};
```
-
- For revenue sharing with ad networks to work properly, ensure the `amount` parameter is passed as a number, not as a
- string.
-
+An `amount` in `eventData` is revenue only when `paymentId` and `currency` are there too. On its own it is ordinary event data. Revenue is shared only when **Send Revenue** is turned on for the event's mapping in the dashboard. See [Meta Ads](/ad-networks/meta-ads) and [Google Ads](/ad-networks/google-ads).
## SDK Package Version
@@ -879,8 +879,7 @@ Connect APNs with Linkrunner
| [`linkrunner.setAdditionalData`](/sdk/react-native#setting-clevertap-id) | Optional | Integration code | When third-party integration IDs are available |
| [`linkrunner.setUserData`](/sdk/react-native#setting-user-data) | Optional | App start, authentication or profile update logic | Right after `init` with your analytics IDs, and when user details become available |
| [`linkrunner.trackEvent`](/sdk/react-native#tracking-custom-events) | Optional | Throughout app, including ecommerce flows | When specific user actions occur, including AddToCart or ViewContent events |
-| [`linkrunner.capturePayment`](/sdk/react-native#capture-payment) | Optional | Payment processing, including ecommerce purchases | When user makes a payment, including Purchase events with ecommerce data |
-| [`linkrunner.removePayment`](/sdk/react-native#removing-payments) | Optional | Refund flow | When payment needs to be removed |
+| [`linkrunner.trackEvent` with payment keys](/sdk/react-native#tracking-payments) | Optional | Payment processing, including ecommerce purchases | When user makes a payment, including Purchase events with ecommerce data |
| [`linkrunner.setPushToken`](/sdk/react-native#uninstall-tracking) | Optional | Push notification setup | When FCM/APNs token is available |
| [`linkrunner.setConsent`](/sdk/react-native#google-integrated-conversion-measurement-optional) | Optional | App initialization or consent flow | Before `init`, and again when consent changes |
| [`linkrunner.enablePIIHashing`](/sdk/react-native#enhanced-privacy-controls) | Optional | Privacy configuration | When enabling PII hashing |
diff --git a/sdk/track-payments.mdx b/sdk/track-payments.mdx
new file mode 100644
index 00000000..4926b270
--- /dev/null
+++ b/sdk/track-payments.mdx
@@ -0,0 +1,993 @@
+---
+title: "Track payments with trackEvent"
+sidebarTitle: "Track payments"
+description: "Record revenue as an in-app event with trackEvent, use payment status correctly, and send completed payments to your ad networks"
+icon: "credit-card"
+---
+
+A payment is an in-app event with revenue. Send it with `trackEvent` whenever a user pays you: a purchase, a subscription charge or renewal, a wallet top-up. Put `amount`, `paymentId`, `currency`, and `userId` in `eventData`, and the SDK sends the event as a payment. Linkrunner stores it as revenue, links it to the user and their install attribution, and forwards completed payments to the ad networks you mapped.
+
+Payment keys in `trackEvent` need these SDK versions or later. On an older version, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
+| Platform | Package | Minimum version |
+| --- | --- | --- |
+| Android | `io.linkrunner:android-sdk` | 5.0.0 |
+| iOS | LinkrunnerKit | 5.0.0 |
+| React Native | `rn-linkrunner` | 4.0.0 |
+| Flutter | `linkrunner` | 5.0.0 |
+| Expo | `expo-linkrunner` | 4.0.0, with `rn-linkrunner` 4.0.0 or later |
+| Capacitor | `capacitor-linkrunner` | 3.0.0 |
+| Cordova | `cordova-linkrunner` | 2.0.0 |
+| Unity | [Unity bridges](/sdk/unity#track-payments) | Bridges built on Android SDK 5.0.0 and LinkrunnerKit 5.0.0 |
+
+Initialize the SDK before you track payments. See your platform's SDK guide for setup.
+
+## How it works
+
+
+
+ You pass the event name, and in `eventData` the amount, payment ID, currency, user ID, and optionally a status and product details. Without `amount`, `paymentId`, and `currency` together, the call stays a normal custom event.
+
+
+ The SDK checks every required field before anything leaves the device. If a value is invalid, nothing is sent and your code gets an error (see [Handle invalid payments](#handle-invalid-payments)).
+
+
+ The SDK sends the payment to Linkrunner. An accepted request only means Linkrunner received it. Linkrunner then processes it: it stores the payment under your event name, links it to the user who paid, and attaches the campaign that user's install was attributed to. Payments from organic users are stored too, without a campaign.
+
+
+ The payment appears in the [Events Log](https://dashboard.linkrunner.io/dashboard/events) with its amount, currency, payment ID, and status. Completed payments count toward revenue and paying users in your reports.
+
+
+ If the event name is mapped for an ad network, such as Meta, Google, or TikTok, or for an affiliate, Linkrunner sends the payment there. Only `PAYMENT_COMPLETED` payments are sent. The value is included only when **Send Revenue** is on for that mapping.
+
+
+ Connected analytics integrations receive the payment with its status, whatever the status is.
+
+
+
+So a payment can be accepted but not yet in reports, in reports but not sent to an ad network (no mapping, or not completed), or sent without a value (**Send Revenue** off). [Verify it works](#verify-it-works) shows how to check each step.
+
+## Payment keys
+
+`eventName` and `eventId` are arguments of `trackEvent`. The other fields are keys in `eventData`.
+
+| Field | Required | What it is for | Why it matters | Example |
+| --- | --- | --- | --- | --- |
+| `eventName` | Yes | The first argument of `trackEvent`: the name of this payment, such as `purchase` or `SUBSCRIPTION_RENEWED`. | It becomes the payment type in reports, and Linkrunner matches it exactly against your ad network and affiliate mappings. No mapping means the payment never reaches that network. | `"purchase"` |
+| `paymentId` | Yes | Your order or transaction ID. Must not be blank. | Linkrunner uses it to recognize the same payment when it arrives again, so retries do not double your revenue. | `"order_8f2k1"` |
+| `userId` | Yes | Your ID for the user who paid. | It links the payment to your user, so paying users, cohorts, and user-level reports are correct. If you pass an empty string, the SDK uses the user saved by `signup`, `setUserData`, or `setCustomerUserId`. | `"user_123"` |
+| `amount` | Yes | The total the user paid. A finite number, 0 or greater. | This is the revenue in your reports and the value ad networks receive. Linkrunner does not add up item prices, so send the order total. | `499.0` |
+| `currency` | Yes | The 3-letter ISO 4217 code of `amount`. Case-insensitive, sent in uppercase. | Linkrunner stores the payment in this currency, and ad networks receive it with the value. A wrong code makes the revenue wrong everywhere. | `"INR"` |
+| `status` | No | Where the payment is in its lifecycle. Defaults to `PAYMENT_COMPLETED`. | Only completed payments count as revenue and reach ad networks. See [Payment status](#payment-status). | `PAYMENT_COMPLETED` |
+| Other `eventData` keys | No | Extra details, such as the products in the order. | Meta and Google read product fields from them. They stay in the event data. | `"content_ids": ["sku_1"]` |
+| `eventId` | No | Your own identifier for the event. This is the third argument of `trackEvent`, not a key in `eventData`. | Use it when your own systems need an event-level reference. | `"evt_123"` |
+
+## Choose the event name
+
+Linkrunner shows the event name as the payment type and matches it exactly, letter case included, against your mappings. Use one name per kind of payment, such as `purchase` for one-time orders and `SUBSCRIPTION_RENEWED` for renewals, and keep it the same across releases.
+
+The 14 old payment types, such as `FIRST_PAYMENT`, `SUBSCRIPTION_RENEWED`, and `DEFAULT`, match your existing mappings only when sent in uppercase, exactly as before. If you already have mappings, reports, or affiliate templates for an old type, keep that name and nothing changes in the dashboard. Every name is stored exactly as sent, so `first_payment` is a new payment type, and any new name needs its own mapping. See [Old payment types](/sdk/migrate-to-track-event#old-payment-types) for the full list.
+
+## Payment status
+
+`status` tells Linkrunner whether money actually moved. Linkrunner stores every status, but only `PAYMENT_COMPLETED` counts as revenue and is sent to ad networks. This keeps an abandoned checkout or a declined card from showing up as revenue in your reports or training your ad campaigns on purchases that never happened.
+
+| Status | Use it when | Revenue and paying users | Ad networks | Analytics integrations |
+| --- | --- | --- | --- | --- |
+| `PAYMENT_INITIATED` | The user started paying, for example opened the checkout or payment sheet. | Stored, not added to revenue. | Not sent. | Sent. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded and the money is yours. | Added to revenue and counts toward paying users. | Sent if the event name is mapped. | Sent. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Not added to revenue. The amount is tracked separately as failed. | Not sent. | Sent. |
+| `PAYMENT_CANCELLED` | The user backed out before paying. | Not added to revenue. The amount is tracked separately as cancelled. | Not sent. | Sent. |
+
+### Status is part of the payment's identity
+
+Linkrunner treats the same `paymentId`, event name, and status as one payment. So:
+
+- Sending `PAYMENT_COMPLETED` twice for `order_123` records it once. Retries are safe.
+- Sending `PAYMENT_INITIATED` and then `PAYMENT_COMPLETED` for `order_123` records two entries. Only the completed one is revenue and reaches ad networks.
+- Sending `order_123` under two different event names records two payments. Pick one name per payment.
+
+### A payment lifecycle
+
+Here is one checkout from start to finish, all with `paymentId` `order_123` and event name `purchase`:
+
+1. The user taps **Pay**. You send `PAYMENT_INITIATED`. It appears in the Events Log, revenue does not change, and ad networks get nothing.
+2. The charge succeeds. You send `PAYMENT_COMPLETED`. Revenue goes up by the amount, the user counts as a paying user, and mapped ad networks receive the purchase.
+3. If the charge fails instead, you send `PAYMENT_FAILED`. If the user closes the payment sheet, you send `PAYMENT_CANCELLED`. Either way, revenue does not change and ad networks get nothing.
+
+You only need the full lifecycle if you want to see drop-off between starting and finishing a payment. If your app only learns about the payment once it has succeeded, such as a store purchase callback, send one call and leave out `status`. It defaults to `PAYMENT_COMPLETED`.
+
+## Examples
+
+All examples use synthetic values. Replace them with your own order ID, user ID, amount, currency, and products. Each SDK reports an invalid payment differently. The examples handle it, and [Handle invalid payments](#handle-invalid-payments) summarizes the behavior.
+
+Every example sends product details in `eventData`. Meta receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id` for [Meta Commerce Manager](/ecommerce-manager/meta-commerce-manager#understanding-event_data), and Google receives `content_ids` as item IDs for [Google Commerce Manager](/ecommerce-manager/google-commerce-manager). `amount` stays the order total.
+
+`status` is a string such as `"PAYMENT_COMPLETED"`. In React Native, Expo, Capacitor, and Cordova, `PaymentStatus` is exported by the package with the same strings. On Android and Flutter, `PaymentStatus.X.name` gives the string, and on iOS `PaymentStatus.x.rawValue` does.
+
+### One-step purchase with products
+
+Use this when your app learns about a payment only after it succeeds. `status` is left out, so the payment is `PAYMENT_COMPLETED`.
+
+
+
+ Call it from your purchase success handler. `trackEvent` is a `suspend` function, so run it in a coroutine.
+
+ ```kotlin
+ import io.linkrunner.sdk.LinkRunner
+ import kotlinx.coroutines.CoroutineScope
+ import kotlinx.coroutines.Dispatchers
+ import kotlinx.coroutines.launch
+
+ fun onPurchaseSucceeded(orderId: String, userId: String) {
+ CoroutineScope(Dispatchers.IO).launch {
+ LinkRunner.getInstance().trackEvent(
+ eventName = "purchase", // becomes the payment type in reports and mappings
+ eventData = mapOf(
+ "paymentId" to orderId, // your order ID, used to deduplicate
+ "userId" to userId, // required, links the payment to your user
+ "amount" to 2298.0, // order total the user paid
+ "currency" to "INR", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ "content_ids" to listOf("sku_blue_tshirt_m", "sku_black_jeans_32"),
+ "contents" to listOf(
+ mapOf("id" to "sku_blue_tshirt_m", "quantity" to 1, "item_price" to 799),
+ mapOf("id" to "sku_black_jeans_32", "quantity" to 1, "item_price" to 1499)
+ ),
+ "content_type" to "product",
+ "num_items" to 2,
+ "order_id" to orderId
+ )
+ ).onFailure { error ->
+ println("Payment not tracked: ${error.message}")
+ }
+ }
+ }
+ ```
+
+ From Java, use `LinkrunnerJava.getInstance().trackEvent(eventName, eventData, eventId, callback)` with the same `eventData` keys and a `LinkRunnerCallback`.
+
+
+ Call it from your purchase success handler inside an `async` context.
+
+ ```swift
+ import LinkrunnerKit
+
+ func onPurchaseSucceeded(orderId: String, userId: String) async {
+ await LinkrunnerSDK.shared.trackEvent(
+ eventName: "purchase", // becomes the payment type in reports and mappings
+ eventData: [
+ "paymentId": orderId, // your order ID, used to deduplicate
+ "userId": userId, // required, links the payment to your user
+ "amount": 2298.0, // order total the user paid
+ "currency": "INR", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ "content_ids": ["sku_blue_tshirt_m", "sku_black_jeans_32"],
+ "contents": [
+ ["id": "sku_blue_tshirt_m", "quantity": 1, "item_price": 799],
+ ["id": "sku_black_jeans_32", "quantity": 1, "item_price": 1499]
+ ],
+ "content_type": "product",
+ "num_items": 2,
+ "order_id": orderId
+ ]
+ )
+ }
+ ```
+
+
+ ```javascript
+ import linkrunner from "rn-linkrunner";
+
+ export async function onPurchaseSucceeded(orderId, userId) {
+ try {
+ await linkrunner.trackEvent("purchase", { // becomes the payment type in reports and mappings
+ paymentId: orderId, // your order ID, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 2298, // order total the user paid
+ currency: "INR", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m", "sku_black_jeans_32"],
+ contents: [
+ { id: "sku_blue_tshirt_m", quantity: 1, item_price: 799 },
+ { id: "sku_black_jeans_32", quantity: 1, item_price: 1499 },
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: orderId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+ ```
+
+
+ ```dart
+ import 'package:flutter/services.dart';
+ import 'package:linkrunner/main.dart';
+
+ Future onPurchaseSucceeded(String orderId, String userId) async {
+ try {
+ await LinkRunner().trackEvent(
+ eventName: 'purchase', // becomes the payment type in reports and mappings
+ eventData: {
+ 'paymentId': orderId, // your order ID, used to deduplicate
+ 'userId': userId, // required, links the payment to your user
+ 'amount': 2298.0, // order total the user paid
+ 'currency': 'INR', // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ 'content_ids': ['sku_blue_tshirt_m', 'sku_black_jeans_32'],
+ 'contents': [
+ {'id': 'sku_blue_tshirt_m', 'quantity': 1, 'item_price': 799},
+ {'id': 'sku_black_jeans_32', 'quantity': 1, 'item_price': 1499},
+ ],
+ 'content_type': 'product',
+ 'num_items': 2,
+ 'order_id': orderId,
+ },
+ );
+ } on PlatformException catch (e) {
+ print('Payment not tracked: ${e.message}');
+ }
+ }
+ ```
+
+
+ Expo apps call `rn-linkrunner`, so the code is the same as React Native.
+
+ ```javascript
+ import linkrunner from "rn-linkrunner";
+
+ export async function onPurchaseSucceeded(orderId, userId) {
+ try {
+ await linkrunner.trackEvent("purchase", { // becomes the payment type in reports and mappings
+ paymentId: orderId, // your order ID, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 2298, // order total the user paid
+ currency: "INR", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m", "sku_black_jeans_32"],
+ contents: [
+ { id: "sku_blue_tshirt_m", quantity: 1, item_price: 799 },
+ { id: "sku_black_jeans_32", quantity: 1, item_price: 1499 },
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: orderId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+ ```
+
+
+ ```typescript
+ import linkrunner from "capacitor-linkrunner";
+
+ export async function onPurchaseSucceeded(orderId: string, userId: string) {
+ try {
+ await linkrunner.trackEvent("purchase", { // becomes the payment type in reports and mappings
+ paymentId: orderId, // your order ID, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 2298, // order total the user paid
+ currency: "INR", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m", "sku_black_jeans_32"],
+ contents: [
+ { id: "sku_blue_tshirt_m", quantity: 1, item_price: 799 },
+ { id: "sku_black_jeans_32", quantity: 1, item_price: 1499 },
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: orderId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+ ```
+
+
+ `linkrunner` is a global after the `deviceready` event, so no import is needed.
+
+ ```javascript
+ function onPurchaseSucceeded(orderId, userId) {
+ return linkrunner.trackEvent("purchase", { // becomes the payment type in reports and mappings
+ paymentId: orderId, // your order ID, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 2298, // order total the user paid
+ currency: "INR", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m", "sku_black_jeans_32"],
+ contents: [
+ { id: "sku_blue_tshirt_m", quantity: 1, item_price: 799 },
+ { id: "sku_black_jeans_32", quantity: 1, item_price: 1499 }
+ ],
+ content_type: "product",
+ num_items: 2,
+ order_id: orderId
+ }).catch(function (error) {
+ console.error("Payment not tracked:", error);
+ });
+ }
+ ```
+
+
+ Uses the `LinkrunnerSDK` wrapper from the [Unity guide](/sdk/unity#track-payments). Pass product details as a JSON string.
+
+ ```csharp
+ using UnityEngine;
+
+ public class Checkout : MonoBehaviour
+ {
+ public void OnPurchaseSucceeded(string orderId, string userId)
+ {
+ // Payment keys, then product details: Meta receives these fields, Google receives content_ids as item IDs
+ string eventDataJson =
+ "{\"amount\":2298.0," + // order total the user paid
+ "\"paymentId\":\"" + orderId + "\"," + // your order ID, used to deduplicate
+ "\"currency\":\"INR\"," + // required, 3-letter ISO 4217 code
+ "\"userId\":\"" + userId + "\"," + // required, links the payment to your user
+ "\"content_ids\":[\"sku_blue_tshirt_m\",\"sku_black_jeans_32\"]," +
+ "\"contents\":[{\"id\":\"sku_blue_tshirt_m\",\"quantity\":1,\"item_price\":799}," +
+ "{\"id\":\"sku_black_jeans_32\",\"quantity\":1,\"item_price\":1499}]," +
+ "\"content_type\":\"product\",\"num_items\":2,\"order_id\":\"" + orderId + "\"}";
+
+ LinkrunnerSDK.TrackEvent("purchase", eventDataJson); // becomes the payment type in reports and mappings
+ }
+ }
+ ```
+
+
+
+### Payment lifecycle with status
+
+Use this when you want to see started, failed, and cancelled payments next to completed ones. Send `PAYMENT_INITIATED` when checkout starts, then one final status with the same `paymentId`, event name, and product details. Only the `PAYMENT_COMPLETED` call adds revenue and reaches ad networks.
+
+
+
+ ```kotlin
+ import io.linkrunner.sdk.LinkRunner
+ import io.linkrunner.sdk.models.PaymentStatus
+ import kotlinx.coroutines.CoroutineScope
+ import kotlinx.coroutines.Dispatchers
+ import kotlinx.coroutines.launch
+
+ enum class CheckoutResult { SUCCEEDED, FAILED, CANCELLED }
+
+ fun trackCheckout(orderId: String, userId: String, status: PaymentStatus) {
+ CoroutineScope(Dispatchers.IO).launch {
+ LinkRunner.getInstance().trackEvent(
+ eventName = "purchase", // same name for every status of this payment
+ eventData = mapOf(
+ "paymentId" to orderId, // same ID for every status of this payment
+ "userId" to userId, // required, links the payment to your user
+ "amount" to 499.0, // order total the user pays
+ "currency" to "INR", // required, 3-letter ISO 4217 code
+ "status" to status.name, // only PAYMENT_COMPLETED counts as revenue and goes to ad networks
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ "content_ids" to listOf("sku_blue_tshirt_m"),
+ "contents" to listOf(mapOf("id" to "sku_blue_tshirt_m", "quantity" to 1, "item_price" to 499)),
+ "content_type" to "product",
+ "num_items" to 1,
+ "order_id" to orderId
+ )
+ ).onFailure { error ->
+ println("Payment not tracked: ${error.message}")
+ }
+ }
+ }
+
+ // When the user taps Pay
+ fun onCheckoutStarted(orderId: String, userId: String) =
+ trackCheckout(orderId, userId, PaymentStatus.PAYMENT_INITIATED)
+
+ // When your payment provider returns a result
+ fun onCheckoutFinished(orderId: String, userId: String, result: CheckoutResult) =
+ trackCheckout(
+ orderId,
+ userId,
+ when (result) {
+ CheckoutResult.SUCCEEDED -> PaymentStatus.PAYMENT_COMPLETED
+ CheckoutResult.FAILED -> PaymentStatus.PAYMENT_FAILED
+ CheckoutResult.CANCELLED -> PaymentStatus.PAYMENT_CANCELLED
+ }
+ )
+ ```
+
+
+ ```swift
+ import LinkrunnerKit
+
+ enum CheckoutResult { case succeeded, failed, cancelled }
+
+ func trackCheckout(orderId: String, userId: String, status: PaymentStatus) async {
+ await LinkrunnerSDK.shared.trackEvent(
+ eventName: "purchase", // same name for every status of this payment
+ eventData: [
+ "paymentId": orderId, // same ID for every status of this payment
+ "userId": userId, // required, links the payment to your user
+ "amount": 499.0, // order total the user pays
+ "currency": "INR", // required, 3-letter ISO 4217 code
+ "status": status.rawValue, // only .completed counts as revenue and goes to ad networks
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ "content_ids": ["sku_blue_tshirt_m"],
+ "contents": [["id": "sku_blue_tshirt_m", "quantity": 1, "item_price": 499]],
+ "content_type": "product",
+ "num_items": 1,
+ "order_id": orderId
+ ]
+ )
+ }
+
+ // When the user taps Pay
+ func onCheckoutStarted(orderId: String, userId: String) async {
+ await trackCheckout(orderId: orderId, userId: userId, status: .initiated)
+ }
+
+ // When your payment provider returns a result
+ func onCheckoutFinished(orderId: String, userId: String, result: CheckoutResult) async {
+ let status: PaymentStatus
+ switch result {
+ case .succeeded: status = .completed
+ case .failed: status = .failed
+ case .cancelled: status = .cancelled
+ }
+ await trackCheckout(orderId: orderId, userId: userId, status: status)
+ }
+ ```
+
+
+ ```javascript
+ import linkrunner, { PaymentStatus } from "rn-linkrunner";
+
+ async function trackCheckout(orderId, userId, status) {
+ try {
+ await linkrunner.trackEvent("purchase", { // same name for every status of this payment
+ paymentId: orderId, // same ID for every status of this payment
+ userId: userId, // required, links the payment to your user
+ amount: 499, // order total the user pays
+ currency: "INR", // required, 3-letter ISO 4217 code
+ status: status, // only PAYMENT_COMPLETED counts as revenue and goes to ad networks
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m"],
+ contents: [{ id: "sku_blue_tshirt_m", quantity: 1, item_price: 499 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: orderId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+
+ // When the user taps Pay
+ export const onCheckoutStarted = (orderId, userId) =>
+ trackCheckout(orderId, userId, PaymentStatus.PAYMENT_INITIATED);
+
+ // When your payment provider returns "succeeded", "failed", or "cancelled"
+ export const onCheckoutFinished = (orderId, userId, result) =>
+ trackCheckout(
+ orderId,
+ userId,
+ {
+ succeeded: PaymentStatus.PAYMENT_COMPLETED,
+ failed: PaymentStatus.PAYMENT_FAILED,
+ cancelled: PaymentStatus.PAYMENT_CANCELLED,
+ }[result],
+ );
+ ```
+
+
+ ```dart
+ import 'package:flutter/services.dart';
+ import 'package:linkrunner/main.dart';
+
+ enum CheckoutResult { succeeded, failed, cancelled }
+
+ Future trackCheckout(String orderId, String userId, PaymentStatus status) async {
+ try {
+ await LinkRunner().trackEvent(
+ eventName: 'purchase', // same name for every status of this payment
+ eventData: {
+ 'paymentId': orderId, // same ID for every status of this payment
+ 'userId': userId, // required, links the payment to your user
+ 'amount': 499.0, // order total the user pays
+ 'currency': 'INR', // required, 3-letter ISO 4217 code
+ 'status': status.name, // only PAYMENT_COMPLETED counts as revenue and goes to ad networks
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ 'content_ids': ['sku_blue_tshirt_m'],
+ 'contents': [
+ {'id': 'sku_blue_tshirt_m', 'quantity': 1, 'item_price': 499},
+ ],
+ 'content_type': 'product',
+ 'num_items': 1,
+ 'order_id': orderId,
+ },
+ );
+ } on PlatformException catch (e) {
+ print('Payment not tracked: ${e.message}');
+ }
+ }
+
+ // When the user taps Pay
+ Future onCheckoutStarted(String orderId, String userId) =>
+ trackCheckout(orderId, userId, PaymentStatus.PAYMENT_INITIATED);
+
+ // When your payment provider returns a result
+ Future onCheckoutFinished(String orderId, String userId, CheckoutResult result) =>
+ trackCheckout(orderId, userId, const {
+ CheckoutResult.succeeded: PaymentStatus.PAYMENT_COMPLETED,
+ CheckoutResult.failed: PaymentStatus.PAYMENT_FAILED,
+ CheckoutResult.cancelled: PaymentStatus.PAYMENT_CANCELLED,
+ }[result]!);
+ ```
+
+
+ Expo apps call `rn-linkrunner`, so the code is the same as React Native.
+
+ ```javascript
+ import linkrunner, { PaymentStatus } from "rn-linkrunner";
+
+ async function trackCheckout(orderId, userId, status) {
+ try {
+ await linkrunner.trackEvent("purchase", { // same name for every status of this payment
+ paymentId: orderId, // same ID for every status of this payment
+ userId: userId, // required, links the payment to your user
+ amount: 499, // order total the user pays
+ currency: "INR", // required, 3-letter ISO 4217 code
+ status: status, // only PAYMENT_COMPLETED counts as revenue and goes to ad networks
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m"],
+ contents: [{ id: "sku_blue_tshirt_m", quantity: 1, item_price: 499 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: orderId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+
+ // When the user taps Pay
+ export const onCheckoutStarted = (orderId, userId) =>
+ trackCheckout(orderId, userId, PaymentStatus.PAYMENT_INITIATED);
+
+ // When your payment provider returns "succeeded", "failed", or "cancelled"
+ export const onCheckoutFinished = (orderId, userId, result) =>
+ trackCheckout(
+ orderId,
+ userId,
+ {
+ succeeded: PaymentStatus.PAYMENT_COMPLETED,
+ failed: PaymentStatus.PAYMENT_FAILED,
+ cancelled: PaymentStatus.PAYMENT_CANCELLED,
+ }[result],
+ );
+ ```
+
+
+ ```typescript
+ import linkrunner, { PaymentStatus } from "capacitor-linkrunner";
+
+ type CheckoutResult = "succeeded" | "failed" | "cancelled";
+
+ async function trackCheckout(orderId: string, userId: string, status: PaymentStatus) {
+ try {
+ await linkrunner.trackEvent("purchase", { // same name for every status of this payment
+ paymentId: orderId, // same ID for every status of this payment
+ userId: userId, // required, links the payment to your user
+ amount: 499, // order total the user pays
+ currency: "INR", // required, 3-letter ISO 4217 code
+ status: status, // only PAYMENT_COMPLETED counts as revenue and goes to ad networks
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m"],
+ contents: [{ id: "sku_blue_tshirt_m", quantity: 1, item_price: 499 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: orderId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+
+ // When the user taps Pay
+ export const onCheckoutStarted = (orderId: string, userId: string) =>
+ trackCheckout(orderId, userId, PaymentStatus.PAYMENT_INITIATED);
+
+ // When your payment provider returns a result
+ export const onCheckoutFinished = (orderId: string, userId: string, result: CheckoutResult) =>
+ trackCheckout(orderId, userId, {
+ succeeded: PaymentStatus.PAYMENT_COMPLETED,
+ failed: PaymentStatus.PAYMENT_FAILED,
+ cancelled: PaymentStatus.PAYMENT_CANCELLED,
+ }[result]);
+ ```
+
+
+ ```javascript
+ function trackCheckout(orderId, userId, status) {
+ return linkrunner.trackEvent("purchase", { // same name for every status of this payment
+ paymentId: orderId, // same ID for every status of this payment
+ userId: userId, // required, links the payment to your user
+ amount: 499, // order total the user pays
+ currency: "INR", // required, 3-letter ISO 4217 code
+ status: status, // only PAYMENT_COMPLETED counts as revenue and goes to ad networks
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["sku_blue_tshirt_m"],
+ contents: [{ id: "sku_blue_tshirt_m", quantity: 1, item_price: 499 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: orderId
+ }).catch(function (error) {
+ console.error("Payment not tracked:", error);
+ });
+ }
+
+ // When the user taps Pay
+ function onCheckoutStarted(orderId, userId) {
+ return trackCheckout(orderId, userId, linkrunner.PaymentStatus.PAYMENT_INITIATED);
+ }
+
+ // When your payment provider returns "succeeded", "failed", or "cancelled"
+ function onCheckoutFinished(orderId, userId, result) {
+ var status = {
+ succeeded: linkrunner.PaymentStatus.PAYMENT_COMPLETED,
+ failed: linkrunner.PaymentStatus.PAYMENT_FAILED,
+ cancelled: linkrunner.PaymentStatus.PAYMENT_CANCELLED
+ }[result];
+ return trackCheckout(orderId, userId, status);
+ }
+ ```
+
+
+ ```csharp
+ using UnityEngine;
+
+ public enum CheckoutResult { Succeeded, Failed, Cancelled }
+
+ public class Checkout : MonoBehaviour
+ {
+ void TrackCheckout(string orderId, string userId, string status)
+ {
+ // Payment keys, then product details: Meta receives these fields, Google receives content_ids as item IDs
+ string eventDataJson =
+ "{\"amount\":499.0," + // order total the user pays
+ "\"paymentId\":\"" + orderId + "\"," + // same ID for every status of this payment
+ "\"currency\":\"INR\"," + // required, 3-letter ISO 4217 code
+ "\"userId\":\"" + userId + "\"," + // required, links the payment to your user
+ "\"status\":\"" + status + "\"," + // only PAYMENT_COMPLETED counts as revenue and goes to ad networks
+ "\"content_ids\":[\"sku_blue_tshirt_m\"]," +
+ "\"contents\":[{\"id\":\"sku_blue_tshirt_m\",\"quantity\":1,\"item_price\":499}]," +
+ "\"content_type\":\"product\",\"num_items\":1,\"order_id\":\"" + orderId + "\"}";
+
+ LinkrunnerSDK.TrackEvent("purchase", eventDataJson); // same name for every status of this payment
+ }
+
+ // When the user taps Pay
+ public void OnCheckoutStarted(string orderId, string userId) =>
+ TrackCheckout(orderId, userId, "PAYMENT_INITIATED");
+
+ // When your payment provider returns a result
+ public void OnCheckoutFinished(string orderId, string userId, CheckoutResult result)
+ {
+ string status = result switch
+ {
+ CheckoutResult.Succeeded => "PAYMENT_COMPLETED",
+ CheckoutResult.Failed => "PAYMENT_FAILED",
+ _ => "PAYMENT_CANCELLED",
+ };
+ TrackCheckout(orderId, userId, status);
+ }
+ }
+ ```
+
+
+
+### Subscription renewal with an old payment type
+
+Use this when you already have mappings or affiliate templates for an old payment type. `SUBSCRIPTION_RENEWED` is an old type, so it keeps matching those mappings with no dashboard change. Use the store's transaction ID for each renewal as `paymentId`, so each renewal is a separate payment. The plan is sent as the product.
+
+
+
+ ```kotlin
+ import io.linkrunner.sdk.LinkRunner
+ import kotlinx.coroutines.CoroutineScope
+ import kotlinx.coroutines.Dispatchers
+ import kotlinx.coroutines.launch
+
+ fun onSubscriptionRenewed(transactionId: String, userId: String) {
+ CoroutineScope(Dispatchers.IO).launch {
+ LinkRunner.getInstance().trackEvent(
+ eventName = "SUBSCRIPTION_RENEWED", // old type name, keeps your existing mappings
+ eventData = mapOf(
+ "paymentId" to transactionId, // a new ID for every renewal, used to deduplicate
+ "userId" to userId, // required, links the payment to your user
+ "amount" to 4.99, // total the user paid for this renewal
+ "currency" to "USD", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ "content_ids" to listOf("pro_monthly"),
+ "contents" to listOf(mapOf("id" to "pro_monthly", "quantity" to 1, "item_price" to 4.99)),
+ "content_type" to "product",
+ "num_items" to 1,
+ "order_id" to transactionId
+ )
+ ).onFailure { error ->
+ println("Payment not tracked: ${error.message}")
+ }
+ }
+ }
+ ```
+
+
+ ```swift
+ import LinkrunnerKit
+
+ func onSubscriptionRenewed(transactionId: String, userId: String) async {
+ await LinkrunnerSDK.shared.trackEvent(
+ eventName: "SUBSCRIPTION_RENEWED", // old type name, keeps your existing mappings
+ eventData: [
+ "paymentId": transactionId, // a new ID for every renewal, used to deduplicate
+ "userId": userId, // required, links the payment to your user
+ "amount": 4.99, // total the user paid for this renewal
+ "currency": "USD", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ "content_ids": ["pro_monthly"],
+ "contents": [["id": "pro_monthly", "quantity": 1, "item_price": 4.99]],
+ "content_type": "product",
+ "num_items": 1,
+ "order_id": transactionId
+ ]
+ )
+ }
+ ```
+
+
+ ```javascript
+ import linkrunner from "rn-linkrunner";
+
+ export async function onSubscriptionRenewed(transactionId, userId) {
+ try {
+ await linkrunner.trackEvent("SUBSCRIPTION_RENEWED", { // old type name, keeps your existing mappings
+ paymentId: transactionId, // a new ID for every renewal, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 4.99, // total the user paid for this renewal
+ currency: "USD", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["pro_monthly"],
+ contents: [{ id: "pro_monthly", quantity: 1, item_price: 4.99 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: transactionId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+ ```
+
+
+ ```dart
+ import 'package:flutter/services.dart';
+ import 'package:linkrunner/main.dart';
+
+ Future onSubscriptionRenewed(String transactionId, String userId) async {
+ try {
+ await LinkRunner().trackEvent(
+ eventName: 'SUBSCRIPTION_RENEWED', // old type name, keeps your existing mappings
+ eventData: {
+ 'paymentId': transactionId, // a new ID for every renewal, used to deduplicate
+ 'userId': userId, // required, links the payment to your user
+ 'amount': 4.99, // total the user paid for this renewal
+ 'currency': 'USD', // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ 'content_ids': ['pro_monthly'],
+ 'contents': [
+ {'id': 'pro_monthly', 'quantity': 1, 'item_price': 4.99},
+ ],
+ 'content_type': 'product',
+ 'num_items': 1,
+ 'order_id': transactionId,
+ },
+ );
+ } on PlatformException catch (e) {
+ print('Payment not tracked: ${e.message}');
+ }
+ }
+ ```
+
+
+ Expo apps call `rn-linkrunner`, so the code is the same as React Native.
+
+ ```javascript
+ import linkrunner from "rn-linkrunner";
+
+ export async function onSubscriptionRenewed(transactionId, userId) {
+ try {
+ await linkrunner.trackEvent("SUBSCRIPTION_RENEWED", { // old type name, keeps your existing mappings
+ paymentId: transactionId, // a new ID for every renewal, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 4.99, // total the user paid for this renewal
+ currency: "USD", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["pro_monthly"],
+ contents: [{ id: "pro_monthly", quantity: 1, item_price: 4.99 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: transactionId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+ ```
+
+
+ ```typescript
+ import linkrunner from "capacitor-linkrunner";
+
+ export async function onSubscriptionRenewed(transactionId: string, userId: string) {
+ try {
+ await linkrunner.trackEvent("SUBSCRIPTION_RENEWED", { // old type name, keeps your existing mappings
+ paymentId: transactionId, // a new ID for every renewal, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 4.99, // total the user paid for this renewal
+ currency: "USD", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["pro_monthly"],
+ contents: [{ id: "pro_monthly", quantity: 1, item_price: 4.99 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: transactionId,
+ });
+ } catch (error) {
+ console.error("Payment not tracked:", error);
+ }
+ }
+ ```
+
+
+ ```javascript
+ function onSubscriptionRenewed(transactionId, userId) {
+ return linkrunner.trackEvent("SUBSCRIPTION_RENEWED", { // old type name, keeps your existing mappings
+ paymentId: transactionId, // a new ID for every renewal, used to deduplicate
+ userId: userId, // required, links the payment to your user
+ amount: 4.99, // total the user paid for this renewal
+ currency: "USD", // required, 3-letter ISO 4217 code
+ // Product details: Meta receives these fields, Google receives content_ids as item IDs
+ content_ids: ["pro_monthly"],
+ contents: [{ id: "pro_monthly", quantity: 1, item_price: 4.99 }],
+ content_type: "product",
+ num_items: 1,
+ order_id: transactionId
+ }).catch(function (error) {
+ console.error("Payment not tracked:", error);
+ });
+ }
+ ```
+
+
+ ```csharp
+ using UnityEngine;
+
+ public class Subscriptions : MonoBehaviour
+ {
+ public void OnSubscriptionRenewed(string transactionId, string userId)
+ {
+ // Payment keys, then product details: Meta receives these fields, Google receives content_ids as item IDs
+ string eventDataJson =
+ "{\"amount\":4.99," + // total the user paid for this renewal
+ "\"paymentId\":\"" + transactionId + "\"," + // a new ID for every renewal, used to deduplicate
+ "\"currency\":\"USD\"," + // required, 3-letter ISO 4217 code
+ "\"userId\":\"" + userId + "\"," + // required, links the payment to your user
+ "\"content_ids\":[\"pro_monthly\"]," +
+ "\"contents\":[{\"id\":\"pro_monthly\",\"quantity\":1,\"item_price\":4.99}]," +
+ "\"content_type\":\"product\",\"num_items\":1,\"order_id\":\"" + transactionId + "\"}";
+
+ LinkrunnerSDK.TrackEvent("SUBSCRIPTION_RENEWED", eventDataJson); // old type name, keeps your existing mappings
+ }
+ }
+ ```
+
+
+
+### Handle invalid payments
+
+The SDK validates every payment before sending it. A `paymentId` sent without `amount` or `currency`, a blank `eventName` or `paymentId`, a negative or non-finite `amount`, a `currency` that is not a 3-letter code, or an unknown `status` stops the payment on the device, and nothing reaches Linkrunner. An `amount` or `currency` without `paymentId` is not an error: the event is sent as a normal custom event. How your code finds out depends on the SDK:
+
+| Platform | What happens on an invalid payment |
+| --- | --- |
+| Android (Kotlin) | `trackEvent` returns a failed `Result` holding an `IllegalArgumentException`. It does not throw. Use `onFailure`. |
+| Android (Java) | `LinkrunnerJava.getInstance().trackEvent(...)` calls `onError` on the `LinkRunnerCallback` you pass. |
+| iOS | `trackEvent` does not throw. The payment is dropped, and debug builds print the reason to the console. |
+| React Native and Expo | On Android, `trackEvent` rejects with the reason. Wrap it in `try`/`catch`. On iOS it does not throw: the payment is dropped and debug builds log the reason. |
+| Flutter | On Android, `trackEvent` throws `PlatformException` with code `TRACK_EVENT_FAILED` and the reason as the message. On iOS it does not throw: the payment is dropped and debug builds log the reason. |
+| Capacitor | On Android, the returned promise rejects with code `TRACK_EVENT_FAILED`. On iOS it resolves: the payment is dropped and debug builds log the reason. |
+| Cordova | On Android, the returned promise rejects. On iOS it resolves: the payment is dropped and debug builds log the reason. |
+| Unity | On Android, `LinkrunnerSDK.OnEventTracked` reports `false` with the error. On iOS the payment is dropped and debug builds log the reason. |
+
+Validation only proves the payment was well formed. Check the [Events Log](#verify-it-works) to confirm Linkrunner received it.
+
+## Send revenue to ad networks
+
+A payment reaches an ad network only when all of these are true:
+
+1. The status is `PAYMENT_COMPLETED`.
+2. The event name has a mapping for that network in the dashboard, such as [Meta event mapping](/ad-networks/meta-ads#2-set-up-event-mapping) or [Google Ads event mapping](/ad-networks/google-ads#3-set-up-event-mapping). The name must match exactly, unless it is an old payment type.
+
+If a payment arrives before the user's install is attributed, Linkrunner holds it and sends it once the install is attributed.
+
+The value and currency are sent only when **Send Revenue** is turned on for that mapping. Otherwise the network gets the conversion without a value.
+
+What the networks receive:
+
+- **Value and currency** come from the payment `amount` and `currency`. Send the order total as `amount`. Linkrunner does not add up item prices. An `amount` without `paymentId` and `currency` is ordinary event data and is not sent as revenue. On iOS, it still counts toward SKAN conversion values.
+- **Meta** receives `content_ids`, `contents`, `content_type`, `num_items`, and `order_id` from `eventData`. See [Meta Commerce Manager](/ecommerce-manager/meta-commerce-manager).
+- **Google** receives `content_ids` from `eventData` as item IDs. See [Google Commerce Manager](/ecommerce-manager/google-commerce-manager).
+
+## Deduplication and retries
+
+Linkrunner keeps one payment per `paymentId`, event name, and status. Use this to make retries safe:
+
+- Use your real order or transaction ID as `paymentId`, not a random value generated on each call. Then a retry after a network error, an app restart, or a store callback that fires twice records one payment.
+- Use a new `paymentId` for each real charge. Every subscription renewal is its own payment.
+- Send each payment from one place. If both your app and your backend send the same purchase, use the same `paymentId` and event name from both, or pick one source.
+
+## Verify it works
+
+
+
+ Use a debug build with an attributed test user. See [SDK Integration Testing](/testing/integration-testing) to get one. Use a new `paymentId`, such as `lr_test_payment_001`.
+
+
+ Open [**Events → Events Log**](https://dashboard.linkrunner.io/dashboard/events) and filter by your event name or test user. Expand the row. It should show **Event Type** as a payment, with the **Amount**, **Currency**, **Payment ID**, and **Payment Status** you sent.
+
+
+ For a `PAYMENT_COMPLETED` payment with a mapped event name, check the network. For Meta, look in **Events Manager → Test Events** or **Overview**. See [Test Custom Events and Payments](/testing/event-and-revenue-testing) for the full test flow.
+
+
+
+### Common problems
+
+
+
+ The SDK may have rejected the payment. Check how your SDK reports it in [Handle invalid payments](#handle-invalid-payments). If the call succeeded, confirm the SDK is initialized with the right project token and the test device finished the [attribution test flow](/testing/integration-testing).
+
+
+ Check that the status is `PAYMENT_COMPLETED` and that the event name has a mapping for that network with the exact same name. If the install was attributed only after the payment arrived, the payment is sent once attribution completes.
+
+
+ Turn on **Send Revenue** for that event's mapping, and send the order total as `amount` together with `paymentId` and `currency`.
+
+
+ Open the payment in the Events Log and compare **Amount** and **Currency** with what you meant to send. A wrong `currency` code, or an amount in the wrong unit, such as cents instead of dollars, makes the revenue wrong in reports and on ad networks.
+
+
+ Only `PAYMENT_COMPLETED` payments count as revenue. Check **Payment Status** on the missing rows. Also check that a second payment did not reuse the `paymentId`, event name, and status of an earlier one.
+
+
+ `amount, payment_id and currency must be sent together` means the request had only one or two of the three payment fields. Send all three. See [Payment fields](/api-reference/event-capture#payment-fields).
+
+
+
+## FAQ
+
+
+
+ `removePayment` was removed in the SDK versions above. To record a refund or a cancellation, send the same event name and `paymentId` again with `status` set to `PAYMENT_CANCELLED`. That payment no longer counts as revenue.
+
+
+ `capturePayment` keeps working on the older versions. When you upgrade, follow [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
+
+ Yes. Send them to `POST /api/v1/capture-event` with `amount`, `payment_id`, and `currency`. The same naming, status, and deduplication rules apply. See the [Revenue Tracking API](/api-reference/revenue-tracking).
+
+
+ No. `trackEvent` is the only method. Add `amount`, `paymentId`, `currency`, and `userId` to `eventData` and the event is a payment. Leave them out and it is a custom event.
+
+
+
+**Need help?** Contact [support@linkrunner.io](mailto:support@linkrunner.io).
diff --git a/sdk/unity.mdx b/sdk/unity.mdx
index 625d109b..4035f2b8 100644
--- a/sdk/unity.mdx
+++ b/sdk/unity.mdx
@@ -59,7 +59,7 @@ In your Unity project, create or edit the file `Assets/Plugins/Android/mainTempl
```gradle
dependencies {
- implementation 'io.linkrunner:android-sdk:4.4.0'
+ implementation 'io.linkrunner:android-sdk:5.0.0'
}
```
@@ -117,11 +117,7 @@ import io.linkrunner.sdk.LinkRunnerCallback;
import io.linkrunner.sdk.models.ConsentStatus;
import io.linkrunner.sdk.models.LinkrunnerConsent;
import io.linkrunner.sdk.models.request.UserDataRequest;
-import io.linkrunner.sdk.models.request.CapturePaymentRequest;
-import io.linkrunner.sdk.models.request.RemovePaymentRequest;
import io.linkrunner.sdk.models.IntegrationData;
-import io.linkrunner.sdk.models.PaymentType;
-import io.linkrunner.sdk.models.PaymentStatus;
import io.linkrunner.sdk.models.response.AttributionData;
import org.json.JSONObject;
@@ -157,32 +153,6 @@ public class LinkrunnerBridge {
return map;
}
- private static PaymentType parsePaymentType(String type) {
- if (type == null) return PaymentType.DEFAULT;
- switch (type) {
- case "FIRST_PAYMENT": return PaymentType.FIRST_PAYMENT;
- case "SECOND_PAYMENT": return PaymentType.SECOND_PAYMENT;
- case "WALLET_TOPUP": return PaymentType.WALLET_TOPUP;
- case "FUNDS_WITHDRAWAL": return PaymentType.FUNDS_WITHDRAWAL;
- case "SUBSCRIPTION_CREATED": return PaymentType.SUBSCRIPTION_CREATED;
- case "SUBSCRIPTION_RENEWED": return PaymentType.SUBSCRIPTION_RENEWED;
- case "ONE_TIME": return PaymentType.ONE_TIME;
- case "RECURRING": return PaymentType.RECURRING;
- default: return PaymentType.DEFAULT;
- }
- }
-
- private static PaymentStatus parsePaymentStatus(String status) {
- if (status == null) return PaymentStatus.PAYMENT_COMPLETED;
- switch (status) {
- case "PAYMENT_INITIATED": return PaymentStatus.PAYMENT_INITIATED;
- case "PAYMENT_COMPLETED": return PaymentStatus.PAYMENT_COMPLETED;
- case "PAYMENT_FAILED": return PaymentStatus.PAYMENT_FAILED;
- case "PAYMENT_CANCELLED": return PaymentStatus.PAYMENT_CANCELLED;
- default: return PaymentStatus.PAYMENT_COMPLETED;
- }
- }
-
// ---- Public API called from Unity C# ----
public static void initialize(String token, String secretKey, String keyId, boolean debug) {
@@ -327,52 +297,6 @@ public class LinkrunnerBridge {
);
}
- public static void capturePayment(String userId, double amount, String paymentId,
- String type, String status, String eventDataJson) {
- Map eventData = jsonToMap(eventDataJson);
- CapturePaymentRequest request = new CapturePaymentRequest(
- paymentId.isEmpty() ? null : paymentId,
- userId,
- amount,
- parsePaymentType(type),
- parsePaymentStatus(status),
- eventData.isEmpty() ? null : eventData
- );
- LinkrunnerJava lr = LinkrunnerJava.getInstance();
- lr.capturePayment(
- request,
- new LinkRunnerCallback() {
- public void onSuccess(Void result) {
- sendToUnity("OnCapturePaymentComplete", "success");
- }
- public void onError(String error) {
- Log.e(TAG, "CapturePayment failed: " + error);
- sendToUnity("OnCapturePaymentComplete", "error:" + error);
- }
- }
- );
- }
-
- public static void removePayment(String userId, String paymentId) {
- RemovePaymentRequest request = new RemovePaymentRequest(
- paymentId.isEmpty() ? null : paymentId,
- userId.isEmpty() ? null : userId
- );
- LinkrunnerJava lr = LinkrunnerJava.getInstance();
- lr.removePayment(
- request,
- new LinkRunnerCallback() {
- public void onSuccess(Void result) {
- sendToUnity("OnRemovePaymentComplete", "success");
- }
- public void onError(String error) {
- Log.e(TAG, "RemovePayment failed: " + error);
- sendToUnity("OnRemovePaymentComplete", "error:" + error);
- }
- }
- );
- }
-
public static void setPushToken(String pushToken) {
LinkrunnerJava lr = LinkrunnerJava.getInstance();
lr.setPushToken(pushToken, null);
@@ -465,7 +389,7 @@ After building your Unity project for iOS and opening the generated Xcode projec
```
https://github.com/linkrunner-labs/linkrunner-ios.git
```
-3. Select the latest version (4.3.1 or later)
+3. Select the latest version (5.0.0+)
4. Click **Add Package** and choose the **LinkrunnerKitStatic** library
@@ -560,30 +484,6 @@ private func parseUserData(_ dict: [String: Any]) -> UserData {
)
}
-private func parsePaymentType(_ str: String?) -> PaymentType {
- switch str {
- case "FIRST_PAYMENT": return .firstPayment
- case "SECOND_PAYMENT": return .secondPayment
- case "WALLET_TOPUP": return .walletTopup
- case "FUNDS_WITHDRAWAL": return .fundsWithdrawal
- case "SUBSCRIPTION_CREATED": return .subscriptionCreated
- case "SUBSCRIPTION_RENEWED": return .subscriptionRenewed
- case "ONE_TIME": return .oneTime
- case "RECURRING": return .recurring
- default: return .default
- }
-}
-
-private func parsePaymentStatus(_ str: String?) -> PaymentStatus {
- switch str {
- case "PAYMENT_INITIATED": return .initiated
- case "PAYMENT_COMPLETED": return .completed
- case "PAYMENT_FAILED": return .failed
- case "PAYMENT_CANCELLED": return .cancelled
- default: return .completed
- }
-}
-
// MARK: - C-callable functions exposed to Unity
@_cdecl("_LinkrunnerInitialize")
@@ -682,45 +582,6 @@ func linkrunnerTrackEvent(_ eventName: UnsafePointer,
}
}
-@_cdecl("_LinkrunnerCapturePayment")
-func linkrunnerCapturePayment(_ userId: UnsafePointer,
- _ amount: Double,
- _ paymentId: UnsafePointer?,
- _ type: UnsafePointer?,
- _ status: UnsafePointer?,
- _ eventDataJson: UnsafePointer?) {
- let uid = String(cString: userId)
- let pid = toOptionalString(paymentId)
- let paymentType = parsePaymentType(toOptionalString(type))
- let paymentStatus = parsePaymentStatus(toOptionalString(status))
- let eventData = toDict(eventDataJson)
-
- Task {
- await LinkrunnerSDK.shared.capturePayment(
- amount: amount,
- userId: uid,
- // LinkrunnerKit 4.x requires a payment id. An empty value is rejected by the SDK.
- paymentId: pid ?? "",
- type: paymentType,
- status: paymentStatus,
- eventData: eventData.isEmpty ? nil : eventData
- )
- sendToUnity("OnCapturePaymentComplete", "success")
- }
-}
-
-@_cdecl("_LinkrunnerRemovePayment")
-func linkrunnerRemovePayment(_ userId: UnsafePointer,
- _ paymentId: UnsafePointer?) {
- let uid = String(cString: userId)
- let pid = toOptionalString(paymentId)
-
- Task {
- await LinkrunnerSDK.shared.removePayment(userId: uid, paymentId: pid)
- sendToUnity("OnRemovePaymentComplete", "success")
- }
-}
-
@_cdecl("_LinkrunnerSetPushToken")
func linkrunnerSetPushToken(_ pushToken: UnsafePointer) {
let token = String(cString: pushToken)
@@ -894,8 +755,6 @@ public class LinkrunnerSDK : MonoBehaviour
public static event Action OnUserDataSet;
public static event Action OnAttributionData;
public static event Action OnEventTracked;
- public static event Action OnPaymentCaptured;
- public static event Action OnPaymentRemoved;
public static event Action OnDeeplinkHandled;
#if UNITY_IOS && !UNITY_EDITOR
@@ -912,11 +771,6 @@ public class LinkrunnerSDK : MonoBehaviour
private static extern void _LinkrunnerTrackEvent(string eventName, string eventDataJson,
string eventId);
[DllImport("__Internal")]
- private static extern void _LinkrunnerCapturePayment(string userId, double amount,
- string paymentId, string type, string status, string eventDataJson);
- [DllImport("__Internal")]
- private static extern void _LinkrunnerRemovePayment(string userId, string paymentId);
- [DllImport("__Internal")]
private static extern void _LinkrunnerSetPushToken(string pushToken);
[DllImport("__Internal")]
private static extern void _LinkrunnerSetAdditionalData(string clevertapId);
@@ -1018,7 +872,7 @@ public class LinkrunnerSDK : MonoBehaviour
}
///
- /// Track a custom event.
+ /// Track an in-app event. Event data with amount, paymentId and currency is tracked as a payment.
///
public static void TrackEvent(string eventName, string eventDataJson = "", string eventId = "")
{
@@ -1031,35 +885,6 @@ public class LinkrunnerSDK : MonoBehaviour
#endif
}
- ///
- /// Track a payment/revenue event.
- ///
- public static void CapturePayment(string userId, double amount, string paymentId,
- string type = "DEFAULT", string status = "PAYMENT_COMPLETED", string eventDataJson = "")
- {
-#if UNITY_ANDROID && !UNITY_EDITOR
- Bridge.CallStatic("capturePayment", userId, amount, paymentId ?? "", type, status, eventDataJson ?? "");
-#elif UNITY_IOS && !UNITY_EDITOR
- _LinkrunnerCapturePayment(userId, amount, paymentId, type, status, eventDataJson);
-#else
- Debug.Log($"[Linkrunner] CapturePayment: {userId}, {amount}");
-#endif
- }
-
- ///
- /// Remove a previously tracked payment (e.g. refund).
- ///
- public static void RemovePayment(string userId, string paymentId = "")
- {
-#if UNITY_ANDROID && !UNITY_EDITOR
- Bridge.CallStatic("removePayment", userId, paymentId ?? "");
-#elif UNITY_IOS && !UNITY_EDITOR
- _LinkrunnerRemovePayment(userId, paymentId);
-#else
- Debug.Log($"[Linkrunner] RemovePayment: {userId}");
-#endif
- }
-
///
/// Set the push notification token (Firebase/APNs).
///
@@ -1181,18 +1006,6 @@ public class LinkrunnerSDK : MonoBehaviour
OnEventTracked?.Invoke(success, message);
}
- void OnCapturePaymentComplete(string message)
- {
- bool success = message == "success";
- OnPaymentCaptured?.Invoke(success, message);
- }
-
- void OnRemovePaymentComplete(string message)
- {
- bool success = message == "success";
- OnPaymentRemoved?.Invoke(success, message);
- }
-
void OnHandleDeeplinkComplete(string message)
{
OnDeeplinkHandled?.Invoke(message);
@@ -1231,6 +1044,7 @@ public class LinkrunnerUserData
public string userCreatedAt;
public bool isFirstTimeUser;
}
+
```
---
@@ -1468,43 +1282,77 @@ LinkrunnerSDK.TrackEvent("purchase", eventData, eventId: "purchase_abc123");
```
-When tracking revenue-related events, the `amount` field must be a number (not a string) for ad network revenue optimization to work correctly.
+An `amount` or `currency` in the event data on its own is ordinary event data. To record revenue, send the payment keys described in [Track Payments](#track-payments).
-### Capture Payment
+### Track Payments
+
+A payment is an in-app event with revenue. Send it with `trackEvent`, and put `amount`, `paymentId`, and `currency` in the event data JSON. The event name becomes the payment type in reports and mappings, so you can use any name, such as `purchase` or `SUBSCRIPTION_RENEWED`. Requires the bridges above, built on Android SDK 5.0.0 and LinkrunnerKit 5.0.0 or later. For examples for every case and how payments reach ad networks, see the full [Track payments](/sdk/track-payments) guide.
+
+This example sends every payment key. `amount`, `paymentId`, `currency`, and `userId` are required for a payment.
```csharp
-LinkrunnerSDK.CapturePayment(
- userId: "user_123",
- amount: 9.99,
- paymentId: "pay_abc123",
- type: "FIRST_PAYMENT",
- status: "PAYMENT_COMPLETED"
-);
-
-// With event data
-string eventData = "{\"product_id\": \"prod_456\", \"currency\": \"USD\"}";
-LinkrunnerSDK.CapturePayment(
- userId: "user_123",
- amount: 49.99,
- paymentId: "pay_xyz789",
- type: "ONE_TIME",
- status: "PAYMENT_COMPLETED",
- eventDataJson: eventData
-);
+// Payment keys and product details in one JSON object
+string eventDataJson =
+ "{\"amount\":1499.0," + // Required for a payment: total the user paid, a finite number >= 0
+ "\"paymentId\":\"order_123\"," + // Required for a payment: your order or transaction ID, used to deduplicate
+ "\"currency\":\"INR\"," + // Required for a payment: 3-letter ISO 4217 code
+ "\"userId\":\"user_123\"," + // Required for a payment: your user ID. "" uses the user saved by Signup or SetUserData
+ "\"status\":\"PAYMENT_COMPLETED\"," + // Optional: defaults to PAYMENT_COMPLETED (see statuses below)
+ // Optional: any other keys stay as event data. Meta and Google read these product fields
+ "\"content_ids\":[\"sku_1\",\"sku_2\"]," +
+ "\"contents\":[{\"id\":\"sku_1\",\"quantity\":1,\"item_price\":999}," +
+ "{\"id\":\"sku_2\",\"quantity\":1,\"item_price\":500}]," +
+ "\"content_type\":\"product\",\"num_items\":2,\"order_id\":\"order_123\"}";
+
+// Reports the result of every TrackEvent call. Subscribe once, for example in Awake
+LinkrunnerSDK.OnEventTracked += (success, message) =>
+{
+ // An invalid payment arrives here as success == false, and nothing is sent
+ if (!success) Debug.LogError($"Error tracking payment: {message}");
+};
+
+LinkrunnerSDK.TrackEvent("purchase", eventDataJson, eventId: "evt_order_123");
```
-**`paymentId`** is required and must be unique for each payment. It is used for idempotent deduplication, so retries cannot double count revenue.
+#### Payment keys in the event data
-**Payment types:** `FIRST_PAYMENT`, `SECOND_PAYMENT`, `WALLET_TOPUP`, `FUNDS_WITHDRAWAL`, `SUBSCRIPTION_CREATED`, `SUBSCRIPTION_RENEWED`, `ONE_TIME`, `RECURRING`, `DEFAULT`
+| Key | Type | Required | Description |
+| --- | ---- | -------- | ----------- |
+| `amount` | `number` | Yes | Total the user paid. Must be a finite number, 0 or greater. This is the revenue value. |
+| `paymentId` | `string` | Yes | Your order or transaction ID. Must not be blank. Used to recognize the same payment when it arrives again. |
+| `currency` | `string` | Yes | 3-letter ISO 4217 currency code, such as `INR` or `USD`. Case-insensitive, sent in uppercase. |
+| `userId` | `string` | Yes | Your ID for the user who paid. If you pass an empty string, the SDK uses the user saved by `signup` or `setUserData`. |
+| `status` | `string` | No | `PAYMENT_INITIATED`, `PAYMENT_COMPLETED` (default), `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`. |
+| Any other key | Any | No | Stays in the event data, such as product details for [Meta Commerce Manager](/ecommerce-manager/meta-commerce-manager). |
-**Payment statuses:** `PAYMENT_INITIATED`, `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, `PAYMENT_CANCELLED`
+How `trackEvent` decides:
-### Remove Payment (Refunds)
+- **`amount`, `paymentId`, and `currency` are all in `eventData`:** the event is a payment. The SDK checks the payment keys and sends them as payment fields.
+- **None of them, or `amount` or `currency` without `paymentId`:** the event is a normal custom event, exactly as before. An `amount` or `currency` key on its own is ordinary event data.
+- **`paymentId` with `amount` or `currency` missing:** The native SDK rejects the event, and `OnEventTracked` reports `false` with `amount, paymentId and currency must be sent together`, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
+- **A negative or non-numeric `amount`, a blank `paymentId`, a currency that is not 3 letters, or an unknown `status`:** The native SDK rejects the event, and `OnEventTracked` reports `false`, and nothing is sent. On iOS the payment is dropped instead and debug builds log the reason.
-```csharp
-LinkrunnerSDK.RemovePayment(userId: "user_123", paymentId: "pay_abc123");
-```
+#### Payment status
+
+`status` is a string in the event data JSON.
+
+| Status | Use it when | Effect |
+| ------ | ----------- | ------ |
+| `PAYMENT_INITIATED` | The user started paying, such as opening the checkout or payment sheet. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_COMPLETED` (default) | The charge succeeded. | Added to revenue and paying users. Sent to ad networks when the event name is mapped. |
+| `PAYMENT_FAILED` | The charge was declined or errored. | Stored for reports and analytics. Not added to revenue and not sent to ad networks. |
+| `PAYMENT_CANCELLED` | The user backed out before paying, or you refunded it. Send it with the same `paymentId` as the completed payment. | Stored for reports and analytics. The payment no longer counts as revenue and nothing is sent to ad networks. |
+
+Linkrunner keeps one payment per `paymentId`, event name, and status, so a retry with the same values is recorded once. See [Payment status](/sdk/track-payments#payment-status) for a full lifecycle example.
+
+
+The event name is stored exactly as you send it. If you used one of the old payment types, pass it in uppercase exactly as before, such as `FIRST_PAYMENT` or `DEFAULT`, so your existing mappings and reports keep working. A different spelling such as `first_payment` is a new payment type. Any other name, such as `purchase`, is also stored as sent, and you need to add a mapping for it in the dashboard before it reaches ad networks.
+
+
+
+`capturePayment` is deprecated. Bridges built against Android SDK 4.x or LinkrunnerKit 4.x keep using it and continue to work unchanged. To move to 5.0.0, see [Migrate payments to in-app events](/sdk/migrate-to-track-event).
+
### Push Token
@@ -1543,8 +1391,7 @@ LinkrunnerSDK.SetDisableAaidCollection(true);
| [`GetAttributionData`](/sdk/unity#get-attribution-data) | Optional | After init completes | When you need campaign/deeplink info |
| [`SetUserData`](/sdk/unity#set-user-data-on-app-open) | Optional | App start, authentication or profile update logic | Right after `Initialize` with your analytics IDs, and when user details become available |
| [`TrackEvent`](/sdk/unity#track-events) | Optional | Throughout the app | On user actions |
-| [`CapturePayment`](/sdk/unity#capture-payment) | Optional | Payment flow | When payment succeeds |
-| [`RemovePayment`](/sdk/unity#remove-payment-refunds) | Optional | Refund flow | When payment is reversed |
+| [`TrackEvent` with payment keys](/sdk/unity#track-payments) | Optional | Payment flow | When payment succeeds |
| [`SetPushToken`](/sdk/unity#push-token) | Optional | After token refresh | When push token changes |
| [`SetConsent`](/sdk/unity#set-consent) | Optional | Before `Initialize` | At launch and whenever consent changes |
| [`SetAdditionalData`](/sdk/unity#set-additional-data) | Optional | Integration setup | When the CleverTap ID is available |
diff --git a/testing/event-and-revenue-testing.mdx b/testing/event-and-revenue-testing.mdx
index 251faef1..ec1c04b0 100644
--- a/testing/event-and-revenue-testing.mdx
+++ b/testing/event-and-revenue-testing.mdx
@@ -12,8 +12,9 @@ Use this guide after [SDK Integration Testing](/testing/integration-testing). It
| What happened | App SDK | Server-side API |
| --- | --- | --- |
| A user action with no authoritative transaction | `trackEvent` | `POST /capture-event` |
-| A payment with an amount and unique transaction ID | `capturePayment` | `POST /capture-payment` |
-| A captured payment must be removed | `removePayment` | `POST /remove-payment` |
+| A payment with an amount and unique transaction ID | `trackEvent` with `amount`, `paymentId`, `currency`, and `userId` in the event data | `POST /capture-event` with `amount`, `payment_id`, and `currency` |
+
+When you attach a payment, the event name becomes the payment type in reports. See [Payment fields](/api-reference/event-capture#payment-fields) for the rules. For SDK examples, payment status, and how payments reach ad networks, see [Track payments](/sdk/track-payments).
The event name alone is not enough. If `purchase_completed` represents a real transaction, send it as a payment. If it only represents a funnel step with no authoritative transaction, send it as a custom event.
@@ -28,7 +29,7 @@ Do not send the same transaction as both a revenue-bearing custom event and a pa
- Purchase initiated when it only marks the start of checkout
- Payment failure shown for product analytics when you do not need a payment record
-### Usually send with `capturePayment`
+### Usually send as a payment
- Completed purchase
- First or second payment
@@ -38,7 +39,7 @@ Do not send the same transaction as both a revenue-bearing custom event and a pa
- A failed or cancelled transaction when you need it recorded with `PAYMENT_FAILED` or `PAYMENT_CANCELLED`
-If you record a failed or cancelled payment, send its final state once. Linkrunner deduplicates payments by the combination of `type` and `payment_id`, so a later call with the same combination is ignored.
+If you record a failed or cancelled payment, send its final state once. Linkrunner deduplicates payments by the combination of event name, `payment_id`, and `status`, so a later call with the same combination is ignored.
## Before you test
@@ -46,7 +47,7 @@ If you record a failed or cancelled payment, send its final state once. Linkrunn
- Complete the [click → install → signup flow](/testing/integration-testing) so the test user is attributed.
- Use the same `user_id` that your app registered through `signup`.
- Generate a fresh `payment_id` for each real transaction.
-- Send `amount` as a number in one reporting currency.
+- Send `amount` as a number, with its 3-letter ISO 4217 `currency`, such as `USD`.
- Pick one source for each payment, either the app SDK or your backend.
- For server-side tests, generate a server key from [**Settings → Data APIs**](https://dashboard.linkrunner.io/settings?p_id=4&s=data-apis).
@@ -58,10 +59,10 @@ If you record a failed or cancelled payment, send its final state once. Linkrunn
```text
user_id: your attributed test user
+ event_name: purchase_completed
payment_id: lr_test_payment_001
amount: a small test amount
- type: DEFAULT
- status: PAYMENT_COMPLETED
+ currency: USD
```
Use a new `payment_id` every time you test a new transaction.
@@ -86,22 +87,22 @@ If you record a failed or cancelled payment, send its final state once. Linkrunn
- Send the actual transaction through your SDK's `capturePayment` method or the [Revenue Tracking API](/api-reference/revenue-tracking).
+ Send the actual transaction through your SDK's `trackEvent` method with a payment, or the [Revenue Tracking API](/api-reference/revenue-tracking). Leaving out `status` records the payment as `PAYMENT_COMPLETED`.
```bash
- curl -X POST https://api.linkrunner.io/api/v1/capture-payment \
+ curl -X POST https://api.linkrunner.io/api/v1/capture-event \
-H "Content-Type: application/json" \
-H "linkrunner-key: YOUR-SERVER-KEY" \
-d '{
+ "event_name": "purchase_completed",
"user_id": "YOUR_ATTRIBUTED_USER_ID",
"payment_id": "lr_test_payment_001",
"amount": 1,
- "type": "DEFAULT",
- "status": "PAYMENT_COMPLETED"
+ "currency": "USD"
}'
```
- A successful API request returns HTTP `201`.
+ A successful API request returns HTTP `200` with `Event capture request received!`. A `400` with `amount, payment_id and currency must be sent together` means one of the three payment fields is missing.
@@ -115,7 +116,7 @@ If you record a failed or cancelled payment, send its final state once. Linkrunn
- Send the same payment again with the same `type` and `payment_id`.
+ Send the same payment again with the same event name, `payment_id`, and `status`.
Expected result: the Events Log still contains one payment for that combination. Linkrunner records the first request and ignores later duplicates.
@@ -135,18 +136,17 @@ If you record a failed or cancelled payment, send its final state once. Linkrunn
| `add_to_cart` | Custom event | `event_name`, `user_id`, product data |
| `checkout_started` | Custom event | `event_name`, `user_id` |
| `purchase_initiated` as a funnel step | Custom event | `event_name`, `user_id` |
-| Completed purchase | Payment | `user_id`, `payment_id`, `amount`, `type`, `status` |
-| Subscription renewal with a charge | Payment | `type: SUBSCRIPTION_RENEWED` |
-| Wallet top-up | Payment | `type: WALLET_TOPUP` |
+| Completed purchase | Payment | `event_name`, `user_id`, `payment_id`, `amount`, `currency` |
+| Subscription renewal with a charge | Payment | `event_name: SUBSCRIPTION_RENEWED` |
+| Wallet top-up | Payment | `event_name: WALLET_TOPUP` |
| Failed payment that must be recorded | Payment | `status: PAYMENT_FAILED` |
| Cancelled payment that must be recorded | Payment | `status: PAYMENT_CANCELLED` |
-| Refund or void of a captured payment | Remove payment | The original `payment_id` |
## Test Meta Purchase forwarding
Use this section only when the payment must reach Meta Commerce Manager.
-1. In Linkrunner, map the payment type you send, such as `DEFAULT` or `FIRST_PAYMENT`, to Meta's standard `Purchase` event.
+1. In Linkrunner, map the payment event name you send, such as `purchase_completed`, to Meta's standard `Purchase` event.
2. Include the required `event_data` fields for `Purchase`, including product IDs, `contents`, `content_type`, `value`, `currency`, `num_items`, and `order_id`.
3. Send a new payment with a new `payment_id`.
4. Check **Meta Commerce Manager → Events**. The real-time hit should appear within about 15 minutes. Full reporting can take a few days.
@@ -155,25 +155,6 @@ Use this section only when the payment must reach Meta Commerce Manager.
See [Meta Commerce Manager](/ecommerce-manager/meta-commerce-manager#understanding-event_data) for the full ecommerce payload.
-## Test payment removal
-
-Remove one test payment using its `payment_id`:
-
-```bash
-curl -X POST https://api.linkrunner.io/api/v1/remove-payment \
- -H "Content-Type: application/json" \
- -H "linkrunner-key: YOUR-SERVER-KEY" \
- -d '{ "payment_id": "lr_test_payment_001" }'
-```
-
-
-The API does not define a partial-refund adjustment flow. Contact support before using `removePayment` for a partial refund.
-
-
-
-Do not test removal with only `user_id` unless you intend to remove every payment attributed to that user.
-
-
## Troubleshooting
@@ -182,23 +163,23 @@ Do not test removal with only `user_id` unless you intend to remove every paymen
- The app or backend sent it through `trackEvent` or `/capture-event`. Send authoritative transactions through `capturePayment` or `/capture-payment` instead.
+ The app or backend sent it without all three payment fields. From the SDK, put `amount`, `paymentId`, and `currency` in the event data. From your backend, send `amount`, `payment_id`, and `currency` at the top level of the request. An `amount` without the other two is a custom event property.
- Check whether it reused the same `type` and `payment_id` as an earlier transaction. Linkrunner treats that combination as a duplicate.
+ Check whether it reused the same event name, `payment_id`, and `status` as an earlier transaction. Linkrunner treats that combination as a duplicate.
Confirm the app and backend are not both sending the transaction with different payment IDs. Choose one source, or use the same stable payment ID for safe retries.
-
- Linkrunner keeps the first record for a `type` and `payment_id` combination. Send the final payment state once instead of sending initiated and completed states with the same combination.
+
+ `status` is part of the deduplication key, so sending `PAYMENT_INITIATED` and then `PAYMENT_COMPLETED` with the same `payment_id` creates two records. This is expected when you track the payment lifecycle. Only the `PAYMENT_COMPLETED` record counts as revenue and is sent to ad networks. If you do not need the lifecycle, send only the final state. See [Payment status](/sdk/track-payments#payment-status).
- Confirm the payment type is mapped to `Purchase`, the ecommerce payload includes every required field, and the test has had at least 15 minutes to reach Meta Events Manager.
+ Confirm the payment event name is mapped to `Purchase`, the payment `status` is `PAYMENT_COMPLETED`, the ecommerce payload includes every required field, and the test has had at least 15 minutes to reach Meta Events Manager.