Apple Tap To Pay SDK

Apple Tap to Pay SDK — Technical Integration Guide


Overview

The NI Apple Tap to Pay SDK (ATTP SDK) enables iOS applications to accept contactless payments directly on iPhone — no external card reader required. The SDK wraps Apple's Tap to Pay on iPhone framework and communicates exclusively with NI's Digital Platform (DP) APIs to handle authentication, payment acquisition, and transaction management.

Supported payment methods: Contactless credit/debit cards (Visa, Mastercard), Apple Pay, and other digital wallets.

Regional availability: United Arab Emirates (UAE) only.

Device requirements:

  • iPhone XS or later (iPhone 11+ recommended)
  • iOS 18.0 or later (beta iOS versions are not supported)
  • iCloud sign-in active
  • Face ID, Touch ID, or passcode configured
  • Screen lock passcode set

Integration Flow

The SDK has three primary operation sequences:

  1. Authorization + proximity reader initializationprepare() authenticates using a device token (attpAccessToken) and fetches a proximityReaderToken (valid 48 hours).
  2. Payment acquisitionprocessPayment() uses the active device token to execute a purchase.
  3. Transaction managementgetTransactions() / getTransactionDetails() / refundTransaction() / voidTransaction() for post-payment operations.

Onboarding Prerequisites

Before any integration work, complete the following platform setup via the NI portal:

  1. Configure hierarchy — Set up merchant, outlet(s), MID, TID, channel, and device.
  2. Configure SoftPOS settings — Enable SoftPOS in the outlet/device settings.
  3. Enable SoftPOS proposition service — Activate the SoftPOS proposition service for the merchant.
  4. Create an API key — Generate an API key via a service account and distribute it to the merchant.

Step 1: Get Access Token

Exchange the apiKey for a short-lived bearer access token (expires in 8 hours).

Request

POST {API_GATEWAY_URL}/identity/auth/access-token
HeaderValue
Content-Typeapplication/vnd.ni-identity.v1+json
Acceptapplication/vnd.ni-identity.v1+json
AuthorizationBearer {api_key}

Request Body

{}

Response (200 OK)

{
  "access_token": "<bearer_token>",
  "expires_in": 28800,
  "token_type": "bearer"
}

Step 2: Get Device Token (attpAccessToken)

The device token identifies the merchant/outlet/device in the DP hierarchy. This is the authToken parameter passed to all SDK methods.

Request

POST {API_GATEWAY_URL}/identity/outlets/{outletRef}/devices/auth
HeaderValue
Content-Typeapplication/vnd.ni-identity.v1+json
Acceptapplication/vnd.ni-identity.v1+json
AuthorizationBearer {fabrick_service_token}

Request Body

{
  "deviceCode": "<device_code>",
  "userCode": "<user_code>",
  "tid": "<tid>",
  "deviceIdentifier": "<deviceIdentifier>"
}

Response (200 OK)

{
  "access_token": "<bearer_token>",
  "expires_in": 28800,
  "token_type": "bearer"
}

Note: Use this access_token value as authToken in all SDK method calls.

Obtaining userCode, deviceCode, and TID for Testing

To generate an attpAccessToken for testing, use the Postman collection (NI_attpAccessToken.postman_collection.json) with the NI UAT.postman_environment.json environment:

  1. Trigger Generate Fabrick token → copy access_token from response.
  2. Open the NI UAT environment, paste the value into fabrick_access_token (current value) and save.
  3. Open Generate Device token, set the body parameters:
    • userCode — Search Fabrick Wallet Service logs for "FB REST Response GET" + "/users". Take userCode from any user in the array. Alternatively: navigate to Shift Management (user icon, top-right) in the app.
    • deviceCode — Search Fabrick Wallet Service logs for "/terminals". Take virtualTerminalCode from any terminal. Alternatively: navigate to Settings > Terminals in the app.
    • tid — From the same terminal entry, take virtualTerminalTid.
  4. Trigger the request → copy access_token from response. This is your attpAccessToken.
  5. For the test app: update mockAuthToken in ViewModel.swift and relaunch.

Step 3: Get Payment Card Reader Token

Called internally by the SDK during prepare(). Documented here for reference — you pass the device token to prepare() and the SDK handles this call automatically.

Request

