Skip to navigation

Pay In

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

The Pay In component 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 with a permission for each operation you use. See OAuth authentication to provision them.
OperationPermission
.capture, .authorize, capture(_:), authorize(_:), captureAuthorizedTransaction(_:)inboundpayments_create
voidTransaction(_:)inboundpayments_void
.storePaymentMethod, addCard(_:options:), addBankAccount(_:options:), addPaymentMethod(_:options:)tokens_create

Installation

Install PayabliSDKPayIn from the sdk-ios Swift package:

1

Add the package

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

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

Or add it to your Package.swift dependencies:

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

Import the modules

Import PayabliSDKCore, which holds the configuration types, and PayabliSDKPayIn where you construct or render the component:

import PayabliSDKCore
import PayabliSDKPayIn

Backend token endpoint

The Pay In component authenticates every request with a short-lived Payabli access token, obtained through a tokenProvider 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 18 or later, 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 and dotenv:

npm install express dotenv
3

Write the token endpoint

Create server.js with the following. It returns the token as { "accessToken": "..." }:

// server.js
import "dotenv/config";
import express from "express";
const app = express();
const PAYABLI_URL = process.env.PAYABLI_URL ?? "https://api-sandbox.payabli.com/api";
// Replace with your app's own check of the caller's session. This example
// accepts a single development credential from APP_SESSION_TOKEN.
async function authenticateUser(req) {
const expected = process.env.APP_SESSION_TOKEN;
if (!expected || req.get("Authorization") !== `Bearer ${expected}`) {
return null;
}
return { id: "dev-user" };
}
// Replace with your app's own rule for who may take payments for this paypoint.
async function mayTakePayments(user) {
return true;
}
app.post("/payabli/token", async (req, res) => {
// Never return a token to a caller you haven't authenticated.
const user = await authenticateUser(req);
if (!user) {
return res.status(401).json({ error: "unauthenticated" });
}
if (!(await mayTakePayments(user))) {
return res.status(403).json({ error: "forbidden" });
}
let accessToken;
try {
const upstream = await fetch(`${PAYABLI_URL}/v2/token/serverside`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clientId: process.env.PAYABLI_CLIENT_ID,
clientSecret: process.env.PAYABLI_CLIENT_SECRET,
}),
redirect: "error", // Never send the client secret to another origin.
signal: AbortSignal.timeout(10_000), // Well inside the SDK's 30-second limit.
});
const body = await upstream.json();
accessToken = upstream.ok ? (body.access_token ?? body.accessToken) : undefined;
} catch {
// A timeout, a redirect, a network failure, or a response that isn't JSON.
}
if (!accessToken) {
return res.status(502).json({ error: "token exchange failed" });
}
res.set("Cache-Control", "no-store").json({ accessToken });
});
app.listen(process.env.PORT ?? 3000, () => console.log("Token server ready"));

Create a .env file next to server.js with your credentials:

PAYABLI_CLIENT_ID=<your sandbox client ID>
PAYABLI_CLIENT_SECRET=<your sandbox client secret>
APP_SESSION_TOKEN=<a long random string your app sends as a bearer token>

Anyone who can call this route gets a token that can charge, store payment methods, and void for your paypoint. Before you go live, replace authenticateUser and mayTakePayments with the same session and permission checks the rest of your app uses.

Keep clientId and clientSecret on your server, in environment variables or a secret store. Never commit .env to source control.

4

Start the server

Start the server:

node server.js
5

Call it from your app

Pass a tokenProvider closure that calls this endpoint and returns the token when you start the session:

let session = try await PayabliSession.initialize(config: PayabliConfig(
entryPoint: "your-entry-point",
environment: .sandbox,
tokenProvider: { try await backend.fetchPayabliAccessToken() }
))

The SDK calls the token provider before its first request, and again when a token is rejected. Concurrent callers share one call, so you don’t need to cache the token or schedule refreshes yourself. Each call has 30 seconds to return. A call that takes longer, throws, or returns a token the SDK can’t use fails with PayabliErrorType.tokenProviderFailed. Return a token from the closure, and don’t make SDK calls from inside it.

