NeteviaSDK is a lightweight, modern SDK designed for iOS apps to enable Tap to Pay functionality, merchant authentication, transaction processing, location management, and digital receipt delivery.
Built with Swift and modularized using Swift Package Manager, NeteviaSDK provides flexible distribution options including Swift Package, CocoaPods, and XCFramework.
- β Tap to Pay support using Appleβs Proximity Reader APIs
- π Merchant login and token handling
- π³ Sale, refund, capture, reverse, and pre-auth transactions
- π§Ύ Receipt delivery via email or SMS
- π Fallback payment links for browser-based checkout
- π Multi-location merchant support
- π§ͺ Written with modern
@Test
-based Swift Testing (iOS 17+) - π¦ Distributed via SPM, CocoaPods, or as a binary XCFramework
Add this to your Package.swift
:
.package(url: "https://github.com/Netevia/netevia-ios.git", from: "1.0.0")
Then add NeteviaSDK
as a dependency in your target.
Add this line to your Podfile
:
pod 'NeteviaSDK', :git => 'https://github.com/Netevia/netevia-ios.git', :tag => '1.0.0'
Then run:
pod install
- Go to the Releases page
- Download
NeteviaSDK.xcframework.zip
- Unzip and drag
NeteviaSDK.xcframework
into your Xcode project - In your targetβs General > Frameworks, Libraries & Embedded Content, select "Embed & Sign"
- Import it in your code:
import NeteviaSDK
This SDK uses DocC to generate rich developer documentation.
Full SDK documentation can be found here: NeteviaSDK Documentation.
- Open
Package.swift
in Xcode (not the.xcodeproj
) - From the menu, select: Product > Build Documentation
- Or Option-click on any symbol to view its documentation
You can also find grouped API overviews in:
Sources/NeteviaSDK/NeteviaSDK.docc/NeteviaSDK.md
Unit tests are written using Swift Testing (iOS 17+).
swift test
Or press βU in Xcode after opening Package.swift
.
This comprehensive guide covers everything you need to know about integrating and using the NeteviaMerchantSDK in your iOS application.
The NeteviaMerchantSDK provides a complete payment processing solution for iOS mPOS applications, including:
- Tap to Pay functionality using Apple's ProximityReader framework
- Card reader session management with automatic token handling
- Transaction processing (sales, preauthorizations, refunds, reversals)
- Location management for multi-location merchants
- Real-time transaction monitoring and history
The SDK manages several types of tokens automatically:
- API Key: Your merchant API key for Netevia services
- Login Token (JWT): Obtained after successful merchant login, used for API authentication
- Card Reader Token: Apple's ProximityReader token for Tap to Pay functionality
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
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 early in your app lifecycle (typically in AppDelegate
or SceneDelegate
):
import NeteviaMerchantSDK
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure SDK options
let options = NeteviaOptions(
environment: .uat, // or .production
loggingLevel: .debug // .debug, .info, .warning, .error, .none
)
// Initialize with your API key
NeteviaMerchantSDK.shared.initialize(
options: options,
apiKey: "your-netvia-api-key"
)
return true
}
}
Before processing any payments, authenticate the merchant:
private func authenticateMerchant() async throws {
do {
// Login with merchant credentials
try await NeteviaMerchantSDK.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
}
}
Retrieve and set the active location:
private func setupLocation() async throws {
do {
// Get available locations
let locations = try await NeteviaMerchantSDK.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
NeteviaMerchantSDK.shared.setActiveLocationID(activeLocation.id)
print("Active location set: \(activeLocation.name)")
} catch {
print("Location setup failed: \(error)")
throw error
}
}
Before accepting payments, prepare the card reader:
private func prepareCardReader() async throws {
do {
// Check if account is linked (required for Tap to Pay)
let isLinked = try await NeteviaMerchantSDK.shared.isAccountLinked()
if !isLinked {
// Link the merchant account to Apple Pay
NeteviaMerchantSDK.shared.linkAccount()
// Wait for linking to complete
// This typically requires user interaction
return
}
// Prepare the card reader session
try await NeteviaMerchantSDK.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 NeteviaMerchantSDK.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)")
}
}
private func processSale() async throws {
// Create payment breakdown (optional)
let breakdown = PaymentBreakdown(
subtotal: 1000, // $10.00 in cents
taxRate: 875, // 8.75% (8.75 * 100)
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")
// Optional: Generate UUID4 for idempotency (prevents duplicate transactions)
let customTransactionId = UUID().uuidString
do {
// Process the sale
let response = try await NeteviaMerchantSDK.shared.sale(
amount: 1288, // Total amount in cents
breakdown: breakdown, // Optional breakdown
currency: currency,
transactionId: customTransactionId, // Optional: Use for idempotency. If nil, Netevia generates one
type: .sale // Transaction type
)
// Handle the response
try await handleTransactionResponse(response)
} catch {
print("Sale failed: \(error)")
throw error
}
}
private func processPreauth() async throws {
let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
// Optional: Generate UUID4 for idempotency (prevents duplicate transactions)
let customTransactionId = UUID().uuidString
do {
// Process preauthorization (no breakdown needed)
let response = try await NeteviaMerchantSDK.shared.preauth(
amount: 1000, // Amount to preauthorize in cents
currency: currency,
transactionId: customTransactionId // Optional: Use for idempotency. If nil, Netevia generates one
)
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
}
}
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 NeteviaMerchantSDK.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)
}
}
}
private func processRefund(transactionId: String, amount: Int? = nil) async throws {
do {
let response = try await NeteviaMerchantSDK.shared.refund(
transactionId: transactionId,
amount: amount // nil for full refund
)
print("Refund successful: \(response.transactionId ?? "Unknown")")
} catch {
print("Refund failed: \(error)")
throw error
}
}
private func reversePreauth(transactionId: String, amount: Int? = nil) async throws {
do {
let response = try await NeteviaMerchantSDK.shared.reverse(
transactionId: transactionId,
amount: amount // nil for full reversal
)
print("Reversal successful: \(response.transactionId ?? "Unknown")")
} catch {
print("Reversal failed: \(error)")
throw error
}
}
Authorize additional amounts on an existing transaction:
private func incrementalAuth(transactionId: String, additionalAmount: Int) async throws {
// Optional: Add breakdown for the additional amount
let breakdown = PaymentBreakdown(
subtotal: additionalAmount,
taxRate: 875, // 8.75%
taxAmount: Int(Double(additionalAmount) * 0.0875),
tipAmount: 0,
tipType: .fixed
)
do {
let response = try await NeteviaMerchantSDK.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 previously authorized transaction:
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: 875, // 8.75%
taxAmount: 88, // $0.88
tipAmount: 300, // $3.00 final tip
tipType: .fixed
)
do {
let response = try await NeteviaMerchantSDK.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
}
}
private func getTransactionHistory() async throws {
do {
// Get recent transactions
let history = try await NeteviaMerchantSDK.shared.transactionHistory()
print("Found \(history.transactions.count) transactions")
// Filter by status
let approvedTransactions = try await NeteviaMerchantSDK.shared.transactionsByStatus("approved")
// Search transactions
let searchResults = try await NeteviaMerchantSDK.shared.searchTransactions("card_number_here")
// Advanced filtering
let filteredTransactions = try await NeteviaMerchantSDK.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
}
}
private func handleSDKError(_ error: Error) {
if let neteviaError = error as? NeteviaMerchantSDKError {
switch neteviaError {
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("Netevia SDK error: \(neteviaError)")
}
} else {
print("General error: \(error)")
}
}
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 NeteviaMerchantSDK.shared.status != .ready {
try? await self.prepareCardReader()
}
}
}
}
private func logout() {
// Clear all session data
NeteviaMerchantSDK.shared.logout()
// Clear any stored transaction IDs
UserDefaults.standard.removeObject(forKey: "lastPreauthId")
print("Logged out successfully")
// Redirect to login screen
}
This is the recommended flow for restaurants and hospitality where tip amounts are added after authorization:
private func preauthCaptureWorkflow() async throws {
let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
let transactionId = UUID().uuidString
// Step 1: Preauthorize base amount
let preauthResponse = try await NeteviaMerchantSDK.shared.preauth(
amount: 1000, // $10.00 base amount
currency: currency,
transactionId: transactionId
)
let authorizedTransactionId = preauthResponse.transactionId!
print("Preauth completed: \(authorizedTransactionId)")
// Step 2: Customer adds tip, create final breakdown
let finalBreakdown = PaymentBreakdown(
subtotal: 1000, // $10.00
taxRate: 875, // 8.75%
taxAmount: 88, // $0.88
tipAmount: 200, // $2.00 tip added
tipType: .fixed
)
// Step 3: Capture with final amount and breakdown
let captureResponse = try await NeteviaMerchantSDK.shared.capture(
transactionId: authorizedTransactionId,
amount: 1288, // $12.88 final amount
breakdown: finalBreakdown
)
print("Capture completed: \(captureResponse.transactionId ?? "Unknown")")
}
For complex scenarios where additional authorizations are needed:
private func incrementalAuthWorkflow() async throws {
let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
let transactionId = UUID().uuidString
// Step 1: Initial preauth
let preauthResponse = try await NeteviaMerchantSDK.shared.preauth(
amount: 1000, // $10.00 initial amount
currency: currency,
transactionId: transactionId
)
let authorizedTransactionId = preauthResponse.transactionId!
// Step 2: Customer orders additional items - incremental auth
let additionalBreakdown = PaymentBreakdown(
subtotal: 500, // $5.00 additional items
taxRate: 875, // 8.75%
taxAmount: 44, // $0.44 additional tax
tipAmount: 0,
tipType: .fixed
)
let authResponse = try await NeteviaMerchantSDK.shared.auth(
transactionId: authorizedTransactionId,
amount: 544, // $5.44 additional amount
breakdown: additionalBreakdown
)
// Step 3: Final capture with tip
let finalBreakdown = PaymentBreakdown(
subtotal: 1500, // $15.00 total
taxRate: 875, // 8.75%
taxAmount: 131, // $1.31 total tax
tipAmount: 300, // $3.00 tip
tipType: .fixed
)
let captureResponse = try await NeteviaMerchantSDK.shared.capture(
transactionId: authorizedTransactionId,
amount: 1931, // $19.31 final amount
breakdown: finalBreakdown
)
print("Final capture completed: \(captureResponse.transactionId ?? "Unknown")")
}
For simple transactions where immediate payment is required:
private func saleWorkflow() async throws {
let breakdown = PaymentBreakdown(
subtotal: 1000, // $10.00
taxRate: 875, // 8.75%
taxAmount: 88, // $0.88
tipAmount: 200, // $2.00
tipType: .fixed
)
let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
let transactionId = UUID().uuidString
// Single sale transaction - immediate capture
let response = try await NeteviaMerchantSDK.shared.sale(
amount: 1288, // $12.88 total
breakdown: breakdown,
currency: currency,
transactionId: transactionId
)
print("Sale completed: \(response.transactionId ?? "Unknown")")
}
- 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
- Amount Formatting: Always use cents (e.g., 1050 for $10.50)
- Location Setting: Set active location before any payment operations
- Session Preparation: Call
prepare()
before each payment session - User Experience: Monitor reader events for better UX feedback
- Transaction Idempotency: Use custom UUID4 transaction IDs to prevent duplicate transactions due to network issues or retries
For critical payment operations, especially in unreliable network conditions, use custom transaction IDs:
// Generate a UUID4 for the transaction
let transactionId = UUID().uuidString
// Use the same ID for retries - Netevia will return the same result
let response = try await NeteviaMerchantSDK.shared.sale(
amount: 1000,
breakdown: nil,
currency: currency,
transactionId: transactionId // This ensures idempotency
)
// If network fails and you retry with the same transactionId,
// Netevia will return the original transaction result instead of processing again
Important:
- Use UUID4 format for transaction IDs (e.g.,
UUID().uuidString
) - Store transaction IDs before making requests for retry scenarios
- Same transaction ID will always return the same result
- This prevents accidental duplicate charges during network issues
- Account Linking Issues: Ensure device has iCloud account and passcode enabled
- Token Expiration: SDK automatically refreshes tokens, but check network connectivity
- Card Reader Not Ready: Call
prepare()
and ensure proper authentication - Missing Location: Verify location is set with
setActiveLocationID()
- Background Issues: SDK handles this automatically, but test thoroughly
This guide provides a complete implementation pattern for integrating NeteviaMerchantSDK into your iOS mPOS application.
Follow these steps to build the NeteviaMerchantSDK.xcframework
for distribution:
- Xcode 16.3 or later
- iOS 17.0+ deployment target
- Valid Apple Developer account for code signing
-
Clean previous builds (optional but recommended):
rm -rf build/
-
Create iOS Device archive:
xcodebuild archive \ -project NeteviaMerchantSDK.xcodeproj \ -scheme NeteviaSDK \ -destination "generic/platform=iOS" \ -archivePath ./build/NeteviaSDK-iOS.xcarchive \ SKIP_INSTALL=NO \ BUILD_LIBRARY_FOR_DISTRIBUTION=YES
-
Create iOS Simulator archive:
xcodebuild archive \ -project NeteviaMerchantSDK.xcodeproj \ -scheme NeteviaSDK \ -destination "generic/platform=iOS Simulator" \ -archivePath ./build/NeteviaSDK-iOS-Simulator.xcarchive \ SKIP_INSTALL=NO \ BUILD_LIBRARY_FOR_DISTRIBUTION=YES
-
Create XCFramework:
xcodebuild -create-xcframework \ -framework ./build/NeteviaSDK-iOS.xcarchive/Products/Library/Frameworks/NeteviaSDK.framework \ -framework ./build/NeteviaSDK-iOS-Simulator.xcarchive/Products/Library/Frameworks/NeteviaSDK.framework \ -output ./build/NeteviaMerchantSDK.xcframework
The built framework will be located at:
./build/NeteviaMerchantSDK.xcframework
This XCFramework supports:
- iOS Device (arm64)
- iOS Simulator (arm64, x86_64)
- Drag
NeteviaMerchantSDK.xcframework
into your Xcode project - In your target's "General" tab, add it to "Frameworks, Libraries, and Embedded Content"
- Set the framework to "Embed & Sign"
import NeteviaMerchantSDK
class PaymentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupSDK()
}
private func setupSDK() {
// Initialize SDK
let options = NeteviaOptions(environment: .uat, loggingLevel: .debug)
NeteviaMerchantSDK.shared.initialize(options: options, apiKey: "your-api-key")
}
private func authenticateAndPrepare() async throws {
// Login with merchant credentials
try await NeteviaMerchantSDK.shared.login(code: "merchant-code", pin: "merchant-pin")
// Get locations and set active location
let locations = try await NeteviaMerchantSDK.shared.locations()
if let firstLocation = locations.first {
NeteviaMerchantSDK.shared.setActiveLocationID(firstLocation.id)
}
// Link account (required for Tap to Pay)
NeteviaMerchantSDK.shared.linkAccount()
// Prepare card reader
try await NeteviaMerchantSDK.shared.prepare()
}
private func processPayment() async throws {
// Create payment breakdown (optional)
let breakdown = PaymentBreakdown(
subtotal: 1000, // $10.00
taxRate: 875, // 8.75%
taxAmount: 88, // $0.88
tipAmount: 200, // $2.00
tipType: .fixed
)
// Create currency
let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar")
// Process sale
let response = try await NeteviaMerchantSDK.shared.sale(
amount: 1288, // $12.88 total (subtotal + tax + tip)
breakdown: breakdown,
currency: currency
)
// Handle response
if let transaction = response.transaction {
switch transaction.status {
case .approved:
print("Payment approved: \(transaction.transactionId)")
case .surchargePending:
// Handle surcharge confirmation
let confirmed = try await NeteviaMerchantSDK.shared.confirm(
transaction: transaction.transactionId,
confirm: true
)
print("Surcharge confirmed, final status: \(confirmed.status)")
case .declined:
print("Payment declined: \(transaction.statusReason ?? "Unknown reason")")
default:
print("Payment status: \(transaction.status)")
}
}
}
}
- Amounts in cents: All monetary values should be specified in cents (e.g., 1050 for $10.50)
- Async operations: Most SDK functions are asynchronous and require
await
- Authentication required: Login and set active location before processing payments
- Card reader preparation: Call
prepare()
before accepting payments - Error handling: Wrap calls in try-catch blocks to handle
NeteviaMerchantSDKError
- iOS 17.0+
- Xcode 16.3+
- Swift 5.9+
As of v1.0.0, the old NeteviaMerchantSDKError
enum has been replaced with a cleaner, developer-friendly NeteviaSDKError
.
- Fewer error cases to manage
- Clearer categories for UI and logging
- Built-in
LocalizedError
support - Structured underlying error handling
Old Error | New Error |
---|---|
.urlForming |
.invalidRequest |
.notAuthorized (401, 403) |
.unauthorized |
.merchantBlocked (423) |
.blockedAccount |
.apiError(ApiErrorDetail) |
.server(message:) |
.unsupportedOSVersion("15.4") |
.unsupportedPlatform("15.4") |
.decodingFailure |
.decodingFailure |
.network(URLError) |
.network(...) |
.unknownError |
.unknown(...) |
do {
try await sdk.login(code: "demo", pin: "1234")
} catch let error as NeteviaSDKError {
showAlert(error.localizedDescription)
}
switch error {
case .unauthorized:
showLoginScreen()
case .server(let message):
showAlert(message ?? "Server error")
case .unknown(let underlying):
logger.error("Unexpected error: \(underlying?.localizedDescription ?? "unknown")")
default:
showAlert(error.localizedDescription)
}
NeteviaMerchantSDKError
is deprecated and will be removed in a future version.
MIT License. See LICENSE for details.
For questions, issues or contributions, please open a GitHub Issue or email support@netevia.com.