Pay In Payment Flow

Natively add card and ACH payments to your mobile app with Pay In Payment Flow
View as MarkdownOpen in Claude
Applies to:Developers

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 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 to provision them.

Installation

Install PayabliSDKPayInPaymentFlow from the sdk-ios Swift package:

1

Add the package

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

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

Or add it to your Package.swift dependencies:

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

Import the package

Import it where you construct or render the component:

1import 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.

1

Create the project

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

Requires: Node.js and npm

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

$mkdir payabli-token-proxy
$cd payabli-token-proxy
$npm init -y
$npm pkg set type=module
2

Install dependencies

Install Express, CORS, and dotenv:

$npm install express cors dotenv
3

Write the token endpoint

Create server.js with the following:

1// server.js
2import "dotenv/config";
3import express from "express";
4import cors from "cors";
5
6const app = express();
7app.use(cors(), express.json());
8
9const PAYABLI_URL = process.env.PAYABLI_URL || "https://api-sandbox.payabli.com/api";
10const { PAYABLI_CLIENT_ID, PAYABLI_CLIENT_SECRET } = process.env;
11
12app.post("/payabli/token", async (req, res) => {
13 const upstream = await fetch(`${PAYABLI_URL}/v2/Token/serverside`, {
14 method: "POST",
15 headers: { "Content-Type": "application/json" },
16 body: JSON.stringify({
17 clientId: PAYABLI_CLIENT_ID,
18 clientSecret: PAYABLI_CLIENT_SECRET,
19 }),
20 });
21
22 res.status(upstream.status).json(await upstream.json());
23});
24
25app.listen(process.env.PORT || 3000, () =>
26 console.log("Token server ready"));

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

4

Start the server

Start the server:

$node server.js
5

Call it from your app

The accessTokenProvider closure calls this endpoint and returns the token:

1let accessTokenProvider: PayabliPayInPaymentFlowAccessTokenProvider = {
2 try await backend.fetchPayInAccessToken()
3}

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:

Loading walkthrough content

Loading walkthrough...

Loading walkthrough...

/// 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.

1import SwiftUI
2import PayabliSDKPayInPaymentFlow
3
4func fetchPayInAccessToken() async throws -> String {
5 var request = URLRequest(url: URL(string: "https://your-backend.example.com/payabli/token")!)
6 request.httpMethod = "POST"
7 request.addValue("application/json", forHTTPHeaderField: "Content-Type")
8
9 struct Response: Decodable { let access_token: String }
10 let (data, _) = try await URLSession.shared.data(for: request)
11 return try JSONDecoder().decode(Response.self, from: data).access_token
12}
13
14@MainActor
15final class CheckoutViewModel: ObservableObject {
16 let paymentFlow: PayabliPayInPaymentFlow
17
18 init(entryPoint: String) {
19 paymentFlow = PayabliPayInPaymentFlow(
20 entryPoint: entryPoint,
21 environment: .sandbox,
22 accessTokenProvider: {
23 try await fetchPayInAccessToken()
24 },
25 operation: .capture,
26 requestConfiguration: PayabliPayInPaymentFlowRequestConfiguration(
27 paymentDetails: PayabliPayInPaymentFlowPaymentDetails(
28 totalAmount: 1.00,
29 currency: "USD"
30 ),
31 orderDescription: "iOS checkout",
32 source: "ios-sdk"
33 )
34 )
35 }
36}
37
38struct CheckoutView: View {
39 @StateObject var viewModel: CheckoutViewModel
40
41 var body: some View {
42 PayabliPayInPaymentFlowView(
43 component: viewModel.paymentFlow,
44 configuration: PayabliPayInPaymentFlowFormConfiguration(),
45 onCompleted: { result in
46 guard let transaction = result.transaction else { return }
47 print("Payment captured:", transaction.paymentTransId ?? "")
48 },
49 onError: { error in
50 print("Payment failed:", error.localizedDescription)
51 }
52 )
53 }
54}

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 for what each one does.

allowedMethods
array[string]Defaults to [.card, .ach]

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

defaultMethod
stringDefaults to .card

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

cardSections
array[object]

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

id
string

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

title
string

Section title, for example Card Information.

titleStyle
object

Per-section text style override.

fields
array[string]Required

Ordered PayabliPayInPaymentFlowField values in this section.

inputVerticalSpacing
number

Vertical spacing between inputs in this section.

inputHorizontalSpacing
number

Horizontal spacing for paired inputs in this section.

fieldVerticalSpacings
object

Per-field vertical spacing overrides, keyed by field.

achSections
array[object]

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

id
string

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

title
string

Section title, for example Card Information.

titleStyle
object

Per-section text style override.

fields
array[string]Required

Ordered PayabliPayInPaymentFlowField values in this section.

inputVerticalSpacing
number

Vertical spacing between inputs in this section.

inputHorizontalSpacing
number

Horizontal spacing for paired inputs in this section.

fieldVerticalSpacings
object

Per-field vertical spacing overrides, keyed by field.

cardFieldOrder
array[string]

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

achFieldOrder
array[string]

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

hiddenValues
object