Anyone who can call your token endpoint gets a token that can charge, store payment methods, and void for your paypoint. Authenticate the caller and check that they’re allowed to take payments before you return a token.

Operations

PayabliPayIn’s operation parameter selects the hosted form’s behavior:

  • .storePaymentMethod — Tokenizes a card or ACH account for later use, without charging it. This is the default.
  • .capture — Authorizes and captures a card or ACH payment in one step.
  • .authorize — Authorizes a card payment without capturing it. The hosted form offers card only in this mode.

PayabliPayIn also exposes direct methods for building your own UI: capture(_:), authorize(_:), captureAuthorizedTransaction(_:), voidTransaction(_:), addCard(_:options:), addBankAccount(_:options:), and addPaymentMethod(_:options:). If you collect card data in your own UI and pass it to these methods, your app handles card numbers and security codes, which brings it into scope for PCI DSS. The hosted form keeps that data out of your app.

Implementation walkthrough

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

Loading walkthrough content

Loading walkthrough...

Loading walkthrough...

/// Add the imports

Import PayabliSDKCore, which holds the session and configuration types, and PayabliSDKPayIn. Then 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 access token. Your backend holds the Payabli clientSecret, not the app. In production, send your app’s own session credential with this request so your token endpoint can authenticate the caller.

/// Start the session

Start one PayabliSession with your entrypoint, environment, and token provider before you build a component. PayabliConfig throws if the entrypoint is blank. Start the session once: calling initialize again with the same settings returns the same session, and different settings throw invalidConfiguration. Pay In and Tap to Pay both run on this session.

/// Create the Pay In component

Construct PayabliPayIn on the session, in .capture mode, with a requestConfiguration describing the amount to charge. Identify the payor in customerData, because a paypoint can require customer identifiers and refuses a charge without them with E7020. Set orderId to your own order reference so you can find the transaction if its outcome is unknown.

/// Render the hosted form and handle the result

Add a view that renders PayabliPayInView and handles the result. The hosted form collects card or ACH details itself. Your view never sees raw card data. If the result has no transaction ID, or the error is PayabliPayInError.submissionInterrupted, the outcome is unknown. Find the transaction by orderId before you let the payor try again.

import SwiftUI
import PayabliSDKCore
import PayabliSDKPayIn
func fetchPayabliAccessToken() async throws -> String {
var request = URLRequest(url: URL(string: "https://your-backend.example.com/payabli/token")!)
request.httpMethod = "POST"
// Your app's own session credential, which your token endpoint verifies.
request.setValue("Bearer \(yourSessionToken)", forHTTPHeaderField: "Authorization")
struct Response: Decodable { let accessToken: String }
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(Response.self, from: data).accessToken
}
func startPayabliSession(entryPoint: String) async throws -> PayabliSession {
try await PayabliSession.initialize(config: PayabliConfig(
entryPoint: entryPoint,
environment: .sandbox,
tokenProvider: { try await fetchPayabliAccessToken() }
))
}
@MainActor
final class CheckoutViewModel: ObservableObject {
let payIn: PayabliPayIn
init(session: PayabliSession, orderId: String) {
payIn = PayabliPayIn(
session: session,
operation: .capture,
requestConfiguration: PayabliPayInRequestConfiguration(
paymentDetails: PayabliPayInPaymentDetails(
totalAmount: 1.00,
currency: "USD"
),
customerData: PayabliPayInCustomerData(
customerNumber: "C-1001",
firstName: "Jane",
lastName: "Doe"
),
orderDescription: "iOS checkout",
orderId: orderId,
source: "ios-sdk"
)
)
}
}
struct CheckoutView: View {
@StateObject var viewModel: CheckoutViewModel
var body: some View {
PayabliPayInView(
component: viewModel.payIn,
configuration: PayabliPayInFormConfiguration(),
onCompleted: { result in
guard let paymentTransId = result.transaction?.paymentTransId else {
// No transaction ID came back. Find the transaction by
// orderId before you charge again.
reconcileByOrderId()
return
}
// Store paymentTransId with your order record. Don't log it.
saveTransactionId(paymentTransId)
},
onError: { error in
if let payInError = error as? PayabliPayInError,
case .submissionInterrupted = payInError {
// The outcome is unknown, and the payor may have been charged.
// Find the transaction by orderId before another attempt.
reconcileByOrderId()
return
}
// Show error.localizedDescription to the payor. Don't log it.
// It can quote what was submitted.
showError(error.localizedDescription)
}
)
}
}

