Skip to main content

iOS SDK

The Omise iOS SDK provides a secure and convenient way to integrate payment processing into your iOS applications. Built for Swift and Objective-C, it handles tokenization of sensitive payment data and supports all major payment methods available in Southeast Asia.

Overviewโ€‹

The iOS SDK enables you to:

  • Tokenize credit cards securely without card data touching your servers
  • Create payment sources for alternative payment methods
  • Implement 3D Secure authentication flows
  • Build custom payment forms with native iOS UI components
  • Support biometric authentication with Touch ID and Face ID
  • Handle errors gracefully with comprehensive error types

Key Featuresโ€‹

  • Native Swift API with async/await support
  • Objective-C compatibility for legacy projects
  • Pre-built UI components for rapid integration
  • Comprehensive input validation
  • Network resilience and automatic retries
  • Full type safety with Swift generics
  • SwiftUI and UIKit support
  • Combine framework integration

Requirementsโ€‹

  • iOS 12.0 or later
  • Xcode 13.0 or later
  • Swift 5.5+ or Objective-C
  • CocoaPods 1.10+, Swift Package Manager, or Carthage

Installationโ€‹

Add the iOS SDK to your project using Swift Package Manager:

  1. In Xcode, go to File > Add Packages
  2. Enter the repository URL:
    https://github.com/omise/omise-ios
  3. Select version 5.0.0 or later
  4. Add to your target

Or add it to your Package.swift:

dependencies: [
.package(url: "https://github.com/omise/omise-ios", from: "5.0.0")
]

CocoaPodsโ€‹

Add the SDK to your Podfile:

platform :ios, '12.0'
use_frameworks!

target 'YourApp' do
pod 'OmiseSDK', '~> 5.0'
end

Then run:

pod install

Carthageโ€‹

Add to your Cartfile:

github "omise/omise-ios" ~> 5.0

Run:

carthage update --use-xcframeworks

Then drag the built OmiseSDK.xcframework into your project.

Quick Startโ€‹

1. Import the SDKโ€‹

import OmiseSDK
// Objective-C
@import OmiseSDK;

2. Configure the Clientโ€‹

Initialize the SDK with your public key:

import OmiseSDK

class PaymentService {
let client: OmiseClient

init() {
// Initialize with public key
client = OmiseClient(publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5")
}
}
// Objective-C
@interface PaymentService : NSObject
@property (nonatomic, strong) OMSClient *client;
@end

@implementation PaymentService

- (instancetype)init {
self = [super init];
if (self) {
_client = [[OMSClient alloc] initWithPublicKey:@"pkey_test_5xyzyx5xyzyx5xyzyx5"];
}
return self;
}

@end

3. Create a Tokenโ€‹

Using Async/Await (Swift 5.5+)โ€‹

func createToken() async throws -> Token {
let request = Token.CreateRequest(
name: "John Doe",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)

let token = try await client.send(request)
print("Token created: \(token.id)")
return token
}

// Usage
Task {
do {
let token = try await createToken()
// Send token to your server
await sendTokenToServer(token.id)
} catch {
print("Error: \(error.localizedDescription)")
}
}

Using Completion Handlersโ€‹

func createToken(completion: @escaping (Result<Token, Error>) -> Void) {
let request = Token.CreateRequest(
name: "John Doe",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)

client.send(request) { result in
DispatchQueue.main.async {
completion(result)
}
}
}

// Usage
createToken { result in
switch result {
case .success(let token):
print("Token created: \(token.id)")
self.sendTokenToServer(token.id)
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}

Objective-Cโ€‹

OMSTokenRequest *request = [[OMSTokenRequest alloc] init];
request.name = @"John Doe";
request.number = @"4242424242424242";
request.expirationMonth = 12;
request.expirationYear = 2025;
request.securityCode = @"123";

[self.client send:request callback:^(OMSToken * _Nullable token, NSError * _Nullable error) {
if (error) {
NSLog(@"Error: %@", error.localizedDescription);
return;
}

NSLog(@"Token created: %@", token.tokenId);
[self sendTokenToServer:token.tokenId];
}];

Configurationโ€‹

Client Configurationโ€‹

// Basic configuration
let client = OmiseClient(publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5")

// Advanced configuration
var config = OmiseClient.Configuration(publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5")
config.apiVersion = "2019-05-29"
config.timeout = 60.0
config.sessionConfiguration = .default

let client = OmiseClient(configuration: config)

Network Configurationโ€‹

// Custom URLSession configuration
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 30
sessionConfig.timeoutIntervalForResource = 60
sessionConfig.requestCachePolicy = .reloadIgnoringLocalCacheData

var config = OmiseClient.Configuration(publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5")
config.sessionConfiguration = sessionConfig

let client = OmiseClient(configuration: config)

Creating Tokensโ€‹

Basic Card Tokenizationโ€‹

func tokenizeCard(
name: String,
number: String,
expirationMonth: Int,
expirationYear: Int,
securityCode: String
) async throws -> Token {
let request = Token.CreateRequest(
name: name,
number: number,
expirationMonth: expirationMonth,
expirationYear: expirationYear,
securityCode: securityCode
)

return try await client.send(request)
}

With Billing Addressโ€‹

func tokenizeCardWithAddress() async throws -> Token {
let address = Token.Address(
street1: "123 Wireless Road",
street2: "Lumpini",
city: "Pathum Wan",
state: "Bangkok",
postalCode: "10330",
country: "TH"
)

let request = Token.CreateRequest(
name: "John Doe",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123",
billingAddress: address
)

return try await client.send(request)
}

Validation Before Tokenizationโ€‹

func validateAndTokenize(
number: String,
expirationMonth: Int,
expirationYear: Int,
securityCode: String
) async throws -> Token {
// Validate card number
guard CardValidator.isValid(number: number) else {
throw ValidationError.invalidCardNumber
}

// Validate expiry
guard CardValidator.isValid(expirationMonth: expirationMonth, year: expirationYear) else {
throw ValidationError.invalidExpiry
}

// Validate CVV
guard CardValidator.isValid(securityCode: securityCode) else {
throw ValidationError.invalidCVV
}

let request = Token.CreateRequest(
name: "John Doe",
number: number,
expirationMonth: expirationMonth,
expirationYear: expirationYear,
securityCode: securityCode
)

return try await client.send(request)
}

enum ValidationError: LocalizedError {
case invalidCardNumber
case invalidExpiry
case invalidCVV

var errorDescription: String? {
switch self {
case .invalidCardNumber:
return "Invalid card number"
case .invalidExpiry:
return "Invalid expiration date"
case .invalidCVV:
return "Invalid security code"
}
}
}

Creating Payment Sourcesโ€‹

Internet Bankingโ€‹

func createInternetBankingSource(amount: Int64, bank: InternetBanking) async throws -> Source {
let request = Source.CreateRequest(
amount: amount,
currency: .thb,
type: .internetBanking(bank)
)

return try await client.send(request)
}

// Usage
let source = try await createInternetBankingSource(
amount: 100000, // 1,000.00 THB
bank: .bay
)

// Redirect user to authorize payment
if let authorizeURL = source.authorizeURL {
UIApplication.shared.open(authorizeURL)
}

Mobile Bankingโ€‹

func createMobileBankingSource(amount: Int64, bank: MobileBanking) async throws -> Source {
let request = Source.CreateRequest(
amount: amount,
currency: .thb,
type: .mobileBanking(bank)
)

return try await client.send(request)
}

// Usage
let source = try await createMobileBankingSource(
amount: 50000, // 500.00 THB
bank: .scb
)

PromptPayโ€‹

func createPromptPaySource(amount: Int64) async throws -> Source {
let request = Source.CreateRequest(
amount: amount,
currency: .thb,
type: .promptPay
)

return try await client.send(request)
}

// Usage
let source = try await createPromptPaySource(amount: 100000)

// Display QR code to user
if let qrCodeURL = source.scanQRCodeURL {
displayQRCode(url: qrCodeURL)
}

TrueMoney Walletโ€‹

func createTrueMoneySource(amount: Int64, phoneNumber: String) async throws -> Source {
let request = Source.CreateRequest(
amount: amount,
currency: .thb,
type: .trueMoney,
phoneNumber: phoneNumber
)

return try await client.send(request)
}

// Usage
let source = try await createTrueMoneySource(
amount: 100000,
phoneNumber: "0812345678"
)

Alipayโ€‹

func createAlipaySource(amount: Int64) async throws -> Source {
let request = Source.CreateRequest(
amount: amount,
currency: .thb,
type: .alipay
)

return try await client.send(request)
}

UI Componentsโ€‹

Built-in Credit Card Formโ€‹

The SDK provides a pre-built credit card form:

import OmiseSDK

class PaymentViewController: UIViewController {

func presentCreditCardForm() {
let publicKey = "pkey_test_5xyzyx5xyzyx5xyzyx5"

let creditCardFormController = CreditCardFormController.makeCreditCardFormViewController(
withPublicKey: publicKey
)

creditCardFormController.delegate = self
creditCardFormController.handleErrors = true

present(creditCardFormController, animated: true)
}
}

extension PaymentViewController: CreditCardFormDelegate {
func creditCardForm(_ controller: CreditCardFormController, didSucceedWithToken token: Token) {
dismiss(animated: true) {
print("Token created: \(token.id)")
self.sendTokenToServer(token.id)
}
}

func creditCardForm(_ controller: CreditCardFormController, didFailWithError error: Error) {
dismiss(animated: true) {
self.showError(error.localizedDescription)
}
}
}

Custom Payment Source Chooserโ€‹

func presentPaymentMethodChooser() {
let capability = try await client.retrieveCapability()

let chooser = PaymentSourceChooserController.makePaymentChooserNavigationController(
publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5",
amount: 100000,
currency: .thb,
capability: capability
)

chooser.delegate = self
present(chooser, animated: true)
}

extension PaymentViewController: PaymentSourceChooserDelegate {
func paymentSourceChooser(
_ controller: PaymentSourceChooserController,
didCompleteWith token: Token
) {
dismiss(animated: true) {
self.sendTokenToServer(token.id)
}
}

func paymentSourceChooser(
_ controller: PaymentSourceChooserController,
didCompleteWith source: Source
) {
dismiss(animated: true) {
self.handlePaymentSource(source)
}
}

func paymentSourceChooser(
_ controller: PaymentSourceChooserController,
didFailWithError error: Error
) {
dismiss(animated: true) {
self.showError(error.localizedDescription)
}
}
}

Input Validationโ€‹

Card Number Validationโ€‹

import OmiseSDK

// Validate card number format
let cardNumber = "4242424242424242"
let isValid = CardValidator.isValid(number: cardNumber)

// Detect card brand
let brand = CardBrand.detect(from: cardNumber)
switch brand {
case .visa:
print("Visa card")
case .masterCard:
print("Mastercard")
case .jcb:
print("JCB card")
default:
print("Unknown brand")
}

// Format card number with spaces
let formatted = CardNumber.format(cardNumber) // "4242 4242 4242 4242"

Expiry Date Validationโ€‹

// Validate expiry date
let month = 12
let year = 2025

let isValid = CardValidator.isValid(expirationMonth: month, year: year)

// Check if card is expired
let isExpired = CardValidator.isExpired(month: month, year: year)

CVV Validationโ€‹

// Validate CVV for card brand
let cvv = "123"
let cardBrand = CardBrand.visa

let isValid = CardValidator.isValid(securityCode: cvv, for: cardBrand)

Custom Validationโ€‹

struct CardFormValidator {
static func validate(
number: String,
expirationMonth: Int,
expirationYear: Int,
securityCode: String,
name: String
) -> ValidationResult {
var errors: [String] = []

// Validate card number
if !CardValidator.isValid(number: number) {
errors.append("Invalid card number")
}

// Validate expiry
if !CardValidator.isValid(expirationMonth: expirationMonth, year: expirationYear) {
errors.append("Invalid or expired card")
}

// Validate CVV
let brand = CardBrand.detect(from: number)
if !CardValidator.isValid(securityCode: securityCode, for: brand) {
errors.append("Invalid security code")
}

// Validate name
if name.trimmingCharacters(in: .whitespaces).isEmpty {
errors.append("Cardholder name is required")
}

return errors.isEmpty ? .valid : .invalid(errors: errors)
}
}

enum ValidationResult {
case valid
case invalid(errors: [String])
}

3D Secure Authenticationโ€‹

Handling 3D Secure Flowโ€‹

import OmiseSDK
import SafariServices

class PaymentViewController: UIViewController {

func processPaymentWithToken(_ tokenId: String) async {
do {
// Create charge on your server
let charge = try await createChargeOnServer(tokenId: tokenId)

// Check if 3D Secure is required
if let authorizeURL = charge.authorizeURL {
await handle3DSecure(url: authorizeURL)
} else {
handleSuccessfulPayment(charge)
}
} catch {
handleError(error)
}
}

func handle3DSecure(url: URL) async {
await withCheckedContinuation { continuation in
let safariVC = SFSafariViewController(url: url)
safariVC.delegate = self
present(safariVC, animated: true) {
self.threeDSecureContinuation = continuation
}
}
}

private var threeDSecureContinuation: CheckedContinuation<Void, Never>?
}

extension PaymentViewController: SFSafariViewControllerDelegate {
func safariViewController(
_ controller: SFSafariViewController,
didCompleteInitialLoad didLoadSuccessfully: Bool
) {
guard didLoadSuccessfully else {
controller.dismiss(animated: true)
threeDSecureContinuation?.resume()
handleError(NSError(domain: "3DS", code: -1, userInfo: nil))
return
}
}

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
threeDSecureContinuation?.resume()
// Verify charge status on your server
Task {
await verifyChargeStatus()
}
}
}

Error Handlingโ€‹

Error Typesโ€‹

do {
let token = try await client.send(request)
} catch let error as OmiseError {
switch error {
case .api(let apiError):
// API error from server
print("API Error: \(apiError.message)")
handleAPIError(apiError)

case .network(let networkError):
// Network connectivity error
print("Network Error: \(networkError.localizedDescription)")
showNetworkError()

case .invalidRequest(let message):
// Invalid request parameters
print("Invalid Request: \(message)")

case .authenticationFailure:
// Invalid public key
print("Authentication failed - check your public key")

default:
print("Unknown error: \(error)")
}
} catch {
print("Unexpected error: \(error)")
}

API Error Handlingโ€‹

func handleAPIError(_ error: APIError) {
switch error.code {
case "invalid_card":
showAlert(title: "Invalid Card", message: "Please check your card details")

case "insufficient_fund":
showAlert(title: "Insufficient Funds", message: "Your card has insufficient funds")

case "failed_processing":
showAlert(title: "Payment Failed", message: "Unable to process your payment")

case "invalid_security_code":
showAlert(title: "Invalid CVV", message: "Please check your security code")

default:
showAlert(title: "Error", message: error.message)
}
}

Retry Logicโ€‹

func createTokenWithRetry(maxAttempts: Int = 3) async throws -> Token {
var lastError: Error?

for attempt in 1...maxAttempts {
do {
return try await createToken()
} catch {
lastError = error

// Don't retry on validation errors
if case OmiseError.invalidRequest = error {
throw error
}

// Wait before retrying
if attempt < maxAttempts {
try await Task.sleep(nanoseconds: UInt64(attempt) * 1_000_000_000)
}
}
}

throw lastError ?? OmiseError.unknown
}

Best Practicesโ€‹

Securityโ€‹

// โœ… DO: Use test key for development
let testKey = "pkey_test_5xyzyx5xyzyx5xyzyx5"

// โŒ DON'T: Hardcode production keys
// let productionKey = "pkey_5xyzyx5xyzyx5xyzyx5"

// โœ… DO: Load keys from configuration
let publicKey = Bundle.main.object(forInfoPlistKey: "OmisePublicKey") as? String

// โŒ DON'T: Log sensitive data
// print("Card number: \(cardNumber)")

// โœ… DO: Use sanitized logging
print("Token created: \(token.id)")

Memory Managementโ€‹

class PaymentManager {
private weak var client: OmiseClient?
private var currentTask: Task<Token, Error>?

func tokenizeCard() async throws -> Token {
// Cancel previous request
currentTask?.cancel()

let task = Task {
try await client?.send(request)
}
currentTask = task

return try await task.value
}

func cancelPendingRequests() {
currentTask?.cancel()
}
}

User Experienceโ€‹

class PaymentViewController: UIViewController {
private let activityIndicator = UIActivityIndicatorView(style: .large)

func processPayment() {
showLoadingIndicator()

Task {
do {
let token = try await createToken()
hideLoadingIndicator()
await sendToServer(token)
} catch {
hideLoadingIndicator()
showError(error)
}
}
}

private func showLoadingIndicator() {
activityIndicator.startAnimating()
view.isUserInteractionEnabled = false
}

private func hideLoadingIndicator() {
activityIndicator.stopAnimating()
view.isUserInteractionEnabled = true
}
}

Testingโ€‹

Test Modeโ€‹

// Use test public key
let client = OmiseClient(publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5")

// Test card numbers
let testCards = [
"4242424242424242", // Successful
"4111111111111111", // Failed
"4000000000000002", // 3D Secure required
]

Unit Testingโ€‹

import XCTest
@testable import YourApp

class PaymentTests: XCTestCase {
var client: OmiseClient!

override func setUp() {
super.setUp()
client = OmiseClient(publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5")
}

func testTokenCreation() async throws {
let request = Token.CreateRequest(
name: "Test User",
number: "4242424242424242",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)

let token = try await client.send(request)

XCTAssertNotNil(token.id)
XCTAssertTrue(token.id.hasPrefix("tokn_test_"))
}

func testInvalidCardNumber() async {
let request = Token.CreateRequest(
name: "Test User",
number: "1234567890123456",
expirationMonth: 12,
expirationYear: 2025,
securityCode: "123"
)

do {
_ = try await client.send(request)
XCTFail("Should throw error for invalid card")
} catch {
XCTAssertNotNil(error)
}
}
}

UI Testingโ€‹

class PaymentUITests: XCTestCase {
var app: XCUIApplication!

override func setUp() {
super.setUp()
app = XCUIApplication()
app.launch()
}

func testPaymentFlow() {
// Navigate to payment screen
app.buttons["Pay Now"].tap()

// Fill in card details
let cardNumberField = app.textFields["Card Number"]
cardNumberField.tap()
cardNumberField.typeText("4242424242424242")

let expiryField = app.textFields["Expiry"]
expiryField.tap()
expiryField.typeText("12/25")

let cvvField = app.secureTextFields["CVV"]
cvvField.tap()
cvvField.typeText("123")

// Submit
app.buttons["Submit"].tap()

// Verify success
XCTAssertTrue(app.alerts["Payment Successful"].exists)
}
}

Troubleshootingโ€‹

Common Issuesโ€‹

Issue: "Invalid public key" error

// Solution: Check your public key format
// Test keys: pkey_test_*
// Live keys: pkey_*

let client = OmiseClient(publicKey: "pkey_test_5xyzyx5xyzyx5xyzyx5")

Issue: Network timeout errors

// Solution: Increase timeout
var config = OmiseClient.Configuration(publicKey: "pkey_test_...")
config.timeout = 60.0
let client = OmiseClient(configuration: config)

Issue: Token creation fails silently

// Solution: Add proper error handling
do {
let token = try await client.send(request)
print("Success: \(token.id)")
} catch {
print("Error: \(error)")
// Add breakpoint here to debug
}

Issue: 3D Secure redirect not working

// Solution: Ensure Info.plist includes LSApplicationQueriesSchemes
// Add to Info.plist:
// <key>LSApplicationQueriesSchemes</key>
// <array>
// <string>https</string>
// </array>

Issue: Build errors after installation

# Solution: Clean build folder
# Xcode > Product > Clean Build Folder
# Or use command line:
rm -rf ~/Library/Developer/Xcode/DerivedData

Debug Modeโ€‹

// Enable debug logging
#if DEBUG
client.enableDebugLogging = true
#endif

// This will log all network requests and responses

Migration Guideโ€‹

Migrating from v4.x to v5.xโ€‹

The v5.x release introduces async/await support and removes some deprecated APIs:

Before (v4.x):

client.send(request) { result in
switch result {
case .success(let token):
print(token.id)
case .failure(let error):
print(error)
}
}

After (v5.x):

// Option 1: Use async/await
let token = try await client.send(request)

// Option 2: Continue using completion handlers (still supported)
client.send(request) { result in
// Same as before
}

Breaking Changes:

  1. Minimum iOS version is now 12.0 (was 10.0)
  2. Minimum Swift version is 5.5 (was 5.0)
  3. OmiseSDKConfiguration renamed to OmiseClient.Configuration
  4. Some deprecated payment method types removed

Frequently Asked Questionsโ€‹

Can I use this SDK with SwiftUI?

Yes, the SDK works perfectly with SwiftUI. You can use the async/await APIs directly in your SwiftUI views and view models.

struct PaymentView: View {
@StateObject private var viewModel = PaymentViewModel()

var body: some View {
Button("Pay Now") {
Task {
await viewModel.processPayment()
}
}
}
}

Does the SDK support Combine?

While the SDK doesn't provide native Combine publishers, you can easily wrap the async/await APIs:

extension OmiseClient {
func sendPublisher(_ request: Token.CreateRequest) -> AnyPublisher<Token, Error> {
Future { promise in
Task {
do {
let token = try await self.send(request)
promise(.success(token))
} catch {
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}
}

Can I customize the built-in UI components?

Yes, you can customize colors, fonts, and other appearance properties:

let theme = CreditCardFormTheme()
theme.primaryColor = .systemBlue
theme.errorColor = .systemRed
theme.font = UIFont.systemFont(ofSize: 16)

creditCardFormController.theme = theme

How do I handle App Store submission?

The SDK is App Store compliant. Make sure to:

  1. Use test keys during development
  2. Switch to production keys for release
  3. Don't log sensitive data in production
  4. Include privacy policy for payment processing

Can I save card details for future use?

The SDK creates tokens that can be saved to your server. Use the Customers API on your backend to attach tokens to customers for future charges.

Does the SDK support Apple Pay?

The SDK focuses on card tokenization. For Apple Pay, use PassKit framework and convert the payment token on your server.

What's the difference between a token and a source?

  • Tokens represent credit/debit cards for immediate charging
  • Sources represent alternative payment methods that require additional steps

How long are tokens valid?

Tokens expire after 30 minutes if not used. Once used to create a charge or attach to a customer, they're stored permanently.

Can I test with real card numbers?

No, always use test card numbers in test mode. Real cards will be declined in test mode.

Does the SDK work with Catalyst?

Yes, the SDK supports Mac Catalyst for iPad apps running on macOS.

Next Stepsโ€‹

  1. Set up your account to get your API keys
  2. Integrate on your server to create charges from tokens
  3. Test your integration with test cards
  4. Go live with production keys

Supportโ€‹

Need help with the iOS SDK?