POST {SDK_API_URL}/softpos/payment-card-reader-token
HeaderValue
Content-Typeapplication/vnd.ni-softpos.v1+json
Acceptapplication/vnd.ni-softpos.v1+json
AuthorizationBearer {device_token}

Request Body

{
  "currency": "AED"
}

Response (200 OK)

{
  "token": "<payment-card-reader-token>"
}

Token expiry: 48 hours. After expiry, call prepare() again to refresh.


iOS SDK Integration

Step 1: App Entitlements

Your app must have the Tap to Pay on iPhone entitlement from Apple Developer. Apply via your Apple Developer account, then add the following to your app's .entitlements file:

KeyTypeValue
com.apple.developer.proximity-reader.payment.acceptanceBooleantrue

Ensure your provisioning profile includes this entitlement.

Step 2: Embed the SDK Framework

  1. Open your project in Xcode.
  2. Go to Project Settings > General.
  3. Scroll to Frameworks, Libraries, and Embedded Content.
  4. Click + and select AppleTapToPaySDK.xcframework.
  5. Set Embed to "Embed & Sign".

The framework must be embedded and signed. Linking without embedding will cause a runtime failure.


Step 3: Initialize AppleTapToPayManager

Create one instance of AppleTapToPayManager at app launch before processing any payments. All SDK operations are performed through this instance.

Parameters

ParameterTypeRequiredDefaultDescription
serverSchemeStringYes"https" or "http"
serverHostStringYesDigital Platform host address
serverPortIntYesServer port (typically 443)
serverBasePathStringYesBase path for API endpoints
locationTrackingEnabledBoolNofalseEnable location tracking
logEnabledBoolNofalseEnable debug logging

Example (Swift)

let tapToPayManager = AppleTapToPayManager(
    serverScheme: "https",
    serverHost: "api-gateway.ngenius-payments.com",
    serverPort: 443,
    serverBasePath: "/softpos",
    locationTrackingEnabled: true,
    logEnabled: true
)

tapToPayManager.onError = { error in
    switch error {
    case .deviceNotSupported:
        print("Device does not support Tap to Pay.")
    case .initializationError(let message):
        print("SDK init failed: \(message)")
    default:
        print("Unknown error: \(error)")
    }
}

Initialization Errors

Error CodeDescriptionResolution
deviceNotSupported (100)Device does not support NFC/Tap to PayEnsure iPhone XS or later
initializationError (101)Internal network client failed to initializeCheck errorMessage for details

Step 4: Check SoftPOS Compatibility

Call isSoftPOSSupported() before any Tap to Pay operation to confirm the device meets hardware requirements.

switch tapToPayManager.isSoftPOSSupported() {
case 0:
    print("Device supports SoftPOS.")
case -2:
    print("Device does not support SoftPOS.")
default:
    print("Unknown status.")
}

Return Values

ValueMeaning
0Device is compatible (iPhone XS or newer)
-2Device is too old to support Apple Tap to Pay

Step 5: Prepare the SDK (Reader Initialization)

prepare() must be called before processing any payment. It fetches and caches the proximity reader token, handles account linking and Terms & Conditions presentation if needed, and creates a payment card reader session.

Method signature

prepare(
    authToken: String,
    currencyCode: String,
    onProgress: ((Int) -> Void)?,
    onSuccess: () -> Void,
    onError: (AppleTapToPaySDKError) -> Void
)

Parameters

ParameterTypeRequiredDescription
authTokenStringYesDevice token (attpAccessToken)
currencyCodeStringYesISO 4217 currency code (e.g., "AED")
onProgress((Int) -> Void)?NoReader session creation progress (1–100)
onSuccess() -> VoidYesCalled when SDK is ready to process payments
onError(AppleTapToPaySDKError) -> VoidYesCalled on failure

Example (Swift)

await tapToPayManager.prepare(
    authToken: deviceToken,
    currencyCode: "AED",
    onProgress: { progress in
        print("Preparing: \(progress)%")
    },
    onSuccess: {
        print("SDK ready. You can now process payments.")
    },
    onError: { error in
        print("Prepare failed: \(error.localizedDescription)")
    }
)

Common errors from prepare()

