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

  1. Your server creates a checkout session with POST /v1/checkout-sessions and a secret key, then hands the slug to the app.
  2. The app renders PaysioPaymentSheet for that slug. The customer types the card into VGS fields; the SDK swaps the number and CVC for aliases.
  3. 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.
  4. The SDK charges the session with the aliases and reports the result to onSuccess or onError. 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-svg

React 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

1

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.

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

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.

3

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.

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

4

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.

5

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.

tsx
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. session is the loaded PaysioSession (totalFormatted, currency, sandbox, cardEnabled, bankEnabled, threeDsEnabled, customFields, items) or null while 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:

  1. A hidden web view loads the issuer's device fingerprint for 3 seconds. The customer sees nothing.
  2. Frictionless approval: the charge proceeds. Most authentications end here.
  3. 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.
  4. 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.

tsx
<PaysioProvider
  sessionSlug={slug}
  theme={{
    scheme: 'auto',
    colors: { primary: '#0B57D0', ring: '#0B57D0' },
    darkColors: { card: '#111111' },
    fontFamily: 'Inter',
    fontFamilyMedium: 'Inter-Medium',
    radius: 16,
  }}
>
TokenUsed forLightDark
primaryPay button, selected radio, links#0B57D0#0B57D0
foregroundText#1B1B1B#F0F0F0
mutedForegroundLabels, placeholders, icons#5D5D5F#A6A6A6
cardPanels#FCFCFD#101010
backgroundScreen background#F3F3F4#0A0A0A
secondaryAddress trigger, chips#EAEAED#181818
borderPicker border and row dividers#ECECECrgba(255,255,255,0.09)
inputField borders#DBDBDC#404040
rowHoverPressed rowrgba(0,0,0,0.04)rgba(255,255,255,0.05)
ringFocus ring, same as hosted checkoutrgba(0,0,0,0.35)rgba(255,255,255,0.4)
destructiveInvalid 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.

CodeMeaningWhat to do
invalid_fieldsA 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_requiredStreet, city, postal code or country is missing.Collect the full billing address. It is required for every card and bank account.
contact_requiredEmail, first name, last name or a required phone is missing.Collect the contact fields or prefill them with customer.
three_ds_failedThe issuer declined the authentication.Ask for a different card.
three_ds_cancelledThe customer closed the Verify your card sheet.Let them try again. Nothing was charged.
three_ds_timeoutThe challenge did not finish within 4 minutes.Let them try again.
declinedThe processor declined. declineCode and transactionId are set. No money moved.Ask for a different payment method. The session stays open.
session_expiredThe session is expired, inactive or already paid.Create a new session on your server.
session_invalidThe slug does not look like a session or payment link.Check what your server sent to the app.
rate_limitedToo many attempts from this device for this session.Wait a minute before retrying.
networkNo connection, or the request timed out before it was sent.Retry. Nothing was charged.
unknownThe 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_unavailableThe VGS tokenization request failed.Retry. Card data was not sent to Paysio.
not_configuredThe 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.

tsx
<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, bank or saved.
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, declined or error.
error{ code }
A PaysioError was 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_SECURE on 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-webview and react-native-svg are bundled in Expo Go. Install them with npx expo install so the versions match your SDK version.
  • iOS privacy manifest: VGSCollectSDK ships its own PrivacyInfo.xcprivacy and 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 INTERNET permission 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.

CardResult
4111 1111 1111 1111Success
4147 4630 1111 01343-D Secure, frictionless (no sheet)
4016 3600 0000 04933-D Secure, challenge (the Verify your card sheet opens; choose approve or deny)
Bank accountResult
Routing 490000018, account 24413815Success in the NMI sandbox. Other account numbers are refused with decline code 300.

Versions

VersionChanges
0.1.0First 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.