> ## Documentation Index
> Fetch the complete documentation index at: https://docs.linkrunner.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate payments to in-app events

Source: https://docs.linkrunner.io/sdk/migrate-to-track-event

Move from capturePayment to trackEvent, which sends each payment as an in-app event with revenue

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 deprecated. They still work in the [new versions](https://docs.linkrunner.io/sdk/migrate-to-track-event#sdk-versions) and show a deprecation warning in your IDE, and they will be removed in a future major version. Apps on older SDK versions keep working with no change. Move to `trackEvent` when you upgrade.

> **Warning:**
>
> **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.

## Migrate with your AI coding agent

If you use Claude Code, Cursor, GitHub Copilot, or Windsurf, install the migration skill in your app's repo:

```bash
npx @linkrunner/skills add migrate-payments
```

Then ask your agent:

```text
Migrate my Linkrunner capturePayment and removePayment calls to trackEvent.
```

The agent finds every payment call, upgrades the SDK to the right version, keeps each payment name exactly as before, and asks you for the currency of your payments. Review its changes and test one payment on a debug build, as in [How to migrate](https://docs.linkrunner.io/sdk/migrate-to-track-event#how-to-migrate). See [Linkrunner Agent Skills](https://docs.linkrunner.io/ai-agent-skills) for setup.

## 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.

**Android**

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](https://docs.linkrunner.io/sdk/android#tracking-payments).

**iOS**

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](https://docs.linkrunner.io/sdk/ios#tracking-payments).

**React Native**

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](https://docs.linkrunner.io/sdk/react-native#tracking-payments).

**Flutter**

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](https://docs.linkrunner.io/sdk/flutter#tracking-payments).

**Expo**

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](https://docs.linkrunner.io/sdk/expo#tracking-payments).

**Capacitor**

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](https://docs.linkrunner.io/sdk/capacitor#tracking-payments).

**Cordova**

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](https://docs.linkrunner.io/sdk/cordova#tracking-payments).

**Unity**

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](https://docs.linkrunner.io/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 replace `removePayment` calls as shown in [Refunds and removePayment](https://docs.linkrunner.io/sdk/migrate-to-track-event#refunds-and-removepayment), 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](https://docs.linkrunner.io/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 | Older versions | New versions (`trackEvent` payments, `capturePayment` deprecated) |
| --- | --- | --- | --- |
| 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](https://docs.linkrunner.io/sdk/unity#track-payments) | Bridges on native 4.x | Bridges on native 5.0.0 |

## Older SDK versions

Linkrunner also records a payment sent with `trackEvent` from an older SDK, so payments are not lost while your users update the app. The event data must have `paymentId`, `amount`, `currency`, and `userId`, spelled exactly like that.

Older SDKs send the event as it is and do not check it on the device, so upgrade when you can:

- If a key is missing or invalid, the event is stored as a custom event. The request still succeeds and your app sees no error. New SDK versions reject the call instead.
- `amount` must be a number of 0 or more, and `currency` a 3-letter ISO 4217 code.
- `status` is optional and must be exactly `PAYMENT_INITIATED`, `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, or `PAYMENT_CANCELLED`.
- `payment_id` in snake case is not read inside the event data.

> **Warning:**
>
> **Send each payment through one path only.** Use `capturePayment` or `trackEvent` for a payment, not both. The same `paymentId` sent through both under the same name is stored once. Under different names it counts as two payments, so your revenue is counted twice.

## Refunds and removePayment

`removePayment` is deprecated. It still works in the new versions and shows a deprecation warning, and it will be removed in a future major version. To record a refund or a cancelled booking, call `trackEvent` with the same event name, `paymentId`, `amount`, and `currency` as the original payment, and `status` set to `PAYMENT_CANCELLED`. All three payment fields are needed, or the SDK rejects the call. 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](https://docs.linkrunner.io/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).
