Running Payments

These examples target the published iOS SDK 1.0.21 and the standard merchant checkout flow: sale, preauth, refunds, and transaction management. Install this version before using them.

If you're ready to start developing, see our SDK installation guide.

Get started with Koard

What you learn

In this guide, you'll learn:

  • How to initialize and authenticate with the Koard Merchant SDK
  • How to set up location management for multi-location merchants
  • How to prepare card reader sessions for tap-to-pay
  • How to process different types of transactions (sale, preauth, refund)
  • How to handle transaction responses and error scenarios

For custom order/refund references, see Transaction Metadata. For deinit(), initialization status, and signing in after a reset, see SDK Lifecycle and Deinitialization.

Active Location and Terminal Routing

Your app must provide a way to select the active location in settings. Authenticate the merchant, load that merchant's available locations, and let the operator select the location where the device is taking payments. The selected location_id resolves the linked terminal_id, whose processor configuration, MID, and TID determine payment routing for that merchant.

Show the active location in settings and on the payment screen. Select it before reader preparation or any payment. If the operator changes merchants, clear the previous selection and reload the new merchant's locations. If you remember a choice between sessions, validate that it still belongs to the authenticated merchant and still has a usable linked terminal. Do not silently reuse another merchant's location or select an arbitrary terminal.

  • iOS: call KoardMerchantSDK.shared.setActiveLocationID(location.id), then follow the account-linking and reader preparation flow.
  • Android: call the suspending sdk.setActiveLocation(location.id), then sdk.enrollDevice() and the reader preparation flow.

Block payment initiation until location selection and required readiness checks have completed. If a location's terminal changes, reload the configuration and complete the SDK's readiness flow before taking another payment.

Before you begin

This comprehensive guide covers everything you need to know about integrating and using the KoardMerchantSDK in your iOS application. For payment concepts and API payloads, explore the Payments guides—including Sale and Preauth. To understand the complete flow, see the Payment Lifecycle guide.

Test on Real Hardware: Keep a dedicated test iPhone with your Sandbox Apple Account signed in. Simulator builds cannot exercise Tap to Pay, and production Apple IDs won't work in the Sandbox environment.

Key Concepts

1. Authentication Tokens

The SDK manages several types of tokens automatically:

  • API Key: Your API key for Koard services
  • Card Reader Token: Apple's ProximityReader token for Tap to Pay functionality

2. Card Reader Sessions

The SDK handles Apple's ProximityReader lifecycle:

  • Preparation: Refreshes tokens and prepares the reader for transactions
  • Transaction Processing: Manages card reading and data collection
  • Session Management: Handles background/foreground transitions automatically

3. Location Management

Multi-location merchants must set an active location before processing payments:

  • Retrieve available locations after login
  • Set the active location for all subsequent transactions
  • Location data is persisted across app sessions

Initialize the SDK

Initialize the SDK early in your app lifecycle (typically in AppDelegate or SceneDelegate):

import UIKit
import KoardSDK

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      
        // Configure SDK options
        let options = KoardOptions(
            environment: .uat,           // .uat | .production | .custom(String)
            loggingLevel: .debug         // .none | .error | .warning | .debug | .verbose
        )
      
        // Initialize with your API key
        KoardMerchantSDK.shared.initialize(
            options: options, 
            apiKey: "your-koard-api-key"
        )
      
        return true
    }
}

Authenticate the Merchant

Before processing any payments, authenticate the merchant. The login function returns a JWT token that is then passed in the Bearer token of all successive requests.

import KoardSDK

private func authenticateMerchant() async throws {
    do {
        // Login with merchant credentials
        try await KoardMerchantSDK.shared.login(
            code: "your-merchant-code",
            pin: "your-merchant-pin"
        )
      
        print("Merchant authenticated successfully")
      
        // Load this merchant's locations into your app settings picker.
        // Call setupLocation(selectedLocationId:) after the operator selects one.
      
    } catch {
        print("Authentication failed: \(error)")
        throw error
    }
}

If you have already resolved the merchant identity into a single opaque string (for example via a QR scan, SSO callback, or server-issued provisioning token), you can log in with an alias instead of a code and PIN. It produces the same session token:

try await KoardMerchantSDK.shared.login(alias: "your-merchant-alias")

Session-token auth: Login is session-token only. The SDK persists the session token and never stores the merchant code, PIN, or alias. When the session expires, call login(...) again to re-authenticate.

Set Location

Retrieve and set the location selected in your app settings. Its linked terminal determines the MID, TID, and processor configuration used for payments. Revalidate a saved selection after login or a merchant change; never automatically select the first location. Block payments until the selected location belongs to the authenticated merchant and has a linked terminal.

private enum LocationSelectionError: Error {
    case invalidSelection
    case noLinkedTerminal
}

private func setupLocation(selectedLocationId: String) async throws {
    do {
        let sdk = KoardMerchantSDK.shared
        let merchant = try await sdk.getMerchantAccount()
        let locations = try await sdk.locations(request: GetLocationsRequest(
            locationId: selectedLocationId,
            merchantId: merchant.id
        ))

        guard let activeLocation = locations.first(where: {
            $0.id == selectedLocationId && $0.accountId == merchant.id
        }) else {
            throw LocationSelectionError.invalidSelection
        }
        guard let terminalId = activeLocation.terminalId, !terminalId.isEmpty else {
            throw LocationSelectionError.noLinkedTerminal
        }

        sdk.setActiveLocationID(activeLocation.id)
      
        print("Active location set: \(activeLocation.name)")
      
    } catch {
        print("Location setup failed: \(error)")
        throw error
    }
}

Prepare a Card Reader Session

After login and active-location selection, call prepare(). If it throws accountNotLinked, show an explicit Accept Tap to Pay terms action. Await linkAccountAsync() from that action, then prepare again. Linking can be declined or fail; keep payments blocked until preparation succeeds.

@MainActor
private func prepareCardReader() async throws {
    try await KoardMerchantSDK.shared.prepare()
}

// Call from the operator's account-linking action, not a background retry.
@MainActor
private func linkAndPrepareCardReader() async throws {
    try await KoardMerchantSDK.shared.linkAccountAsync()
    try await prepareCardReader()
}

The SDK owns the reader's event subscription. Display readiness by polling KoardMerchantSDK.shared.status while the payment view is visible, as shown in Session Management. Do not add a second readerEvents iterator.

Process Sale Transactions

Sale transactions are single-step auth + capture that immediately capture funds. Use sales when the final amount is known at payment time.

For more details on when to use Sale vs Preauth, compare the Sale and Preauth guides.

@MainActor
private func processSale(eventId: String) async throws {
    // Create payment breakdown (optional)
    let breakdown = PaymentBreakdown(
        subtotal: 1000,        // $10.00 in cents
        taxRate: 8.75,         // 8.75% as a percent value (not a 0-1 decimal)
        taxAmount: 88,         // $0.88 in cents
        tipAmount: 200,        // $2.00 in cents
        tipType: .fixed        // or .percentage
    )
  
    // Create currency
    let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
  
    do {
        // Process the sale
        let response = try await KoardMerchantSDK.shared.sale(
            amount: 1288,          // Total amount in cents
            breakdown: breakdown,  // Optional breakdown
            currency: currency,
            eventId: eventId,       // Persisted before submitting this operation
            type: .sale,           // Defaults to .sale
            metadata: ["order_id": "order-123", "channel": "mobile"]
        )
      
        // Handle the response
        try handleTransactionResponse(response)
      
    } catch {
        print("Sale failed: \(error)")
        throw error
    }
}

Tracking and idempotency: sale and preauth take an optional eventId (UUID4), not a transactionId. Koard generates the transaction ID and returns it on response.transactionId. The reader is driven internally, so a Tap to Pay sheet is presented during these calls.

Persist each operation ID in your app’s order store before calling the SDK. The helpers accept those saved IDs; confirmations and second-card payments use their own IDs. If the response is lost, reconcile the saved ID before considering another submission.

Process Preauthorization Transactions

Preauthorization transactions authorize funds without capturing them. They can be incrementally authorized, captured, or reversed. Use preauth when the final amount is uncertain (e.g., restaurant with tip) or when you need to verify funds availability.