Error CodeCode #Description
invalidAuthToken102Token missing or expired
networkError110Could not connect to Digital Platform
readerTokenNotAvailable501Reader token could not be fetched
termsAndConditionsError520User must accept Terms & Conditions

Step 6: Check Account Linking

Before the first payment, verify that the merchant account is linked (Terms & Conditions accepted).

Method signature

isAccountLinked(
    authToken: String,
    currencyCode: String,
    onSuccess: (Bool) -> Void,
    onError: (AppleTapToPaySDKError) -> Void
)

Example (Swift)

tapToPayManager.isAccountLinked(
    authToken: deviceToken,
    currencyCode: "AED",
    onSuccess: { isLinked in
        if isLinked {
            print("Account linked — ready for payments.")
        } else {
            print("Account not linked. Prompt T&C acceptance.")
        }
    },
    onError: { error in
        print("Account link check failed: \(error.localizedDescription)")
    }
)

If isLinked returns false, the merchant must:

  1. Obtain a JWT from NI (includes merchant identifier).
  2. Call linkAccount(using:) on a PaymentCardReader instance.
  3. The system presents the Terms & Conditions UI — merchant must accept.

Once accepted, the account stays linked across device switches.


Payment Operations

Purchase (Direct Payment)

processPayment() initiates a contactless payment. Internally calls POST /softpos/payment/card/purchase.

Method signature

processPayment(
    authToken: String,
    paymentType: PaymentType,
    amount: String,
    currencyCode: String,
    dryRun: Bool,
    onProgress: ((Int) -> Void)?,
    onSuccess: (PaymentResponse) -> Void,
    onError: (AppleTapToPaySDKError) -> Void
)

Parameters

ParameterTypeRequiredDescription
authTokenStringYesDevice token
paymentTypePaymentTypeYesPayment type (e.g., .contactless)
amountStringYesAmount in microcurrency (no decimal — e.g. "12550" for AED 125.50)
currencyCodeStringYesISO 4217 (e.g., "AED")
dryRunBoolNotrue = test mode, no real transaction. Default: false
onProgress((Int) -> Void)?NoProgress updates (1–100)
onSuccess(PaymentResponse) -> VoidYesCalled with payment result
onError(AppleTapToPaySDKError) -> VoidYesCalled on failure

Amount formatting

CurrencyStandardMicrocurrency (pass as amount)
AED 125.50125.50"12550"
USD 10.0010.00"1000"

Example (Swift)

tapToPayManager.processPayment(
    authToken: deviceToken,
    paymentType: .contactless,
    amount: "12550",
    currencyCode: "AED",
    dryRun: false,
    onProgress: { progress in
        print("Payment progress: \(progress)%")
    },
    onSuccess: { response in
        print("Payment successful.")
        print("Reference: \(response.reference)")
        print("Order reference: \(response.orderReference)")
        print("State: \(response.state)")
    },
    onError: { error in
        print("Payment failed: \(error.localizedDescription)")
    }
)

processPayment() Error Codes

Error CodeCode #Description
deviceNotSupported100Device older than iPhone XS
initializationError101SDK or network client not initialized
invalidCurrencyCode202Currency code is not a valid 3-character ISO string
invalidAmount203Transaction amount is negative
readerTokenNotAvailable501Reader token could not be retrieved
readerTokenInvalid510Reader token is invalid or missing
termsAndConditionsError520Merchant has not accepted Terms & Conditions
processPaymentReadError530Card reader UI dismissed or failed to present
invalidPaymentProcessResponse210DP response could not be parsed into PaymentResponse
networkUnauthorizedError111HTTP 401/403 — authentication failure
networkError110General network failure
genericError1Unexpected error — check errorMessage

Auth + Capture (Two-Step Payment)

Note: The /authorise endpoint is available directly at the DP level but is not exposed through the SDK's processPayment() method. Use this flow for server-side integration or scenarios requiring deferred capture.

Auth Request

POST {SDK_API_URL}/softpos/payment/card/authorise
Authorization: Bearer {device_token}
Content-Type: application/vnd.ni-softpos.v1+json
{
  "language": "EN",
  "locale": "",
  "tid": "<tid>",
  "payment": "<CARD_ENV_ENCRYPTED_DATA>"
}

Auth Response (200 OK)

