Mobile SDK
Take card and bank payments inside your iOS and Android app.
The Paysio Mobile SDK takes a card or a US bank account inside your app, runs 3-D Secure when the workspace has it on, and completes a checkout session without leaving the app. Card data is typed into VGS secure fields and goes straight to the VGS vault over TLS. Your server and Paysio only ever see aliases.
One product, three packages with the same API shape: @paysio/react-native for React Native and Expo (ready today), Paysio for iOS (Swift) and com.paysio:paysio-android for Android (Kotlin). The SDK is for physical goods and real-world services. Digital goods consumed in the app must use the store billing. See App Store and Google Play rules.
How it works
- Your server creates a checkout session with
POST /v1/checkout-sessionsand a secret key, then hands theslugto the app. - The app renders
PaysioPaymentSheetfor that slug. The customer types the card into VGS fields; the SDK swaps the number and CVC for aliases. - When 3-D Secure is on, the SDK fingerprints the device in a hidden web view and shows the bank challenge in a sheet only when the issuer asks for one.
- The SDK charges the session with the aliases and reports the result to
onSuccessoronError. Fulfil from your webhook, as on the web.
No keys in the app
Never ship a secret key (sk_) or a publishable key (pk_) in a mobile app. The session slug is the only handle the app needs. It is single-use and can expire, and a publishable key can read business data.
Platform
Install
The package is pure TypeScript: plain TextInput components plus fetch. No native code, no pod install, and it runs in Expo Go. react-native-webview carries the 3-D Secure sheet and react-native-svg draws the card brand marks.
npx expo install @paysio/react-native react-native-webview react-native-svgReact Native 0.73 or newer and React 18 or newer. The SDK pins its own copy of @vgs/collect-react-native, so there is nothing to configure for VGS.
Quickstart
Create a checkout session on your server
Call POST /v1/checkout-sessions with your secret key and send the returned slug to the app. A payment link slug from the dashboard works too.
// On your server, never in the app.
const res = await fetch('https://paysio.com/api/v1/checkout-sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_...',
'Content-Type': 'application/json',
},
body: JSON.stringify({
line_items: [{ product_id: 'prod_...', quantity: 1 }],
customer_email: '[email protected]',
metadata: { order_ref: '12345' },
}),
})
const { data } = await res.json()
// data.slug looks like "api-a1b2c3d4e5f6". Send it to the app.Wrap your screen in PaysioProvider
PaysioProvider loads the session, holds the field state and talks to Paysio. Give it the sessionSlug and, optionally, your bundle id or package name as appId so the transaction detail in the dashboard shows which app took the payment.
Render PaysioPaymentSheet
The sheet is the complete checkout: contact fields, the payment method picker with card and bank rows, billing address, custom fields from the session, 3-D Secure and the pay button. onSuccess receives a PaysioPayResult; onError receives a PaysioError.
import { PaysioProvider, PaysioPaymentSheet, type PaysioError } from '@paysio/react-native'
export function CheckoutScreen({ slug }: { slug: string }) {
return (
<PaysioProvider sessionSlug={slug} appId="com.example.app">
<PaysioPaymentSheet
customer={{ email: '[email protected]', firstName: 'Ann', lastName: 'Lee' }}
onSuccess={(result) => {
// result.status is "pending_settlement"
console.log(result.transactionId, result.orderLabel)
}}
onError={(error: PaysioError) => {
console.log(error.code, error.message)
}}
/>
</PaysioProvider>
)
}A 200 from the charge is always a success and result.status is pending_settlement on every processor. Confirm fulfilment from the payment.completed webhook or GET /v1/checkout-sessions/:id, not from the app alone.
Test with sandbox cards
Create the session with a test secret key (sk_test_). The sheet shows a Test mode chip and accepts the test cards below, including the two 3-D Secure cards.
Go live
Create sessions with your live secret key (sk_live_). Live sessions point the SDK at the live vault automatically through the session's vaultConfig. Nothing changes in the app.
Custom layout
The sheet is built from pieces you can use on their own. Wrap your screen in PaysioProvider, read usePaysio(), place the fields where you want them and call pay() from your own button. The pieces share one state, so a card typed into PaysioCardFields is what pay() charges.
import { Button, View } from 'react-native'
import {
PaysioProvider, PaysioCardFields, PaysioBillingAddress,
usePaysio, type PaysioError,
} from '@paysio/react-native'
function CustomCheckout() {
const { session, pay, state } = usePaysio()
if (!session) return null
async function onPay() {
try {
const result = await pay({ metadata: { order_ref: '12345' } })
console.log(result.transactionId)
} catch (err) {
const e = err as PaysioError
if (e.code === 'invalid_fields') console.log(e.fieldErrors)
if (e.code === 'declined') console.log(e.declineCode)
}
}
return (
<View>
<PaysioCardFields />
<PaysioBillingAddress />
<Button
title={'Pay ' + session.totalFormatted}
onPress={onPay}
disabled={!state.canPay || state.step !== 'idle'}
/>
</View>
)
}
export function Screen({ slug }: { slug: string }) {
return (
<PaysioProvider sessionSlug={slug}>
<CustomCheckout />
</PaysioProvider>
)
}usePaysio(){ session, pay, state }- Reads the provider.
sessionis the loadedPaysioSession(totalFormatted,currency,sandbox,cardEnabled,bankEnabled,threeDsEnabled,customFields,items) ornullwhile loading. PaymentMethodPicker{ options, value, onChange }- The radio list the sheet uses, with a row per option that expands into its panel. Pass your own
options(id,icon,label,trailing,panel) when you want the same look around your own rows. The same picker the hosted checkout uses. PaysioCardFields- Card number, expiry and CVC as one stacked box, with the brand mark and the lock icon. The number and CVC are VGS fields.
PaysioBankFields- Routing number and account number, with the Checking or Savings and Personal or Business chips. The account number is a VGS field.
PaysioBillingAddress- Name, country, street, city, state and postal code. Required for every manually entered card or bank account.
pay(options?)Promise<PaysioPayResult>- Validates, tokenizes with VGS, runs 3-D Secure when needed and charges. Throws a
PaysioError.options:items(overrides the session items),discountCode,customFieldValues,shippingAddress,metadata. state.stepPaysioPayStep- One of
idle,validating,aliasing,three_ds,charging,done. Use it to disable the button and show progress. state.canPayboolean- True when every required field is valid and the session is open.
pay() mints one Idempotency-Key per call and reuses it for a single retry on a network error only. It never retries on a response. A timeout on the charge is reported as unknown with the key, never as a decline, so you can reconcile by webhook.
3-D Secure
There is nothing to configure. When 3D Secure is on for the workspace (Settings → Checkout), the SDK runs the full flow after tokenization and before the charge:
- A hidden web view loads the issuer's device fingerprint for 3 seconds. The customer sees nothing.
- Frictionless approval: the charge proceeds. Most authentications end here.
- Challenge: a white sheet titled Verify your card opens with the bank's own page (a code by text message, an app approval, or similar). It is a page sheet on iOS and full screen on Android, with a Cancel button.
- The sheet closes when the bank answers. The SDK then charges with the authentication result.
Cancel reports three_ds_cancelled and charges nothing. A declined authentication reports three_ds_failed. The challenge waits at most 4 minutes, then reports three_ds_timeout. The SDK never charges without the authentication when the workspace requires it.
Bank accounts
When bank payments are enabled on the workspace and the session allows bank, the picker shows a Bank account row: routing number, account number, Checking or Savings and Personal or Business. The account number is a VGS field, so your app never sees it. The billing address is required, as for cards.
The ACH authorization text comes from the session and is shown under the form with your business name and the amount filled in. Submitting records the authorization with the charge. On Paysio Debit & Payouts a first instant ACH debit can return result.bankAuthUrl; open it in the browser so the customer can sign in to their bank, or the debit never settles.
Saved methods
Coming next. The picker already carries a Saved method row and the PaysioSavedMethod type, and the backend accepts a saved method with a portal token. Charging a saved card from the app, with Quick Checkout email codes to prove the customer owns it, ships in the next version.
Theming
Pass a theme to PaysioProvider. scheme is light, dark or auto (follows the system). Override any token with colors, and separately for dark mode with darkColors. The defaults are copied from the Paysio dashboard, so a sheet with no theme matches the hosted checkout.
<PaysioProvider
sessionSlug={slug}
theme={{
scheme: 'auto',
colors: { primary: '#0B57D0', ring: '#0B57D0' },
darkColors: { card: '#111111' },
fontFamily: 'Inter',
fontFamilyMedium: 'Inter-Medium',
radius: 16,
}}
>| Token | Used for | Light | Dark |
|---|---|---|---|
| primary | Pay button, selected radio, links | #0B57D0 | #0B57D0 |
| foreground | Text | #1B1B1B | #F0F0F0 |
| mutedForeground | Labels, placeholders, icons | #5D5D5F | #A6A6A6 |
| card | Panels | #FCFCFD | #101010 |
| background | Screen background | #F3F3F4 | #0A0A0A |
| secondary | Address trigger, chips | #EAEAED | #181818 |
| border | Picker border and row dividers | #ECECEC | rgba(255,255,255,0.09) |
| input | Field borders | #DBDBDC | #404040 |
| rowHover | Pressed row | rgba(0,0,0,0.04) | rgba(255,255,255,0.05) |
| ring | Focus ring, same as hosted checkout | rgba(0,0,0,0.35) | rgba(255,255,255,0.4) |
| destructive | Invalid field border, error text | #C70A24 | #DC2626 |
fontFamilystring- Font for every text in the SDK. Defaults to the platform font.
fontFamilyMediumstring- Font for labels and buttons when your medium weight is a separate family.
radiusnumber- Outer radius of the picker. Default 18. Fields and rows scale from it.
The 3-D Secure sheet stays white in both schemes because the bank's page is designed for a white background.
Errors
Every failure is a PaysioError: code is the stable part, message is plain language you can show the customer. Depending on the code it also carries fieldErrors, declineCode, transactionId, idempotencyKey and httpStatus. Messages never contain card data.
| Code | Meaning | What to do |
|---|---|---|
| invalid_fields | A field is empty or invalid. fieldErrors has one message per field. | Show the messages next to the fields. The sheet does this itself. |
| billing_address_required | Street, city, postal code or country is missing. | Collect the full billing address. It is required for every card and bank account. |
| contact_required | Email, first name, last name or a required phone is missing. | Collect the contact fields or prefill them with customer. |
| three_ds_failed | The issuer declined the authentication. | Ask for a different card. |
| three_ds_cancelled | The customer closed the Verify your card sheet. | Let them try again. Nothing was charged. |
| three_ds_timeout | The challenge did not finish within 4 minutes. | Let them try again. |
| declined | The processor declined. declineCode and transactionId are set. No money moved. | Ask for a different payment method. The session stays open. |
| session_expired | The session is expired, inactive or already paid. | Create a new session on your server. |
| session_invalid | The slug does not look like a session or payment link. | Check what your server sent to the app. |
| rate_limited | Too many attempts from this device for this session. | Wait a minute before retrying. |
| network | No connection, or the request timed out before it was sent. | Retry. Nothing was charged. |
| unknown | The charge was sent but no answer arrived. idempotencyKey is set. | Do not assume a decline. Check the payment.completed webhook or the session status before charging again. |
| vault_unavailable | The VGS tokenization request failed. | Retry. Card data was not sent to Paysio. |
| not_configured | The workspace has no vault, so in-app card entry is off. | Contact support to enable the vault on the workspace. |
Events
onEvent on PaysioProvider is an analytics hook. It never carries card data.
<PaysioProvider
sessionSlug={slug}
onEvent={(event) => {
if (event.type === 'pay_result') analytics.track('checkout_result', { status: event.status })
if (event.type === 'error') analytics.track('checkout_error', { code: event.code })
}}
>ready{ sandbox }- The session loaded and the fields are mounted.
field_focus{ field }- A field gained focus.
method_change{ method }- The customer picked
card,bankorsaved. pay_start{ method }pay()began.three_ds_start- The 3-D Secure flow began (fingerprint).
three_ds_challenge- The Verify your card sheet opened.
three_ds_result{ approved }- The authentication finished.
pay_result{ status }- The charge answered:
pending_settlement,declinedorerror. error{ code }- A
PaysioErrorwas raised.
App Store and Google Play rules
Both stores forbid in-app purchase for things used outside the app, which is exactly where in-app card entry is allowed. Both stores require their own billing for digital goods and features used inside the app.
- Physical goods and real-world services (products shipped to the customer, bookings, deliveries, tickets, memberships to a physical location): card entry with this SDK is allowed and carries no store fee.
- Digital goods and in-app features (subscriptions to app content, credits, unlocks): must use In-App Purchase on iOS and Google Play Billing on Android. Do not use this SDK for those sales.
Sources: Apple App Review Guideline 3.1.3(e) and the Google Play Payments policy.
Security and PCI
Card data is entered into VGS-provided fields and transmitted directly to the VGS PCI DSS Level 1 environment. It is not transmitted to or stored by your servers or Paysio's servers. What leaves the fields is an alias, the expiry, the first six digits, the brand and the last four.
The app remains the point of entry, as with every mobile payment SDK. To keep it that way:
- Keep the SDK current. Updates carry security fixes from VGS and Paysio.
- Do not screenshot or screen record the checkout screen. On Android set
FLAG_SECUREon the payment activity. The CVC field uses secure text entry. - Keep every secret on your server. The vault id in the session is public by design; the slug is the only per-payment handle, and it is rate limited per device and session, gated by the session's status and protected by 3-D Secure and your blocklist rules.
- Never log the result of a VGS submit or anything from the field components. The SDK's events and errors already exclude card data.
Expo notes
- Works in Expo Go and in development builds. There is no config plugin and no native module to link.
react-native-webviewandreact-native-svgare bundled in Expo Go. Install them withnpx expo installso the versions match your SDK version.- iOS privacy manifest:
VGSCollectSDKships its ownPrivacyInfo.xcprivacyand the Paysio package declares nothing extra: no tracking, no required reason APIs. If you fill in App Privacy in App Store Connect, declare that payment info is collected for app functionality and is not linked to the user by Paysio. - Android: nothing extra. The
INTERNETpermission is already part of every Expo project.
Test cards
Sandbox sessions relax the Luhn check because the 3-D Secure test cards are not Luhn valid. Use any future expiry and any 3-digit CVC.
| Card | Result |
|---|---|
| 4111 1111 1111 1111 | Success |
| 4147 4630 1111 0134 | 3-D Secure, frictionless (no sheet) |
| 4016 3600 0000 0493 | 3-D Secure, challenge (the Verify your card sheet opens; choose approve or deny) |
| Bank account | Result |
|---|---|
| Routing 490000018, account 24413815 | Success in the NMI sandbox. Other account numbers are refused with decline code 300. |
Versions
| Version | Changes |
|---|---|
| 0.1.0 | First release of @paysio/react-native: card and bank payments, 3-D Secure, the payment method picker, theming and events. Every request carries X-Paysio-SDK: react-native/0.1.0. iOS and Android packages follow. |