To complete a preauth, capture the payment using the transaction ID. For the complete flow, see Preauth and Capture.

@MainActor
private func processPreauth(eventId: String) async throws {
    let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
  
    do {
        // Process preauthorization (breakdown is optional — pass nil)
        let response = try await KoardMerchantSDK.shared.preauth(
            amount: 1000,          // Amount to preauthorize in cents
            breakdown: nil,        // Optional breakdown
            currency: currency,
            eventId: eventId,       // Persisted before submitting this operation
            metadata: ["order_id": "order-123"]
        )
      
        try handleTransactionResponse(response)
      
        // Store transaction ID for later capture/reverse
        UserDefaults.standard.set(response.transactionId, forKey: "lastPreauthId")
      
    } catch {
        print("Preauth failed: \(error)")
        throw error
    }
}

Handle Transaction Responses

Payment methods return a TransactionResponse. Read the rich domain object from response.transaction (a KoardTransaction?). Its status is the public KoardTransaction.Status enum, which includes pending, authorized, captured, surchargePending, surchargeApplied, approved, declined, refunded, reversed, pickupCard, timedOut, canceled, cancelled, error, settled, and unknown.

private enum PaymentError: Error {
    case invalidResponse
}

private func requireTransaction(_ response: TransactionResponse) throws -> KoardTransaction {
    guard let transaction = response.transaction else {
        throw PaymentError.invalidResponse
    }
    return transaction
}

@MainActor
private func handleTransactionResponse(_ response: TransactionResponse) throws {
    renderChargeResult(try requireTransaction(response))
}

@MainActor
private func renderChargeResult(_ transaction: KoardTransaction) {
    // A partial approval is not payment of the entire order.
    if transaction.isPartialApproval {
        requestPartialApproval(transaction)
        return
    }

    switch transaction.status {
    case .captured, .authorized, .approved, .surchargeApplied, .settled:
        showReceipt(transaction)
    case .surchargePending:
        requestSurchargeConfirmation(transaction)
    case .pending:
        reconcileTransaction(transaction)
    case .declined:
        showDeclined(transaction.statusReason ?? "Transaction declined")
    case .canceled, .cancelled:
        showCanceled("Transaction canceled")
    case .error, .timedOut, .pickupCard, .reversed, .refunded, .unknown:
        showUnresolved(transaction.statusReason ?? "Check the transaction outcome")
    @unknown default:
        showUnresolved("Unrecognized transaction status")
    }
}

@MainActor
private func confirmSurcharge(transactionId: String, accepted: Bool, eventId: String) async throws {
    // eventId is the separately persisted cardholder-decision operation ID.
    let transaction = try await KoardMerchantSDK.shared.confirm(
        transaction: transactionId,
        confirm: accepted,
        eventId: eventId
    )
    // confirm returns KoardTransaction directly, not TransactionResponse.
    renderChargeResult(transaction)
}

The show..., request..., and reconcileTransaction functions are application UI callbacks. Save the transaction ID before displaying the next screen. For surcharge confirmation, display surchargeDisclosure, the surcharge, and the final total from the returned transaction; persist a new decision ID before calling confirmSurcharge. An authorized preauth is a hold, so its receipt must say authorized, rather than captured or settled. Pending, missing, and partial results must retain their IDs for outcome recovery; do not automatically submit another sale.

Handle Partial Approvals

When the issuer authorizes less than the requested amount, the transaction comes back with a statusReason of partial_approval (the StatusReason.partialApproval case). The top-level status still reads as approved/captured. Use the partialAuthApproval method to accept the partial amount as final, or reject it to release the hold:

private func handlePartialApproval(_ transaction: KoardTransaction, eventId: String) async throws {
    guard transaction.isPartialApproval else { return }

    // remainingAmount is the amount still owed after the partial authorization
    if let remaining = transaction.remainingAmount {
        print("Partial approval — remaining balance: \(remaining) cents")
    }

    // Accept the partial amount as final (or pass approve: false to release the hold)
    let settled = try await KoardMerchantSDK.shared.partialAuthApproval(
        transactionId: transaction.transactionId,
        approve: true,
        eventId: eventId       // Persisted partial-approval decision ID
    )

    print("Partial-approval decision status: \(settled.status.string)")
}

