> This is Payabli documentation. For a complete page index, fetch https://docs.payabli.com/llms.txt — append .md to any page URL for lightweight markdown. For section-level indexes, query parameters, and other AI-optimized access methods, see https://docs.payabli.com/ai-agents.md

# Pay In Payment Flow

> Natively add card and ACH payments to your mobile app with Pay In Payment Flow

Pay In Payment Flow is Payabli's native mobile UI for card and ACH payments in your app. It owns sensitive field state and renders a hosted form, so your app never touches raw card or bank data. See [Tap to Pay](/guides/mobile-components-tap-to-pay) if you're looking for Payabli's contactless payment component instead.

Right now, only iOS is supported.

## Prerequisites

Before you start, confirm you have:

* **Xcode 15 or later** and **Swift 5.9 or later**
* **iOS 16.7 or later** as your deployment target
* **A Payabli sandbox `entryPoint`** with card and ACH enabled
* **A Payabli `clientId` and `clientSecret`.** See [OAuth authentication](/developers/oauth-authentication) to provision them.

## Installation

Install `PayabliSDKPayInPaymentFlow` from the [`sdk-ios`](https://github.com/payabli/sdk-ios) Swift package:

#### Add the package

In Xcode, choose **File → Add Packages…** and enter the `sdk-ios` repository URL:

```txt
 https://github.com/payabli/sdk-ios.git
```

Or add it to your `Package.swift` dependencies:

```swift
.package(url: "https://github.com/payabli/sdk-ios.git", branch: "main")
```

#### Link the product

Link the `PayabliSDKPayInPaymentFlow` product to your app target:

```swift
.product(name: "PayabliSDKPayInPaymentFlow", package: "sdk-ios")
```

#### Import the package

Import it where you construct or render the component:

```swift
import PayabliSDKPayInPaymentFlow
```

## Backend token endpoint

The Pay In Payment Flow component authenticates every request with a short-lived Payabli access token, obtained through an `accessTokenProvider` closure you implement.
Your backend holds the Payabli `clientId` and `clientSecret`, exchanges them for the access token, and never exposes them to your mobile app.

#### Create the project

This example server uses JavaScript with Node.js and Express.

**Requires:** [Node.js](https://nodejs.org/en/download/) and [npm](https://www.npmjs.com/)

Create a new Node.js project and enable ES modules:

```bash
mkdir payabli-token-proxy
cd payabli-token-proxy
npm init -y
npm pkg set type=module
```

#### Install dependencies

Install Express, CORS, and dotenv:

```bash
npm install express cors dotenv
```

#### Write the token endpoint

Create `server.js` with the following:

```js
// server.js
import "dotenv/config";
import express from "express";
import cors from "cors";

const app = express();
app.use(cors(), express.json());

const PAYABLI_URL = process.env.PAYABLI_URL || "https://api-sandbox.payabli.com/api";
const { PAYABLI_CLIENT_ID, PAYABLI_CLIENT_SECRET } = process.env;

app.post("/payabli/token", async (req, res) => {
  const upstream = await fetch(`${PAYABLI_URL}/v2/Token/serverside`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      clientId: PAYABLI_CLIENT_ID,
      clientSecret: PAYABLI_CLIENT_SECRET,
    }),
  });

  res.status(upstream.status).json(await upstream.json());
});

app.listen(process.env.PORT || 3000, () =>
  console.log("Token server ready"));
```

Store `clientId` and `clientSecret` in environment variables on your server. Never commit them to source control.

#### Start the server

Start the server:

```bash
node server.js
```

#### Call it from your app

The `accessTokenProvider` closure calls this endpoint and returns the token:

```swift
let accessTokenProvider: PayabliPayInPaymentFlowAccessTokenProvider = {
    try await backend.fetchPayInAccessToken()
}
```

The component calls this closure fresh before every request, so it never sends a stale token. You don't need to cache the token yourself. If a request still fails with a `401 Unauthorized` response, the error surfaces through `onError` — resubmitting calls the closure again for a new token.

## Operations

`PayabliPayInPaymentFlow`'s `operation` parameter selects the hosted form's behavior:

* **`.storePaymentMethod`** — Tokenizes a card or ACH account for later use, without charging it.
* **`.capture`** — Authorizes and captures a card or ACH payment in one step.
* **`.authorize`** — Authorizes a card payment without capturing it. Card only. ACH and stored payment methods are rejected before the request is sent.

## Implementation walkthrough

This walkthrough builds a minimal card or ACH capture, from constructing `PayabliPayInPaymentFlow` through rendering the hosted form:

/// Add the import

Import `PayabliSDKPayInPaymentFlow` and create a view model that holds the component. You can design your own API client and view model conventions. This walkthrough uses a minimal `ObservableObject`.

/// Implement the backend token provider

Add a function that calls your backend's token endpoint and returns a Payabli mobile access token. Your backend holds the Payabli `clientSecret`, not the app.

/// Create the Pay In Payment Flow component

Construct `PayabliPayInPaymentFlow` in `.capture` mode. Pass your `accessTokenProvider` closure and a `requestConfiguration` describing the amount to charge.

/// Render the hosted form and handle the result

Add a view that renders `PayabliPayInPaymentFlowView` and logs the result. The hosted form collects card or ACH details itself. Your view never sees raw card data.

```swift
import SwiftUI
import PayabliSDKPayInPaymentFlow

func fetchPayInAccessToken() async throws -> String {
    var request = URLRequest(url: URL(string: "https://your-backend.example.com/payabli/token")!)
    request.httpMethod = "POST"
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")

    struct Response: Decodable { let access_token: String }
    let (data, _) = try await URLSession.shared.data(for: request)
    return try JSONDecoder().decode(Response.self, from: data).access_token
}

@MainActor
final class CheckoutViewModel: ObservableObject {
    let paymentFlow: PayabliPayInPaymentFlow

    init(entryPoint: String) {
        paymentFlow = PayabliPayInPaymentFlow(
            entryPoint: entryPoint,
            environment: .sandbox,
            accessTokenProvider: {
                try await fetchPayInAccessToken()
            },
            operation: .capture,
            requestConfiguration: PayabliPayInPaymentFlowRequestConfiguration(
                paymentDetails: PayabliPayInPaymentFlowPaymentDetails(
                    totalAmount: 1.00,
                    currency: "USD"
                ),
                orderDescription: "iOS checkout",
                source: "ios-sdk"
            )
        )
    }
}

struct CheckoutView: View {
    @StateObject var viewModel: CheckoutViewModel

    var body: some View {
        PayabliPayInPaymentFlowView(
            component: viewModel.paymentFlow,
            configuration: PayabliPayInPaymentFlowFormConfiguration(),
            onCompleted: { result in
                guard let transaction = result.transaction else { return }
                print("Payment captured:", transaction.paymentTransId ?? "")
            },
            onError: { error in
                print("Payment failed:", error.localizedDescription)
            }
        )
    }
}
```

## Implementation reference

`PayabliPayInPaymentFlowFormConfiguration` and `PayabliPayInPaymentFlowRequestConfiguration` control field layout, labels, and request data for every operation. `PayabliPayInPaymentFlowSheetConfiguration` controls the sheet presentation.

### `PayabliPayInPaymentFlowFormConfiguration` fields

This hosted form is shared by all three operations. See [Operations](/guides/mobile-components-payin#operations) for what each one does.

Hosted methods the payor can select. For `.authorize`, this is normalized to card only.

Initial selected method. Falls back to the first allowed method if `.card` isn't allowed.

Custom section grouping for card fields. Required and payment-summary fields are appended automatically if missing.

#### properties

Optional stable identifier. Defaults to the section title or its joined field names.

Section title, for example `Card Information`.

Per-section text style override.

Ordered `PayabliPayInPaymentFlowField` values in this section.

Vertical spacing between inputs in this section.

Horizontal spacing for paired inputs in this section.

Per-field vertical spacing overrides, keyed by field.

Custom section grouping for ACH fields. `achSecCode` is never rendered, even if listed.

#### properties

Optional stable identifier. Defaults to the section title or its joined field names.

Section title, for example `Card Information`.

Per-section text style override.

Ordered `PayabliPayInPaymentFlowField` values in this section.

Vertical spacing between inputs in this section.

Horizontal spacing for paired inputs in this section.

Per-field vertical spacing overrides, keyed by field.

Flat card field order, used only when `cardSections` is `nil`.

Flat ACH field order, used only when `achSections` is `nil`.

Values submitted with the form without rendering an editable field.

#### properties

`.personal` or `.business`.

`.ppd`, `.web`, `.tel`, `.ccd`, or `.boc`.

Default or hidden customer data, merged with any visible customer fields.

#### properties

Custom identifier fields, when the paypoint uses custom identifiers instead of name/email/customer ID.

Title, subtitle, submit button text, field labels, and placeholders.

#### properties

Visible and accessibility label text, keyed by `PayabliPayInPaymentFlowField`.

Placeholder text, keyed by `PayabliPayInPaymentFlowField`.

`.external` shows visible labels. `.placeholder` uses placeholder-first inputs.

Global visible-label override. Leave unset to follow `labelLayout`.

Fields to hide the visible label for. Accessibility labels are unaffected.

Card number spacing, expiration separator, and ACH account masking.

#### properties

Adds visual card number grouping.

Separator used in expiration entry. An empty string resolves to `/`.

Masks ACH account entry in the hosted field.

Default and per-field input sizes. Height is clamped to the accessibility minimum touch target.

#### properties

Default `PayabliPayInPaymentFlowInputSize`: optional `width`, `height` (default `52`), and `horizontalPadding` (default `14`).

Per-field `PayabliPayInPaymentFlowInputSize` overrides, keyed by `PayabliPayInPaymentFlowField`.

`.leading`, `.trailing`, or `.hidden`.

`.top` or `.aboveSubmitButton`.

Optional visible fields to require. `.amount` is always required.

Read-only amount and fee display for capture and authorize forms.

#### properties

Font and color for the label text.

Font and color for the value text.

Card, ACH, customer, and payment-summary fields available for `fields`, `cardFieldOrder`, `achFieldOrder`, `requiredFields`, and `hiddenFieldLabels`.

#### cases

Name on card. Card field.

Card number. Card field.

Expiration. Card field.

CVV. Card field.

Postal Code. Card field.

Account holder. ACH field.

Routing number. ACH field.

Account number. ACH field.

Account type. ACH field.

Holder type. ACH field, or hidden value.

SEC code. Hidden by default; not rendered by `achSections`.

Device. Optional ACH metadata.

Description. Stored-method metadata.

First name. Customer field.

Last name. Customer field.

Customer number. Customer field.

Billing email. Customer field.

Billing Postal Code. Customer field.

Amount. Read-only payment summary field.

Fee. Read-only payment summary field.

### `PayabliPayInPaymentFlowRequestConfiguration` fields

Used by hosted capture and authorize forms, and by the direct capture/authorize/authorized-capture APIs. Not used for `.storePaymentMethod`.

#### properties

Must be greater than 0.

Must not be negative.

For example, `USD`.

Defaults merged with form-entered customer data.

#### properties

Custom identifier fields, when the paypoint uses custom identifiers instead of name/email/customer ID.

Client-side validation, run before a request is sent.

#### properties

Runs a Luhn check on card numbers.

Runs an ABA checksum on ACH routing numbers.

### `PayabliPayInPaymentFlowSheetConfiguration` fields

Controls the sheet presentation for `.payabliPayInPaymentFlowSheet(...)`. The sheet wraps the same hosted form as `PayabliPayInPaymentFlowView`, so `configuration`, `requestConfiguration`, and `style` still apply:

```swift
Button("Add Payment Method") {
    isPresented = true
}
.payabliPayInPaymentFlowSheet(
    isPresented: $isPresented,
    component: paymentFlow,
    configuration: configuration,
    sheetConfiguration: PayabliPayInPaymentFlowSheetConfiguration(
        title: "Add Payment Method",
        dismissButton: .close
    ),
    onCompleted: { result in
        print(result.storedPaymentMethod?.storedMethodId ?? "")
    }
)
```

Sheet title. Falls back to the form's `labels.title` when `movesFormHeaderToSheetHeader` is `true`.

Sheet subtitle. Falls back to the form's `labels.subtitle` when `movesFormHeaderToSheetHeader` is `true`.

`.close`, `.back`, or `.hidden`.

Dismisses the sheet automatically after `onCompleted` fires.

SwiftUI presentation detents. An empty set resolves to `.large`.

SwiftUI drag indicator visibility.

Padding around the sheet's content.

Moves the form's title and subtitle into the sheet header instead of rendering them inline.

Adds a detent sized to the form's measured content height, when it fits.

Expands to `.large` if the form's content doesn't fit the selected detent.

## Response reference

`PayabliPayInPaymentFlowResult` is a unified wrapper for both token-storage and transaction operations.

`.storedPaymentMethod` or `.transaction`.

Stored-method result code, or MoneyIn response code.

Human-readable result or failure reason, when the backend returns one.

Present for `.transaction` results. Additional detail beyond `reason`.

Present for `.transaction` results. Suggested next step, when the backend returns one.

Present when `kind` is `.storedPaymentMethod`.

#### properties

Present when `kind` is `.transaction`. Covers capture, authorize, and capture-authorized results.

#### properties

Processor-level response detail. See [Make a transaction (v2)](/developers/api-reference/moneyinV2/make-a-transaction) for the underlying field semantics.

#### properties

Present for `.transaction` results. The raw response the transaction fields above were parsed from.

#### properties

Same shape as `transaction` above.

## Theming & diagnostics

Style the hosted form to match your app and inspect network activity for debugging:

#### Style

Use `PayabliPayInPaymentFlowStyle` to style the hosted form. Pass it directly to `PayabliPayInPaymentFlowView` or `.payabliPayInPaymentFlowSheet(...)`, or inject it with `.payabliPayInPaymentFlowStyle(_:)`.

| Style area         | Type                                       | Covers                                                                      |
| ------------------ | ------------------------------------------ | --------------------------------------------------------------------------- |
| Title and subtitle | `PayabliPayInPaymentFlowTextStyle`         | Font, color                                                                 |
| Section titles     | `PayabliPayInPaymentFlowTextStyle`         | Font, color, per-section overrides                                          |
| Labels             | `PayabliPayInPaymentFlowTextStyle`         | Font, color                                                                 |
| Inputs             | `PayabliPayInPaymentFlowInputStyle`        | Font, UIKit font, text/placeholder color, background, border, corner radius |
| Submit button      | `PayabliPayInPaymentFlowSubmitButtonStyle` | Font, colors, corner radius, height, padding                                |
| Error message      | `PayabliPayInPaymentFlowTextStyle`         | Font, color                                                                 |
| Layout             | `PayabliPayInPaymentFlowLayoutStyle`       | Content, header, field, and section spacing                                 |

To use a custom font:

#### Add the font to your app

Add the font file to your app target, then add its filename to `UIAppFonts` in `Info.plist`.

#### Reference it in your style

Use `Font.custom(_:size:)` for SwiftUI text, and `UIFont(name:size:)` for `PayabliPayInPaymentFlowInputStyle.uiFont`.

#### Diagnostics

Diagnostics are disabled by default. Enable them for local debugging or QA. Don't enable them in production builds shipped to end users.

```swift
let diagnostics = PayabliPayInPaymentFlowDiagnostics.enabled { entry in
    print(entry.phase, entry.method, entry.statusCode ?? 0)
}
```

Each `PayabliPayInPaymentFlowDiagnosticEntry` includes `phase` (`.request`, `.response`, `.failure`), `timestamp`, `method`, `url`, `statusCode`, `headers`, `body`, `durationMilliseconds`, and `errorDescription`.

Diagnostics redact authorization headers, access and request tokens, card number, CVV, ACH account and routing numbers, stored method IDs, customer identifiers, names, emails, phones, and addresses before they reach your handler. You don't need to add more redaction yourself, but don't log the redacted output somewhere less secure than the app itself.

## Example app

The `sdk-ios` repository includes `PayabliDemo`, a SwiftUI example app that exercises `PayabliPayInPaymentFlow` and `PayabliTTP` (card/ACH capture and Tap to Pay), alongside a bundled local Node.js token server for testing.

#### Clone the repository

Clone `sdk-ios` and open the example project:

```bash
git clone https://github.com/payabli/sdk-ios.git
cd sdk-ios/Example/PayabliDemo
```

#### Configure your secrets

Copy the secrets template, then fill in your sandbox identifiers and configure Pay In Payment Flow to use the bundled local token server:

/// Set your sandbox identifiers

Copy `Secrets.swift.sample` to `Secrets.swift`, then set your sandbox `entryPoint` and `appId` (`<TEAM_ID>.<BUNDLE_ID>`):

/// Configure Pay In Payment Flow to use the local token server

`fetchPaymentMethodAccessToken()` calls `partnerPaymentMethodAccessTokenEndpoint`. Set it to the bundled `LocalTokenServer` so the Pay In Payment Flow tabs work without a real backend:

```swift
enum Secrets {
    static let entryPoint = "your-sandbox-entry-point"
    static let appId = "ABCDE12345.com.yourcompany.app"
    static let placeholderAccessToken = "placeholder-token"
    static let partnerTokenEndpoint = URL(string: "https://your-backend.example.com/payabli/token")!
    static let partnerPaymentMethodAccessTokenEndpoint = URL(string: "http://127.0.0.1:8787/payabli/access-token")!
    static let paymentMethodDiagnosticsEnabled = false
    static let paymentCaptureDiagnosticsEnabled = paymentMethodDiagnosticsEnabled

    static func fetchAccessToken() async throws -> String {
        var request = URLRequest(url: partnerTokenEndpoint)
        request.httpMethod = "POST"
        let (data, _) = try await URLSession.shared.data(for: request)
        struct TokenResponse: Decodable { let accessToken: String?; let access_token: String? }
        let response = try JSONDecoder().decode(TokenResponse.self, from: data)
        if let token = response.accessToken ?? response.access_token { return token }
        throw URLError(.userAuthenticationRequired)
    }

    static func fetchPaymentMethodAccessToken() async throws -> String {
        var request = URLRequest(url: partnerPaymentMethodAccessTokenEndpoint)
        request.httpMethod = "POST"
        let (data, _) = try await URLSession.shared.data(for: request)
        struct PaymentMethodTokenResponse: Decodable { let accessToken: String?; let access_token: String? }
        let response = try JSONDecoder().decode(PaymentMethodTokenResponse.self, from: data)
        if let token = response.accessToken ?? response.access_token { return token }
        throw URLError(.userAuthenticationRequired)
    }

    static func fetchPaymentCaptureAccessToken() async throws -> String {
        try await fetchPaymentMethodAccessToken()
    }
}
```

#### Start the local token server

In a separate terminal, start the bundled `LocalTokenServer`:

```bash
cd LocalTokenServer
cp .env.example .env
```

Edit `.env` and set `PAYABLI_ACCESS_TOKEN` to a short-lived sandbox access token, then start the server:

```bash
node server.mjs
```

See the [`LocalTokenServer` README](https://github.com/payabli/sdk-ios/blob/main/Example/PayabliDemo/LocalTokenServer/README.md) for the credential-exchange mode and physical-device networking notes.

#### Run the app

Open `PayabliDemo.xcodeproj` in Xcode, select the `PayabliDemo` scheme, and run it. The Pay In Payment Flow tabs work in the Simulator. Tap to Pay requires a physical device. See [Requirements](/guides/pay-in-tap-to-pay-overview#requirements).

The example app assumes a sandbox `entryPoint` with card, ACH, and Tap to Pay enabled. Contact your Payabli representative if your sandbox paypoint doesn't have these enabled yet.

## Related resources

See these related resources to help you get the most out of Payabli.

#### Prerequisites

* **[Mobile components overview](/guides/mobile-components-overview)** - Learn how Payabli's native mobile SDK components let you accept payments in your iOS app

#### Related topics

* **[EmbeddedMethod UI](/guides/pay-in-components-embeddedmethod-ui)** - Learn how to use the EmbeddedMethod UI embedded component to add the ability to securely store a payment profile or execute a sale
* **[Tap to Pay](/guides/mobile-components-tap-to-pay)** - Add native Tap to Pay payments to your mobile app with PayabliSDKTapToPay