> For the complete documentation index, see [llms.txt](https://docs.coda.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.coda.co/codapay/in-app-payments/ios-link-out-integration/ios-integration-guidelines.md).

# iOS Integration Guidelines

{% hint style="success" %}
For end‑to‑end context and field definitions, see [**Hosted Payment Page**](/codapay/hosted-payment-page-integration.md).
{% endhint %}

## Integration Steps

On iOS, integrating the Hosted Payment Page involves four steps:&#x20;

1. Initiating a payment request on your backend
2. Opening the Hosted Page in the device browser
3. Handling the redirect back into your app
4. Confirming the transaction status.

### 1. Initiate Payment Request

Your backend calls Codapay’s `Payment/init` API to create a transaction and uses the returned `txnId` to build the Hosted Page URL:

**Sandbox**

```url
https://sandbox.codapayments.com/airtime/begin?browser_type=mobile-web&txn_id={txnId}
```

**Production**

```url
https://airtime.codapayments.com/airtime/begin?browser_type=mobile-web&txn_id={txnId}
```

Return this URL to your app.

{% hint style="info" %}
See [**Initiate Payment Request**](/codapay/hosted-payment-page-integration/initiate-a-payment-request.md) for full parameter list and examples
{% endhint %}

### 2. Display Hosted Payment Page

From your iOS app, open the Hosted Page URL in the system's default browser

```swift
if UIApplication.shared.canOpenURL(url) {
    UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
```

{% hint style="info" %}
See [**Display Hosted Page**](/codapay/hosted-payment-page-integration/display-the-hosted-payment-page.md) for URL formats and additional details
{% endhint %}

### 3. Redirect URLs

After payment success or failure, Codapay redirects the user to your configured Redirect URL. On iOS, you need to capture this redirect and return the user into your app session.

Two options are available:

* **Universal Links (recommended)** – Provide the smoothest user experience and better security.
* **Custom URL Schemes (alternative)** – Simpler to implement, but can be intercepted and are less reliable.

{% hint style="info" %}
See how to set up [**Redirect URLs**](/codapay/getting-started/set-up-payments.md#step-2-set-up-your-landing-url-and-the-default-callback-url) on Coda portal and configuration examples
{% endhint %}

{% hint style="info" %}
Detailed iOS setup steps are available in the [**Implementation Details**](#implementation-details) section below
{% endhint %}

### 4. Check Payment Status

Do not rely only on the redirect URL, since it can be forged. Always check the actual status with Codapay.

* **Webhook notification (recommended):** Codapay sends the final result to your backend.
* **Inquiry API:** If the webhook has not yet been received, your backend can query the transaction status.

**Best practice:**

1. Codapay sends **webhook notification** to your backend.
2. Browser **redirects user back** into the app (Universal Link or scheme).
3. App **queries backend** for the result.
4. Backend updates the user’s inventory or entitlements.

{% hint style="info" %}
See [**Transaction Status Notifications**](/codapay/hosted-payment-page-integration/get-notified-of-a-transaction-status-change.md) and [**Check Payment Status**](/codapay/hosted-payment-page-integration/check-a-transaction-status.md) in the general documentation for specifications and examples
{% endhint %}

## Implementation Details

This section provides the step-by-step configuration needed to support redirects on iOS. Use this if you need the full Apple setup.

### Universal Links (recommended)

1. **Configure Associated Domains** in your Apple Developer account:
   1. In Apple Developer, create or edit your App Identifier and **enable Associated Domains**.
   2. Note your **Team ID** and **Bundle ID** — together they form the **App ID** you will declare in the JSON file.
2. **Create and host `apple-app-site-association` on your domain**
   1. Define the file content using your App ID (Team ID + Bundle ID) and the URL paths you want to match (e.g., success, pending, failure).  See an example below.
   2. Save the file exactly as `apple-app-site-association`&#x20;
   3. Upload the file to `https://<your-domain>/.well-known/apple-app-site-association`&#x20;

Example:

```json
{
  "applinks": {
    "details": [
      {
        "appIDs": ["KV7GMR96XX.com.coda.outlinkdemo"],
        "components": [
          { "/": "/payments/success", "?", "*", "comment": "Match success redirect URL" },
          { "/": "/payments/pending", "?", "*", "comment": "Match pending redirect URL" }
        ]
      }
    ]
  }
}

```

3. **Enable Associated Domains in Xcode:** In *Signing & Capabilities*, add *Associated Domains* and register your domain with the `applinks:` prefix
4. **Handle Universal Links in the app**

<details>

<summary><strong>AppDelegate</strong> (iOS ≤12)</summary>

```swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        
        // Handle Universal Links
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let url = userActivity.webpageURL else { return false }
        
        handleUniversalLink(url)
        return true
    }
    
    private func handleUniversalLink(_ url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            print("❌ Failed to parse Universal Link URL")
            return
        }
        
        // Parse the URL to extract payment status
        if let status = components.queryItems?.first(where: { $0.name == "status" })?.value {
            handlePaymentStatus(status: status)
        } else if components.path.contains("success") {
            handlePaymentStatus(status: "success")
        } else if components.path.contains("failure") {
            handlePaymentStatus(status: "failure")
        }
    }
    
    private func handlePaymentStatus(_ status: String) {
        // Cross check with the backend
    }
}

```

</details>

<details>

<summary><strong>SceneDelegate (iOS 13+)</strong></summary>

```swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        // Handle Universal Links
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let url = userActivity.webpageURL else { return }
        
        handleUniversalLink(url)
    }
    
    private func handleUniversalLink(_ url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            print("❌ Failed to parse Universal Link URL")
            return
        }
        
        // Parse the URL to extract payment status
        if let status = components.queryItems?.first(where: { $0.name == "status" })?.value {
            handlePaymentStatus(status: status)
        } else if components.path.contains("success") {
            handlePaymentStatus(status: "success")
        } else if components.path.contains("failure") {
            handlePaymentStatus(status: "failure")
        }
    }
    
    private func handlePaymentStatus(_ status: String) {
        // Cross check with the backend
    }
}

```

</details>

<details>

<summary><strong>SwiftUI</strong></summary>

```swift
import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    // Handle Universal Links
                    handleUniversalLink(url)
                }
        }
    }
    
    private func handleUniversalLink(_ url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            print("❌ Failed to parse Universal Link URL")
            return
        }
        
        // Parse the URL to extract payment status
        if let status = components.queryItems?.first(where: { $0.name == "status" })?.value {
            handlePaymentStatus(status: status)
        } else if components.path.contains("success") {
            handlePaymentStatus(status: "success")
        } else if components.path.contains("failure") {
            handlePaymentStatus(status: "failure")
        }
    }
    
    private func handlePaymentStatus(_ status: String) {
        // Cross check with the backend
    }
}
```

</details>

### Custom URL Schemes (alternative)

{% hint style="danger" %}
Use only when Universal Links cannot be used. Behaviour is undefined if multiple apps register the same scheme.

Universal Links are recommended for a smoother experience.
{% endhint %}

1. **Register the scheme in Info.plist**

Add your custom scheme under `CFBundleURLTypes`&#x20;

Example:

```xml
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.yourcompany.yourapp</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <!-- Make sure the scheme is unique -->
      <string>coda</string>
    </array>
    <key>CFBundleURLHosts</key>
    <array>
      <string>yourapp.yourcompany.com</string>
    </array>
  </dict>
</array>
```

2. **Handle the scheme in the app**

<details>

<summary>AppDelegate (iOS ≤12)</summary>

```swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ app: UIApplication,
                   open url: URL,
                   options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    // Handle custom scheme URL
    handleCustomSchemeURL(url)
    return true
  }

  private func handleCustomSchemeURL(_ url: URL) {
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
      print("❌ Failed to parse custom scheme URL")
      return
    }

    // Parse the URL to extract payment status
    if let status = components.queryItems?.first(where: { $0.name == "status" })?.value {
      handlePaymentStatus(status: status)
    }
  }

  private func handlePaymentStatus(_ status: String) {
    // Post notification for view controllers to handle
    let notificationName: Notification.Name
    switch status.lowercased() {
    case "success": notificationName = .paymentSuccess
    case "failure": notificationName = .paymentFailure
    default:
      print("⚠ Unknown payment status: \(status)")
      return
    }

    DispatchQueue.main.async {
      NotificationCenter.default.post(name: notificationName, object: nil)
    }
  }
}

extension Notification.Name {
  static let paymentSuccess = Notification.Name("PaymentSuccess")
  static let paymentFailure = Notification.Name("PaymentFailure")
}

```

</details>

<details>

<summary>SceneDelegate (iOS 13+)</summary>

```swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let urlContext = URLContexts.first else { return }
    let url = urlContext.url
    handleCustomSchemeURL(url)
  }

  private func handleCustomSchemeURL(_ url: URL) {
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
      print("❌ Failed to parse custom scheme URL")
      return
    }

    if let status = components.queryItems?.first(where: { $0.name == "status" })?.value {
      handlePaymentStatus(status: status)
    }
  }

  private func handlePaymentStatus(_ status: String) {
    let notificationName: Notification.Name
    switch status.lowercased() {
    case "success": notificationName = .paymentSuccess
    case "failure": notificationName = .paymentFailure
    default:
      print("⚠ Unknown payment status: \(status)")
      return
    }

    DispatchQueue.main.async {
      NotificationCenter.default.post(name: notificationName, object: nil)
    }
  }
}

```

</details>

<details>

<summary>SwiftUI</summary>

```swift
import SwiftUI

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL { url in
          handleCustomSchemeURL(url)
        }
    }
  }

  private func handleCustomSchemeURL(_ url: URL) {
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
      print("❌ Failed to parse custom scheme URL")
      return
    }

    if let status = components.queryItems?.first(where: { $0.name == "status" })?.value {
      handlePaymentStatus(status: status)
    }
  }

  private func handlePaymentStatus(_ status: String) {
    let notificationName: Notification.Name
    switch status.lowercased() {
    case "success": notificationName = .paymentSuccess
    case "failure": notificationName = .paymentFailure
    default:
      print("⚠ Unknown payment status: \(status)")
      return
    }

    DispatchQueue.main.async {
      NotificationCenter.default.post(name: notificationName, object: nil)
    }
  }
}

```

</details>

{% hint style="danger" %}
**Security Considerations**

* **Do not trust redirect URLs alone.** They can be forged. Always cross-check payment status with your backend using webhook or inquiry
* **Validate all incoming URLs.** Ensure they come from your expected domain (for Universal Links) or your registered scheme (for custom URLs)
* **Universal Links are safer than custom schemes.** They verify domain ownership and prevent interception by other apps
  {% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.coda.co/codapay/in-app-payments/ios-link-out-integration/ios-integration-guidelines.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