Values submitted with the form without rendering an editable field.

achHolderType
string

.personal or .business.

achSecCode
stringDefaults to .web

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

achDevice
string
methodDescription
string
customerData
object

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

firstName
string
lastName
string
customerNumber
string
customerId
number
company
string
billingEmail
string
billingPhone
string
billingAddress1
string
billingAddress2
string
billingCity
string
billingState
string
billingZip
string
billingCountry
string
shippingAddress1
string
shippingAddress2
string
shippingCity
string
shippingState
string
shippingZip
string
shippingCountry
string
identifierFields
object

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

additionalData
object
labels
object

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

title
stringDefaults to Save Payment Method
subtitle
string
submitButton
stringDefaults to Add Payment Method
fieldLabels
object

Visible and accessibility label text, keyed by PayabliPayInPaymentFlowField.

fieldPlaceholders
object

Placeholder text, keyed by PayabliPayInPaymentFlowField.

labelLayout
stringDefaults to .external

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

showsFieldLabels
boolean

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

hiddenFieldLabels
array[string]

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

formatting
objectDefaults to card spaces on, / separator, ACH masking on

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

insertsCardNumberSpaces
booleanDefaults to true

Adds visual card number grouping.

expirationSeparator
stringDefaults to /

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

masksACHAccountEntry
booleanDefaults to true

Masks ACH account entry in the hosted field.

inputSizing
objectDefaults to 52pt height, 14pt padding

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

defaultSize
object

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

fieldSizes
object

Per-field PayabliPayInPaymentFlowInputSize overrides, keyed by PayabliPayInPaymentFlowField.

cardBrandIconPlacement
stringDefaults to .trailing

.leading, .trailing, or .hidden.

errorMessagePlacement
stringDefaults to .aboveSubmitButton

.top or .aboveSubmitButton.

requiredFields
array[string]

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

paymentSummary
object

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

amountLabelText
string
amountValueText
string
feeLabelText
string
feeValueText
string
currencySymbol
stringDefaults to $
labelStyle
objectDefaults to subheadline, secondary color

Font and color for the label text.

valueStyle
objectDefaults to subheadline (semibold), primary color

Font and color for the value text.

rowSpacing
numberDefaults to 8
PayabliPayInPaymentFlowField
enum

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

.cardholderName
case
Name on card. Card field.
.cardNumber
case
Card number. Card field.
.cardExpiration
case
Expiration. Card field.
.cardCvv
case
CVV. Card field.
.cardZip
case
Postal Code. Card field.
.achHolder
case
Account holder. ACH field.
.achRouting
case
Routing number. ACH field.
.achAccount
case
Account number. ACH field.
.achAccountType
case
Account type. ACH field.
.achHolderType
case
Holder type. ACH field, or hidden value.
.achSecCode
case
SEC code. Hidden by default; not rendered by achSections.
.achDevice
case
Device. Optional ACH metadata.
.methodDescription
case
Description. Stored-method metadata.
.firstName
case
First name. Customer field.
.lastName
case
Last name. Customer field.
.customerNumber
case
Customer number. Customer field.
.billingEmail
case
Billing email. Customer field.
.billingZip
case
Billing Postal Code. Customer field.
.amount
case
Amount. Read-only payment summary field.
.serviceFee
case
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.

paymentDetails
objectRequired
totalAmount
numberRequired

Must be greater than 0.

serviceFee
number

Must not be negative.

currency
string

For example, USD.

checkNumber
string
checkUniqueId
string
accountId
string
customerData
object

Defaults merged with form-entered customer data.

firstName
string
lastName
string
customerNumber
string
customerId
number
company
string
billingEmail
string
billingPhone
string
billingAddress1
string
billingAddress2
string
billingCity
string
billingState
string
billingZip
string
billingCountry
string
shippingAddress1
string
shippingAddress2
string
shippingCity
string
shippingState
string
shippingZip
string
shippingCountry
string
identifierFields
object

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

additionalData
object
ipAddress
string
orderDescription
string
orderId
string
source
string
subdomain
string
subscriptionId
number
idempotencyKey
string
achValidation
boolean
forceCustomerCreation
boolean
validation
objectDefaults to Luhn and ACH routing checks on

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

requiresLuhnCheck
booleanDefaults to true

Runs a Luhn check on card numbers.

validatesACHRoutingChecksum
booleanDefaults to true

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:

1Button("Add Payment Method") {
2 isPresented = true
3}
4.payabliPayInPaymentFlowSheet(
5 isPresented: $isPresented,
6 component: paymentFlow,
7 configuration: configuration,
8 sheetConfiguration: PayabliPayInPaymentFlowSheetConfiguration(
9 title: "Add Payment Method",
10 dismissButton: .close
11 ),
12 onCompleted: { result in
13 print(result.storedPaymentMethod?.storedMethodId ?? "")
14 }
15)
title
string

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

subtitle
string

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

dismissButton
stringDefaults to .close

.close, .back, or .hidden.

dismissesOnSuccess
booleanDefaults to true

Dismisses the sheet automatically after onCompleted fires.

detents
array[string]Defaults to [.medium, .large]

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

