> This is Payabli documentation. For a complete page index, fetch https://docs.payabli.com/llms.txt — append .md to any page URL for lightweight markdown. For section-level indexes, query parameters, and other AI-optimized access methods, see https://docs.payabli.com/ai-agents.md

# Accept Tap to Pay payments

> Integrate Tap to Pay in your native iOS app, from paypoint registration through your first charge

This guide walks through integrating Tap to Pay using Payabli's mobile SDK. To learn how Tap to Pay works, see the [Tap to Pay overview](/guides/pay-in-tap-to-pay-overview).

## Prerequisites

Before you start, confirm you have:

* **A physical device.** An iPhone XS or newer running iOS 16.7 or later.
* **Payabli sandbox credentials.** A Payabli sandbox `entryPoint` with Tap to Pay enabled. Contact your Payabli representative if you don't have one yet.
* **Your app registered on the paypoint allowlist.** Your app's `appId` (`<TEAM_ID>.<BUNDLE_ID>`) must be authorized on the entrypoint before device attestation succeeds. See [Integration steps](/guides/pay-in-developer-tap-to-pay#integration-steps).
* **Apple's Tap to Pay entitlement.** `com.apple.developer.proximity-reader.payment.acceptance`. Request it through the [Apple Developer Portal](https://developer.apple.com/documentation/proximityreader/setting-up-the-entitlement-for-tap-to-pay-on-iphone).
* **Xcode 15 or later and Swift 5.9 or later.**

