Running Payments
Process payments with the Koard Merchant SDK to enable tap-to-pay functionality in your iOS application.
If you're ready to start developing, see our SDK installation guide.
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
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 KoardSDK
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure SDK options
let options = KoardOptions(
environment: .uat, // or .production
loggingLevel: .debug // .debug, .info, .warning, .error, .none, .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")
// After login, set up location
try await setupLocation()
} catch {
print("Authentication failed: \(error)")
throw error
}
}
Set Location
Retrieve and set the active location. Locations are attached to terminals which determines the MID, TID and Processor Configuration to be used for the payments API. This determines whether the merchant is leveraging TSYS, Payroc, Fiserv, or Elavon payment processing rails.
For information about processor-specific options, see the Processor Configurations guide.
private func setupLocation() async throws {
do {
// Get available locations
let locations = try await KoardMerchantSDK.shared.locations()
guard !locations.isEmpty else {
throw PaymentError.noLocationsAvailable
}
// For single location merchants, use the first location
let activeLocation = locations.first!
// For multi-location merchants, let user select
// let activeLocation = userSelectedLocation
// Set the active location
KoardMerchantSDK.shared.setActiveLocationID(activeLocation.id)
print("Active location set: \(activeLocation.name)")
} catch {
print("Location setup failed: \(error)")
throw error
}
}
Prepare a Card Reader Session
Before accepting payments, prepare the card reader. The reader preparation leverages a PaymentCardReader.Token associated with the merchant session.
import KoardSDK
private func prepareCardReader() async throws {
do {
// Check if account is linked (required for Tap to Pay)
let isLinked = try await KoardMerchantSDK.shared.isAccountLinked()
if !isLinked {
// Link the merchant account to Apple Pay
KoardMerchantSDK.shared.linkAccount()
// Wait for linking to complete
// This typically requires user interaction
return
}
// Prepare the card reader session
try await KoardMerchantSDK.shared.prepare()
print("Card reader prepared and ready")
// Optional: Monitor reader status
monitorReaderStatus()
} catch {
print("Card reader preparation failed: \(error)")
throw error
}
}
private func monitorReaderStatus() {
Task {
// Monitor reader events
for await event in KoardMerchantSDK.shared.readerEvents {
DispatchQueue.main.async {
self.handleReaderEvent(event)
}
}
}
}
private func handleReaderEvent(_ event: PaymentCardReader.Event) {
switch event {
case .readyForTap:
print("Ready for tap")
case .cardDetected:
print("Card detected")
case .readCompleted:
print("Card read completed")
case .readCancelled:
print("Card read cancelled")
default:
print("Reader event: \(event.description)")
}
}
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.
private func processSale() async throws {
// Create payment breakdown (optional)
let breakdown = PaymentBreakdown(
subtotal: 1000, // $10.00 in cents
taxRate: 0.0875, // 8.75% expressed as a 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(
transactionId: "CUSTOM_UUID4", // optional. if not passed, Koard will gen.
amount: 1288, // Total amount in cents
breakdown: breakdown, // Optional breakdown
currency: currency,
type: .sale // Transaction type
)
// Handle the response
try await handleTransactionResponse(response)
} catch {
print("Sale failed: \(error)")
throw error
}
}
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.
private func processPreauth() async throws {
let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
do {
// Process preauthorization (no breakdown needed)
let response = try await KoardMerchantSDK.shared.preauth(
transactionId: "CUSTOM_UUID4", // optional. if not passed, Koard will gen.
amount: 1000, // Amount to preauthorize in cents
currency: currency
)
print("Preauth successful: \(response.transactionId ?? "Unknown")")
// Store transaction ID for later capture/reverse
UserDefaults.standard.set(response.transactionId, forKey: "lastPreauthId")
} catch {
print("Preauth failed: \(error)")
throw error
}
}
Handle Transaction Responses
private func handleTransactionResponse(_ response: TransactionResponse) async throws {
guard let transaction = response.transaction else {
throw PaymentError.invalidResponse
}
switch transaction.status {
case .approved:
print("Transaction approved!")
print("Transaction ID: \(transaction.transactionId)")
print("Amount: $\(Double(transaction.totalAmount) / 100.0)")
case .surchargePending:
print("Surcharge pending - customer approval required")
// Show surcharge disclosure to customer
if let disclosure = transaction.surchargeDisclosure {
let approved = try await showSurchargeDisclosure(disclosure)
// Confirm or deny the surcharge
let confirmedTransaction = try await KoardMerchantSDK.shared.confirm(
transaction: transaction.transactionId,
confirm: approved
)
print("Final transaction status: \(confirmedTransaction.status)")
}
case .declined:
print("Transaction declined: \(transaction.statusReason ?? "Unknown reason")")
case .error:
print("Transaction error: \(transaction.statusReason ?? "Unknown error")")
default:
print("Transaction status: \(transaction.status.string)")
}
}
private func showSurchargeDisclosure(_ disclosure: String) async throws -> Bool {
// Show disclosure to customer and get their approval
// This should be implemented based on your UI requirements
return await withCheckedContinuation { continuation in
DispatchQueue.main.async {
let alert = UIAlertController(
title: "Surcharge Notice",
message: disclosure,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Accept", style: .default) { _ in
continuation.resume(returning: true)
})
alert.addAction(UIAlertAction(title: "Decline", style: .cancel) { _ in
continuation.resume(returning: false)
})
// Present alert (you'll need to implement this based on your view hierarchy)
// self.present(alert, animated: true)
}
}
}
Transaction Management
Refund a Transaction
import KoardSDK
private func processRefund(transactionId: String, amount: Int? = nil) async throws {
do {
let response = try await KoardMerchantSDK.shared.refund(
transactionId: transactionId,
amount: amount // nil for full refund
)
print("Refund successful: \(response.transactionId ?? "Unknown")")
} catch {
print("Refund failed: \(error)")
throw error
}
}
Reverse a Preauthorization
private func reversePreauth(transactionId: String, amount: Int? = nil) async throws {
do {
let response = try await KoardMerchantSDK.shared.reverse(
transactionId: transactionId,
amount: amount // nil for full reversal
)
print("Reversal successful: \(response.transactionId ?? "Unknown")")
} catch {
print("Reversal failed: \(error)")
throw error
}
}
Note: Transactions can be partially reversed and refunded. When an authorization is reversed to 0 or a capture is refunded to 0, the transaction status becomes "cancelled". 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) async throws {
// Optional: Add breakdown for the additional amount
let breakdown = PaymentBreakdown(
subtotal: additionalAmount,
taxRate: 0.0875, // 8.75% expressed as a decimal
taxAmount: Int(Double(additionalAmount) * 0.0875),
tipAmount: 0,
tipType: .fixed
)
do {
let response = try await KoardMerchantSDK.shared.auth(
transactionId: transactionId,
amount: additionalAmount,
breakdown: breakdown // Optional
)
print("Incremental auth successful: \(response.transactionId ?? "Unknown")")
} 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, finalAmount: Int? = nil) async throws {
// Optional: Update breakdown with final tip amount
let finalBreakdown = PaymentBreakdown(
subtotal: 1000, // $10.00
taxRate: 0.0875, // 8.75% expressed as a decimal
taxAmount: 88, // $0.88
tipAmount: 300, // $3.00 final tip
tipType: .fixed
)
do {
let response = try await KoardMerchantSDK.shared.capture(
transactionId: transactionId,
amount: finalAmount, // nil to capture full authorized amount
breakdown: finalBreakdown // Optional: updated breakdown with final tip
)
print("Capture successful: \(response.transactionId ?? "Unknown")")
} catch {
print("Capture failed: \(error)")
throw error
}
}
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
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 .missingMerchantCode:
print("Merchant not authenticated")
// Redirect to login
case .TTPPaymentFailed(let ttpError):
print("Tap to Pay error: \(ttpError)")
// Handle specific TTP errors
default:
print("Koard SDK error: \(koardError)")
}
} else {
print("General error: \(error)")
}
}
Session Management
private func handleAppLifecycle() {
// The SDK automatically handles background/foreground transitions
// But you can monitor the status if needed
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
Task {
// Check if card reader needs re-preparation
if KoardMerchantSDK.shared.status != .ready {
try? await self.prepareCardReader()
}
}
}
}
Logout and Cleanup
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 base 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 Events: Track reader events for better UX feedback
- 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:
- Payment Lifecycle guide
- Sale and Preauth
- Capture and Refund
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.0+
- 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:
- Payment Lifecycle - Complete payment flow guide
- Adding Tap to Pay - Enable contactless payments
- Installing the SDK - SDK installation guide
- Creating a Sandbox Apple Account - Maintain test identities
- Getting Ready for Production - Switch schemes and API keys
- Apple Best Practices - iOS development guidelines