dragIndicatorVisibility
stringDefaults to .visible

SwiftUI drag indicator visibility.

contentInsets
objectDefaults to top 20, leading 20, bottom 24, trailing 20

Padding around the sheet’s content.

movesFormHeaderToSheetHeader
booleanDefaults to true

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

sizesToContentWhenPossible
booleanDefaults to true

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

expandsToLargeWhenContentDoesNotFit
booleanDefaults to true

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.

kind
string

.storedPaymentMethod or .transaction.

code
string

Stored-method result code, or MoneyIn response code.

reason
string

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

explanation
string

Present for .transaction results. Additional detail beyond reason.

action
string

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

storedPaymentMethod
object

Present when kind is .storedPaymentMethod.

storedMethodId
string
methodReferenceId
string
resultCode
number
resultText
string
customerId
number
responseText
string
transaction
object

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

paymentTransId
string
gatewayTransId
string
orderId
string
method
string
transStatus
number
paypointId
number
totalAmount
number
netAmount
number
feeAmount
number
settlementStatus
number
operation
string
responseData
object

Processor-level response detail. See Make a transaction (v2) for the underlying field semantics.

resultCode
string
resultCodeText
string
response
string
responseText
string
authCode
string
transactionId
string
avsResponse
string
avsResponseText
string
cvvResponse
string
cvvResponseText
string
orderId
string
responseCode
string
responseCodeText
string
customerVaultId
string
emvAuthResponseData
string
type
string
source
string
isValidatedACH
boolean
transactionTime
string
achSecCode
string
achHolderType
string
ipAddress
string
walletType
string
apiResponse
object

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

code
string
reason
string
explanation
string
action
string
data
object

Same shape as transaction above.

token
string

Theming & diagnostics

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

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

Style areaTypeCovers
Title and subtitlePayabliPayInPaymentFlowTextStyleFont, color
Section titlesPayabliPayInPaymentFlowTextStyleFont, color, per-section overrides
LabelsPayabliPayInPaymentFlowTextStyleFont, color
InputsPayabliPayInPaymentFlowInputStyleFont, UIKit font, text/placeholder color, background, border, corner radius
Submit buttonPayabliPayInPaymentFlowSubmitButtonStyleFont, colors, corner radius, height, padding
Error messagePayabliPayInPaymentFlowTextStyleFont, color
LayoutPayabliPayInPaymentFlowLayoutStyleContent, header, field, and section spacing

To use a custom font:

1

Add the font to your app

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

2

Reference it in your style

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

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

1let diagnostics = PayabliPayInPaymentFlowDiagnostics.enabled { entry in
2 print(entry.phase, entry.method, entry.statusCode ?? 0)
3}

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.

1

Clone the repository

Clone sdk-ios and open the example project:

$git clone https://github.com/payabli/sdk-ios.git
$cd sdk-ios/Example/PayabliDemo
2

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:

Loading walkthrough content

Loading walkthrough...

Loading walkthrough...

/// 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:

1enum Secrets {
2 static let entryPoint = "your-sandbox-entry-point"
3 static let appId = "ABCDE12345.com.yourcompany.app"
4 static let placeholderAccessToken = "placeholder-token"
5 static let partnerTokenEndpoint = URL(string: "https://your-backend.example.com/payabli/token")!
6 static let partnerPaymentMethodAccessTokenEndpoint = URL(string: "http://127.0.0.1:8787/payabli/access-token")!
7 static let paymentMethodDiagnosticsEnabled = false
8 static let paymentCaptureDiagnosticsEnabled = paymentMethodDiagnosticsEnabled
9
10 static func fetchAccessToken() async throws -> String {
11 var request = URLRequest(url: partnerTokenEndpoint)
12 request.httpMethod = "POST"
13 let (data, _) = try await URLSession.shared.data(for: request)
14 struct TokenResponse: Decodable { let accessToken: String?; let access_token: String? }
15 let response = try JSONDecoder().decode(TokenResponse.self, from: data)
16 if let token = response.accessToken ?? response.access_token { return token }
17 throw URLError(.userAuthenticationRequired)
18 }
19
20 static func fetchPaymentMethodAccessToken() async throws -> String {
21 var request = URLRequest(url: partnerPaymentMethodAccessTokenEndpoint)
22 request.httpMethod = "POST"
23 let (data, _) = try await URLSession.shared.data(for: request)
24 struct PaymentMethodTokenResponse: Decodable { let accessToken: String?; let access_token: String? }
25 let response = try JSONDecoder().decode(PaymentMethodTokenResponse.self, from: data)
26 if let token = response.accessToken ?? response.access_token { return token }
27 throw URLError(.userAuthenticationRequired)
28 }
29
30 static func fetchPaymentCaptureAccessToken() async throws -> String {
31 try await fetchPaymentMethodAccessToken()
32 }
33}
3

Start the local token server

In a separate terminal, start the bundled LocalTokenServer:

$cd LocalTokenServer
$cp .env.example .env

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

$node server.mjs

See the LocalTokenServer README for the credential-exchange mode and physical-device networking notes.

4

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.

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.

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