Apple's Tap to Pay entitlement (`com.apple.developer.proximity-reader.payment.acceptance`) needs Apple's approval before you can use it. Request it through the [Apple Developer Portal](https://developer.apple.com/documentation/proximityreader/setting-up-the-entitlement-for-tap-to-pay-on-iphone). Approval can take 2–8 weeks. Apply for this entitlement as soon as you decide to integrate Tap to Pay. Don't wait until the rest of your integration is ready.

Your app's provisioning profile also needs `com.apple.developer.devicecheck.appattest-environment`, set to `development` for development builds or `production` for release builds. This value must match the `environment` you pass to `PayabliTTP`.

## Integration steps

Register your app, set up a backend token proxy, then work through the SDK from installation to your first charge:

#### Register your app on the paypoint allowlist

Before your app can complete device attestation, authorize its `appId` (`<TEAM_ID>.<BUNDLE_ID>`) on the entrypoint's allowlist. Do this once per app, from your backend or a provisioning script.

```bash
curl -X POST "https://api-sandbox.payabli.com/api/v2/paypoint/{entryPoint}/apps" \
  -H "Content-Type: application/json" \
  -H "requestToken: $API_KEY" \
  -d '{
    "deviceOs": "ios",
    "appId": "TEAM123456.com.yourcompany.app",
    "friendlyName": "Terragon Field Tech iOS App"
  }'
```

`deviceOs` must be `"ios"`. `friendlyName` is optional. This endpoint is idempotent on `(entryPoint, deviceOs, appId)`, so it's safe to call from an automated provisioning script. If `appId` isn't registered, the SDK's `initialize()` method rejects attestation with `PayabliTTPError.attestationFailed`.

#### Build the backend token proxy

Tap to Pay uses the same access token model as the rest of the mobile SDK: your backend holds the Payabli `clientSecret`, exchanges it for a short-lived access token, and never exposes it to the app. See [Backend token endpoint](/guides/mobile-components-tap-to-pay#backend-token-endpoint) in the Tap to Pay component reference for more information.

#### Install the SDK

Add `PayabliSDKTapToPay` from the `sdk-ios` Swift package and link the `PayabliSDKTapToPay` product. See [Installation](/guides/mobile-components-tap-to-pay#installation) in the Tap to Pay component reference for more information.

#### Initialize and handle first-time activation

On a device's first launch, `initialize()` throws `PayabliTTPError.devicePendingActivation`. See [How first-time activation works](/guides/pay-in-tap-to-pay-overview#first-time-device-activation) for the full sequence. Handle it by prompting for a code and calling `activateDevice(activationCode:)`.

/// Add the import and token provider

Import `PayabliSDKTapToPay` and add a function that exchanges credentials with your backend for a Payabli access token. See [Integration steps](/guides/pay-in-developer-tap-to-pay#integration-steps) for the backend side of this exchange.

/// Create the PayabliTTP instance

Construct `PayabliTTP` with your `entryPoint`, the app's `appId` (`<TEAM_ID>.<BUNDLE_ID>`), and your token provider. The `environment` you pass here must match the `appattest-environment` entitlement in your provisioning profile.

/// Handle first-time activation

Call `initialize()` on launch. On a device's first run, it throws `PayabliTTPError.devicePendingActivation` instead of completing. Your backend requests a one-time activation code from Payabli and delivers it to the user. The SDK doesn't generate the code itself. Prompt the user for the code, call `activateDevice(activationCode:)`, then retry `initialize()`.

```swift
import SwiftUI
import PayabliSDKTapToPay

func fetchPayabliAccessToken() async throws -> String {
    var request = URLRequest(url: URL(string: "https://your-backend.example.com/payabli/token")!)
    request.httpMethod = "POST"

    struct Response: Decodable { let access_token: String }
    let (data, _) = try await URLSession.shared.data(for: request)
    return try JSONDecoder().decode(Response.self, from: data).access_token
}

@MainActor
final class TapToPayViewModel: ObservableObject {
    let ttp: PayabliTTP

    init(entryPoint: String, appId: String) async throws {
        ttp = PayabliTTP(
            accessToken: try await fetchPayabliAccessToken(),
            tokenProvider: {
                try await fetchPayabliAccessToken()
            },
            entryPoint: entryPoint,
            appId: appId,
            environment: .sandbox
        )
    }

    func start() async {
        do {
            try await ttp.initialize()
        } catch PayabliTTPError.devicePendingActivation {
            let code = await promptForActivationCode()
            do {
                try await ttp.activateDevice(activationCode: code)
                try await ttp.initialize()
            } catch {
                print("Activation failed:", error.localizedDescription)
            }
        } catch {
            print("Tap to Pay initialization failed:", error.localizedDescription)
        }
    }
}
```

#### Process a payment

Once `start()` completes without throwing, `ttp` is in the `.ready` state and can accept a charge:

```swift
func charge(amount: Decimal, invoiceNumber: String?) async {
    guard ttp.isReady else {
        print("Tap to Pay isn't ready yet.")
        return
    }
    do {
        let result = try await ttp.charge(
            type: .sale,
            paymentDetails: PayabliTTPPaymentDetails(amount: amount),
            invoice: PayabliTTPInvoiceData(invoiceNumber: invoiceNumber)
        )
        print("Transaction captured. ID:", result.paymentTransId)
        // Store result.paymentTransId alongside your order record.
    } catch {
        print("Charge failed:", error.localizedDescription)
    }
}
```

`charge(type:paymentDetails:customer:invoice:orderDescription:)` does three things in order: it calls `POST /MoneyIn/initiate` (which creates the transaction's `paymentTransId`), prompts the NFC card read, then calls `PATCH /MoneyIn/update/{paymentTransId}`. See [Implementation reference](/guides/mobile-components-tap-to-pay#implementation-reference) for the full parameter list.

On success, `charge(...)` returns a `TransactionResult` with a single field, `paymentTransId`. Store it alongside your order record — it's the identifier you use to find or reconcile the transaction later.

### Capturing `paymentTransId` for reconciliation

If the final `/update` call fails after retries, the transaction may still be authorized by the processor even though `charge(...)` throws, and the thrown `PayabliTTPError.updateFailed(reason:)` doesn't carry the transaction ID. Subscribe to `ttp.events()` and store `paymentTransId` from `.chargeInitiated`, which fires as soon as `/initiate` succeeds, before any card is read:

```swift
for await event in ttp.events() {
    switch event {
    case .chargeInitiated(let paymentTransId):
        pendingPaymentTransId = paymentTransId
    case .updateFailed(let paymentTransId, let error):
        print("Update failed for", paymentTransId, "-", error, "- reconcile manually.")
    default:
        break
    }
}
```

## Testing

Tap to Pay requires a physical device to test. There are two ways to test Tap to Pay in your app:

* **NFC test cards.** The recommended option during development and testing. A physical card is easier to use than a second device, because you don't have to unlock a phone and tap it for every test payment. Check with your Payabli representative for supported test card vendors and ordering details.
* **A second device with a test wallet card.** Create an [Apple Pay sandbox tester account](https://developer.apple.com/apple-pay/sandbox-testing/), sign in to a second iPhone with it, then add test cards to Wallet on that device. Tap the provisioned card against the device running your app. This option is free and works well for routine development. You should still use test cards before you finish your integration.

## Next steps

[Tap to Pay](/guides/mobile-components-tap-to-pay) documents the full `PayabliTTP` implementation reference, plus a complete example app you can clone and run.

## Related resources

See these related resources to help you get the most out of Payabli.

#### Prerequisites

* **[Tap to Pay overview](/guides/pay-in-tap-to-pay-overview)** - Learn how Tap to Pay lets your app accept contactless, card-present payments with no external hardware

#### Related topics

* **[Tap to Pay](/guides/mobile-components-tap-to-pay)** - Add native Tap to Pay payments to your mobile app with PayabliSDKTapToPay