To collect the remaining balance on a second card, pass the original transaction's id as partialAuthTransactionId on the follow-up sale (or preauth). This links the second-card payment to the original partial approval. It opens a new Tap to Pay read; inspect the remainder's returned transaction too:

let remainder = try await KoardMerchantSDK.shared.sale(
    amount: remainingAmount,
    breakdown: nil,
    currency: CurrencyCode(currencyCode: "USD", displayName: "US Dollar"),
    eventId: remainderEventId, // Separately persisted second-card operation ID
    partialAuthTransactionId: transaction.transactionId
)

Transaction Management

These helpers return the actual KoardTransaction. The caller must inspect its status and operation amounts before showing success. A returned API response alone is not an approval; pending outcomes need reconciliation.

Refund a Transaction

import KoardSDK

private func processRefund(transactionId: String, eventId: String, amount: Int? = nil) async throws -> KoardTransaction {
    do {
        let response = try await KoardMerchantSDK.shared.refund(
            transactionId: transactionId,
            amount: amount,        // nil for full refund
            eventId: eventId,      // Persisted refund operation ID
            metadata: ["order_id": "order-123", "reason": "customer_return"]
        )
      
        return try requireTransaction(response)
      
    } catch {
        print("Refund failed: \(error)")
        throw error
    }
}

To run a card-present refund (presenting the Tap to Pay sheet and capturing card data with the refund), pass withTap: true. When withTap is set, an amount is required:

let response = try await KoardMerchantSDK.shared.refund(
    transactionId: transactionId,
    amount: 1288,          // Required when withTap is true
    eventId: eventId,
    withTap: true
)

Reverse a Preauthorization

private func reversePreauth(transactionId: String, eventId: String, amount: Int? = nil) async throws -> KoardTransaction {
    do {
        let response = try await KoardMerchantSDK.shared.reverse(
            transactionId: transactionId,
            amount: amount,  // nil for full reversal
            eventId: eventId
        )
      
        return try requireTransaction(response)
      
    } catch {
        print("Reversal failed: \(error)")
        throw error
    }
}

Note: Transactions can be partially reversed and refunded. Inspect the returned status and cumulative reversed/refunded amounts. Do not infer cancellation solely from a zero remaining amount; REST uses canceled, while SDK enums can include legacy aliases. For more details, see our Payment Lifecycle guide.

Incremental Authorization

Authorize additional amounts on an existing preauth transaction. This is useful for adding incidental charges (e.g., hotel mini bar, additional restaurant items):

private func incrementalAuth(transactionId: String, additionalAmount: Int, eventId: String) async throws -> KoardTransaction {
    do {
        let response = try await KoardMerchantSDK.shared.auth(
            transactionId: transactionId,
            amount: additionalAmount,
            breakdown: nil,
            eventId: eventId
        )
      
        return try requireTransaction(response)
      
    } catch {
        print("Incremental auth failed: \(error)")
        throw error
    }
}

Capture a Transaction

Capture a previously authorized preauth transaction. You can capture the full authorized amount or a partial amount (e.g., adjust for final tip):

private func captureTransaction(transactionId: String, eventId: String, finalAmount: Int? = nil) async throws -> KoardTransaction {
    do {
        let response = try await KoardMerchantSDK.shared.capture(
            transactionId: transactionId,
            amount: finalAmount,      // nil to capture full authorized amount
            breakdown: nil,            // Supply a breakdown matching the final amount if needed
            eventId: eventId
        )
      
        return try requireTransaction(response)
      
    } catch {
        print("Capture failed: \(error)")
        throw error
    }
}

Adjust the Tip

Adjust the tip on a completed transaction (for example, after a customer finalizes a tip in table service):

