Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.zennopay.in/llms.txt

Use this file to discover all available pages before exploring further.

The Zennopay iOS SDK is a thin, dependency-free wrapper that hands a payment intent off to the Zennopay-hosted checkout. Modeled on the Stripe Checkout pattern, it opens the checkout URL in a system browser tab via ASWebAuthenticationSession — the user always sees a real URL bar and an Apple-mediated consent sheet. When checkout completes, the browser redirects back to your app via a registered URL scheme and the SDK surfaces a typed PaymentResult.

Requirements

  • iOS 13.0+
  • Swift 5.9+
  • Xcode 15+
  • Only Foundation + AuthenticationServices — no third-party dependencies

Install via Swift Package Manager

In Xcode: File → Add Package Dependencies… and paste the repository URL:
https://github.com/amanpal108/zennopay-ios-sdk
Select Up to Next Major Version starting from 0.1.0, then add the Zennopay library product to your app target. If you maintain your own Package.swift, declare:
.package(url: "https://github.com/amanpal108/zennopay-ios-sdk", from: "0.1.0")
and add "Zennopay" to your target’s dependencies.

Register a URL scheme

The SDK delivers the payment result by redirecting from the checkout web to a URL scheme your app owns. Register it in your Info.plist:
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.yourapp.payment-result</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>yourapp</string>
    </array>
  </dict>
</array>
The redirect URL the checkout web fires is yourapp://payment-result?intent_id=...&status=.... You do not need to handle it in SceneDelegate / AppDelegateASWebAuthenticationSession captures the callback before it reaches the OS routing layer and delivers it directly to the SDK’s completion handler.

Quickstart

import Zennopay

// 1. Your backend creates the intent + mints the JWT (RS256, scoped to one intent)
let intentID = "zp_AbCd1234EfGh5678"
let jwt = "eyJhbGciOi..."

// 2. Hand off to the Zennopay-hosted checkout
Zennopay.openCheckout(
    intentID: intentID,
    jwt: jwt,
    returnScheme: "yourapp"
) { result in
    switch result {
    case .success(let payment):
        print("Payment \(payment.status) for intent \(payment.intentID)")
    case .failure(let error):
        print("Error: \(error)")
    }
}
The completion handler is delivered on the main queue.

Async / await

do {
    let result = try await Zennopay.openCheckout(
        intentID: intentID,
        jwt: jwt,
        returnScheme: "yourapp"
    )
    print("Payment \(result.status) for intent \(result.intentID)")
} catch {
    print("Error: \(error)")
}

Result + status

public enum PaymentStatus: String {
    case success, failed, canceled, pending
}

public struct PaymentResult {
    public let intentID: String
    public let status: PaymentStatus
}
.pending means the checkout web could not synchronously confirm the outcome (async settlement, payment-network review, etc.). Your backend should poll the intent or wait for a webhook for the final state.

Error handling

public enum ZennopayError: Error {
    case invalidJWT
    case malformedToken
    case intentMismatch
    case jwtExpired
    case jwtMissingClaim
    case userCanceled
    case returnURLMalformed
    case presentationAnchorMissing
    case networkError(Error)
}
The four errors you are most likely to handle explicitly:
CaseMeaning
intentMismatchThe JWT’s zennopay:intent_id claim does not match the intentID you passed to openCheckout. Caught client-side before the browser opens.
jwtExpiredThe JWT’s exp is in the past (30-second clock-skew tolerance). Mint a fresh token from your backend and retry.
malformedTokenThe JWT is not three segments, the base64 doesn’t decode, or the payload isn’t a JSON object.
invalidIssuerReserved for parity with the Android SDK; on iOS, missing-claim cases surface as jwtMissingClaim.
All four are surfaced synchronously — the SDK fails fast before opening the system browser, so a bad token never leaks into a URL.

How it works

  1. Your backend exchanges its Zennopay API key for a short-lived JWT scoped to one intent_id.
  2. Your app calls Zennopay.openCheckout(...) with that JWT.
  3. The SDK verifies the JWT’s zennopay:intent_id claim matches the intentID argument before opening the browser. Mismatch raises ZennopayError.intentMismatch synchronously.
  4. The SDK opens https://checkout.zennopay.com/flow/{intent_id}/scan#token={jwt} in ASWebAuthenticationSession. The token rides in the URL fragment (after #), so it never reaches the HTTP server in logs or proxies.
  5. The user completes (or cancels) checkout in the system browser.
  6. The checkout web redirects to yourapp://payment-result?intent_id=...&status=....
  7. The SDK parses the redirect and calls your completion handler with a PaymentResult (or a ZennopayError).