{
  "reference": "d2994a07-da67-49dc-8bf1-f3c4173b7329",
  "state": "AUTHORISED",
  "updateDateTime": "2023-07-19T10:08:23.725Z",
  "amount": {
    "currencyCode": "AED",
    "value": 2500
  },
  "outletId": "87f2ebb4-6301-4b13-b59e-ea15ecc07b43",
  "orderReference": "c4155842-84b1-4025-8890-f9a6b5c14f32",
  "authResponse": {
    "authorizationCode": "448430",
    "success": true,
    "resultCode": "00",
    "resultMessage": "Successful approval/completion",
    "mid": "434334344242"
  }
}

Capture Request

Use orderReference and paymentReference from the auth response.

POST {SDK_API_URL}/softpos/payment/card/orders/{orderRef}/payments/{paymentRef}/capture
Authorization: Bearer {device_token}
Content-Type: application/vnd.ni-softpos.v1+json
{
  "amount": {
    "currencyCode": "AED",
    "value": 1000
  }
}

Capture Response (200 OK)

{
  "reference": "d2994a07-da67-49dc-8bf1-f3c4173b7329",
  "state": "PARTIALLY_CAPTURED",
  "amount": {
    "currencyCode": "AED",
    "value": 2500
  },
  "orderReference": "c4155842-84b1-4025-8890-f9a6b5c14f32",
  "_embedded": {
    "cnp:capture": [
      {
        "amount": { "currencyCode": "AED", "value": 200 },
        "createdTime": "2023-07-19T10:10:30.687Z",
        "state": "SUCCESS"
      }
    ]
  }
}

Transaction Management

Retrieve Transactions List

getTransactions() fetches a paginated list of transactions for the merchant associated with the device token. Maps to GET /reporting/payments internally.

Method signature

getTransactions(
    limit: Int,
    offset: Int,
    authToken: String,
    onSuccess: (GetTransactionsResponse) -> Void,
    onError: (AppleTapToPaySDKError) -> Void
)

Parameters

ParameterTypeRequiredDescription
limitIntYesMax records to return
offsetIntYesStarting index (0-based)
authTokenStringYesDevice token

Example (Swift)

tapToPayManager.getTransactions(
    limit: 20,
    offset: 0,
    authToken: deviceToken,
    onSuccess: { response in
        print("Loaded \(response.items.count) transactions")
        response.items.forEach { txn in
            print("\(txn.reference) — \(txn.status) — \(txn.amount)")
        }
    },
    onError: { error in
        print("Failed to load transactions: \(error.errorMessage)")
    }
)

GetTransactionsResponse

{
  "items": [
    {
      "reference": "txn_12345",
      "createdDateTime": "2024-02-18T12:34:56Z",
      "action": "PURCHASE",
      "currencyCode": "AED",
      "amount": "100.00",
      "refundedAmount": null,
      "status": "SUCCESS",
      "channel": "SoftPOS",
      "outletName": "Merchant A",
      "cardScheme": "VISA",
      "cardholderName": "John Doe",
      "cardPan": "**** **** **** 1234",
      "lastEvent": "Completed",
      "rrn": "123456789",
      "authCode": "987654"
    }
  ]
}

Error Codes

Error CodeCode #Description
initializationError101Network client not initialized
networkError110Network failure
invalidAuthToken102Auth token invalid or expired
dataParsingError310Could not decode GetTransactionsResponse
genericError1Unexpected error

Retrieve Transaction Details

getTransactionDetails() fetches order-level details including whether refund or void is available. Maps to GET /reporting/orders internally.

Method signature

getTransactionDetails(
    reference: String,
    authToken: String,
    onSuccess: (GetTransactionDetailsResponse) -> Void,
    onError: (AppleTapToPaySDKError) -> Void
)

Example (Swift)

tapToPayManager.getTransactionDetails(
    reference: "txn_123456",
    authToken: deviceToken,
    onSuccess: { details in
        print("Can refund: \(details.refundEnabled)")
        print("Can void: \(details.voidEnabled)")
    },
    onError: { error in
        print("Details fetch failed: \(error.localizedDescription)")
    }
)

SDK Response

{
  "refundEnabled": true,
  "voidEnabled": false
}

Get Order Details API (Full Response)

For complete order data including capture history, payment info, and event log, call the DP reporting API directly:

GET {SDK_API_URL}/softpos/reporting/orders/{orderRef}
Authorization: Bearer {device_token}
Accept: application/vnd.ni-softpos.v1+json

Response (200 OK)

{
  "reference": "616241a9-c4ba-42e2-a35d-db2ac48213bd",
  "action": "AUTH",
  "amount": {
    "currencyCode": "AED",
    "value": 10100,
    "formattedValue": "AED101.00"
  },
  "status": "OPEN",
  "channel": "SoftPOS",
  "createDateTime": "2023-08-25T09:11:59.314Z",
  "outletId": "87f2ebb4-6301-4b13-b59e-ea15ecc07b43",
  "outletName": "Testuser",
  "device": {
    "reference": "cd516042-59dc-4951-aa06-6275a2a38434",
    "code": "000C69",
    "name": "iPhoneX"
  },
  "tid": "434334344299",
  "paymentCards": [
    {
      "expiry": "2025-12",
      "cardholderName": "Test Card",
      "paymentMethod": "MASTERCARD",
      "paymentReference": "f9682e21-8853-4bbf-915b-7e38948dda2c",
      "cardScheme": "MASTERCARD",
      "paymentCurrency": "AED",
      "paymentAmount": 10100,
      "maskedPanNumber": "511111******1118"
    }
  ],
  "paymentInfo": {
    "authorised": { "value": 10100, "currencyCode": "AED", "minorUnit": 2 },
    "refunded":   { "value": 0,     "currencyCode": "AED", "minorUnit": 2 },
    "captured":   { "value": 6600,  "currencyCode": "AED", "minorUnit": 2 },
    "net":        { "value": 6600,  "currencyCode": "AED", "minorUnit": 2 },
    "pending":    { "value": 3500,  "currencyCode": "AED", "minorUnit": 2 }
  },
  "captureStatus": "PARTIALLY_CAPTURED",
  "capturesNRefunds": [
    {
      "eventGroupType": "PARTIAL_CAPTURE",
      "eventName": "PARTIALLY_CAPTURED",
      "status": "SUCCESS",
      "timestamp": "2023-08-25T09:12:19.123Z",
      "payload": {
        "reference": "616241a9-c4ba-42e2-a35d-db2ac48213bd",
        "amount": { "currencyCode": "AED", "value": 3300 },
        "paymentProcessor": "MPGS"
      },
      "action": "VOID_CAPTURE"
    }
  ],
  "_embedded": {
    "events": []
  }
}

getTransactionDetails() Error Codes

Error CodeCode #Description
networkError110Network failure
networkUnauthorizedError111HTTP 401/403
genericError1Unexpected error

Refund a Transaction

Maps to POST /payment/card/orders/{orderReference}/actions/refund internally.

Method signature

refundTransaction(
    authToken: String,
    transactionId: String,
    amount: String,
    currencyCode: String,
    onSuccess: (RefundTransactionResponse) -> Void,
    onError: (AppleTapToPaySDKError) -> Void
)

Parameters

ParameterTypeRequiredDescription
authTokenStringYesDevice token
transactionIdStringYesTransaction reference to refund
amountStringYesRefund amount in microcurrency
currencyCodeStringYesISO 4217 (e.g., "AED")

Example (Swift)

tapToPayManager.refundTransaction(
    authToken: deviceToken,
    transactionId: "txn_123456",
    amount: "12550",
    currencyCode: "AED",
    onSuccess: { response in
        print("Refund ID: \(response.refundId), Status: \(response.status)")
    },
    onError: { error in
        print("Refund failed: \(error.localizedDescription)")
    }
)

Response

{
  "refundId": "rfn_987654",
  "status": "SUCCESS"
}

Error Codes

Error CodeCode #Description
networkError110Network failure
invalidTransactionIdTransaction ID not found
invalidAmount203Refund amount exceeds original
networkUnauthorizedError111HTTP 401/403
dataParsingError610Response parsing failure
genericError1Unexpected error

Void a Transaction

Maps to POST /payment/card/orders/{orderReference}/actions/cancel internally.

Method signature

voidTransaction(
    authToken: String,
    transactionId: String,
    onSuccess: (VoidTransactionResponse) -> Void,
    onError: (AppleTapToPaySDKError) -> Void
)

Example (Swift)