private func adjustTip(transactionId: String, newTipTotal: Int, eventId: String) async throws -> KoardTransaction {
    let response = try await KoardMerchantSDK.shared.tipAdjust(
        transactionId: transactionId,
        amount: newTipTotal,   // New tip amount in cents
        tipType: .fixed,       // .fixed or .percentage (PaymentBreakdown.TipType)
        eventId: eventId       // Persisted tip-adjust operation ID
    )

    return try requireTransaction(response)
}

Send a Receipt

Deliver a receipt for a completed transaction by email, SMS, or both. Pass at least one of email / phoneNumber:

private func sendReceipt(transactionId: String) async throws {
    let response = try await KoardMerchantSDK.shared.sendReceipts(
        transactionId: transactionId,
        email: "customer@example.com",
        phoneNumber: "+15551234567"
    )

    print("Receipt delivery: \(response)")
}

When a tap cannot complete — an unsupported card, a reader problem, or a customer who would rather pay on their own device — generate a hosted payment link for the same amount:

private func createFallback(amount: Int, breakdown: PaymentBreakdown?) async throws {
    let fallback = try await KoardMerchantSDK.shared.createFallbackLink(
        amount: amount,
        breakdown: breakdown
    )

    print("Fallback link: \(fallback)")
    // Share the link with the customer (SMS, email, or QR code)
}

Transaction History

The SDK provides methods to retrieve and filter transaction history:

import KoardSDK

private func getTransactionHistory() async throws {
    do {
        // Get recent transactions
        let history = try await KoardMerchantSDK.shared.transactionHistory()
      
        print("Found \(history.transactions.count) transactions")
      
        // Filter by status
        let approvedTransactions = try await KoardMerchantSDK.shared.transactionsByStatus("approved")
      
        // Search transactions
        let searchResults = try await KoardMerchantSDK.shared.searchTransactions("card_number_here")
      
        // Advanced filtering
        let filteredTransactions = try await KoardMerchantSDK.shared.searchTransactionsAdvanced(
            startDate: Date().addingTimeInterval(-86400 * 7), // Last 7 days
            endDate: Date(),
            statuses: ["approved", "declined"],
            types: ["sale", "refund"],
            minAmount: 100,  // $1.00
            maxAmount: 10000, // $100.00
            limit: 50
        )
      
    } catch {
        print("Transaction history failed: \(error)")
        throw error
    }
}

Note: For webhook-based transaction monitoring, see our Available Events guide.

Error Handling

SDK calls throw KoardMerchantSDKError. Use its errorDescription property for a user-facing message. Match the cases you care about:

private func handleSDKError(_ error: Error) {
    if let koardError = error as? KoardMerchantSDKError {
        switch koardError {
        case .missingLocationID:
            print("No active location set")
            // Prompt user to select location

        case .accountNotLinked:
            print("Tap to Pay account is not linked")
            // Show the account-linking action; it awaits
            // linkAndPrepareCardReader() and handles any thrown error.

        case .readerTokenInvalid(let message):
            print("Reader token invalid: \(message ?? "retry preparation")")
            // Usually transient — retrying prepare() recovers

        case .unauthorized:
            print("Not authenticated or session expired")
            // Redirect to login

        case .blockedAccount:
            print("This merchant account is blocked")

        case .rateLimited(let message):
            print("Rate limited: \(message ?? "slow down and retry")")
            // Back off and retry later

        case .network(let description, _):
            print("Network error: \(description)")

        case .server(let message):
            print("Server error: \(message ?? "try again later")")

        case .TTPPaymentFailed(.canceled):
            print("Customer canceled at the Tap to Pay sheet")
            // Benign outcome — treat as canceled, not a failure

        case .TTPPaymentFailed(let ttpError):
            print("Tap to Pay error: \(ttpError)")
            // Handle other reader errors

        case .invalidParameters(let message):
            print("Invalid parameters: \(message)")

        default:
            print("Koard SDK error: \(koardError.errorDescription)")
        }
    } else {
        print("General error: \(error)")
    }
}

Error type: The SDK's error type is KoardMerchantSDKError. Earlier releases added .rateLimited(message:) for HTTP 429 and TTPPaymentError.canceled for Tap to Pay sheet cancellations. Network/transport failures are thrown as .network(description:underlying:) rather than raw URLError, and a missing session on a payment/refund/pre-auth throws .unauthorized.

