Appearance
Track payments with trackEvent
Record revenue as an in-app event with trackEvent, use payment status correctly, and send completed payments to your ad networks
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.
| 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 | 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
Your app calls trackEvent with payment keys
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 validates the payment
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).
Linkrunner receives and stores the payment
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 shows up in your dashboard
The payment appears in the Events Log with its amount, currency, payment ID, and status. Completed payments count toward revenue and paying users in your reports.
Completed payments go to your ad networks
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.
Analytics integrations receive every payment
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 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_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 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_COMPLETEDtwice fororder_123records it once. Retries are safe. - Sending
PAYMENT_INITIATEDand thenPAYMENT_COMPLETEDfororder_123records two entries. Only the completed one is revenue and reaches ad networks. - Sending
order_123under 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:
- The user taps Pay. You send
PAYMENT_INITIATED. It appears in the Events Log, revenue does not change, and ad networks get nothing. - 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. - If the charge fails instead, you send
PAYMENT_FAILED. If the user closes the payment sheet, you sendPAYMENT_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 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, and Google receives content_ids as item IDs for 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<Void>.
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
}
)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}")
}
}
}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 to confirm Linkrunner received it.
Send revenue to ad networks
A payment reaches an ad network only when all of these are true:
- The status is
PAYMENT_COMPLETED. - The event name has a mapping for that network in the dashboard, such as Meta event mapping or Google Ads 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
amountandcurrency. Send the order total asamount. Linkrunner does not add up item prices. AnamountwithoutpaymentIdandcurrencyis 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, andorder_idfromeventData. See Meta Commerce Manager. - Google receives
content_idsfromeventDataas item IDs. See 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
paymentIdfor 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
paymentIdand event name from both, or pick one source.
Verify it works
Send a test payment
Use a debug build with an attributed test user. See SDK Integration Testing to get one. Use a new paymentId, such as lr_test_payment_001.
Find it in the Events Log
Open Events → Events Log 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.
Check the ad network
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 for the full test flow.
Common problems
Nothing appears in the Events Log
The SDK may have rejected the payment. Check how your SDK reports it in 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.
The payment is in the Events Log but not on the ad network
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.
The ad network gets the conversion without a value
Turn on Send Revenue for that event's mapping, and send the order total as amount together with paymentId and currency.
Revenue looks wrong
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.
Revenue is missing for some payments
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.
A server-side payment shows up as a custom event
The request had only one or two of amount, payment_id, and currency. Linkrunner stores it as a custom event and returns 200. Send all three. See Payment fields.
FAQ
Can I refund or remove a payment?
removePayment is deprecated in the SDK versions above. It still works and shows a deprecation warning, and it will be removed in a future major version. To record a refund or a cancellation, call trackEvent with the same event name and paymentId and status set to PAYMENT_CANCELLED. That payment no longer counts as revenue.
I still call capturePayment
capturePayment keeps working on the older versions, and it is deprecated in the versions above: it still works and shows a deprecation warning, and it will be removed in a future major version. Move to trackEvent by following Migrate payments to in-app events.
Can I send payments from my backend?
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.
Is there a separate payment method?
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.