Implementation reference

PayabliPayInFormConfiguration controls the hosted form’s fields, labels, and layout for every operation, and its hiddenValues and options carry the data sent when the form stores a payment method. PayabliPayInRequestConfiguration carries the request data for capture and authorize forms. PayabliPayInSheetConfiguration controls the sheet presentation.

PayabliPayInFormConfiguration fields

This hosted form is shared by all three operations. See Operations for what each one does.

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

Hosted methods the payor can select. For .authorize, the form offers 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 PayabliPayInField 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.

style
stringDefaults to .inputs

.inputs for a section of input fields, or .summary for the section that shows the operation’s amounts. A capture or authorize form without a .summary section gets one. A .summary section with no title is headed Payment. An .inputs section with no title has no heading.

bankSections
array[object]

Custom section grouping for ACH fields. .secCode 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 PayabliPayInField 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.

style
stringDefaults to .inputs

.inputs for a section of input fields, or .summary for the section that shows the operation’s amounts. A capture or authorize form without a .summary section gets one. A .summary section with no title is headed Payment. An .inputs section with no title has no heading.

cardFieldOrder
array[string]

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

bankFieldOrder
array[string]

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

hiddenValues
object

Values submitted with the form without rendering an editable field.

accountHolderType
string

.personal or .business.

secCode
stringDefaults to .web

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

deviceId
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
options
object

Options sent when the form stores a payment method with .storePaymentMethod. These correspond to the options of Save a payment method.

achValidation
boolean
createAnonymous
boolean
forceCustomerCreation
boolean
temporary
boolean
customerData
object
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
vendorData
object

vendorId and vendorNumber.

fallbackAuth
boolean
fallbackAuthAmount
number
methodDescription
string
source
string
subdomain
string
validation
object

Client-side validation, run before a request is sent. Same fields and defaults as validation in PayabliPayInRequestConfiguration.

labels
object

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

title
string

Form title. Unset by default, so the form shows no title.

subtitle
string
submitButton
string

Submit button text. Unset by default, so the button names the operation: Pay for .capture, Authorize for .authorize, and Save for .storePaymentMethod. While a submission runs, the button shows the operation in progress, such as Paying….

fieldLabels
object

Visible and accessibility label text, keyed by PayabliPayInField.

fieldPlaceholders
object

Placeholder text, keyed by PayabliPayInField.

total
stringDefaults to Total

Label on the payment summary’s Total row.

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

masksAccountNumber
booleanDefaults to true

Masks ACH account number entry in the hosted field. The routing number isn’t masked.

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 PayabliPayInInputSize: optional width, height (default 52), and horizontalPadding (default 14).

fieldSizes
object

Per-field PayabliPayInInputSize overrides, keyed by PayabliPayInField.

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

Text styles and spacing for the read-only amount and fee rows on capture and authorize forms. Forms that store a payment method leave the rows out. The row labels come from labels.fieldLabels for .amount, .serviceFee, and .surchargeFee, and the Total row’s label from labels.total.

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
PayabliPayInField
enum

Card, ACH, customer, and payment-summary fields available for fields, cardFieldOrder, bankFieldOrder, 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.
.accountHolder
case
Account holder. ACH field.
.routingNumber
case
Routing number. ACH field.
.accountNumber
case
Account number. ACH field.
.accountType
case
Account type. ACH field.
.accountHolderType
case
Holder type. ACH field, or hidden value.
.secCode
case
SEC code. Hidden by default; not rendered by bankSections.
.deviceId
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.
.surchargeFee
case
Surcharge. Read-only payment summary field.

PayabliPayInRequestConfiguration fields

Used by hosted capture and authorize forms. Not used for .storePaymentMethod. The direct capture(_:) and authorize(_:) methods take a PayabliPayInRequest instead, which has the same fields plus a required paymentMethod.

paymentDetails
objectRequired
totalAmount
numberRequired