Upgrading to 1.0.20: Two behavior changes require code updates.

1. prepare() no longer auto-links the Tap to Pay account. It now throws KoardMerchantSDKError.accountNotLinked instead. Guard for that case and drive linking explicitly with linkAccount() (or linkAccountAsync()), then call prepare() again.

2. linkAccountAsync() now throws when linking fails or is declined — previously it never threw. Wrap it in try/catch and keep showing your "link account" prompt on failure.

prepare() can also throw the new .readerTokenInvalid; a retry typically recovers.

Session Management

The SDK manages the reader lifecycle. The standard demo polls the public status rather than subscribing to the reader event stream. A SwiftUI .task stops when its view disappears, so polling does not accumulate across navigation:

import SwiftUI
import KoardSDK

struct ReaderStatusView: View {
    @State private var statusMessage = "Reader not ready"
    @State private var isReady = false

    var body: some View {
        Text(statusMessage)
            .accessibilityValue(isReady ? "Ready" : "Not ready")
            .task { @MainActor in
                while !Task.isCancelled {
                    let status = KoardMerchantSDK.shared.status
                    statusMessage = status.info
                    isReady = status.isReady
                    do {
                        try await Task.sleep(nanoseconds: 250_000_000)
                    } catch {
                        return
                    }
                }
            }
    }
}

Use readiness to gate the payment button. Prepare after login, a location change, or a readiness failure, and handle accountNotLinked through the operator's explicit linking action. Avoid launching overlapping preparation tasks when the app returns to the foreground.

Logout and Cleanup

logout() signs out the merchant while leaving the SDK initialized. To also clear the SDK API key and require initialization again, call KoardMerchantSDK.shared.deinit(). Read KoardMerchantSDK.shared.isInitialized to display that state. See SDK Lifecycle and Deinitialization for the reset sequence and enrollment behavior.

private func logout() {
    // Clear all session data
    KoardMerchantSDK.shared.logout()
  
    print("Logged out successfully")
  
    // Redirect to login screen
}

Best Practices

SDK Management

  • Token Management: The SDK handles all token refresh automatically
  • Error Handling: Always wrap SDK calls in try-catch blocks
  • Background Handling: The SDK manages background transitions automatically
  • Session Preparation: Call prepare() before each payment session

Payment Processing

  • Amount Formatting: Always use minor currency units (e.g., 1050 cents for $10.50)
  • Include Breakdowns: Provide detailed breakdowns for accurate tax and tip reporting
  • Location Setting: Set active location before any payment operations
  • Store Transaction IDs: Save transaction IDs for all follow-up operations

User Experience

  • Monitor Reader Status: Read the SDK status for readiness; leave its reader event subscription to the SDK
  • Handle All States: Implement handlers for all transaction states
  • Provide Clear Feedback: Show clear messages for declined or failed transactions

Gateway Considerations

  • Know Your Gateway: Different gateways (TSYS, Payroc) have different features
  • Batch Management: Understand your gateway's batch requirements
  • Response Codes: Response codes vary by gateway

For more best practices, see:

Troubleshooting

Common Issues

Account Linking Issues

  • Ensure device has iCloud account configured
  • Verify device has passcode enabled
  • Check that device supports Apple Tap to Pay on iPhone

Token Expiration

  • SDK automatically refreshes tokens
  • Check network connectivity
  • Verify API key is valid

Card Reader Not Ready

  • Call prepare() before processing payments
  • Ensure merchant is authenticated with login()
  • Check that account is linked with isAccountLinked()

Missing Location

  • Verify location is set with setActiveLocationID()
  • Ensure location has valid terminal configuration
  • Check that location belongs to authenticated merchant

Transaction Errors

  • Check transaction state before performing operations
  • Verify amounts are within valid ranges
  • Review gateway response for detailed error information

For more troubleshooting help, see:

Requirements

  • iOS 17.4+
  • Xcode 16.3+
  • Swift 5.9+

See also

This wraps up payment processing with the iOS SDK. See the links below for next steps in your integration: