Skip to content

iOS integration (Swift) ​

Who this is for ​

Native iOS apps (Swift / SwiftUI or UIKit) that need to show the amount due and collection details in their own UI and refresh the order state once the buyer has paid. The example uses URLSession + Codable, with no third-party dependency.

Before you start ​

  • You have an API KEY (see Get started).
  • You have configured a collection address and set it active.
  • You have your own server. The API KEY does not go into the app.

The KEY cannot live inside an IPA

iOS bundles can be decompiled, and strings in Info.plist or the binary are not safe. Order requests from the app go to your own server.

Install ​

No extra dependency is needed — the built-in URLSession is enough. Use SwiftUI for the UI as you prefer.

Minimal working example ​

1. Order creation on your server ​

Shown in a Swift server style (Vapor, or any backend); the core is "call the Open API with the KEY":

swift
import Foundation

func createOrder(orderNo: String, amount: String) async throws -> Data {
    var req = URLRequest(url: URL(string: "\(ProcessInfo.processInfo.environment["API_BASE"]!)/api/pay/open/createOrder")!)
    req.httpMethod = "POST"
    req.setValue(ProcessInfo.processInfo.environment["PAY_API_KEY"], forHTTPHeaderField: "X-Pay-Api-Key")
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.httpBody = try JSONSerialization.data(withJSONObject: [
        "merchantOrderNo": orderNo,      // idempotency key
        "chainId": 2,
        "tokenId": 1,
        "amount": amount,                // decimal string in token units
        "subject": "Order \(orderNo)",
        "notifyUrl": "https://your-shop.com/pay/notify",
    ])
    let (data, _) = try await URLSession.shared.data(for: req)
    return data
}

2. App side: only talk to your own backend ​

swift
struct CheckoutRequest: Codable { let orderNo: String; let amount: String }

struct CheckoutResponse: Codable {
    let code: Int
    let orderNo: String?
    let payAddress: String?
    let payAmountDisplay: String?
    let payAmountMinimal: String?
    let chainKey: String?
    let msg: String?
}

struct StatusResponse: Codable {
    let code: Int
    let status: Int          // 1..6; only 3/4 mean paid
    let statusText: String?
    let txHash: String?
}

enum ShopAPI {
    static func checkout(_ body: CheckoutRequest) async throws -> CheckoutResponse {
        var req = URLRequest(url: URL(string: "https://your-shop.com/api/checkout")!)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.httpBody = try JSONEncoder().encode(body)   // no KEY here
        let (data, _) = try await URLSession.shared.data(for: req)
        return try JSONDecoder().decode(CheckoutResponse.self, from: data)
    }

    static func status(orderNo: String) async throws -> StatusResponse {
        let url = URL(string: "https://your-shop.com/api/order-status?orderNo=\(orderNo)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(StatusResponse.self, from: data)
    }
}
swift
@MainActor
final class CheckoutViewModel: ObservableObject {
    enum State { case loading, ready(CheckoutResponse), pending(CheckoutResponse),
                 paid(CheckoutResponse), error(String) }

    @Published var state: State = .loading

    func start(orderNo: String, amount: String) async {
        do {
            let o = try await ShopAPI.checkout(.init(orderNo: orderNo, amount: amount))
            guard o.code == 0 else { state = .error(o.msg ?? "create failed"); return }
            state = .ready(o)

            while true {
                try await Task.sleep(for: .seconds(3))
                let s = try await ShopAPI.status(orderNo: o.orderNo ?? orderNo)
                switch s.status {
                case 1: state = .ready(o)          // waiting for payment
                case 2: state = .pending(o)        // submitted, awaiting confirmations
                case 3, 4: state = .paid(o); return
                case 5: state = .error("Order expired"); return
                case 6: state = .error("Manual review required — contact support"); return
                default: break
                }
            }
        } catch {
            state = .error(error.localizedDescription)
        }
    }
}

You can also open the platform's payment counter directly: {cashierBaseUrl}/pay/cashier/m/{merchantNo}, using UIApplication.shared.open. Both values come from the merchant-side GET /api/pay/getMyProfile.

Callback handling ​

Callbacks are received by your server. A Swift server-side verification example:

swift
import CryptoKit

func verify(rawBody: String, ts: String, sig: String, apiKey: String) -> Bool {
    let key = SymmetricKey(data: Data(apiKey.utf8))
    var mac = HMAC<SHA256>.init(key: key)
    mac.update(data: Data(ts.utf8))        // timestamp + raw body, string concatenation
    mac.update(data: Data(rawBody.utf8))
    let want = mac.finalize().map { String(format: "%02x", $0) }.joined()
    // constant-time comparison
    return constantTimeEqual(want, sig)
}

Handling notes:

  • Idempotent: callbacks retry; deduplicate on merchantOrderNo.
  • Return fast: about a 10-second timeout; move slow logic to a background task.
  • Query is authoritative: verify with Query order first.

Wallet hand-off and app-to-app jumps ​

  • Most crypto wallets on iOS support universal links or a custom scheme; UIApplication.shared.open will try to open the wallet.
  • Declare the schemes you want to probe in LSApplicationQueriesSchemes in Info.plist before using canOpenURL.
  • Always show the QR code as a fallback, so users without a wallet can pay from another device.

FAQ ​

QuestionAnswer
Can the KEY go in Info.plist / Keychain?No. The app does not hold the KEY; orders go through your backend
Polling stops when iOS suspends the appPause polling on background, and on scenePhase returning to foreground resume and query once immediately
Can I embed checkout in a WebView?Possible, but opening the platform checkout in the system browser is simpler
How do I know it is paid?Only states 3 / 4 — never compare numeric values
Will duplicate requests create duplicate orders?No — the same merchantOrderNo is idempotently reused

Web3 payment infrastructure for global merchants