tapToPayManager.voidTransaction(
    authToken: deviceToken,
    transactionId: "txn_123456",
    onSuccess: { response in
        print("Void ID: \(response.voidId), Status: \(response.status)")
    },
    onError: { error in
        print("Void failed: \(error.localizedDescription)")
    }
)

Response

{
  "voidId": "vld_987654",
  "status": "SUCCESS"
}

Error Codes

Error CodeCode #Description
networkError110Network failure
invalidTransactionIdTransaction ID not found
networkUnauthorizedError111HTTP 401/403
dataParsingError710Response parsing failure
genericError1Unexpected error

Unbind Device TID Mapping

If a TID was previously mapped to an older device or SDK installation that has since been uninstalled, you must unbind it before re-registering.

Request

DELETE {SDK_API_URL}/softpos/outlets/{outletRef}/devices/tids/{tid}/unbind
Authorization: Bearer {device_token}
Content-Type: application/vnd.ni-softpos.v1+json

Response

HTTP 204 NO_CONTENT

No response body on success.


Complete Error Code Reference

SDK Errors

CodeDescription
1Generic error — check error message for details
100Device does not support Apple Tap to Pay
101Initialization error — check error message
102Invalid auth token
110Generic network communication error
111Request unauthorized (HTTP 401/403)
112Proposition service not enabled
113Invalid PAN (Primary Account Number)
114Unsupported card scheme
115Invalid Terminal ID (TID)
116Unsupported order action
118Amount limit exceeded
119Operation cannot be performed

Payment Processing Errors

CodeDescription
201Invalid transaction type
202Invalid currency code
203Invalid transaction amount (e.g., negative)
210Invalid payment response from Digital Platform

Transaction Retrieval Errors

CodeDescription
301Transactions cannot be retrieved
310Invalid transaction response from server

Refund Errors

CodeDescription
610Invalid refund transaction response from server

Void Errors

CodeDescription
710Invalid void transaction response from server

Proximity Reader Errors

CodeDescription
501Unable to retrieve reader token from Digital Platform
510Invalid reader token provided
515Account linking check failed
516Configuration error during PaymentCardReader preparation
520Error during Terms & Conditions presentation
530Card reader UI dismissed or failed to show

Location Tracking Errors

CodeDescription
801Location manager not initialized
802User denied location tracking permission
803Error receiving location updates
810Error retrieving country (reverse geocoding failed)

Best Practices

Pre-warm the reader session. Call prepare() at app launch or when the payment screen becomes visible — before the merchant needs to tap. This eliminates reader initialization delay at checkout.

Cache and refresh the device token. The device token expires in 8 hours. Implement token refresh logic and pass the refreshed token to each SDK method call.

Reader token expiry. The payment card reader token is valid for 48 hours. Track the expiry and call prepare() before it lapses to avoid readerTokenNotAvailable errors mid-payment.

Check compatibility first. Always call isSoftPOSSupported() at launch. If the return value is -2, disable the payment feature and surface an appropriate message before the user reaches checkout.

Follow Apple's Human Interface Guidelines for Tap to Pay on iPhone. Promotional content must align with Apple's official branding standards.

Dry run testing. Set dryRun: true in processPayment() to run integration tests without posting real transactions to the Digital Platform.


FAQ

Is Tap to Pay on iPhone secure?
Yes. Card numbers and PINs are never stored on the device or sent to Apple's servers. For transactions above the contactless limit, PIN entry occurs directly on the iPhone — Apple prevents screenshots or screen recording during this step.

Which payment methods are supported?
Contactless credit and debit cards (Visa, Mastercard), Apple Pay (iPhone and Apple Watch), and other NFC digital wallets. Cards must display the contactless symbol.

Is there a maximum transaction amount?
No fixed cap. Transactions above the contactless limit prompt PIN entry on the iPhone.

Is Tap to Pay available outside the UAE?
No. This integration is currently UAE-only.

What if a TID is already mapped to an old device?
Call the Unbind API (DELETE /softpos/outlets/{outletRef}/devices/tids/{tid}/unbind) to remove the old mapping before registering the new device.

Can I use this SDK on iOS beta?
No. Tap to Pay on iPhone is not supported on iOS beta versions.


© Network International LLC. All Rights Reserved.