Must be greater than 0 after rounding to two decimal places, so 0.001 is refused.

serviceFee
number

Must not be negative.

surchargeFee
number
currency
string

For example, USD.

checkNumber
string
checkUniqueId
string
accountId
string
customerData
object

Defaults merged with form-entered customer data. A paypoint can require customer identifiers, such as customerNumber, and refuses a charge without them with E7020. When the form stores a payment method, pass customer data in hiddenValues.customerData or options.customerData instead.

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

Your own order reference. Set it on every request so you can find the transaction in the Payabli Portal if the outcome is unknown.

source
string
subdomain
string
subscriptionId
number
idempotencyKey
string

The SDK generates a new key for each call when you don’t set one, so calling again without your own key is a second payment, not a retry.

achValidation
boolean
forceCustomerCreation
boolean
validation
objectDefaults to card number and routing number checks on

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

requiresLuhnCheck
booleanDefaults to true

Runs a checksum on card numbers to catch typos before the request is sent.

validatesRoutingNumberChecksum
booleanDefaults to true

Runs an ABA checksum on ACH routing numbers.

PayabliPayInSheetConfiguration fields

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

Button("Add Payment Method") {
isPresented = true
}
.payabliPayInSheet(
isPresented: $isPresented,
component: payIn,
configuration: configuration,
sheetConfiguration: PayabliPayInSheetConfiguration(
title: "Add Payment Method",
dismissButton: .close
),
onCompleted: { result in
print(result.storedPaymentMethod?.storedMethodId ?? "")
}
)
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

PayabliPayInResult 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

nil when the service’s response doesn’t include an ID.

method
string

.card or .bankAccount.

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

Optional, and can be absent even in a completed result. When it’s absent, find the transaction by your orderId before you charge again.

gatewayTransId
string
orderId
string

The order reference sent with the request, when present.

method
string
transStatus
number
paypointId
number
totalAmount
number
netAmount
number
feeAmount
number
surchargeFee
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

The order reference the processor returned, when present.

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

Outcomes and errors

Every charge ends in one of three outcomes. Only not charged is safe to retry.

A returned result means something different for each call:

CallA returned result means
.capture, capture(_:)The payment was charged.
.authorize, authorize(_:)An amount is held on the card. Nothing is charged until you capture it.
captureAuthorizedTransaction(_:)The held amount was charged.
.storePaymentMethod, addCard(_:options:), addBankAccount(_:options:), addPaymentMethod(_:options:)The payment method was saved.
voidTransaction(_:)The transaction was voided.
ResultOutcomeWhat to do
The call returns a PayabliPayInResult, or onCompleted firesThe call succeeded, as the table above describesStore the transaction ID or the stored method ID with your order.
PayabliPayInError.transactionFailed, for example a declineNot chargedYou can retry.
PayabliPayInError.invalidInput or .submissionInProgressNot charged. Refused before anything was sent.Fix the input, or wait for the running submission.
A core PayabliError whose type is .tokenProviderFailedNot charged. Your token provider failed.Fix the token provider.
PayabliPayInError.submissionInterruptedUnknownDon’t charge again until you’ve checked. After capture(_:) or authorize(_:), find the transaction by orderId in the Payabli Portal. After captureAuthorizedTransaction(_:) or voidTransaction(_:), look up the transaction ID you passed with Get transaction details.
Another core PayabliError, such as a validation failure, a refused credential, or a rate limitNot chargedBranch on its type.
PayabliPayInTokenStorageErrorThe payment method wasn’t saved.invalidInput was refused before sending. .saveFailed is the service’s response.
A core PayabliError from addCard, addBankAccount, or addPaymentMethod whose type is .networkError, .serverError, or .decodingErrorUnknown. The method may have been saved.Read the stored methods back before saving again.

For voidTransaction(_:), transactionFailed means the void was refused. It doesn’t mean the original payment wasn’t charged.

Show error.localizedDescription to the payor, since it names what the service rejected. Don’t log it, because it can quote what was submitted. Log (error as? any PayabliError)?.code instead.

Theming & diagnostics

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

Use PayabliPayInStyle to style the hosted form. Pass it directly to PayabliPayInView or .payabliPayInSheet(...), or inject it with .payabliPayInStyle(_:).

Style areaTypeCovers
Title and subtitlePayabliPayInTextStyleFont, color
Section titlesPayabliPayInTextStyleFont, color, per-section overrides
LabelsPayabliPayInTextStyleFont, color
InputsPayabliPayInInputStyleFont, uiFont for the text fields, text/placeholder color, background and focused background, border and focused border, corner radius, picker icon color
Submit buttonPayabliPayInSubmitButtonStyleFont, colors, corner radius, height, padding
Error messagePayabliPayInTextStyleFont, color
LayoutPayabliPayInLayoutStyleContent, 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 PayabliPayInInputStyle.uiFont.

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

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

Pass diagnostics when you construct PayabliPayIn. Each PayabliPayInDiagnosticEntry includes phase (.request, .response, .failure), timestamp, method, url, statusCode, headers, body, durationMilliseconds, and errorDescription.

Diagnostics redact authorization headers, access and request tokens, idempotency keys, 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 PayabliPayIn 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 point the app at the bundled local token server:

cp App/Configuration/Secrets.swift.sample App/Configuration/Secrets.swift
Loading walkthrough content

Loading walkthrough...

Loading walkthrough...

/// Set your sandbox identifiers

In App/Configuration/Secrets.swift, set the sandbox entry in entryPoints to your sandbox entryPoint. There’s no App ID to set: the SDK reads it from the signed app, and the TapToPay tab shows the value to register as an authorized app. localNetworkHost is the fallback host a physical device uses when you don’t pass -PayabliTokenHost. Set it to the output of scutil --get LocalHostName on your Mac, followed by .local:

/// Point the app at the local token server

Both token endpoints ship pointing at a placeholder backend. Switch them to DemoConfiguration.TokenServer.accessTokenURL, which resolves to 127.0.0.1 in the Simulator, so the Save and Capture tabs work there without a real backend. The TapToPay tab needs a physical device, which also needs the server setup in the next step:

import Foundation
enum Secrets {
static let entryPoints: [String: String] = [
"sandbox": "your-sandbox-entry-point"
]
static let localNetworkHost = "your-mac-hostname.local"
static var partnerTokenEndpoint: URL { DemoConfiguration.TokenServer.accessTokenURL }
static var partnerPaymentMethodAccessTokenEndpoint: URL { DemoConfiguration.TokenServer.accessTokenURL }
static let paymentMethodDiagnosticsEnabled = false
static let paymentCaptureDiagnosticsEnabled = paymentMethodDiagnosticsEnabled
static func fetchAccessToken() async throws -> String {
try await token(from: partnerTokenEndpoint)
}
static func fetchPaymentMethodAccessToken() async throws -> String {
try await token(from: partnerPaymentMethodAccessTokenEndpoint)
}
static func fetchPaymentCaptureAccessToken() async throws -> String {
try await fetchPaymentMethodAccessToken()
}
private static func token(from url: URL) async throws -> String {
var request = URLRequest(url: url)
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)
guard let token = response.accessToken ?? response.access_token else {
throw URLError(.userAuthenticationRequired)
}
return token
}
}
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

The server binds only to 127.0.0.1 by default and authenticates no caller, so run it only on your own machine.

To test on a physical device, which Tap to Pay requires, the phone has to reach the server over your network:

  1. Start the server on all interfaces instead, only on a trusted network, and stop it when you’re done:

    PAYABLI_LOCAL_TOKEN_SERVER_HOST=0.0.0.0 node server.mjs
  2. In Xcode, choose Product → Scheme → Edit Scheme → Run → Arguments and pass your Mac’s LAN address, on the same network as the phone:

    -PayabliTokenHost 192.168.1.42
  3. Allow Local Network access when the app asks for it on the phone. See the LocalTokenServer README for the credential-exchange mode and physical-device networking notes.

4

Run the app

Open PayabliDemo.xcodeproj in Xcode and set DEVELOPMENT_TEAM on the PayabliDemo target. Select the PayabliDemo sandbox scheme, which picks the sandbox entry from Secrets.entryPoints, and run it. The Save and Capture tabs work in the Simulator. The TapToPay tab 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.