iOS SDK - Accept Payments
Learn how to accept payments in your iOS application using the Omise iOS SDK. This guide covers creating charges, handling 3D Secure authentication, and implementing various payment flows.
Overviewโ
The Omise iOS SDK provides native Swift and Objective-C interfaces for accepting payments in iOS applications. The SDK handles:
- Tokenization of payment information
- 3D Secure authentication flows
- Apple Pay integration
- Card scanning capabilities
- Payment method selection UI
Key Featuresโ
- Native iOS Implementation - Built with Swift for optimal performance
- Secure Token Creation - Client-side tokenization without PCI compliance burden
- 3D Secure 2.0 Support - Seamless authentication handling
- Apple Pay Ready - Built-in Apple Pay support
- Customizable UI - Pre-built payment forms or custom implementations
- Offline Support - Queue payments for later processing
Prerequisitesโ
Before implementing payment acceptance:
- Complete SDK installation (see Developer Tools > iOS SDK)
- Configure your public key
- Set up your backend to create charges
- Test with development credentials
Payment Flow Overviewโ
The typical iOS payment flow:
User enters card details
โ
SDK tokenizes card (client-side)
โ
Token sent to your backend
โ
Backend creates charge with token
โ
3D Secure authentication (if required)
โ
Payment completed
Creating a Basic Chargeโ
Step 1: Collect Card Informationโ
import OmiseSDK
class PaymentViewController: UIViewController {
func processPayment(amount: Int64, currency: String) {
// Create a payment request
let request = OmiseTokenRequest(
name: "John Appleseed",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)
// Create token
let client = OmiseSDKClient(publicKey: "pkey_test_123")
client.send(request) { [weak self] result in
switch result {
case .success(let token):
self?.createCharge(with: token.id, amount: amount, currency: currency)
case .failure(let error):
self?.handleError(error)
}
}
}
func createCharge(with tokenId: String, amount: Int64, currency: String) {
// Send to your backend
let parameters: [String: Any] = [
"token": tokenId,
"amount": amount,
"currency": currency,
"return_uri": "myapp://payment/complete"
]
// Make API call to your backend
APIClient.shared.post("/charges", parameters: parameters) { result in
switch result {
case .success(let charge):
self.handleChargeResult(charge)
case .failure(let error):
self.handleError(error)
}
}
}
}
Step 2: Using Pre-built Payment Formโ
import OmiseSDK
class CheckoutViewController: UIViewController {
func presentPaymentForm() {
let capability = loadCapability() // Load from your backend
let paymentConfiguration = PaymentCreatorConfiguration(
publicKey: "pkey_test_123",
amount: 100000, // 1,000.00 in smallest currency unit
currency: .thb,
capability: capability
)
let paymentController = PaymentCreatorController.makePaymentCreatorController(
configuration: paymentConfiguration
) { [weak self] result in
self?.handlePaymentResult(result)
}
present(paymentController, animated: true)
}
func handlePaymentResult(_ result: Result<PaymentToken, Error>) {
switch result {
case .success(let token):
// Send token to backend to create charge
createChargeOnBackend(token: token)
case .failure(let error):
showError(error.localizedDescription)
}
}
func createChargeOnBackend(token: PaymentToken) {
let parameters: [String: Any] = [
"source": token.id,
"amount": 100000,
"currency": "THB",
"return_uri": "myapp://payment/complete"
]
APIClient.shared.post("/charges", parameters: parameters) { result in
// Handle charge creation result
self.processChargeResponse(result)
}
}
}
Handling 3D Secure Authenticationโ
Implementing 3D Secure Flowโ
import OmiseSDK
import SafariServices
class PaymentProcessor {
var authorizingViewController: SFSafariViewController?
func handleChargeResponse(_ charge: Charge) {
guard charge.status != .successful else {
// Payment completed without 3DS
showSuccess()
return
}
// Check if 3D Secure authentication is required
if charge.status == .pending,
let authorizeURL = charge.authorizeURL {
present3DSecure(url: authorizeURL)
}
}
func present3DSecure(url: URL) {
let safariVC = SFSafariViewController(url: url)
safariVC.delegate = self
authorizingViewController = safariVC
present(safariVC, animated: true)
}
// Handle return from 3D Secure
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
guard url.scheme == "myapp",
url.host == "payment" else {
return false
}
// Close Safari view controller
authorizingViewController?.dismiss(animated: true) {
// Verify payment status with backend
self.verifyPaymentStatus()
}
return true
}
func verifyPaymentStatus() {
// Check charge status from your backend
APIClient.shared.get("/charges/verify") { result in
switch result {
case .success(let charge):
if charge.status == .successful {
self.showSuccess()
} else if charge.status == .failed {
self.showError("Payment failed")
}
case .failure(let error):
self.showError(error.localizedDescription)
}
}
}
}
extension PaymentProcessor: SFSafariViewControllerDelegate {
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
// User cancelled 3DS
showError("Authentication cancelled")
}
}
Configure URL Schemeโ
In your Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.mycompany.myapp</string>
</dict>
</array>
Saved Card Paymentsโ
Charging a Saved Cardโ
class SavedCardPayment {
func chargeWithSavedCard(customerId: String, cardId: String, amount: Int64) {
let parameters: [String: Any] = [
"customer": customerId,
"card": cardId,
"amount": amount,
"currency": "THB",
"return_uri": "myapp://payment/complete"
]
APIClient.shared.post("/charges", parameters: parameters) { result in
switch result {
case .success(let charge):
self.handleChargeResponse(charge)
case .failure(let error):
self.handleError(error)
}
}
}
func displaySavedCards(customerId: String) {
APIClient.shared.get("/customers/\(customerId)/cards") { result in
switch result {
case .success(let cards):
self.showCardSelection(cards)
case .failure(let error):
self.handleError(error)
}
}
}
func showCardSelection(_ cards: [Card]) {
let alert = UIAlertController(
title: "Select Card",
message: "Choose a payment method",
preferredStyle: .actionSheet
)
for card in cards {
let action = UIAlertAction(
title: "\(card.brand) โขโขโขโข \(card.lastDigits)",
style: .default
) { _ in
self.chargeSelectedCard(card)
}
alert.addAction(action)
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
}
Saving a New Cardโ
func saveCardForFutureUse(customerId: String) {
let request = OmiseTokenRequest(
name: "John Appleseed",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)
let client = OmiseSDKClient(publicKey: "pkey_test_123")
client.send(request) { result in
switch result {
case .success(let token):
self.attachCardToCustomer(customerId: customerId, tokenId: token.id)
case .failure(let error):
self.handleError(error)
}
}
}
func attachCardToCustomer(customerId: String, tokenId: String) {
let parameters = ["card": tokenId]
APIClient.shared.post("/customers/\(customerId)/cards", parameters: parameters) { result in
switch result {
case .success:
self.showSuccess("Card saved successfully")
case .failure(let error):
self.handleError(error)
}
}
}
Custom Payment Formโ
Building a Custom UIโ
import OmiseSDK
class CustomPaymentForm: UIViewController {
@IBOutlet weak var cardNumberField: UITextField!
@IBOutlet weak var expiryField: UITextField!
@IBOutlet weak var cvvField: UITextField!
@IBOutlet weak var cardholderField: UITextField!
@IBOutlet weak var payButton: UIButton!
let client = OmiseSDKClient(publicKey: "pkey_test_123")
@IBAction func payButtonTapped(_ sender: UIButton) {
guard validateForm() else {
showError("Please fill all required fields")
return
}
createToken()
}
func validateForm() -> Bool {
guard let number = cardNumberField.text, !number.isEmpty,
let expiry = expiryField.text, !expiry.isEmpty,
let cvv = cvvField.text, !cvv.isEmpty,
let name = cardholderField.text, !name.isEmpty else {
return false
}
// Validate card number using Luhn algorithm
let isValid = OmiseCardValidator.validate(number: number)
return isValid
}
func createToken() {
let expiry = parseExpiry(expiryField.text ?? "")
let request = OmiseTokenRequest(
name: cardholderField.text ?? "",
number: cardNumberField.text ?? "",
expirationMonth: expiry.month,
expirationYear: expiry.year,
securityCode: cvvField.text ?? ""
)
showLoading()
client.send(request) { [weak self] result in
self?.hideLoading()
switch result {
case .success(let token):
self?.processPayment(with: token.id)
case .failure(let error):
self?.showError(error.localizedDescription)
}
}
}
func parseExpiry(_ text: String) -> (month: Int, year: Int) {
let components = text.components(separatedBy: "/")
let month = Int(components[0].trimmingCharacters(in: .whitespaces)) ?? 1
let year = Int(components[1].trimmingCharacters(in: .whitespaces)) ?? 2025
return (month, year)
}
}
Card Number Formattingโ
extension CustomPaymentForm: UITextFieldDelegate {
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
guard textField == cardNumberField else {
return true
}
let currentText = textField.text ?? ""
let newText = (currentText as NSString).replacingCharacters(in: range, with: string)
let digitsOnly = newText.components(separatedBy: .decimalDigits.inverted).joined()
// Limit to 16 digits
guard digitsOnly.count <= 16 else {
return false
}
// Format with spaces
let formatted = formatCardNumber(digitsOnly)
textField.text = formatted
// Update card brand icon
updateCardBrandIcon(digitsOnly)
return false
}
func formatCardNumber(_ number: String) -> String {
var formatted = ""
for (index, char) in number.enumerated() {
if index > 0 && index % 4 == 0 {
formatted += " "
}
formatted.append(char)
}
return formatted
}
func updateCardBrandIcon(_ number: String) {
let brand = OmiseCardBrand.detect(from: number)
// Update UI with card brand icon
cardBrandImageView.image = UIImage(named: brand.rawValue)
}
}
Apple Pay Integrationโ
Implementing Apple Payโ
import PassKit
class ApplePayProcessor: NSObject {
func startApplePay(amount: Decimal, currency: String) {
let request = PKPaymentRequest()
request.merchantIdentifier = "merchant.com.mycompany.myapp"
request.supportedNetworks = [.visa, .masterCard, .amex]
request.merchantCapabilities = .capability3DS
request.countryCode = "TH"
request.currencyCode = currency
let item = PKPaymentSummaryItem(
label: "Total",
amount: NSDecimalNumber(decimal: amount)
)
request.paymentSummaryItems = [item]
guard let controller = PKPaymentAuthorizationController(paymentRequest: request) else {
return
}
controller.delegate = self
controller.present()
}
}
extension ApplePayProcessor: PKPaymentAuthorizationControllerDelegate {
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
) {
// Convert Apple Pay token to Omise token
processApplePayToken(payment.token) { result in
switch result {
case .success:
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
case .failure(let error):
let errors = [error]
completion(PKPaymentAuthorizationResult(status: .failure, errors: errors))
}
}
}
func paymentAuthorizationControllerDidFinish(
_ controller: PKPaymentAuthorizationController
) {
controller.dismiss()
}
func processApplePayToken(_ token: PKPaymentToken,
completion: @escaping (Result<Void, Error>) -> Void) {
let paymentData = token.paymentData
// Send to backend to create Omise token and charge
APIClient.shared.post("/apple-pay/process", parameters: [
"payment_data": paymentData.base64EncodedString()
]) { result in
completion(result.map { _ in () })
}
}
}
Error Handlingโ
Comprehensive Error Handlingโ
enum PaymentError: LocalizedError {
case tokenizationFailed(Error)
case chargeFailed(String)
case authenticationFailed
case networkError(Error)
case invalidCard
case insufficientFunds
case cardDeclined
var errorDescription: String? {
switch self {
case .tokenizationFailed(let error):
return "Failed to process card: \(error.localizedDescription)"
case .chargeFailed(let message):
return message
case .authenticationFailed:
return "Payment authentication failed"
case .networkError:
return "Network error. Please check your connection."
case .invalidCard:
return "Invalid card information"
case .insufficientFunds:
return "Insufficient funds"
case .cardDeclined:
return "Card declined by bank"
}
}
}
class PaymentErrorHandler {
func handleError(_ error: Error) {
if let omiseError = error as? OmiseError {
handleOmiseError(omiseError)
} else {
showGenericError(error)
}
}
func handleOmiseError(_ error: OmiseError) {
switch error.code {
case .invalidCard:
showError("Please check your card information")
case .insufficientFunds:
showError("Insufficient funds on card")
case .stolenOrLostCard:
showError("Card reported as lost or stolen")
case .failedProcessing:
showError("Payment processing failed")
default:
showError(error.message)
}
// Log for analytics
logError(error)
}
func showError(_ message: String) {
let alert = UIAlertController(
title: "Payment Error",
message: message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
// Present alert
if let topVC = UIApplication.topViewController() {
topVC.present(alert, animated: true)
}
}
}
Common Use Casesโ
Subscription Paymentโ
class SubscriptionPayment {
func processSubscriptionPayment(plan: String, customerId: String) {
// First payment with card
let parameters: [String: Any] = [
"customer": customerId,
"amount": 99900, // 999.00 THB
"currency": "THB",
"description": "Monthly \(plan) subscription",
"metadata": [
"plan": plan,
"type": "subscription"
]
]
APIClient.shared.post("/charges", parameters: parameters) { result in
switch result {
case .success(let charge):
if charge.status == .successful {
// Create schedule on backend
self.createRecurringSchedule(customerId: customerId, plan: plan)
}
case .failure(let error):
self.handleError(error)
}
}
}
func createRecurringSchedule(customerId: String, plan: String) {
let parameters: [String: Any] = [
"customer": customerId,
"amount": 99900,
"period": "month",
"description": "\(plan) subscription"
]
APIClient.shared.post("/schedules", parameters: parameters) { result in
self.handleScheduleCreation(result)
}
}
}
One-Time Purchaseโ
class OneTimePurchase {
func processCheckout(items: [CartItem]) {
let total = items.reduce(0) { $0 + $1.price }
// Show payment form
showPaymentForm(amount: total) { tokenId in
self.createOrder(items: items, tokenId: tokenId)
}
}
func createOrder(items: [CartItem], tokenId: String) {
let orderItems = items.map { [
"id": $0.id,
"quantity": $0.quantity
]}
let parameters: [String: Any] = [
"token": tokenId,
"items": orderItems,
"return_uri": "myapp://order/complete"
]
APIClient.shared.post("/orders", parameters: parameters) { result in
self.handleOrderCreation(result)
}
}
}
Multi-Currency Checkoutโ
class MultiCurrencyCheckout {
func processInternationalPayment(amount: Int64, currency: String) {
// Convert currency if needed
convertCurrency(amount: amount, from: "THB", to: currency) { convertedAmount in
self.createPayment(amount: convertedAmount, currency: currency)
}
}
func createPayment(amount: Int64, currency: String) {
let parameters: [String: Any] = [
"amount": amount,
"currency": currency,
"description": "International purchase"
]
showPaymentForm(parameters: parameters)
}
}
Best Practicesโ
Securityโ
-
Never Store Card Data
// โ Good - Use tokens
let token = createToken(from: cardData)
sendToBackend(token: token.id)
// โ Bad - Don't store raw card data
UserDefaults.standard.set(cardNumber, forKey: "card") -
Validate on Client and Server
func validateCard() -> Bool {
guard OmiseCardValidator.validate(number: cardNumber) else {
return false
}
guard OmiseCardValidator.validateCVV(cvv, for: cardBrand) else {
return false
}
return true
} -
Use Secure Communication
let configuration = URLSessionConfiguration.default
configuration.tlsMinimumSupportedProtocolVersion = .TLSv12
User Experienceโ
-
Show Clear Loading States
func showPaymentProgress() {
let hud = MBProgressHUD.showAdded(to: view, animated: true)
hud.label.text = "Processing payment..."
hud.detailsLabel.text = "Please wait"
} -
Provide Payment Feedback
func showPaymentSuccess() {
let alert = UIAlertController(
title: "Payment Successful",
message: "Your payment has been processed",
preferredStyle: .alert
)
// Haptic feedback
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
} -
Handle Network Issues
func retryPayment() {
let alert = UIAlertController(
title: "Connection Issue",
message: "Unable to process payment. Retry?",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Retry", style: .default) { _ in
self.processPayment()
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
Performanceโ
-
Optimize Token Creation
private var tokenCreationTask: URLSessionTask?
func createToken() {
tokenCreationTask?.cancel()
tokenCreationTask = client.send(request) { result in
// Handle result
}
} -
Cache Payment Methods
private var cachedCards: [Card]?
private var cacheTimestamp: Date?
func loadSavedCards(forceRefresh: Bool = false) {
if !forceRefresh,
let cached = cachedCards,
let timestamp = cacheTimestamp,
Date().timeIntervalSince(timestamp) < 300 {
displayCards(cached)
return
}
fetchCardsFromServer()
}
Troubleshootingโ
Common Issuesโ
Token Creation Fails
// Check public key configuration
let client = OmiseSDKClient(publicKey: "pkey_test_123")
// Verify card details
print("Card number valid: \(OmiseCardValidator.validate(number: number))")
print("CVV valid: \(OmiseCardValidator.validateCVV(cvv, for: brand))")
3D Secure Not Working
// Ensure return URI is registered
// Check Info.plist for URL scheme
// Verify authorization URL is valid
if let url = charge.authorizeURL {
print("Auth URL: \(url)")
present3DSecure(url: url)
}
Apple Pay Not Available
if PKPaymentAuthorizationController.canMakePayments() {
if PKPaymentAuthorizationController.canMakePayments(
usingNetworks: [.visa, .masterCard]
) {
showApplePayButton()
}
} else {
print("Apple Pay not available on this device")
}
Debug Modeโ
#if DEBUG
extension OmiseSDKClient {
func enableDebugMode() {
// Log all requests
self.logger = { request in
print("API Request: \(request)")
}
}
}
#endif
FAQโ
General Questionsโ
Q: Do I need to handle PCI compliance for iOS apps?
A: No. The Omise iOS SDK tokenizes card data on the client side before it reaches your servers, significantly reducing PCI compliance requirements. However, you should still follow security best practices.
Q: Can I accept payments offline?
A: Partial support. You can collect card information offline and create tokens later, but the actual charge requires an internet connection for authorization.
Q: What iOS versions are supported?
A: The Omise iOS SDK supports iOS 12.0 and later. For best results and latest features, we recommend supporting iOS 14.0+.
Q: How do I test payments in development?
A: Use test card numbers (4242 4242 4242 4242) with any future expiry date and any 3-digit CVV. Use your test public key (pkey_test_xxx) for testing.
Q: Can I customize the payment form UI?
A: Yes. You can either use the pre-built payment controller with customization options or build your own UI and use the SDK for tokenization only.
Q: How long are tokens valid?
A: Tokens expire after they're used to create a charge or attached to a customer. They cannot be reused.
3D Secure Questionsโ
Q: Is 3D Secure required for all payments?
A: Not all payments require 3D Secure. It depends on the card issuer, amount, and merchant settings. Your implementation should handle both flows.
Q: What happens if users cancel 3D Secure?
A: The payment fails and the charge status remains pending or failed. Handle this in your Safari view controller delegate.
Q: Can I test 3D Secure authentication?
A: Yes. Use test card 4000000000003063 to simulate 3D Secure authentication in test mode.
Q: How do I handle 3D Secure timeouts?
A: Implement timeout detection in your authentication handler and provide users with the option to retry or cancel the payment.
Apple Pay Questionsโ
Q: Do I need special approval for Apple Pay?
A: Yes. You need an Apple Developer account, merchant ID configuration, and payment processing certificate. See Apple Pay documentation.
Q: Can I use Apple Pay in test mode?
A: Yes, but you need to configure test certificates and use Sandbox environment credentials.
Q: What's the difference between Apple Pay and card payments?
A: Apple Pay uses tokenized card data and provides enhanced security. The implementation flow is different but the backend charge creation is similar.
Related Resourcesโ
- Developer Tools - iOS SDK Setup
- Mobile Payments Overview
- Charges API Reference
- Tokens API Reference
- 3D Secure Guide
- Apple Pay Integration
- Error Codes Reference
- Testing Guide