> ## Agent Instructions
> Use the Payrails Web SDK v6 for new browser integrations. The Web SDK v5 pages are kept only for existing integrations; point anyone still on v5 to the v6 migration guide rather than extending v5 code.
> Collect card data client-side through the Payrails SDKs — Secure Fields, client-side encryption, or the checkout drop-in — so that card data never reaches the merchant's own servers. Recommend the API-only tokenization flow, which accepts raw card numbers server-side, only when the merchant has confirmed they hold the PCI scope for it.
> Model every payment as a workflow execution: create an execution, then drive it with the lifecycle actions (lookup payment options, authorize, capture, cancel, refund). Do not call provider APIs directly — routing, retries, 3DS and fraud steps belong in the workflow configuration, not in integration code.
> Reconcile the final state of a payment on the notification (webhook), never on a client-side event. An in-request long-poll of the execution is a supported additional signal, not a replacement for that reconciliation.
> Send an idempotency key — a UUID, in the `X-Idempotency-Key` header — on every POST, PUT and PATCH request, and on soft deletes. GET requests need none, and hard deletes cannot be idempotent.
> Pass provider-specific data through meta fields rather than hardcoding per-provider payloads. Payrails translates meta fields into each provider's own format.
> Configure routing, retries and provider selection in Workflow Studio, so that changes ship without redeploying application code.
# Set up your Payrails account and environment
Source: https://docs.payrails.com/docs/account-setup
Set up the account foundations every Payrails module shares: API credentials and mTLS, users and roles, and workspaces.
Before integrating any Payrails module—whether it’s Payment Acceptance, Token Vault, Fraud Management, Reconciliation, or Analytics—you’ll need to set up the foundational elements of your account and environment. This section guides you through everything required to get started securely and efficiently.
Payrails is modular, but your setup is centralized: once configured, these settings apply across all modules and teams in your organization, ensuring a consistent and secure foundation for your integration.
## What You’ll Set Up
Each of the following sections walks you through a key part of your integration foundation. You will be provided staging and production environments to separate testing from live traffic, and will manage below aspects for each separately.
### 1. API Credentials and mTLS
Learn how to generate, rotate, and manage API credentials for secure access across environments, as well as configure the mutual TLS for added security.
### 2. Users, Roles & Permissions
Invite your team, assign permissions, and control access across workspaces and modules.
### 3. Organizations & Workspaces
Understand how organizations are structured in Payrails and how workspaces isolate data, environments, and permissions.
# API Credentials
Source: https://docs.payrails.com/docs/account-setup/api-credentials
Create and manage the client ID and client secret that authenticate your Payrails API calls, scoped per environment and workspace.
## Overview
When starting to work with Payrails, one of the first steps is obtaining your API credentials for your account to authenticate Payrails APIs. Authentication ensures that only authorized users or systems can access the API, helping protect sensitive data, prevent misuse, and maintain the integrity of services.
API credentials are created and managed in the Payrails Portal. The Client ID and Client Secret are the credentials you use to start interacting with the Payrails API in both staging and production environments. Credentials are scoped per environment and can be created at the organization level or for a specific workspace to match your operational model.
Use these credentials from your backend services to request an access token, and then call Payrails APIs with that token.
## Prerequisites
* Admin or Developer role in the Payrails Portal to access the API credentials page. See roles and permissions for details.
* Decision on scope: organization‑level or a specific workspace.
* A secrets manager available to store the `Client Secret` securely.
## Configuration steps
Follow these steps to create credentials in the Portal.
1. Log in to the Payrails Portal.
2. Navigate to `Settings → API credentials`.
3. Select Create credentials.
4. Name the credential:
* **Name** (required): Use a descriptive label (for example, “Checkout backend — staging”).
* **Description** (optional): Add operational context (owner, system, rotation policy).
5. Choose scope:
* **Organization** — credential is valid for the entire organization.
* **Workspace** — credential is limited to a selected workspace. Choose the workspace from the selector.
* Apply least‑privilege. Grant only what the system needs.
6. Create the credential. The portal shows the `Client ID` and the `Client Secret`.
7. Copy both values and store the secret in your secrets manager. The secret is displayed once during creation.
8. Use these credentials in your system to request an access token to call Payrails APIs.
## Rotate and revoke
Rotating your client secret improves security and supports compliance.
1. Navigate to `Settings → API Credentials`.
2. Select the API credential for which you want to rotate the secret.
3. Click on "Rotate Secret" and follow instructions.
4. Copy the new `Client Secret` and update your services to use it for token requests.
5. After rotation, the previous Client Secret becomes invalid immediately.
Notes
* You can have multiple `Client IDs` (and their secrets) active at the same time. Use this to ensure uninterrupted access during migrations.
* Revoke a credential if it's no longer needed or suspected to be compromised. Revocation is immediate and irreversible.
## FAQs and edge cases
Admin and Developer roles.
Yes. Create separate API Credentials for systems or deployments to isolate risk and simplify rotation.
Yes. Multiple `Client IDs` and secrets can be active concurrently to ensure uninterrupted access during changes.
No. Create separate credentials for Staging and Production.
No. Copy it during creation or rotation and store it in a secrets manager.
One hour. Request a new token after expiry. There is no upper limit for generating access tokens.
# Idempotent Requests
Source: https://docs.payrails.com/docs/account-setup/idempotent-requests
Use the X-Idempotency-Key header to retry a Payrails API request safely, without creating duplicate records or repeating an update.
Our API offers idempotency features to ensure safe retries of requests without unintentional duplicate operations. Integrating an idempotency key can prove invaluable if you're working on actions like record creation or updates. This key allows you to re-initiate a request, especially in situations like connectivity disruptions, ensuring no unwanted duplications or repeated updates.
## Making an idempotent request
For an idempotent request, include an extra header: `X-Idempotency-Key: `
Upon receiving a request with a unique idempotency key, our system commits to memory the status code and the response body. This holds irrespective of whether the request was a success or a failure. Any subsequent requests with the same key will fetch identical responses, preserving this consistency even in case of server errors.
## What is an idempotency key?
This key is a unique marker, generated on the client side, which our server leans on to detect and avert potential repetitions stemming from retried requests. Key aspects include:
* **Generation:** While the method for generating unique keys rests with you, we accept only UUIDs as idempotency keys. This ensures a low likelihood of overlapping keys.
* **Storage considerations:** It's important to note that results are only stored if an API endpoint has started processing. Should the incoming parameters fail validation, or if another conflicting request is being processed simultaneously, no idempotent result will be stored. In such scenarios, it's safe to reinitiate these requests.
## Idempotency in different HTTP Methods
* **POST, PUT, PATCH:** All requests must be paired with idempotency keys to ensure safe retries.
* **GET:** Attaching idempotency keys to GET requests is unnecessary and should be refrained from, given that it is inherently idempotent.
* **DELETE:** We require an idempotency key in soft-delete operations. However, hard-delete operations cannot be idempotent because the previous record will not exist on subsequent calls.
# mTLS Configuration
Source: https://docs.payrails.com/docs/account-setup/mtls-configuration
Learn how to set up a secure connection with Payrails through mTLS.
## Overview
Mutual TLS (mTLS) adds certificate‑based authentication to your API traffic with Payrails. In addition to OAuth client credentials, mTLS verifies the calling system by requiring a valid client certificate during the TLS handshake. This strengthens inbound authentication and helps meet enterprise security and compliance requirements.
This guide covers two flows:
* Certificate exchange: request, issue, download, rotate, and revoke mTLS client certificates in the Payrails Portal.
* Use and test: configure Postman (or another HTTP client) to present the certificate and validate calls.
Certificates are scoped per environment: you will need to create one for staging and one for production.
## Prerequisites
* Admin or Developer role in the Payrails Portal.
* Existing API credentials (Client ID/Secret) for your service if you also use OAuth. See API credentials
* A secure place to store private keys and certificates (for example, your secrets manager or key vault).
* Postman desktop app (for the test flow), a terminal with curl or any HTTP client that supports client certificates.
## Configuration steps
### Request and manage mTLS certificates
Create a client certificate for your service and download it for installation.
1. In the Payrails Portal, go to `Settings → mTLS certificates`.
2. Select `Create certificate` and provide a description if desired: owner, rotation policy or related service.
3. Provide organization details to generate the CSR command:
* Organization name: your legal company name.
* Country and state/province: if unsure, use your headquarters. These inputs are not binding.
4. Generate your private key and CSR:
* The interface displays a pre‑filled OpenSSL command. Copy it and run it in your terminal from the directory where you want the files saved.
* The Common Name (`CN`) of the CSR must be the value that the Payrails Portal shows in the command. Payrails rejects a CSR with any other `CN`, so copy the command from the Payrails Portal each time instead of reusing a saved command or script.
* The command creates two files: a private key and a Certificate Signing Request (CSR).
* Store the private key securely; you will use it when configuring your client later.
* Keep the CSR file ready for submission in the next step.
5. Submit the CSR generated in step 4.
6. The certificate is issued automatically after submission: click `Download` to download it.
7. Install the certificate in your service or keep it ready for local testing (see the Postman section below).
8. Click on `Continue to configuration` to [test and configure your mTLS certificate](#test-and-configure-your-mtls-certificate).
## Validate and test
### Test and configure your mTLS certificate
You can test your mTLS certificate with cURL or Postman, follow the instructions on the screen.
### Configure curl to use mTLS
The exact command to use mTLS with CLI depends on the tool you're using. Here's an example with curl to retrieve an access token from Payrails API:
```
curl --cert domain.pem --key domain.key \
--location --request POST 'https:///auth/token/' \
--header 'Accept: application/json' \
--header 'x-api-key: '
```
* `domain.pem`: the certificate file [downloaded](#request-and-manage-mtls-certificates) (or sent to you) in Step 2,
* `domain.key`: the private key file generated in Step 1,
* If the private key requires a passphrase, you will also need to provide it with `--pass` argument.
* `Client ID` and `Client Secret` are the API credentials. [See how to retrieve them](/docs/account-setup/api-credentials#configuration-steps).
### Configure Postman to use mTLS
From certificate details
1. Open the certificate from the list to view `Certificate details`.
2. Select `Download .pem` and save the file.
3. Select `Configure in Postman` to open the guided steps.
Step 1 of 3 — Test with cURL (optional)
* Copy the cURL command shown in the interface and run it in your terminal from the directory where your `-####.key` and `-####.pem` are saved.
* Replace placeholders with a valid `Client ID` and `Client Secret` from your API credentials.
* On success, the response includes an access token. If it fails, verify the credentials and that both the key and certificate paths are correct.
Step 2 of 3 — Configure in Postman
1. Open Postman and go to `Settings → Certificates`, then select `Add certificate`.
2. Host: enter the host value shown in the interface for your environment.
3. `CRT file`: select the downloaded `-####.pem` certificate from Payrails.
4. `KEY file`: select the previously saved `-####.key` private key you generated during the CSR step.
5. Select `Add` to save the certificate mapping.
Step 3 of 3 — Test in Postman
1. Create a `POST` request to the endpoint indicated in the interface (for example, `/token`).
2. Include your API credentials as instructed on screen (for example, `client_id` in path variables and `client_secret` in authorization details).
3. Send the request. A successful TLS handshake and authorization returns an access token.
## Troubleshooting
* Handshake failed in Postman:
* Verify the host and port match the values shown in the interface.
* Ensure the client certificate is attached to that host entry and imported correctly.
* If a passphrase was shown at download time, confirm it is entered correctly.
* `CommonName (CN) '' must be ''` when you submit the CSR: the `CN` in the CSR is not the value that the Payrails Portal shows. This happens when you reuse a saved OpenSSL command or script, for example one that sets the `CN` to an API domain. Copy the current command from the Payrails Portal, run it again, and submit the new CSR.
* 401/403 after handshake: OAuth or API authorization failed. Acquire a valid access token and ensure the calling client has required permissions.
* Revoked certificate: issuance of a new certificate is required; revoked certificates cannot be re‑enabled.
* Maximum number of active certificates reached: we limit the number of certificates to prevent abuse. If you reached your limit, either revoke an existing certificate that is no longer in use or contact your Payrails administrator to increase your limit.
## Why is my certificate marked as Deprecated?
As part of an improvement to the stability of our platform, we are migrating away from Cloudflare as a Certificate Authority (CA) for our MTLS authentication.
We have introduced new Payrails-managed CAs for both staging and production. These CAs are already active and currently operate in parallel with the existing Cloudflare CAs.
**All Cloudflare issued certificates are still valid but marked as deprecated as they would need to be replaced by certificates issued by Payrails CA instead.**\
**Once the transition to Payrails CA is done, Cloudflare certificates will automatically be revoked.**
**Action required by 23 March 2026**
To ensure continued mTLS connectivity, please:
1. Regenerate your client certificate via the Payrails Portal (for staging and/or production, as applicable). Please note that the Portal now supports direct certificate generation for staging, in addition to production.
2. Deploy the newly generated certificate in your environment.
3. Confirm that traffic is no longer relying on the previous Cloudflare CA.
All newly generated certificates are now signed by the Payrails CAs. Once all merchants have migrated, we will fully decommission the Cloudflare CA.
We would appreciate it if you could complete the migration **by 23 March 2026** to help ensure continued, uninterrupted mTLS connectivity.
If you have questions about the transition, please let us know.
# Organization
Source: https://docs.payrails.com/docs/account-setup/organization
Your organization with unified view across all your workspaces, ensuring streamlined access and management.
## Introduction
The **organization context** represents your Payrails account and consolidates all [workspaces](/docs/account-setup/workspaces) under a single organizational structure.
This update simplifies navigation, centralizes settings, and enhances role-based permissions (see: [User Roles & Permissions](/docs/account-setup/user-management)).
### Key Features
1. **Global insights**
Analyze aggregated data from all workspaces, with filters for workspace-specific details.
2. **Improved [User Roles & Permissions](/docs/account-setup/user-management) system**
Define roles and permissions at both the organization and workspace context to ensure that each team member has appropriate access, enhancing both security and operational efficiency.
3. **Flexible workspace management**
Switch seamlessly between the organization-level context and individual workspaces using the new picker in the left menu, which also includes the sign-out option. This feature enhances your ability to compare statistics across workspaces and manage configurations from a centralized perspective.
### Roles & permissions
The new organization context includes a refined roles and permissions system, enabling precise access control across organizations and workspaces.
**Role categories**
1. **Organization roles**
* **Admin**: Full control over the organization, including access rights, users, and all connected workspaces. Ideal for strategic leads like CFOs or Heads of Payment.
* **Developer**: Designed for technical roles, with access to manage APIs, integrations, and certificates while viewing aggregated workspace data. Note: Developers cannot access user data.
* **Viewer**: Read-only access to organization-level insights, suited for oversight roles.
2. **Workspace roles**
* **Admin**: Comprehensive access to configure and manage specific workspaces. Suitable for general managers or other leadership roles.
* **Operator**: Limited access focused on operational tasks, such as processing refunds, without configuration permissions. Ideal for support roles like customer support or payment managers.
* **Viewer**: Read-only access designed for monitoring individual workspaces.
Admins can assign organization-level roles and adjust workspace-specific permissions as needed, enabling tailored access for each user. This flexibility ensures enhanced security and operational efficiency.
### User management: simplified invitations
To ensure smooth setup, organization admins automatically gain admin rights across all workspaces. Workspace-level roles for developers or viewers must be assigned manually. Admins can invite users to the organization and assign roles during the invitation process.
**Clone workspaces: accelerate setup**
The new cloning feature simplifies creation of additional workspaces. Duplicate configurations, integrations, notification settings, and user roles to maintain consistency and save time.
### How does this benefit you?
* **Simplified Management:** Enjoy a holistic view of your organization's payment setups and operations, leading to smarter decision-making and greater adaptability.
* **Enhanced Security:** Clearly defined roles ensure team members can access only what they need, reducing risks while maintaining flexibility.
* **Efficient Onboarding and Configuration:** Reduce the time and complexity involved in setting up new workspaces, making it easier to expand your operations quickly and securely.
# User Management
Source: https://docs.payrails.com/docs/account-setup/user-management
Learn about user authentication, roles and permissions, and how to manage lifecycle of users.
## Role-Based Access Control (RBAC)
Role-Based Access Control (RBAC) is a robust security model that organizes access permissions based on user roles. In RBAC, users are assigned specific roles, each with well-defined permissions. Rather than assigning permissions directly to individual users, access is governed through roles. This streamlined approach simplifies administration, enhances overall security, and ensures that users only possess the access necessary for their designated roles. This minimizes the risk of unauthorized actions, contributing to improved system management.
There are two primary methods through which users interact with the Payrails system. Users can utilize the merchant portal for tasks such as configuring workflows or gaining insights into executions and reports. Alternatively, they can interact programmatically via API and SDK. In both scenarios, user roles are assigned to the individuals managing user accounts or to the machines involved in machine-to-machine communication.
## Best Practices
As part of our commitment to security and compliance with PCI DSS requirements, we provide the following guidelines to help customers create strong authentication credentials and protect their accounts from unauthorized access.
* **Least Privilege Principle:** Assign the minimum necessary permissions to users based on their roles. This reduces the risk of unauthorized access and potential security breaches.
* **Regular Audits:** Periodically review and update user roles and permissions to align with organizational changes. Remove unnecessary access for users who have changed roles or responsibilities.
* **Training:** Provide training sessions for users on the assigned roles and the associated permissions. This ensures that everyone is aware of the actions they can perform within the system.
### Guidance on Selecting Strong Authentication Factors
A strong password is critical for securing your account. When creating a password, follow these best practices:
* Use a minimum of 12 characters (longer is better).
* Include a mix of uppercase and lowercase letters, numbers, and special characters.
* Avoid dictionary words, common phrases, or easily guessable information (e.g., "password," "123456," "qwerty," your name, or birthdate).
* Do not use variations of personal information, such as reversed spellings or adding numbers to familiar words.
* Consider using passphrases (e.g., "Blue!Ocean&77\@Sunset") for enhanced security.
### How to Protect Your Authentication Factors
Your password is the key to your account. Take the following measures to keep it secure:
* Do not share your password with anyone, including colleagues, friends, or customer support personnel.
* Do not write down your password or store it in an easily accessible location.
* Use a password manager to securely store and generate unique passwords.
* Enable multi-factor authentication (MFA) whenever possible for added security.
### Avoid Reusing Previously Used Passwords
To prevent unauthorized access, never reuse passwords from other systems or previous credentials. Each account should have a unique password. This prevents attackers from using stolen passwords from one system to gain access to another.
### Changing Your Password if Compromised
If you suspect that your password has been compromised, take immediate action:
1. **Change your password immediately** using the password reset feature.
2. **Notify Payrails security team** at \[[security@payrails.com](mailto:security@payrails.com)] if you suspect unauthorized access.
3. **Monitor your account activity** for any suspicious transactions.
4. **Update any other accounts** that used the same password (if applicable).
By following these guidelines, you can help protect your account and ensure the security of your authentication credentials. If you have any questions, please contact our security team.
## FAQ
For answers to common questions about roles, passwords, and SSO access, see [Frequently asked questions](/docs/account-setup/user-management/frequently-asked-questions).
# Authentication
Source: https://docs.payrails.com/docs/account-setup/user-management/authentication
Learn more about possible alternative authentication methods.
## User authentication
The Payrails portal supports two user authentication methods:
1. Single Sign-On (SSO)
2. Non-SSO
Each user type has distinct characteristics regarding access and permissions within the portal.
### Single Sign-On (SSO)
SSO users access the portal through a centralized authentication system. Upon joining the portal, SSO users are assigned the default user role called "Viewer." This role is designed for limited access, allowing users to view only the Dashboard section within the merchant portal. Users requiring additional access must contact their administrator to obtain relevant permissions for the portal.
#### Activating SSO authentication for you Payrails portal
By default, SSO access is not activated for merchants within the Payrails portal. Merchants interested in using SSO functionality must contact their Payrails business partner. At the moment merchants portal supports OKTA as an SSO provider.
### Non-Single Sign-On (Non-SSO)
Non-SSO users access the portal through traditional authentication methods. Unlike SSO users, they do not receive automatic role assignments upon joining the portal. Instead, administrators manually assign roles and permissions based on the user's responsibilities and access requirements.
# Frequently Asked Questions
Source: https://docs.payrails.com/docs/account-setup/user-management/frequently-asked-questions
Learn answers to frequently asked questions.
To request additional permissions, reach out to your administrator or the designated role manager. They can assess your request, considering the principle of least privilege, and make necessary adjustments if required.
No, each account is assigned a single role to maintain clarity and adhere to the principle of least privilege. If your responsibilities change, contact the administrator to reassess and adjust your role accordingly.
If you suspect unauthorized access or notice any unusual activity, immediately report it to your administrator. They will investigate the issue and take appropriate measures to secure the system. Additionally, change your password immediately and ensure that multi-factor authentication (MFA) is enabled.
It is recommended to change your password periodically and immediately if you suspect it has been compromised. Avoid reusing previous passwords and use a password manager to generate and store secure passwords.
Yes, role changes take effect immediately upon assignment. However, it's recommended to log out and log back in to ensure the updated roles and permissions are applied consistently.
Roles and permissions should be audited regularly, at least quarterly, or whenever there are organizational changes. Regular audits help ensure that access levels align with current business requirements and reduce security risks.
No, the roles are predefined to maintain consistency and security. If you have specific access requirements, discuss them with your administrator, and they can assess whether adjustments are necessary within the existing role structure.
There is a known limitation with our portal which prevents direct connection from your SSO portal (for example, Okta) to our portal. To access the portal with your SSO login, you need to access via the direct portal URL instead (for example, `https://yourcompany.payrails.io/`).
# Roles & Permissions
Source: https://docs.payrails.com/docs/account-setup/user-management/roles-permissions
Find all assigned roles and permissions for each API service.
## User Roles
There are dedicated roles for [workspace](/docs/account-setup/workspaces) and [organization](/docs/account-setup/organization) context within the portal.
### Organization roles
* **Admin** - Can access and manage everything, including all workspaces and their aggregated data. Will be Admin in all workspaces.
* **Developer** - Can access workspaces they are added to and manage everything except users for the organization. Will have access to aggregated data from the workspaces they are added to.
* **Viewer** - Can access workspaces they are added to and view data related to the organization. Will have access to aggregated data from the workspaces they are added to.
* **None** - Can only access workspaces they are added to. Will not have access to the organization or an aggregated view.
### Workspace roles
* **Admin** - Can manage everything in the workspace, including users and access.
* **Operator** - Can view data and trigger actions such as refunds, but has no access to configurations.
* **Viewer** - Can only view data.
## Roles & Permissions Matrix
Visualize the roles and their corresponding permissions across various services with our comprehensive matrix. This tool illustrates the access levels of each role to specific actions within key service categories, including Auth, Ledger, Merchant, and Payment.
Below, we present individual Role & Permissions Matrices for each service:
### Portal
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| View organization context | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
| List all workspaces | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ |
| List assigned workspaces | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create a workspace | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Update a workspace | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
### User Management
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| List users | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| List external users | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| View user details | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| View user logged in info | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Export list of users | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Export list of external users | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Create users | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Assign role | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Assign/Unassign organization and workspace roles | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Assign/Unassign workspace roles | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Block a user | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Unblock a user | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Reset a user password | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Reset a user MFA | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Sync a user or all users | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| List roles | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| List organization and workspace roles | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| View role details | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
### Authentication
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| List clients | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| View client details | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Create a client | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Delete a client | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Rotate API secret | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| List API secret | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Create API secret | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
| List MTLS certificate | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Read a MTLS certificate | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Create a MTLS certificate | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Import a MTLS certificate | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Revoke a MTLS certificate | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
### Executions & Payments & Actions
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| List executions | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View execution details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Lookup | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Authorize an execution | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Cancel an execution | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Capture an execution | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Confirm an execution | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Create an execution | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Refund an execution | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| List payments | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View payment details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List actions | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View action details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
### Alerts
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| Update a field | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| List alert logs | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Evaluate an alert | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Update an alert | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| View alert configs details | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| List alert configs | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Create an alert config | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Update an alert config | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Delete an alert config | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
### Notifications
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| View notification config details | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| List notification configs | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Create a notification config | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Update a notification config | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Delete a notification config | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
### Merchant Configuration
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| List workflows | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
| View workflow details | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
| Create a workflow | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| List rulesets | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
| View ruleset details | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
| Create ruleset | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| Consumercheckout read config (SDK) | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
| Consumercheckout write config (SDK) | ✓ | ✗ | ✗ | ✓ | ✗ | ✗ |
| List frauds | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View fraud details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List holders | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
| View holder details | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ |
| Create Holder | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Read config (SDK) | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
| View field details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List fields | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create a field | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Read BINs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List all currencies from current payments | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List raw notifications sent by providers | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
### Integrations
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| List providers active | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List providers | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| View provider details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Update providers | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Proxy providers | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
| List provider configuration | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Get provider configuration by id | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create provider config | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
| Delete provider config | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Update provider config | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
| Assign provider config to workspaces | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
| List integrations | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
### Tokenization
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| Get instrument details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List Instruments | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create instrument | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Update instrument | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Delete instrument | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Tokenize instrument | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| List tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Get token details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create token | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Update token | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Delete token | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Vault read config | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| View and select vault in the portal | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| List API logs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Get API log details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
### Analytics
| **Action description** | **Org-Admin** | **Org-Developer** | **Org-Viewer** | **WS-Admin** | **WS-Operator** | **WS-Viewer** |
| - | - | - | - | - | - | - |
| Get dataset details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List datasets | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Get export details | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| List exports | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create an export | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| Read analytics | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
# Single Sign-On
Source: https://docs.payrails.com/docs/account-setup/user-management/single-sign-on
How to configure and maintain SAML SSO in Payrails using your organization's identity provider.
## Overview
Payrails supports Single Sign‑On (SSO) using SAML, allowing organizations to authenticate users with their existing identity provider (IdP). SSO is configured per environment (Staging and Production) in the Payrails Portal. This centralizes authentication, improves your security posture, and reduces operational overhead associated with managing separate credentials.
Supported IdPs include Okta, JumpCloud, Google Workspace, Microsoft Entra/Azure AD, and Auth0 (as an external IdP).
## Prerequisites
* Access to your IdP with permission to create or configure a SAML application.
* A test user in your IdP to validate end‑to‑end authentication before broad rollout.
* A SAML metadata URL provided by your IdP. File uploads/XML are not supported—URL only.
* Recommended reading to understand the basics: Okta’s Beginner's Guide to SAML
— [https://developer.okta.com/docs/concepts/saml/](https://developer.okta.com/docs/concepts/saml/)
## Configuration Steps
1. In the Payrails Portal, go to `Settings → Identity → SSO`.
2. From the provider dropdown, choose your IdP (Okta, JumpCloud, Google Workspace, Microsoft Entra/Azure AD, or Auth0). This helps the Portal tailor the guidance you see.
3. Copy the values shown by Payrails for your IdP setup:
* `Entity ID / Audience URI`
* `ACS (Assertion Consumer Service) URL`
4. In your IdP, create a new SAML application and set:
* Audience/Entity ID = Payrails `Entity ID / Audience URI`
* ACS/Callback URL = Payrails `ACS URL`
5. Back in Payrails, paste your IdP’s `SAML Metadata URL` into the configuration form (URL only).
6. Select `Save` to apply the configuration. The connection becomes active for that environment.
7. To rotate certificates/metadata later, press `Refresh` to pull the latest metadata from the same URL. Use `Edit configuration` only if the metadata URL itself changes.
Only one SSO configuration is supported per environment.
## Validation & Testing
Validate the configuration in Staging before enabling Production.
1. Assign a test user to the SAML application in your IdP.
2. Sign out of the Portal, then initiate sign‑in using SSO in your staging/non‑production environment.
3. Complete the IdP login; you should be redirected back to Payrails and signed in.
4. Confirm the user lands in the expected workspace(s) and can access permitted areas.
5. Assign the appropriate roles and permissions at the workspace or organization level so the user has the intended access.
6. If rotating certificates or metadata, use `Refresh` (as long as the metadata URL hasn’t changed). If the URL changed, open `Edit configuration`, update the URL, save, and re‑test.
## Troubleshooting
Use the following checks to diagnose common setup issues (no specific error messages referenced):
* Verify the `SAML Metadata URL` is reachable and current (after any cert rotation).
* Confirm the `Entity ID/Audience URI` and `ACS URL` exactly match the Payrails values (no whitespace or typos).
* Ensure the IdP NameID resolves to the user’s email used in Payrails.
* Check time synchronization (IdP and client clocks) to avoid assertion validity issues.
* Make sure the user is assigned to the SAML application in your IdP.
## FAQs & Edge Cases
Not currently. Only a single SSO configuration per environment is supported.
Disabling is not supported. You can remove the configuration. Removing SSO immediately stops new SSO logins for that environment.
SSO accounts are created as new identities. Username/password users can continue using their credentials unless your organization admins choose to disable those accounts.
Not supported.
## Related Topics
* Workspaces: [/docs/account-setup/workspaces](/docs/account-setup/workspaces)
* Organizations: [/docs/account-setup/organization](/docs/account-setup/organization)
* User Roles & Permissions: [/docs/account-setup/organization](/docs/account-setup/organization)
* Roles & Permissions (overview): \[roles-permissions)
# Workspaces
Source: https://docs.payrails.com/docs/account-setup/workspaces
Learn about workspaces which helps to create segregated and separately managed spaces within your organization.
## Introduction
Workspaces in Payrails are designed to help you effectively manage and organize your account. They provide a dedicated environment for configurations, payments, executions, and integrations. Workspaces improve team collaboration, while maintaining secure access control, ensuring that your operations run smoothly. Each Workspace is linked to your Payrails account [Organization](/docs/account-setup/organization).
### What are the benefits of using workspaces?
Workspaces are a great tool for ensuring Simplicity and Security.
#### Simplicity
* Each workspace has a dedicated payment acceptance workflow configuration. This allows you to setup different workflows for separate workspaces.
* They let you organize your payment management by business vertical, geographical location, organizational structure or any other logical separation you want to use.
* Segregate integration configurations for different workspaces to avoid managing all your integrations in one page.
* View and monitor insights on workflows (payments, executions) and analytics separately.
#### Security
* Manage people having access to the Payrails portal and specific workspaces with [Role-Based Access Control (RBAC)](/docs/account-setup/organization)
### How to set up workspaces?
The agnostic nature of workspaces allows you to let them represent your companies business setup or any other logical structure you choose. You can use them to structure your Payrails account by:
* **Country:** Each workspace represents a country. You can configure individual payment acceptance workflows for each country. (e.g. Germany, Brazil, UAE, etc.) This is the most used version.
* **Region:** Each workspace represents a geographic country. You can configure individual payment acceptance workflows for each region. (North America, South America, EU, DACH, Benelux, etc.)
* **Business verticals:** Each workspace represents a business vertical relevant to you. (e.g. Car Sharing, Taxi, Food Delivery) or currencies (EUR, USD, YEN)
* **Currency:** Each workspace represents a separate currency. (e.g. EUR, USD, YEN)
* **Payment methods:** Each workspace represents a payment method or provider. (Visa, Mastercard, Amex, etc.)
### How are payments linked to workspaces?
Copy the workspace ID from the Portal and add them to the execution API calls as described in the API reference: [Create an execution](/reference/createexecution)
### What parts of the Payrails portal are managed in the workspace context?
Within the Portal you'll find two logical contexts.
1. [Organization](/docs/account-setup/organization) - Your organization is your Payrails account. This is where global settings, configuration & users are managed. Data from all workspaces are aggregated in the organization context.
2. Workspace - Is always part of your organization. Data, settings and user management are restricted and confined within a given workspace.
Payments, executions and users can be managed on both contexts. But, only data relevant for a given context will be shown. Let's assume there is a workspace for Germany and France and the organization is called ACME GmbH.
Payments of Germany will not be shown in the France workspaces but are accessible in the organization context of ACME GmbH. More info on data separation and access can be found on the
[Organization](/docs/account-setup/organization) page.
### How to create a new workspace?
#### Prerequisites
Only Organization-Admins have the permission to create new workspaces. If you think you should have that role please reach out to your Organization-Admin to change your role accordingly.
#### Create a workspace
There are 3 ways to create a new workspace:
1. Portal:
1. Create a new blank workspace,
2. Create a new workspace by cloning an existing workspace and all associated settings and users.
2. API: Create a new blank workspace via API.
**Create an execution**
Once you have created your workspace, you can execute a workflow for the particular workspace by passing the workspace ID as a parameter using the following curl command:
```sh theme={null}
curl --request POST \
--url https://demo-api.staging.payrails.io/merchant/workflows/workflowCode/executions \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{ "workspaceId": "some-workspaceID-string" }'
```
For subsequent operations on the execution, such as refunding or canceling a payment, passing the workspace ID is not necessary because the workspace information will be inherited from the execution data.
However, if you want to programmatically create a new provider configuration, you can pass the workspace ID as a parameter to create it within the workspace. For reference, check the API documentation: [/reference/listactiveproviderconfigs](/reference/listactiveproviderconfigs).
For a detailed API guide to executions, check the following URL:\
[/reference/createexecution](/reference/createexecution).
**Note:** If no workspace ID parameter is passed, the execution will be performed in the default workspace, which is called "Default Workspace" in the portal.
### FAQ
No, at the moment, this is not possible, but it's part of the future development roadmap.
Yes, upon creation of a new workspace, you have the option to copy workflow configurations, notification configurations and integration instances from any other workspace
No. As long as you have access to the Portal, you can access a workspace within the Portal.
Yes, notifications (webhooks) can be defined per workspace, default, or other workspace.
# Dashboards
Source: https://docs.payrails.com/docs/analytics-and-reporting/dashboards
Filter a Payrails analytics dashboard, save the filter state as a custom view, and set a default view for each time you open it.
## Apply filters and save custom views
* Navigate to Analytics and choose the dashboard that aligns with your analytical needs.
* Apply filters available in the top bar.
* Click My Views and save the current filter state.
* You can set a saved view as the default for the dashboard. When you reopen the dashboard, the default saved view will automatically be applied. You can change the default to another saved view or remove the default at any time.
## Change granularity of date axis
* Navigate to Analytics and choose the dashboard that aligns with your analytical needs.
* Select desired granularity per dashboard or visualisation.
## Drill down or into a visualisation
Interact with the visualisations with the click of a mouse. Drills link elements in the visualisations to other visualisations, letting you browse your data seamlessly.
### Drill down
Drill down opens the same visualisation in a new dialog displaying it broken down to child attributes, e.g. week - day - hour - minute, or region - country.
* Navigate to Analytics and choose the dashboard that aligns with your analytical needs.
* Click on a data point on a visualisation which you want to drill-down.
* Select from the available drill down options.
### Drill into
Drill into visualisations opens a visualisation in a new dialog. This visualisation may contain the same metric as the original one, but it can slice that metric with a new attribute, while filtering for the attribute value selected in the original visualisation.
* Navigate to Analytics and choose the dashboard that aligns with your analytical needs.
* Click on a data point on a visualisation in which you want to drill.
* Select from the available drill into options.
## Export data
### Export individual visualisation data as XLSX or CSV
* Navigate to Analytics and choose the dashboard that aligns with your analytical needs.
* Hover your mouse over the visualisation that you want to export.
* Click the three dots in the top right corner.
* Click Export to XLSX or Export to CSV.
* Save the file.
### Export dashboard as pdf
* Navigate to Analytics and choose the dashboard that aligns with your analytical needs.
* Click the three dots icon in the top right corner.
* Click Export to PDF.
* Save the file.
**Coming soon**
# Getting started
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started
Connect your payment providers and data sources to start reporting in Payrails.
Use this section to configure data ingestion for each supported provider.
# Connect your data sources
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources
Add your payment providers as data sources in the Payrails Portal to power analytics, reporting, and fee monitoring.
To power Analytics & Reporting in Payrails (specifically as standalone product
and for fee monitoring), connect your payment providers as **data sources**.
With **self-serve data integrations**, you create and configure each
integration yourself in the Payrails portal - no manual setup or intervention
from Payrails teams is required. You can add a data source in the Payrails
Portal by following the setup guides below.
Each setup guide walks you through how to connect a provider as a data source in Payrails. It includes:
* Required prerequisites
* Provider-side steps to generate API credentials or gather SFTP details
* How to add and configure the integration in the Payrails portal (self-serve)
Use these guides when adding a new data source or reviewing an existing setup.
***
## How the self-serve flow works
After gathering the required credentials from your provider (see each provider guide), connect the data source in Payrails:
1. Log in to the **Payrails portal**.
2. Open **Settings → Integrations → Data**.
3. Click **Add instance** in the top-right corner of the Data integrations list.
4. On the **Select provider** screen, choose your provider and click **Continue**.
5. On the **Configure account** screen, enter the **Integration instance name**, the provider's **settings and credentials**, and *(optionally)* **Additional data**.
6. Click **Save account**.
The new instance appears in the **Data integrations** list with its status, and Payrails begins ingesting data automatically.
***
## Edit or disable an integration instance
You can manage an existing integration at any time from **Settings → Integrations → Data**:
* **Edit:** open the instance from the Data integrations list to update its name, settings, or credentials, then click **Save account**. Use this to rotate an API key, add a merchant account, or adjust the start date.
* **Disable / enable:** change the instance's status. Disabling stops Payrails from pulling new data for that instance without deleting its configuration — you can re-enable it again later.
## What to expect after enabling
* **First run:** once you save and enable an instance, it is picked up on the next scheduled pipeline run.
* **Freshness:** the pipeline pulls from each provider on a **daily** basis. Data is expected to be available in Payrails within **\~3 hours** of the pipeline run completing.
* **Where data lands:** ingested data feeds **Analytics & Reporting** — it appears in the reports and dashboards (Unified Analytics) for the configured workspace.
## Backfilling historical data
Self-serve backfilling is **not available yet**. If you need historical data
ingested (for a period before your start date), reach out to your **Payrails
account manager** to arrange it. Self-serve backfilling is in progress and
will be enabled over self-serve in a future release.
***
## Data integration setup guides
These providers are configured exclusively through the self-serve flow, with
new providers added regularly.
Adyen
Data integration
Amex
Data integration
Braintree
Data integration
Checkout.com
Data integration
dLocal
Data integration
HiPay
Data integration
J.P.Morgan
Data integration
Klarna
Data integration
MangoPay
Data integration
PayPal
Data integration
Payplug
Data integration
Stripe
Data integration
TAP
Data integration
Unlimit
Data integration
Vipps/MobilePay
Data integration
# Adyen
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/adyen
Generate Adyen API credentials and a report user, then connect Adyen as a data source for transaction and settlement data.
This guide will help you generate Adyen API credentials and connect Adyen as a data source in Payrails to ingest transaction & settlement data. With the self-serve flow you configure the integration yourself in the Payrails portal.
## Prerequisites
* An Adyen account.
* To be able to create API credentials, you must have one of the following user roles:
* **Merchant admin**
* **Manage API credentials**
***
## Step-by-step instructions
### Step 1: Log into your Adyen account to configure a set of automatic reports
1. Click the company name located in the top-left corner of the page, and choose the first merchant account from the dropdown menu. This step is crucial because these reports are configured per merchant account, allowing us to gather the most comprehensive details possible.
2. Click **Reports** in the left-side navigation panel.
3. Use the **search bar** to search for each of the following reports one at a time:
* **3D Secure authentication**
* **Aggregate settlement details**
* **Dispute transaction details**
* **Interchange and scheme fee report**
* **Payment accounting** (please select to generate **ALL** fields in the report)
* **Received payment details** (please select to generate **ALL** fields in the report)
* **Settlement details**
4. Click **Generate** beside each report, then click **Automated (generate on schedule)**. Turn **Automatic generation** on and select **.CSV** for the File type.
### Step 2: Create a Report User in Your Adyen Account
1. Log in to your Customer Area, and go to **Developers > API credentials**. This opens a list with all API credentials linked to your company account.
2. Select **Create new credential**.
3. Under **Credential type**, select **Report service user**. You can add a description for the credential here.
4. **Important:** Make sure you have provided the API key with the **Merchant Report Download role**.
5. Select **Create credential**.
6. To generate an API key select the API key tab under **Server settings > Authentication**. Select **Generate API key**, copy the API key using the copy icon and store your API key securely in your system.
### Step 3: Add the Adyen integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **Adyen** and click **Continue**.
4. On **Configure Adyen account**, fill in:
* **Integration instance name** — a unique label that helps you identify this account.
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the self-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **Is Live** — enable for a production (live) account; leave off for test.
* Input required for (at least) one of the below items:
* **Company account** — your Adyen company account name.
* **Merchant accounts** — add each merchant account you want to ingest data for (use **+** to add more).
* **API key** — the report service user API key from Step 2. (If you use Adyen for Platforms, also provide the Balance Platform report API key as instructed by the form.)
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# Amex
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/amex
Obtain American Express SFTP credentials and encryption keys, then connect Amex as a data source for settlement and payout data.
This guide will help you obtain SFTP credentials and encryption keys and connect American Express (AMEX) as a data source in Payrails to ingest settlement and payout data. With the self-serve flow you configure the integration yourself in the Payrails portal.
## Prerequisites
You will need:
* An active American Express Merchant Account
* **SFTP Access:** A dedicated SFTP username and password assigned to your merchant account
* **PGP Encryption:** A PGP key pair (you must hold the Private Key) for file decryption
## Step-by-step instructions
### Step 1: Gather SFTP Credentials and Connection Details
To configure the secure file transfer, you will need to gather the following information:
**1. SFTP host and port**
Gather the following credentials:
* **Host:** `fsgateway.americanexpress.com`
* **Username:** the AMEX SFTP username assigned to your merchant account (e.g. `PAYRAILS...`)
* **Password:** the password associated with this SFTP user
* **Port:** confirm standard port **22** is used
**PGP decryption**
* **PGP Private Key:** the full PEM-encoded private key block to decrypt the files
* **Passphrase:** *(optional)* if the private key is password-protected
**2. Verify data lifecycle & storage**
Before integrating, confirm the file lifecycle logic on your AMEX SFTP server to ensure consistent data retrieval and support for backfills:
* **Folder structure:** confirm that new reports land in the `/outbox/` directory and are automatically moved to `/sent/` after download.
* **Inbox retention:** verify how long files remain in `/outbox/` if not picked up (standard is typically 7 days).
* **Archive retention:** verify how long files persist in `/sent/` before permanent deletion (standard is typically 24 hours).
* **Backfill availability:** determine if older files are available in `/sent/` or a separate archive path for re-ingestion.
***
### Step 2: Add the Amex integration in the Payrails portal (self-serve)
1. Log in to the Payrails portal and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In **New integration instance**.
4. On **Select provider**, choose **American Express** and click **Continue**.
5. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label.
* **Start Date** — the date of today or tomorrow (currently backfilling data is not available over the self-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **SFTP host** — `fsgateway.americanexpress.com`
* **SFTP username** and **password**
* **Port** — `22`
* **PGP private key** and, if applicable, the **passphrase**
* *(Optional)* **Additional data**.
6. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# Braintree
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/braintree
Connect Braintree over two channels: the API for transaction and dispute data, and SFTP fee reports for detailed fee data.
This guide will help you connect Braintree settlement, payment, **and fee** data to your Payrails account. Payrails ingests Braintree data over two channels:
* **API** — transaction, payment, and dispute data (requires Braintree API credentials, below).
* **SFTP fee reports** — Braintree's **Disbursement Fee Report (DFR)** and **AIB Transaction Fee Report**, which carry detailed fee data the API does not expose. These require Braintree Dropzone (SFTP) credentials.
Providing both gives the most complete fee reporting in Payrails.
## Prerequisites
* A Braintree account with admin access.
* (For fee reports) A Braintree **Dropzone** provisioned for your account — the DFR is delivered for US/Fiserv-acquired accounts, the AIB report for EU-acquired accounts. If you're unsure whether your Dropzone is set up, ask your Braintree/Fiserv contact.
***
## Part A — API credentials
We recommend creating a limited-access set of API keys for Payrails reporting.
### Step 1: Log into the Braintree production environment as an admin
### Step 2: Create a new role
1. Follow the instructions in the Braintree documentation to create a new role.
2. Assign the following permissions to this new role under “Rights Granted“:
* Transactions
* Download Transactions with Masked Payment Data
* Reporting
* Create, Run, and Download Reports
* View Dashboard Graphs
* Read-only Access
* View Merchant Accounts
* View Payment Methods
* View Transactions
* View Verifications
* Download Files
* Statements
* View statements
* Search
* Search Transactions
* Search Verifications
### Step 3: Create a new user and generate API keys
1. Follow the Braintree documentation to create a new user, with the following configuration:
* Enable API Access.
* Assign the role created in Step 2.
* Select all merchant accounts for which you would like to send data.
2. Log in with this new user and store the following credentials:
* Public key
* Private key
### Step 4: Add the Braintree integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **Braintree** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label.
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the sel-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **Is Live / environment** — select production.
* **Public key** and **Private key** — from the reporting user created in Step 3.
* **Merchant accounts** — add each merchant account you want to ingest data for.
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
***
## Part B — SFTP fee-report credentials (DFR + AIB)
The DFR and AIB reports are delivered to a Braintree **Dropzone** over SFTP. To let Payrails collect them, share your Dropzone credentials.
### Step 1: Confirm your Dropzone is provisioned
Confirm with your Braintree/Fiserv contact that your account has a Dropzone delivering the Disbursement Fee Report (US/Fiserv) and/or the AIB Transaction Fee Report (EU). These reports are scheduled deliveries — no action is needed in the Braintree Control Panel.
### Step 2: Obtain your Dropzone (SFTP) username and password
Your Dropzone uses **password-based SFTP** at `dropzone.braintreepayments.com`. You only need the **username** and **password** (the host and port are standard). If you've previously connected with a tool like Cyberduck or FileZilla, these are the same credentials.
### Step 3: Share the API credentials with Payrails
1. Share your API credentials securely with Payrails — use a shared secrets manager/vault (1Password Shared Vaults, AWS Secrets Manager, GCP Secret Manager).
2. If a shared vault isn't possible, share them via a PGP-encrypted file.
***
## What Payrails ingests, and the fee detail you unlock
With the API credentials, Payrails ingests your transactions, payment-level fees, and disputes. Adding the Dropzone fee reports additionally surfaces detailed **fee categories** in your Payrails fee reporting and dashboards, including:
* **Scheme (card-network) fees**, including scheme fees charged on declined transactions.
* **Cross-border / international fees.**
* **EU acquiring fees** (from the AIB report) — often the only source of this detail for EU-acquired volume.
* **Chargeback fees.**
These complement the interchange and processing fees already available via the API, giving a complete view of your Braintree cost of payments in Payrails.
# Checkout
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/checkout
Create limited-access Checkout.com API keys, enable scheduled report downloads, and ingest settlement and payment data.
This guide will help you obtain your Checkout API credentials and connect Checkout.com as a data source in Payrails to ingest settlement and payment data. With the self-serve flow you configure the integration yourself in the Payrails portal.
## Prerequisites
A Checkout.com account & Developer or Admin permissions.
## Step-by-step instructions
We recommend that you create a limited access set of API keys for Payrails reporting.
### Step 1: Log into Checkout live environment
### Step 2: Create API keys
1. Follow the instructions in [Checkout.com's API authentication documentation](https://www.checkout.com/docs/business-operations/use-the-dashboard/developers#Manage_API_authentication) to create a set of secret API keys.
2. Use “Customize“ option to limit the scopes to the following: `reports` , `reports:view`, `financial-actions`, `financial-actions:view`, `payments:search`.
3. Under processing channels, select "Allow any processing channel"
4. After creating the key, validate its the correct by making sure the key type is "SECRET" and the key value starts with "sk\_".
5. Create API keys for each of the entities you would like Payrails to have access to.
### Step 3: Enable scheduled reports download
1. Select reports, go to Schedules and click on `Create schedule`
2. Select the following input and click on `Create`
1. Delivery Type = SFTP
2. Report Type: all reports, each report needs to be set up separately, so you have to redo Step 3 multiple times to enable all reports.
3. Time zone = UTC
4. Report details
1. Frequency = Daily
2. Schedule name: can stay as proposed from the system
3. customize included fields = Select all
### Step 4: Create credentials for each entity ID and save for Payrails portal setup
1. Select the entity on the left side of your company profile
2. Then go to the settings on the right side in the portal and click on `Account Details`
3. Find the entity ID in the form of `ent_xxx` and save it to the clipboard
4. Repeat steps 1 to 3 for each entity
### Step 5: Add the Checkout integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **Checkout.com** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label.
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the sel-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **API key** — the secret key (`sk_...`) created in Step 2. Add one instance per entity/key as needed.
* **Entity ID(s)** — the `ent_xxx` IDs gathered in Step 4.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# dLocal
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/dlocal
Enable the required dLocal reports and set up SFTP access to ingest payins and settlement data into Payrails.
This guide will help you enable the required dLocal reports and set up SFTP access and connect dLocal as a data source in Payrails to ingest payins and settlement data. With the self-serve flow you configure the integration yourself in the Payrails dashboard — you no longer share credentials with Payrails.
## Prerequisites
* An active dLocal merchant account
* A point of contact with your **dLocal Technical Account Manager (TAM)**
* SFTP access to dLocal's reporting server (set up with your TAM, see Step 2)
***
## Step-by-step instructions
### Step 1: Enable the required reports in dLocal
Ask your dLocal Technical Account Manager to enable the following **Payins reports** for your account. dLocal tailors reports to your processing model (cross-border or local-to-local), so enable the ones that match your setup:
* **Daily Payins — Cross-border Model** - daily breakdown of cleared transactions for the cross-border model (non-local legal entity setup).
* **Daily Payins — Local-to-local Model** - daily summary of cleared transactions for the local-to-local model (local legal entity setup).
* **Settlement — Cross-border Payins Model** - settlement breakdown of cleared transactions for cross-border operations.
* **Settlement — Local-to-local Payins Model** - settlement breakdown of cleared transactions for local-to-local operations.
These reports cover charges, settlements, fees, authorizations and failure reasons, which Payrails maps into its unified datasets for analytics and reconciliation.
### Step 2: Set up SFTP access with dLocal
dLocal delivers these reports over **SFTP**. Coordinate with your TAM to set up access:
1. Provide the following to your dLocal TAM:
* The **IP address(es)** Payrails will connect from, to be whitelisted on dLocal's SFTP server. *(Payrails provides these connection IPs - request them from your Payrails contact.)*
* The **public key** of an SSH key pair (must be OpenSSH-compatible; generate with `ssh-keygen -t rsa -b 4096` or PuTTYgen). Keep the matching **private key** - you will enter it in Payrails in Step 3.
2. Once your TAM confirms the SFTP setup is complete, they will share your SFTP connection details:
* **Host**
* **Port**
* **Username**
Backfilling historical dLocal reports is arranged through your TAM and can take **1–2 weeks** to provision. Self-serve backfilling is not available yet.
### Step 3: Add the dLocal integration in the Payrails dashboard (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In **New instance**, select the **dLocal**, then continue.
4. On the **Configure dLocal account** screen, fill in (all fields are required):
* **Integration instance name** - a unique name that helps you identify this account.
* **Start Date** - the date of today or tomorrow (currently we can not backfill over the sel-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* Under **SFTP Credentials**, enter the details shared by your dLocal TAM:
* **Host**
* **Port**
* **Username**
* **Private Key** — the private key matching the SSH public key you shared with dLocal in Step 2.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# HiPay
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/hipay
Request Console API credentials from your HiPay account manager, then add the HiPay integration in the Portal.
This guide explains how to enable Console API access in HiPay and connect HiPay as a data source in Payrails to ingest settlement, processing activity, payout, and dispute data. With the self-serve flow, you configure the integration yourself in the Payrails portal.
## Prerequisites
* An active HiPay merchant account
* A point of contact who can provision Console API access, such as your HiPay account manager
## Overview
Payrails ingests HiPay data through the Console API using a username and password login. HiPay exchanges this login for a short-lived access token on each request, so it does not store a static API key.
## Step-by-step instructions
### Step 1: Request Console API credentials from HiPay
1. Contact your HiPay account manager.
2. Request a Console API login with read access to the Transaction and Financial Report exports. Ask for these two values:
1. Username (typically an email address)
2. Password
3. Store the credentials securely after HiPay provisions them. Treat the password like any other login credential.
### Step 2: Add the HiPay integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance**.
3. In **New instance**, select **HiPay** as the provider and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name**: a unique label.
* **Start Date**: today. Payrails cannot backfill data through the self-serve flow. Contact your Payrails account manager if you need historical data.
* **Username**: your HiPay Console API username.
* **Password**: your HiPay Console API password. Payrails stores this value masked.
5. Click **Save account**. The instance appears in the Data integrations list, and Payrails begins ingesting data the next day.
# J.P. Morgan
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/jp-morgan
Gather J.P. Morgan SFTP credentials and confirm report delivery, then connect the integration to ingest daily reports.
This guide will help you enable the JP Morgan Reports integration and connect it as a data source in Payrails to ingest daily reports. With the self-serve flow you configure the integration yourself in the Payrails portal.
## Prerequisites
You will need:
* An active JP Morgan merchant account
* SFTP access: a dedicated SFTP username and password assigned to your account
* Report delivery enabled for the required report codes (see Step 2)
## Step-by-step instructions
### Step 1: Gather SFTP credentials and connection details
**1. SFTP host and port**
Gather the following:
* Hostname
* Username
* Password
* Port (default `22`)
* Root directory path (if applicable)
**2. Verify data lifecycle and storage**
Before integrating, confirm the file lifecycle logic on the JP Morgan SFTP server to ensure consistent data retrieval and support for backfills:
* **Folder structure:** confirm where new reports land (root directory or a specific subdirectory).
* **File format and naming:** Payrails expects files to follow JP Morgan’s standard naming convention `....dfr.zip`. Only daily files are parsed; monthly or yearly files are still transferred but skipped by the parser.
* **Compression:** files are expected to be compressed and password protected. The decryption password should match the SFTP password.
* **Backfill availability:** ensure historical files are accessible for backfill, especially if you need to ingest past data during onboarding.
### Step 2: Confirm report delivery
Confirm that the following reports are enabled on the JP Morgan side:
* `FIN0010` Deposit Activity Summary
* `FIN0011` Service Charge Detail
* `FIN0025` Financial Activity Summary
* `ACT0002` Exception Detail
* `ACT0010` Deposit Detail
* `ACT0036` Authorization Detail
For authorization reports, enable:
* Interchange Detail Option
* Debit Detail Option
### Step 3: Add the J.P. Morgan integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **JPMorgan** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label.
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the sel-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **SFTP host**, **username**, **password**, **port** (`22`), and **root directory path** (if applicable).
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# Klarna
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/klarna
Create API keys in the Klarna portal and connect Klarna as a data source for settlement and payout data.
This guide will help you obtain API credentials and connect Klarna as a data source in Payrails to ingest settlement and payout data. With the self-serve flow you configure the integration yourself in the Payrails portal.
## Prerequisites
* A Klarna account
## Step-by-step instructions
### Step 1: Create API keys
1. To create a set of API keys, simply navigate to **Settings > Klarna API Keys** in the Klarna portal and then click **Generate new Klarna API key**.
2. Download the API credentials as `.txt`and store securely.
### Step 2: Add the Klarna integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **Klarna** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label.
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the self-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **API credentials** — the username/password from the Klarna API key generated in Step 1.
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# MangoPay
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/mangopay
Create MangoPay API keys with the required scopes, then add them to the Portal to ingest settlement and payout data.
This guide will help you to obtain API credentials to ingest settlement and payout data from MangoPay into your Payrails account.
## Prerequisites
* A MangoPay account.
* **Role:** Administrator or Developer (required to view/generate API keys and configure permissions).
* Access to the MangoPay Dashboard.
* **Contact:** You must identify the MangoPay account admin to coordinate permissions and report scheduling.
## Environment Selection
When generating your API keys, ensure you are targeting the correct MangoPay environment. Payrails supports both:
* **Sandbox (Test):** `https://api.sandbox.mangopay.com`
* **Production (Live):** `https://api.mangopay.com`
> Note: MangoPay uses separate credentials for each environment. You can configure both in Payrails if needed.
## Step-by-step instructions
### Step 1: Create API keys and Configure Scopes
To create a set of API keys, log in to your MangoPay Dashboard and navigate to the **API** or **Developer** section.
**1. Gather Credentials**
* **Client ID:** Locate your unique MangoPay Client ID.
* **API Key:** Generate a new API Key (often referred to as the "Passphrase" or "Client Password" in some MangoPay contexts).
* **Sandbox Access:** If you have a sandbox account, repeat this process to gather the test credentials for initial integration testing.
**2. Configure Permissions (Scopes)**
Ensure your API key is configured with the following specific scopes to allow Payrails to ingest data:
* **Clients:** `READ` and `WRITE` permissions.
* **Reports:** `Reports` scope (Required for both POST and GET calls to the reports endpoint).
* **Disputes:** `READ` permission on the `Disputes.General` scope (Required if dispute processing is needed).
**3. Verify Reporting Requirements**
* **Data Range:** Confirm availability of historical data (typically 24 to 36 months).
***
### Step 2: Add the credentials to the portal
With the self-serve flow you configure the integration yourself in the Payrails portal. There's no need to share credentials over a vault or PGP-encrypted file.
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **MangoPay** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Account Name:** a unique internal identifier for this instance (e.g. `mangopay_primary`).
* **Account Display Name:** a human-readable label shown in the Payrails dashboard (e.g. `Mangopay Primary`).
* **Start Date:** the date of today or tomorrow (currently we can not backfill over the self-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **Client ID:** the MangoPay Client ID gathered in Step 1.
* **API Key:** the MangoPay API Key (referred as Passphrase/Client Password) gathered in Step 1. This value is masked and stored securely once saved.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# PayPal
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/paypal
Connect PayPal through the REST API, SFTP, or both, to ingest transaction, settlement, dispute, and fee data.
This guide will help you obtain your PayPal credentials and connect PayPal as a data source in Payrails to ingest transaction, settlement, dispute and fee data. With the self-serve flow you configure the integration yourself in the Payrails portal.
PayPal offers two integration types, and you can add either or both depending on which reports you need:
* **PayPal API** — ingests transaction data via PayPal's Transaction Search API.
* **PayPal SFTP** — ingests the full report set (transaction details, settlement, disputes/chargebacks, fees, PayPal advantage) via PayPal's reporting SFTP server.
***
## PayPal API
### Prerequisites
* An active PayPal business account.
* Access to the PayPal Developer Dashboard.
### Step-by-step instructions
#### Step 1: Create a REST API app in the PayPal Developer Dashboard
1. Log in to the PayPal Developer Dashboard and go to **Apps & Credentials**.
2. Select **Create App**, give it a name (e.g. `Payrails Reporting`), and click **Create App**.
3. On the app details page, copy and store the **Client ID** and **Client Secret**.
4. Use the **Sandbox / Live** toggle to generate credentials for each environment you need. Create a separate set of credentials for Live (production) and, if needed, Sandbox.
#### Step 2: Add the PayPal API integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **PayPal (API)** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label.
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the self-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **Client ID** and **Client Secret** — from the app created in Step 1.
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
***
## PayPal SFTP
### Prerequisites
* An active PayPal business account.
* Ability to contact PayPal Customer Service to enable report access (this is not self-service on PayPal's side).
### Step-by-step instructions
#### Step 1: Request SFTP report access from PayPal
1. Contact PayPal Customer Service (or your PayPal account manager) and ask to have your account enabled for **report access**. This is required before an SFTP user can be created.
2. Allow up to 48 hours for PayPal to enable this. PayPal sends an email to your account's primary address once it's ready.
#### Step 2: Create an SFTP user
1. Log in to your PayPal account and go to **Account Settings → Reports → Secure FTP Users**.
2. Click **Add** to create a new SFTP user, and verify your identity when prompted.
3. Choose and store a **username** and **password** for the SFTP account.
4. Note the connection details:
* **Host**: `reports.paypal.com`
* **Port**: `22` (default)
PayPal provisions SFTP access **per region**. If you operate in multiple regions, repeat these steps for each region and create one Payrails integration instance per region.
#### Step 3: Add the PayPal SFTP integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **PayPal (SFTP)** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label (mention the region if adding more than one).
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the self-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **SFTP Host**, **SFTP Username**, **SFTP Password** — from the SFTP user created in Step 2.
* **SFTP Port** — leave blank to use the default (`22`).
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# Payplug
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/payplug
Request Technical account API credentials from your Payplug account manager, then add the Payplug integration in the Portal.
This guide explains how to obtain Payplug **Technical account** API credentials and connect Payplug as a data source in Payrails to ingest settlement, processing activity, payout, and dispute data. With the self-serve flow, you configure the integration yourself in the Payrails portal.
## Prerequisites
* An active Payplug merchant account
* A point of contact who can provision **Technical account** API access, such as your Payplug account manager
## Overview
Payrails ingests Payplug data through the **Report API** with **Technical account** credentials. These OAuth2 client credentials provide read-only access to your Payplug reports. A Payplug account spans every processing account (ledger) you hold, so one credential pair covers all your currencies and processing accounts.
## Step-by-step instructions
### Step 1: Request Technical account API credentials from Payplug
1. Contact your Payplug account manager.
2. Request a **Technical account** with read-only **Report API** access. Ask for these three values:
1. Client ID
2. Client Secret
3. Scope (your account's realm path)
3. Store the credentials securely after Payplug provisions them. Treat the Client Secret like a password, since you typically cannot retrieve it again later.
### Step 2: Add the Payplug integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance**.
3. In **New instance**, select **Payplug** as the provider and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name**: a unique label.
* **Start Date**: today. Payrails cannot backfill data through the self-serve flow. Contact your Payrails account manager if you need historical data.
* **Client ID**: your Payplug **Technical account** Client ID.
* **Client Secret**: your Payplug **Technical account** Client Secret. Payrails stores this value masked.
* **Scope**: your Payplug **Technical account** realm path.
5. Click **Save account**. The instance appears in the Data integrations list, and Payrails begins ingesting data the next day.
# Stripe
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/stripe
Generate Stripe live and sandbox keys, then connect Stripe as a data source for financial reports and transaction data.
This guide will help you generate the necessary API credentials and connect Stripe as a data source in Payrails to ingest financial reports and transaction data. With the self-serve flow you configure the integration yourself in the Payrails portal.
## Prerequisites
* A Stripe account with Administrator or Developer permissions
* Access to the [Stripe Dashboard](https://dashboard.stripe.com/)
## Overview
To ensure we can ingest data for both your production and testing environments, you will need to generate two separate API keys:
* One for **Live Mode** (production)
* One for **Test Mode** (sandbox)
We recommend using **Restricted API Keys** for both. This follows security best practices by limiting our access strictly to the reporting data we need.
***
## Step-by-step instructions
### Step 1: Generate your Live Mode Key
1. Log into your **Stripe Dashboard**
2. Ensure the **Test mode** toggle (usually in the top-right or top-left corner) is **OFF**
3. Navigate to **Developers** > **API keys**
4. Under the "Restricted keys" section, click **Create restricted key**
* *Note: Do not use the "Secret key" unless specifically required*
5. Name the key: `Payrails Live Reporting`
6. Under the **Permissions** section, configure the following access:
* **Core - Read:**
* **Balance** (balance\_read) — Required to retrieve the account balance snapshot
* **Balance Transaction Sources** (balance\_transaction\_source\_read) — Required to retrieve the balance transaction ledger that drives settlement and fee reporting
* **Charges and Refunds** (charge\_read) — Required to retrieve charge and refund objects so transaction-level fees resolve back to their originating payment
* **Customers** (customer\_read) — Required to retrieve Customer objects referenced by payments
* **Events** (event\_read) — Required for freshness and drift detection across Stripe state changes
* **Files** (file\_read) — Required to download the generated report files
* **Payment Disputes** (dispute\_read) — Required to enrich chargeback reporting with dispute reasons, evidence due dates, and timeline
* **Payment Intents** (payment\_intent\_read) — Required to retrieve PaymentIntent objects and link payments to Payrails metadata
* **Payouts** (payout\_read) — Required for payout-level reconciliation: arrival date, status, failure reason, destination
* **Setup Intents** (setup\_intent\_read) — Required to retrieve SetupIntent objects for stored-card flows
* **Reporting - Write:**
* **Financial Reports** (report\_runs\_and\_report\_types\_write) — Required to generate new reports on your behalf
7. Click **Create key**
8. Complete the 2FA verification if prompted
9. **Copy the key** (it will start with `rk_live_...`) and store it securely. You will not be able to view it again
### Step 2: Generate your Sandbox (Test Mode) Key
1. In the Stripe Dashboard, switch the **Test mode** toggle to **ON**
2. Navigate to **Developers** > **API keys**
3. Under the "Restricted keys" section, click **Create restricted key**
4. Name the key: `Payrails Sandbox Reporting`
5. Configure the **same permissions** as the Live key (see above)
6. Click **Create key**
7. **Copy the key** (it will start with `rk_test_...`) and store it securely
### Step 3: Add the Stripe integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **Stripe** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name** — a unique label.
* **Start Date** — the date of today or tomorrow (currently we can not backfill over the sel-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **Is Live** — enable for the Live key; leave off for the Test/Sandbox key.
* **API key** — the restricted key: `rk_live_...` for production, `rk_test_...` for sandbox. Create a separate instance for each environment.
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# Tap
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/tap
Request read-only API keys from your Tap account manager for both environments, then add the Tap integration in the Portal.
## Prerequisites
* An active TAP merchant account
* A point of contact with your TAP account manager
## Overview
TAP does not currently offer self-service API key creation through their merchant portal. To get an API key, you need to **reach out to your TAP account manager** and request **read-only API keys** for the environments below.
To ensure we can ingest data for both your production and testing environments, please request two separate keys:
* One for **Live Mode** (production): key starts with `sk_live_...`
* One for **Sandbox / Test Mode**: key starts with `sk_test_...`
**Note on sandbox payouts**
TAP's sandbox environment does not support payouts. We will still accept a sandbox key for testing the charges, authorizations and refunds flows, but no payout data will be ingested for the sandbox environment.
## Step-by-step instructions
### Step 1: Request your API keys from TAP
1. Contact your TAP account manager
2. Request **two read-only API keys** scoped for reporting access:
* One Live Mode key (`sk_live_...`) suggested label: `Payrails Live Reporting`
* One Sandbox / Test Mode key (`sk_test_...`) suggested label: `Payrails Sandbox Reporting`
3. Once TAP has provisioned the keys, store them securely they typically cannot be retrieved again later
### Step 2: Add the TAP integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **TAP** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name**: a unique label (e.g. clearly mark Live vs Sandbox).
* **Start Date**: the date of today or tomorrow (currently we can not backfill over the sel-serve capability. In the need of backfilling, please reach out to your Payrails account manager).
* **Is Live**: enable for the Live key; leave off for the Sandbox key.
* **API key**: `sk_live_...` for production, `sk_test_...` for sandbox. Create a separate instance for each environment.
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data.
# Unlimit
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/unlimit
Use your Unlimit terminal code and password to connect Unlimit settlement reports as a data source in the Payrails Portal.
This guide explains how to connect Unlimit as a data source in Payrails. Payrails pulls your settlement reports from the Unlimit Reports API every day, and you configure the integration yourself in the Payrails portal.
## Prerequisites
You need:
* An active Unlimit merchant account with at least one live wallet
* The **terminal code** and **password** of the terminal you process payments with. The terminal code is the **Wallet ID** shown in the Unlimit Merchant Account dashboard, and the password is under that wallet's **Show credentials**
* The **Reports API** enabled on that terminal (see Step 2)
If Payrails already processes payments for you through Unlimit, the terminal
code and password are the same ones stored in your Unlimit payment
integration. No new credential is required.
## Overview
Unlimit publishes one settlement report per wallet and settlement date. A single terminal code gives access to the reports of every wallet under your Unlimit client, so one integration instance covers your whole account. Payrails ingests both the **regular settlement** report and the **interchange++ (IC++) settlement** report.
## Step-by-step instructions
### Step 1: Locate your terminal code and password
1. Log in to the Unlimit Merchant Account dashboard: [production](https://unlimit.com/ma/#/dashboard/home) or [sandbox](https://sandbox.unlimit.com/ma-new/#/dashboard).
2. Open the wallet you process payments with and note its **Wallet ID**. This is the terminal code.
3. Go to **Settings → Wallet settings → Show credentials** on that wallet to view the terminal **password**.
### Step 2: Confirm the Reports API is enabled
The Reports API is a separate permission on the terminal, enabled by Unlimit. If it's missing, the first report request fails with `403 Missing required grant`. Ask your Unlimit account manager to enable **Reports API access** for your terminal before you continue, or if Payrails reports the error after setup.
### Step 3: Add the Unlimit integration in the Payrails portal (self-serve)
1. Log in to the Payrails portal and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In New instance, select provider: **Unlimit** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name**: a unique label.
* **Start Date**: the date from which Payrails fetches reports. Use today's date. For earlier data, contact your Payrails account manager.
* **API host**: the Unlimit environment your terminal belongs to: `https://cardpay.com` for the global platform, `https://psp.in.unlimit.com` for the India platform. Use the matching `sandbox.` host for a test terminal.
* **Terminal Code**: the Wallet ID from Step 1.
* **Password**: the terminal password from Step 1.
* *(Optional)* **Additional data**.
5. Click **Save account**. The instance appears in the Data integrations list and Payrails begins ingesting data on the next daily run.
## Data availability
Unlimit publishes IC++ settlement reports weekly, on Wednesdays. Each report covers transactions from Monday to Sunday of the previous week. Payrails loads the report the next day, on Thursday, and the dashboards show it from Friday.
A transaction therefore appears on the dashboards 5 to 11 days after it happens, depending on the day of the week. For example, a transaction on Sunday 27 September appears from Friday 2 October, while one on Monday 28 September appears from Friday 9 October.
Regular settlement reports are built every Friday and cover the previous seven days.
# Vipps/MobilePay
Source: https://docs.payrails.com/docs/analytics-and-reporting/getting-started/connect-your-data-sources/vippsmobilepay
Request accounting partner API credentials from your Vipps/MobilePay account manager, then add the integration in the Portal.
This guide explains how to obtain your Vipps/MobilePay **accounting partner** API credentials and connect Vipps/MobilePay as a data source in Payrails to ingest funds, fees, and payout data. With the self-serve flow, you configure the integration yourself in the Payrails portal.
## Prerequisites
* An active Vipps/MobilePay merchant account
* A point of contact who can provision **accounting partner** API access, such as your Vipps/MobilePay account manager or **accounting partner** administrator
## Overview
Payrails ingests Vipps/MobilePay data through the **Report API** using **accounting partner** credentials. Do not use the merchant sales unit API keys used for taking payments. **Accounting partner** credentials use OAuth2 client credentials and provide read-only access to funds and fee reports across all of your merchant's ledgers, so you need one credential pair regardless of the number of currencies or ledgers you operate.
## Step-by-step instructions
### Step 1: Request accounting partner API credentials from Vipps/MobilePay
1. Contact your Vipps/MobilePay account manager or accounting partner administrator.
2. Request an **accounting partner** Client ID and Client Secret with read-only **Report API** access for funds and fee reports.
3. Store the credentials securely after Vipps/MobilePay provisions them. Treat the Client Secret like a password, since you typically cannot retrieve it again later.
### Step 2: Add the Vipps/MobilePay integration in the Payrails portal (self-serve)
1. Log in to the Payrails dashboard and open **Settings → Integrations → Data**.
2. Click **Add instance** (top-right).
3. In **New instance**, select provider: **Vipps/MobilePay** and click **Continue**.
4. On the **Configure account** screen, fill in:
* **Integration instance name**: a unique label.
* **Start Date**: the date of today or tomorrow. Payrails cannot backfill data through the self-serve flow. Contact your Payrails account manager if you need historical data.
* **Client ID**: your Vipps/MobilePay **accounting partner** Client ID.
* **Client Secret**: your Vipps/MobilePay **accounting partner** Client Secret. Payrails stores this value masked.
5. Click **Save account**. The instance appears in the Data integrations list, and Payrails begins ingesting data.
# Reports
Source: https://docs.payrails.com/docs/analytics-and-reporting/reports
Export financial and performance data from Payrails as CSV files, either through the Portal or via API.
Payrails Reports let you export financial and operational data as CSV files for reconciliation, accounting, and business analysis. Reports are generated asynchronously — you request a report run, and once it completes, you download the file via a pre-signed URL. You can generate reports through the Payrails Portal or programmatically via the API.
## Available report types
| Report code | Description | Schema |
| - | - | - |
| `payments` | Gain deep insights into your authorization funnel and processing efficiency with a unified breakdown of payment statuses, failure results, and card-level metadata.. | [View schema](/docs/analytics-and-reporting/reports/payments) |
| `transactions` | This report provides details on all transactions which impact your account balance.. | [View schema](/docs/analytics-and-reporting/reports/transactions) |
| `settlements` | This report provides a comprehensive breakdown of all transaction-level movements and associated costs that impact your merchant settlement balance.. | [View schema](/docs/analytics-and-reporting/reports/settlements) |
| `disputes` | Chargeback and dispute data, linked back to the original disputed transaction where it was processed through Payrails. | [View schema](/docs/analytics-and-reporting/reports/disputes) |
You can list all reports available to your organization using the [Search & list reports](/reference/getreports) endpoint.
***
## Download reports via API
Generating a report is an asynchronous process. Follow these steps:
### Step 1: Get an access token
Authenticate using your API credentials to obtain a bearer token. See [Authentication](/reference/getoauthtoken) for details.
### Step 2: Create a report run
Submit a `POST` request to `/analytics/reportRuns` with your desired report type and date range.
When requesting `settlements` reports, please do not specify any Workspace Id as `settlements` report is only available on organization level.
```json theme={null}
POST /analytics/reportRuns
Authorization: Bearer
{
"reportCode": "payments",
"parameters": {
"intervalStart": "2026-04-01",
"intervalEnd": "2026-04-07",
"timezone": "Etc/UTC"
}
}
```
The `workspaceId` field is optional. Include it only if you want to scope the report to a specific workspace. Omitting it returns data across all workspaces your credentials have access to.
**Parameters**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `reportCode` | string | ✓ | One of `payments`, `transactions`, `settlements`, or `disputes` |
| `parameters.intervalStart` | string | ✓ | Start date in `YYYY-MM-DD` format (inclusive) |
| `parameters.intervalEnd` | string | ✓ | End date in `YYYY-MM-DD` format (inclusive) |
| `parameters.timezone` | string | | TZ database name (e.g. `Europe/Berlin`). Defaults to `UTC` if omitted. See [IANA Time Zone Database](https://www.iana.org/time-zones) for valid values. |
| `workspaceId` | uuid | | Scope the report to a specific workspace |
The API responds with `202 Accepted` and a report run object in `pending` status:
```json theme={null}
{
"id": "cadb0015-3c26-4d7a-a9d3-2f8768b6d45a",
"createdAt": "2026-04-08T10:20:00.000Z",
"report": {
"code": "payments",
"name": "Payments"
},
"parameters": {
"intervalStart": "2026-04-01",
"intervalEnd": "2026-04-07",
"timezone": "Etc/UTC"
},
"status": "pending"
}
```
### Step 3: Poll for completion
Report generation runs asynchronously. Poll `GET /analytics/reportRuns/{id}` using the `id` from the previous response until `status` changes from `pending` to `successful`.
```json theme={null}
GET /analytics/reportRuns/cadb0015-3c26-4d7a-a9d3-2f8768b6d45a
Authorization: Bearer
```
Most report runs complete within 2–3 minutes. We recommend polling every 5–10 seconds.
Once complete, the response includes a `result` object with a pre-signed download URL:
```json theme={null}
{
"id": "cadb0015-3c26-4d7a-a9d3-2f8768b6d45a",
"status": "successful",
"result": {
"fileName": "payments_2026-04-01_2026-04-07",
"fileType": "csv",
"url": "https://payrails-reports.s3.amazonaws.com/payments_2026-04-01_2026-04-07.csv",
"expiresAt": "2026-04-09T10:20:00.000Z"
},
"completedAt": "2026-04-08T10:21:00.000Z"
}
```
### Step 4: Download the CSV
Execute a `GET` request on `result.url` to download the file. This URL is pre-signed and expires at `result.expiresAt` — make sure to download the file before it expires.
***
## Download reports via Portal
1. Log in to the [Payrails Portal](https://portal.payrails.com).
2. Navigate to **Reports**.
3. Select the report type and date range.
4. Click **Download** to export a CSV file.
***
## Limitations
* **File format**: Reports are only available as CSV files.
* **File size**: Reports are capped at 5 GB per file. If your date range produces a report that exceeds this limit, the report run will fail. Reduce the date range and re-run the report.
* **File expiry**: The pre-signed download URL expires after a fixed TTL. Download the file before `result.expiresAt`. If it has expired, calling the `GET /analytics/reportRuns/{id}`endpoint will generate a new pre-signed download URL.
* **No webhook or push notification**: There is currently no callback or event mechanism to notify you when a report run is ready. Polling `GET /analytics/reportRuns/{id}` is the only way to check completion status. Build your integration to poll actively and handle both `Successful` and `Failed` terminal states.
* **Report availability**: The report types available to your organization depend on your Payrails module configuration. Use the [Search & list reports](/reference/getreports) endpoint to confirm which report codes are enabled for your account.
***
## Error handling
Report runs can complete with a `Failed` status. Your integration should handle this explicitly. Do not assume a run will always succeed.
When a run fails, the response includes an `error` field with a code describing the reason.
```json theme={null}
{
"id": "3c052d86-7f1d-44e2-b1b2-d331d492ebde",
"status": "Failed",
"error": "FILE_SIZE_EXCEEDED"
}
```
| Error code | Description | Resolution |
| - | - | - |
| `NO_RECORD_FOUND` | No data was found for the requested date range and parameters. | Verify that your `intervalStart` and `intervalEnd` are correct and that data exists for this period. |
| `FILE_SIZE_EXCEEDED` | The report output exceeds the 5 GB file size limit. | Reduce the date range and re-run the report. For high-volume exports, consider splitting into weekly or bi-weekly batches. |
| `REPORT_UPLOAD_ERROR` | The report was generated but failed to upload. | Retry the report run. If the issue persists, contact Payrails support. |
| `REPORT_GENERATION_ERROR` | An internal error occurred during report generation. | Retry the report run. If the issue persists, contact Payrails support. |
***
## FAQ
Most report runs complete within 2–3 minutes. Polling every 5–10 seconds is
recommended.
Not currently. There is no webhook or callback mechanism available — polling
is the only way to check on a report run. Send a `GET` request to
`/analytics/reportRuns/{id}` at a regular interval (every 5–10 seconds is
recommended) until the status is `Successful` or `Failed`. Make sure your
integration handles both terminal states explicitly.
The report run will fail with an error. Reduce the date range in your request
and re-run the report. If you regularly need to export large volumes of data,
consider breaking your date range into smaller weekly or bi-weekly batches.
Use a [TZ database name](https://www.iana.org/time-zones) such as
`Europe/Berlin` or `America/New_York`. If you omit the `timezone` parameter,
it defaults to `UTC`. Choosing the right timezone ensures that day boundaries
in the report align with your local business hours.
Yes. If it has expired, calling the `GET /analytics/reportRuns/{id}`endpoint
will generate a new pre-signed download URL.
Yes. Pass the `workspaceId` field in the request body to filter results to a
specific workspace. Omit it to include data across all workspaces your
credentials have access to.
The endpoint returns the most recent report runs in descending order by
`createdAt`, with a default page size of 10. You can paginate using
`page[after]`, `page[before]`, and `page[size]`, and filter by
`filters[status]`, `filters[id]`, and `filters[workspaceId]`.
Report availability is tied to which Payrails modules are configured for your
account. Contact your Payrails account manager if you need access to a report
type that is not listed.
Each report includes three reference fields you can use to link records:
| Field | Description |
| - | - |
| `payrails_payment_id` | The ID generated by Payrails. Use this to link records across the Payments, Transactions, and Settlements reports. |
| `merchant_reference` | Your own reference for the record. Use this to link report data back to records in your internal systems. |
| `provider_reference` | The payment service provider's reference for the record. |
# Disputes report
Source: https://docs.payrails.com/docs/analytics-and-reporting/reports/disputes
Details on all customer-disputed transactions, including chargeback status, reason codes, and the linked original transaction.
The Disputes report only returns data for merchants with the Chargebacks (disputes) module enabled. If the module isn't enabled, the report still generates successfully but is empty.
## Rows fall into two categories
* **Payrails-orchestrated disputes** — the original payment was processed through Payrails. These rows have `ORIG_TRANSACTION_ID`, `ORIG_TRANSACTION_DATE`, `ORIG_TRANSACTION_AMT`, `ORIG_TRANSACTION_CURR`, `PAYMENT_METHOD`, `ISSUER_NAME`, `BIN`, and `ISSUER_COUNTRY_CODE` populated.
* **External disputes** — raised directly at the PSP, outside Payrails orchestration. The fields above are blank, since that information isn't available in the dispute event.
## Sample report
(CSV file).
## Columns
| Column | Type | Description |
| - | - | - |
| CREATED\_AT | TIMESTAMP | Dispute creation timestamp. |
| DEFENSE\_PERIOD\_EXPIRES\_AT | TIMESTAMP | Final deadline by which the dispute must be responded to or defended. |
| DEFENSE\_STATUS | STRING | Most recent defense status. One of: `NotDefendable`, `Undefended`, `ReadyToSubmit`, `Submitted`, `UnderReview`, `Won`, `Lost`, `Accepted`, `Resolved`, `Unknown`. |
| PROVIDER\_REFERENCE | STRING | The payment provider's unique identifier for this dispute record. |
| AMOUNT | NUMERIC | Disputed amount, in the dispute currency. |
| CURRENCY | STRING | Currency of the disputed amount (e.g. `EUR`, `USD`, `GBP`). |
| PROVIDER\_NAME | STRING | Payment provider (PSP) handling the dispute, per your provider configuration (e.g. `stripedispute`). |
| ISSUER\_NAME | STRING | Name of the card-issuing bank, from the BIN lookup on the original payment's instrument. Blank for external disputes or non-card payments. |
| PAYMENT\_METHOD | STRING | Payment method used on the original transaction (e.g. `card`, `applePay`). Blank for external disputes. |
| REASON\_CODE | STRING | Reason code assigned by the issuing bank, per payment network rules (e.g. `10.4`, `4853`). |
| REASON\_DESCRIPTION | STRING | Human-readable description of the dispute reason. |
| ASSIGNED\_USER\_ID | STRING | ID of the internal operator assigned to handle this dispute. |
| ORIG\_TRANSACTION\_ID | UUID | Payrails payment ID of the original disputed transaction. Blank for external disputes. |
| ORIG\_TRANSACTION\_DATE | TIMESTAMP | Creation timestamp of the original payment. Blank for external disputes. |
| ORIG\_TRANSACTION\_AMT | NUMERIC | Amount of the original payment, in its processing currency. Blank for external disputes. |
| ORIG\_TRANSACTION\_CURR | STRING | Processing currency of the original payment. Blank for external disputes. |
| WORKSPACE\_ID | UUID | Workspace identifier. |
| MERCHANT\_ID | STRING | Merchant identifier. |
| DISPUTE\_ID | UUID | Payrails' internal identifier for the dispute. |
| MERCHANT\_REFERENCE | STRING | Your own reference for the payment, from the dispute event or, if absent, the original payment. |
| CHARGEBACK\_TYPE | STRING | Dispute stage as reported by the provider. One of: `Created`, `FraudReport`, `Retrieval`, `Chargeback`, `PreArbitration`, `Arbitration`, `Unknown`. |
| CHARGEBACK\_STATUS | STRING | Chargeback status as reported by the provider. One of: `Incoming`, `Executed`, `Reversed`, `SecondChargeback`, `IssuerReponseTimeframeExpired`, `Unknown`. |
| SCHEME\_REASON\_CODE | STRING | Scheme-specific reason code, when provided by the PSP. |
| LIABILITY\_SHIFTED | BOOLEAN | Whether 3DS liability shift applied to this dispute. |
| BIN | STRING | Bank Identification Number of the card used in the original payment. Blank for external disputes or non-card payments. |
| ISSUER\_COUNTRY\_CODE | STRING | ISO country code of the card-issuing bank. |
| SHOPPER\_COUNTRY\_CODE | STRING | ISO country code of the shopper, when sent on the original payment. |
| ARN | STRING | Acquirer Reference Number, once assigned by the acquirer. |
# Payments report
Source: https://docs.payrails.com/docs/analytics-and-reporting/reports/payments
A standardized view of authorization health across providers, covering success rates, failure reasons, 3D Secure status, and tokens.
This report provides a standardized view of authorization health and transaction outcomes across your entire processing portfolio. By aggregating fragmented provider data, it offers granular visibility into authorization success rates, failure reasons, and technical payment attributes like 3DS status and token types.
## Sample report
(CSV file).
## Columns
| Column | Type | Description |
| :- | :- | :- |
| created\_at | TIMESTAMP\_NTZ | Date when Payment has been created. |
| updated\_at | TIMESTAMP\_NTZ | Date when latest payment event has happened. |
| timezone | VARCHAR | Timezone in which the session happened. |
| merchant\_id | VARCHAR | Unique identifier of the merchant. |
| workspace\_id | VARCHAR | Workspace identifier mapped from the merchant, used for Payrails reporting. |
| payrails\_payment\_id | VARCHAR | Unique identifier of the Payment in Payrails. |
| payrails\_execution\_id | VARCHAR | Unique identifier of the execution. |
| holder\_id | VARCHAR | ID of the payer (holder). |
| merchant\_reference | VARCHAR | Payment reference provided by the merchant. |
| provider\_name | VARCHAR | Provider who processed the payment. |
| provider\_account\_name | VARCHAR | Provider account display name. |
| provider\_reference | VARCHAR | Unique identifier of the Payment in an external system, e.g. PSP. |
| is\_zero\_auth | BOOLEAN | If payment is zero authorization. |
| status | VARCHAR | Final status of the payment. |
| is\_authorized | BOOLEAN | If payment was authorized. |
| authorization\_failure\_result | VARCHAR | Transformed authorization failure response code, provides more details on failed transactions than status. |
| processing\_type | VARCHAR | Payment processing type, values: Regular, Subscription, CardOnFile, UnscheduledCardOnFile |
| currency | VARCHAR | Three-letter ISO currency code, in uppercase. |
| amount | NUMBER | Payment amount in local currency. |
| captured\_amount | NUMBER | Amount that was captured from the Payment in local currency. If no capture is initiated for the payment, the value will be 0 until a successful capture occurs. |
| refunded\_amount | NUMBER | Amount that was refunded from the Payment in local currency. If multiple partial refunds have been made, this will be the sum of them. If no refund is initiated for the payment, the value will remain 0 until a successful refund occurs. |
| has\_threeds | BOOLEAN | Whether payment went through 3ds. |
| threeds\_id | VARCHAR | Unique identifier of the associated 3ds object. |
| eci | VARCHAR | Electronic commerce indicator |
| payment\_method\_type | VARCHAR | Payment method type, values: card, easypaisa, jazzCash, fawryPay, etc. |
| bin | NUMBER | First 6-8 digits of the Card number. Also known as IIN (Issuer Identification Number). Card only. |
| issuer | VARCHAR | Card issuing bank. |
| issuer\_country\_code | VARCHAR | ISO alpha-2 issuer country. |
| issuer\_country\_name | VARCHAR | English name of the country of the bank or institution that issued the card. Card only. |
| card\_network | VARCHAR | Card network that supports the card, e.g. Visa, Mastercard, American Express. Card only. |
| card\_local\_network | VARCHAR | Card local network that supports the card, e.g. CartesBancaires, Dankort, Mada, Bancontact. |
| card\_segment | VARCHAR | Segment of the card, e.g. gold, black, business. Card only. |
| card\_type | VARCHAR | Card type, values: credit, debit, notset, prepaid. |
| card\_type\_details | VARCHAR | More information about the card type, e.g. personal, commercial. Card only. |
| token\_id | VARCHAR | Instrument Token ID related to the payment. |
| token\_type | VARCHAR | Type of the token, values: network, vault, psp, networkOffers. |
| error\_reason\_source | VARCHAR | Source of the error reason (null if payment was successful). |
| error\_reason\_detail | VARCHAR | Details of the error reason (null if payment was successful). |
| error\_reason\_result | VARCHAR | End result of the error reason (null if payment was successful). |
| error\_reason\_category | VARCHAR | Category of error reason (null if payment was successful). |
| error\_detail | VARCHAR | Details of the error (null if payment was successful). |
| is\_success | BOOLEAN | Whether execution action was succeeded. |
| milliseconds\_to\_completion | NUMBER | Time from request to completion in milliseconds. |
| raw\_psp\_response | VARIANT | PSP result details for Authorize operation. |
| meta | VARIANT | Complete merchant's metadata details (ex: customer, order, risk, tags, etc.). |
# Settlements report
Source: https://docs.payrails.com/docs/analytics-and-reporting/reports/settlements
A unified view of the transaction lifecycle and fees across providers, covering settlement amount, FX rate, and fee type.
This report provides a unified, standardized view of the transaction lifecycle and fee structures for both orchestrator and non-orchestrator users. By mapping complex provider data into granular fields—including SETTLEMENT\_AMOUNT, FX\_RATE, and specific FEE\_TYPE categories—it offers full transparency into every movement of funds.
## Sample report
(CSV file).
## Columns
| Column | Type | Description |
| :- | :- | :- |
| workspace\_id | VARCHAR | Workspace identifier mapped from the merchant, used for Payrails reporting. |
| merchant\_id | VARCHAR | Unique identifier of the merchant. |
| provider\_name | VARCHAR | Provider who processed the payment. |
| acquirer | VARCHAR | Acqurier which settled the payment. |
| provider\_merchant\_account | VARCHAR | The identifier of merchant account with a provider. |
| provider\_payment\_reference | VARCHAR | The provider payment identifier. |
| created\_at | TIMESTAMP | Date when transaction has been created. |
| settled\_at | TIMESTAMP | Date when transaction has been settled. |
| disbursed\_at | TIMESTAMP | Date when transaction has been disbursed to merchant. |
| timezone | VARCHAR | The timezone of date fields. |
| provider\_transaction\_reference | VARCHAR | The provider transaction identifier. |
| other\_transaction\_references | VARCHAR | Stringified JSON of additional PSP-specific identifiers |
| transaction\_type | VARCHAR | Type of transaction / money movement, values: capture, chargeback, fee, partial capture, payout, payout returned, refund. |
| fee\_type | VARCHAR | Fee category for transaction type fee, values: paypal fee, scheme fee, interchange, psp add-on, processing fee, chargeback. |
| fee\_details | VARCHAR | Fee details for transaction type fee. |
| decline\_reason | VARCHAR | Reason why the transaction was not processed successfully. |
| transaction\_currency | VARCHAR | The 3-letter ISO code that identifies the currency of the transaction. |
| transaction\_amount | FLOAT | The transaction amount, expressed in the transaction currency. |
| settlement\_currency | VARCHAR | The 3-letter ISO code that identifies the currency of the settlement. |
| settlement\_amount | FLOAT | The transaction amount, expressed in the settlement currency. |
| fx\_rate | FLOAT | Transaction to settlement currency exchange rate. |
| provider\_customer\_reference | VARCHAR | PSP-native identifier of the customer. |
| payment\_method | VARCHAR | Payment method, values: card, paypal\_account. |
| card\_type | VARCHAR | Card type, values: credit, debit, notset, prepaid. |
| card\_network | VARCHAR | Card network that supports the card, e.g. Visa, Mastercard, American Express. Card only. |
| card\_bin | VARCHAR | The payment method’s Bank Identification Number (BIN). |
| card\_issuer | VARCHAR | The bank that issued the customer’s card. This will return an empty string for alternative payment methods such as Android Pay or Apple Pay. |
| card\_issuer\_country | VARCHAR | Name of the country that issued the card. |
| card\_issuer\_region | VARCHAR | Region of the country that issued the card. |
| provider\_payout\_reference | VARCHAR | The identifier of the funds transfer from provider to merchant. |
| is\_payrails | BOOLEAN | Whether the payment was processed by Payrails. |
| payrails\_payment\_id | VARCHAR | Unique identifier of the Payment in Payrails. |
| payrails\_execution\_id | VARCHAR | Unique identifier of the execution. |
| meta | VARIANT | Full merchant's meta field details. |
# Transactions report
Source: https://docs.payrails.com/docs/analytics-and-reporting/reports/transactions
A transaction-level report for reconciling individual payments, with a downloadable CSV sample and a definition of every column.
This report can be used to reconcile individual transactions.
## Sample report
(CSV file).
## Columns
| Column | Type | Description |
| :- | :- | :- |
| workspace\_id | VARCHAR | Workspace identifier mapped from the merchant, used for Payrails reporting. |
| merchant\_id | VARCHAR | Unique identifier of the merchant. |
| payrails\_payment\_id | VARCHAR | Unique identifier of the Payment in Payrails. |
| payrails\_execution\_id | VARCHAR | Unique identifier of the execution. |
| action\_id | VARCHAR | Unique identifier of this execution action. |
| merchant\_reference | VARCHAR | The merchant reference related to the transaction. For example, the order\_id. |
| action | VARCHAR | Type of action (authorize, refund, cancel). |
| created\_at | TIMESTAMP\_NTZ | Timestamp at which the action was created. |
| completed\_at | TIMESTAMP\_NTZ | Timestamp at which the action was completed. |
| timezone | VARCHAR | Timezone in which the session happened. |
| milliseconds\_to\_completion | NUMBER | Time from request to completion in milliseconds |
| is\_success | BOOLEAN | Whether execution action was succeeded |
| is\_completed | BOOLEAN | Whether event action is completed. |
| currency | VARCHAR | Three-letter ISO currency code, in uppercase |
| amount | NUMBER | Payment amount in local currency |
| provider\_name | VARCHAR | Provider who processed the payment |
| provider\_account | VARCHAR | Provider account display name. |
| provider\_reference | STRING | The providers reference for the payment which this transaction relates to. |
| error\_reason\_source | VARCHAR | Source of the error reason (null if payment was successful). |
| error\_reason\_detail | VARCHAR | Details of the error reason (null if payment was successful) |
| error\_reason\_result | VARCHAR | End result of the error reason (null if payment was successful). |
| error\_reason\_category | VARCHAR | Category of error reason (null if payment was successful). |
| error\_detail | VARCHAR | Details of the error (null if payment was successful). |
| has\_threeds | BOOLEAN | Whether payment went through 3ds. |
| eci | VARCHAR | Electronic commerce indicator. |
| payment\_method\_type | VARCHAR | Payment method type, values: card, easypaisa, jazzCash, fawryPay, etc |
| card\_bin | VARCHAR | 4-digit BIN of the card. |
| card\_network | VARCHAR | Card network that supports the card, e.g. Visa, Mastercard, American Express. Card only. |
| card\_type | VARCHAR | The type of card used for processing, values: credit, debit, notset, prepaid. |
| card\_type\_details | VARCHAR | More information about the card type, e.g. personal, commercial. Card only. |
| card\_segment | VARCHAR | Segment of the card, e.g. gold, black, business. Card only. |
| card\_issuer | VARCHAR | Card issuing bank. |
| card\_issuer\_country\_code | VARCHAR | ISO alpha-2 country code of card issuer. |
| card\_issuer\_country\_name | VARCHAR | Full country name of card issuer. |
| token\_id | VARCHAR | Instrument Token ID related to the payment |
| token\_type | VARCHAR | Type of the token, values: network, vault, psp, networkOffers. |
| authorization\_code | VARCHAR | Authorization code from issuing bank. |
| retrieval\_reference\_number | VARCHAR | Retrieval Reference Number (RRN). |
| network\_transaction\_reference | VARCHAR | Scheme-level transaction reference. |
| provider\_response\_additional\_fields | VARIANT | PSP-specific additional response fields. |
| mapped\_psp\_response | VARIANT | Mapped/normalized PSP result details. |
| meta | VARIANT | Complete merchant's meta details. |
| instrument\_email | VARCHAR | Customer email captured on the payment instrument. Wallet only, e.g. Apple Pay, Google Pay. |
# Unified Analytics
Source: https://docs.payrails.com/docs/analytics-and-reporting/unified-analytics
The Payrails analytics dashboards centralize payment data from every provider into one view of authorization rates and cost.
Designed to offer a holistic view of payment performance, the analytics section centralises all your payment data in a unified, accessible format. This solution equips you with data-driven insights to refine your payment strategies, improve authorization rates, reduce errors, and increase profitability. Each dashboard focuses on a specific aspect of payment operations, enabling businesses to track key metrics, uncover trends, and make well-informed decision.
## Payments analytics
Our analytical dashboards give you access to payment performance, payment volumes, detailed decline reasons, payment methods, customer demographics and many more breakdowns. The comprehensive set of dashboards and visualisations allows you to monitor and optimise your payment performance.
* **Dashboard:** Get a high-level overview of payment performance across key metrics. This dashboard is ideal for a quick health check on payment operations and includes metrics such as authorization rates, successful payments, processed volume and decline reasons.
* **Payments overview:** Dive deeper into the payment performance and compare authorization rates and payment attempts for different providers, processing types, payment methods, card networks or issuers.
* **Declines:** Identify why transactions are declined to improve acceptance rates. View declines grouped into categories such as insufficient funds or fraud or drill-down into raw decline reasons. Compare decline reasons for different providers, processing types, payment methods, card networks or issuers.
* **Card acceptance:** Gain a deeper understanding of card payment performance. This dashboard is designed to explore and compare authorization rates of different card networks, segments, types or BINs.
* **Payment volume:** Understand trends in payment volumes and successful payments counts broken down by providers, processing types, payment methods, card networks or issuers.
* **Retry payments:** Analyze the performance of payments which were automatically retried after an initial failure. This dashboard allows you to compare retry success rates across different workflows and optimize the routing logic by evaluating retry success rates based on initial failure reasons.
* **Fraud prevention:** Monitor fraud check performance and decision trends over time. Analyze volume trends, shifts in decision proportions, and identify patterns across payment methods, card networks, or issuer details.
* **Workflow performance:** Provides deeper visibility into execution trends, helping you analyze success rates of different workflow configurations. You can see how actions, e.g. authorizations, refunds, and fraud checks perform and break down the data by key dimensions.
* **Fees:** Monitor and compare payment processing costs to identify saving opportunities. The fees dashboard includes overall and categorised fees breakdowns by provider, payment methods or issuer.
* **Instruments:** Displays the number of instruments created, instruments deleted, and instruments that remain active. Gain insights into stored payment instruments and observe trends by payment method, future usage, issuer region, issuer country, card network and card type.
# Upload files
Source: https://docs.payrails.com/docs/analytics-and-reporting/upload-files
Send your own files to Payrails through the file upload API or the Portal, to extend the analytics data set and feed reconciliation.
In order to extend your data set with us directly we provide you with the option to upload files. These files are processed to enhance our data models and improve the accuracy of visualizations and insights available through our analytics tools. This option also gives ability to share files that are specifically needed in order to run our reconciliation product for you.
You can upload files in the following ways
* Upload files programmatically using our FileUpload API
* *Upload files through the Portal view (coming soon)*
Supported Files
* Generic transactions- or order files
* Adyen reports
* Checkout reports
## Structure Overview
To ensure successful processing, please adhere to the following specifications:
* **Format:** Only **CSV** files are currently supported.
* **Size Limit:** Maximum **5GB** per file.
* **Number Format:** In order to prevent possible ambiguities in the numbers interpretation, we are restricting the format to only support **decimal dot separators** in the files that are being uploaded.
* **Schema:** Files must follow the same schema as defined in the [Orders Report](/docs/analytics-and-reporting/upload-files/orders-report)
## Upload files via API - How It Works
1. **Request an Upload URL**
Call the `POST /analytics/files/upload` endpoint to request a temporary, pre-signed S3 URL. This URL acts as a secure, one-time gateway to our storage. Request Body as below:
```
{
"datasetType": "ordersV1",
"file": {
"name": "merchant_orders_2026-01-01.csv",
"type": "csv"
}
}
```
2. **Upload Your File (Technical Implementation)**
Once you have the pre-signed URL, you can upload your data file using a standard HTTP `PUT` request.
**Using curl:** For a quick implementation or testing, you can use the following `curl` command. We recommend using the `--write-out` flag during testing to verify the HTTP status code returned by the server.
```bash theme={null}
curl --request PUT \
--upload-file '{your_file_path}' \
'{presigned_url}' \
--write-out "\nHTTP_STATUS:%{http_code}\n"
```
* **Note:** The `-write-out` portion is optional but highly useful for debugging; a successful upload will return an `HTTP 200`.
* **Expiration:** The pre-signed URL is valid for **1 hour**. If the upload does not start within this window, you must request a new URL.
3. **Processing & Validation**
After the upload is complete, our system automatically begins validation in the background. We check the file for structural integrity and header alignment.
4. **Check File Status**
Since large files (up to 5GB) may take time to process, you can monitor the progress programmatically.
* **Endpoint:** `GET /analytics/files/{id}`
* **Details:** Retrieve the current processing status (`Available`, `Processing`, `Imported`, `Completed`, or `Failed`) and view import statistics once finished.
* **Available:** The file has been successfully validated and is ready to be imported.
* **Processing:** The system is currently validating or parsing the file.
* **Imported:** The file has been successfully imported and analysed.
* **Completed:** The final state of a successful process, indicating that all data has been fully processed and is now Available for reporting.
* **Failed:** An error occurred during ingestion.
5. **Data Integration & Visualization**
Once the status is marked as Complete, your data is automatically merged into our analytics engine where we further process it. Once processing is complete, your custom data will now power your dashboards, providing a unified view of your performance and reconciliation insights.
**Note on Performance**
Files containing large volumes of data may take
longer to process and visualize. We recommend monitoring the file status via
the API for high-volume uploads.
# Orders Report
Source: https://docs.payrails.com/docs/analytics-and-reporting/upload-files/orders-report
The CSV format for an orders file upload, with the required and optional fields, their types, and an example value for each column.
## File Format
* **Format:** Only **CSV** files are currently supported.
## Required Fields
| Field Name | Type | Description | Example |
| - | - | - | - |
| provider | string | Payment provider name (must be supported) | `stripe` |
| provider\_account | string | Provider account identifier | `acct_12345` |
| provider\_reference | string | Provider reference for the transaction | `ref_67890` |
| acquirer | string | Acquirer name | `acquirerX` |
| type | string | Transaction type: `capture`, `refund`, or `chargeback` | `capture` |
| currency | string | 3-letter ISO 4217 currency code | `USD` |
| amount | float | Transaction amount (must be > 0) | `100.50` |
| created\_at | string | RFC3339 timestamp | `2025-12-03T10:15:30Z` |
| order\_reference | string | Merchant order reference | `order_001` |
## Optional Fields
| Field Name | Type | Description | Example |
| - | - | - | - |
| timezone | string | IANA timezone name | `Europe/Berlin` |
| transaction\_reference | string | Merchant transaction reference | `txn_002` |
| holder\_reference | string | Merchant holder reference | `holder_003` |
| auth\_code | string | 6-digit authorization code | `123456` |
| arn | string | 23-digit Acquirer Reference Number | `12345678901234567890123` |
| rrn | string | 12-digit Retrieval Reference Number | `123456789012` |
## Value References
**`type` allowed values:** `capture`, `refund`, `chargeback`
## Validation Rules
* All required fields must be present and non-empty.
* `currency` must be a valid 3-letter ISO 4217 code.
* `amount` must be a positive number.
* `created_at` must be a valid RFC3339 timestamp.
* `auth_code`, `arn`, and `rrn` must match their respective digit lengths if provided.
* `timezone` must be a valid IANA timezone name if provided.
## Example CSV
```
provider,provider_account,acquirer,provider_reference,type,created_at,timezone,currency,amount,order_reference,transaction_reference,holder_reference,auth_code,arn,rrn
stripe,acct_123,stripe,pi_123,capture,2024-09-12T10:15:30Z,Europe/London,USD,123.45,order-001,txn-001,holder-001,123456,12345678901234567890123,123456789012
adyen,acct_456,adyen,psp_456,refund,2024-10-01T08:00:00Z,,EUR,10.00,order-002,,,,,
```
# Getting started
Source: https://docs.payrails.com/docs/chargebacks/getting-started
How Payrails abstracts provider-specific dispute logic into one chargeback lifecycle that works the same across every integration.
## Introduction
Payrails offers a unified and provider-agnostic dispute management infrastructure designed to handle the full chargeback lifecycle across integrations. Unlike conventional approaches where each PSP requires dedicated\
integration and manual intervention, Payrails abstracts provider-specific dispute logic behind a single orchestration layer—giving merchants a consistent interface regardless of which integrations process their transactions.
What sets Payrails apart is its end-to-end approach to dispute resolution: from real-time ingestion of chargeback notifications to automated defense workflows, evidence submission, and outcome tracking. Merchants can configure representment strategies, set defense deadlines, and monitor dispute status across all their integrations from a single platform—without building bespoke integrations for each one.
## Key Features
* **Unified Ingestion**: Receive and normalize chargeback notifications from all connected integrations through a single, consistent event model.
* **Automated Defense Workflows**: Configure representment plans with evidence submission rules, triggering automated defense actions before deadlines expire.
* **Real-Time Lifecycle Tracking**: Webhook-driven status updates keep your system in sync at every stage—from notification of chargeback through to reversal or loss.
* **Liability Shift Detection:** Automatically detect 3DS liability shifts to inform defense eligibility and representment strategy.
* **Evidence Management**: Attach, store, and submit evidence packages (documents, receipts, transaction data) directly through the platform.
* **Audit Trail**: Every status change, defense action, and provider communication is logged as an activity for full auditability.
## How it works
Chargebacks in Payrails are managed as a workflow, just like payment acceptance or fraud management.
You configure your integration(s) in the Payrails system, and Payrails will automatically receive and process chargeback notifications from those integrations. Each chargeback moves through a defined lifecycle—from the initial chargeback notification through defense, representment, and final resolution—with your configured workflows determining which actions to take at each stage.
# Integrations
Source: https://docs.payrails.com/docs/chargebacks/integrations
Connect dispute service providers to Payrails to process chargebacks, and find the setup guide for each supported dispute integration.
A dispute service integration is a company that processes electronic chargebacks for merchants. It enables customers to manage disputes.
Payrails makes it easy to work with multiple dispute integrations through a single integration. This gives you more flexibility without needing separate integrations for each.
## Connect your providers to Payrails
To accept chargebacks with Payrails, you must connect at least one dispute integration. You can create and configure an integration in the Payrails Portal by following the setup guides below.
Each setup guide walks you through how to configure a dispute integration in Payrails. It includes:
* Required prerequisites
* Step-by-step instructions to create and configure the integration
* Specific credentials and webhook setup
Use these guides when adding a new dispute provider or reviewing an existing setup.
# Adyen
Source: https://docs.payrails.com/docs/chargebacks/integrations/adyen
Configure an Adyen Disputes integration so Payrails processes your chargebacks, covering the merchant account and credentials.
**Who should use this guide**
This guide is intended for merchants who:
* Use **Payrails** as chargeback
* Use **Adyen Disputes**
*We use a server-to-server integration with Adyen, so your account must be enabled to process dispute. If this isn’t enabled, chargebacks won’t work in production. Please contact your Adyen account manager or support team to activate it.*
1. Locate the credential used for the integration with Payrails (eg "WS\_XXXXXX").
1. On the section "Permissions > UNCATEGORIZED"
2. locate and check the checkbox: "\[X] API dispute management"
3. Click "Save changes"
2. Email **[support@adyen.com](mailto:support@adyen.com)**.
Optionally copy the Payrails Support team.
In your request, confirm that **Payrails will act as the chargeback orchestrator**:
Dear Adyen team,
Please enable "Permissions > UNCATEGORIZED > API dispute management" on WS user "WS\_XXXXX" for "MERCHANT\_NAME" to be used with Payrails Integration.
***
## Create and configure an Adyen integration in Payrails
1. Log in to the **Payrails** portal.
2. Go to **Settings** → **Integrations**.
3. Select **Add instance** to create a new Adyen integration configuration.
4. Select the **Workspaces** where this integration should be available.
**About Workspaces**
Workspaces determine where this integration is available. They let you isolate provider setups by region or business line, or share the same configuration across multiple Workspaces.
***
## Step 1: Choose the integration type
* Select **Dispute** and continue to the next step.
***
## Step 2: Choose the provider
* Select **Adyen** and continue to the next step.
***
## Step 3: Configure your Adyen integration
**What you’ll need from Adyen**
Before you begin, make sure you have access to:
* Adyen merchant account name
* Adyen API key (Web Service User)
* Prefix (production only)
* Webhook HMAC key
[Log in](https://ca-live.adyen.com/ca/ca/login.shtml) to your Adyen merchant dashboard to complete this step.
***
### Integration instance name
**In Payrails**
* Enter an **Instance name** for your integration.
***
### Merchant account name
**In Adyen**
1. [Log in](https://ca-live.adyen.com/ca/ca/login.shtml) to your Adyen dashboard.
2. Click the company logo or name in the top-left corner.
3. From the list of merchant accounts, copy the **Merchant account name**\
(not the company account).
**In Payrails**
* Paste the value into the **Merchant account name** field.
***
### Prefix (production only)
The prefix is required in **production** but not in **staging**. You can leave
this field blank in staging environments.
**In Adyen**
1. Go to **Developers** → **API URLs**.
2. Copy the **Prefix** value.
**In Payrails**
* Paste the value into the **Prefix** field.
***
### API key
Use a dedicated Web Service User API key and store it securely.
**In Adyen**
1. Go to **Developers** → **API credentials**.
2. Select **Create new credential**.
3. Choose **Web Service User**.
4. Create the credential and copy the **API key**.
**In Payrails**
* Paste the value into the **API key** field.
***
### HMAC key (webhook signing key)
Payrails uses Adyen webhooks to receive asynchronous chargeback status
updates.
Create the webhook in the same environment (test or live) as your Payrails
integration.
**In Payrails**
* Copy the **Notification URL** from the webhook section.
**In Adyen**
1. Go to **Developers** → **Webhooks**.
2. Create a new **Standard webhook**.
3. Select **only** the merchant account you are configuring.
4. Paste the Payrails notification URL into the **URL** field.
5. In **Security**, generate a new **HMAC key** and copy it.
6. In **Events**, enable all standard events.
Save the webhook configuration.
**In Payrails**
* Paste the HMAC key into the **HMAC key** field.
* Return to the integration overview after your changes are saved.
***
## Enable the integration
**In Payrails**
* Select **Save account** Save the configuration to enable the integration.
The **Enabled** status confirms that the Adyen integration is active.
Your Adyen integration is now ready to process chargebacks
***
## Next steps
1. Run a test payment triggering a chargeback using an Adyen [test card](https://docs.adyen.com/risk-management/disputes-api/test-applicable-dispute-reasons).
2. Verify that:
* The dispute appears in Adyen.
* The chargeback status updates correctly in Payrails.
3. Once verified in test mode, repeat the setup in live mode.
# Checkout
Source: https://docs.payrails.com/docs/chargebacks/integrations/checkout
Configure a Checkout.com Disputes integration so Payrails processes your chargebacks, covering account details and credentials.
**Who should use this guide**
This guide is intended for merchants who:
* Use **Payrails** as a chargeback
* Use **Checkout.com Disputes**
*We use a server-to-server integration with Checkout.com, so your account must be enabled to process dispute. If this isn’t enabled, chargebacks won’t work in production. Please contact your Checkout.com account manager or support team to activate it.*
## Create and configure a Checkout.com integration in Payrails
1. Log in to the **Payrails** portal.
2. Go to **Settings** → **Integrations**.
3. Select **Add instance** to create a new Checkout.com integration configuration.
4. Select the **Workspaces** where this integration should be available.
**About Workspaces**
Workspaces determine where this integration is available. They let you isolate provider setups by region or business line, or share the same configuration across multiple Workspaces.
***
## Step 1: Choose the integration type
* Select **Dispute**.
* Continue to the next step.
***
## Step 2: Choose the provider
* Select **Checkout.com**.
* Continue to the next step.
***
## Step 3: Configure your Checkout.com integration
**What you’ll need from Checkout.com**
Before you begin, make sure you have access to:
* Processing Channel ID (`pc_XXX`)
* Checkout.com **Access Key ID**
* Checkout.com **Access Key Secret**
* Webhook signature key
[Sign in](https://dashboard.checkout.com) to your Checkout.com dashboard to complete this step.
***
### Integration instance name
**In Payrails**
* Enter an **Instance name** for your integration.
***
### Account details
**In Checkout.com**
1. [Sign in](https://dashboard.checkout.com) to the **Checkout.com Dashboard**.
2. Locate your **Processing Channel ID** in the account or processing channel settings.
3. Copy the Processing Channel ID (`pc_XXXX`).
**In Payrails**
* Paste the value into the **Processing Channel ID** field.
***
### API credentials
**In Checkout.com**
1. Go to **Developers**.
2. Select **Create new key**.
3. In **Key details**, select **Access key pair**.
4. Add a **Description** to help identify the key.
5. Under **Scopes** → **Customise** → **Custom scopes**, enable:
* `disputes:accept`
* `disputes:provide-evidence`
* `disputes:view`
* `files`
* `gateway`
* `payments:search`
* `issuing:disputes-read`
* `issuing:disputes-write`
* `issuing:transactions-read`
* `issuing:transactions-write`
6. Select **Allow any processing channel** (or restrict if required).
7. If needed, locate your Processing Channel ID here (`pc_XXX`).
8. Select **Create new key**.
9. Copy the **Access Key ID** and **Access Key Secret**.
**In Payrails**
* Paste the credentials into the corresponding fields.
***
### Webhooks (notifications)
Payrails uses Checkout.com webhooks to receive asynchronous chargeback status updates.
Create the webhook in the same mode (test or live) as your Payrails integration.
**In Checkout.com**
1. Go to **Developers** → **Webhooks**.
2. Select **New webhook**.
3. Enter a name for the webhook.
4. Paste the **Notification URL** from Payrails.
5. Generate a **Signature key**.
6. Select:
7. Save the webhook.
**In Payrails**
* Paste the **Signature key** into the **HMAC key** field.
***
## Enable the integration
**In Payrails**
* Select **Save account** to enable the integration.
The **Enabled** status indicates that the Checkout.com integration is active.
Your Checkout.com integration is now ready to process chargebacks.
***
## Next steps
1. Run a test payment triggering a chargeback using a supported Checkout.com [test card](https://www.checkout.com/docs/developer-resources/testing/disputes-testing).
2. Verify that:
* The dispute appears in Checkout.com.
* The chargeback status updates correctly in Payrails.
3. Once verified in test mode, repeat the setup in live mode.
# Klarna
Source: https://docs.payrails.com/docs/chargebacks/integrations/klarna
Configure a Klarna Disputes integration so Payrails processes your chargebacks, covering the API credential and instance setup.
**Who should use this guide**
This guide is intended for merchants who:
* Use **Payrails** as a chargeback
* Use **Klarna Disputes**
*We use a server-to-server integration with Klarna, so your account must be enabled to process dispute. If this isn’t enabled, chargebacks won’t work in production. Please contact your Klarna account manager or support team to activate it.*
## Create and configure a Klarna integration in Payrails
1. Log in to the **Payrails** portal.
2. Go to **Settings** → **Integrations**.
3. Select **Add instance** to create a new Klarna integration configuration.
4. Select the **Workspaces** where this integration should be available.
**About Workspaces**
Workspaces determine where this integration is available. They let you isolate provider setups by region or business line, or share the same configuration across multiple Workspaces.
***
## Step one: Choose the integration type
* Select **Dispute**.
* Continue to the next step.
***
## Step two: Choose the provider
* Select **Klarna**.
* Continue to the next step.
***
## Step three: Configure your Klarna integration
**What you’ll need from Klarna**
Before you begin, make sure you have access to:
* **Username**
* **Password**
* Webhook signing keys
***
### Integration instance name
**In Payrails**
* Enter an **Instance name** for your integration.
***
### API Credential
**In Klarna**
* [Sign in](https://portal.klarna.com/) to the **Klarna Portal Dashboard**.
* Navigate to **Settings** > **Klarna API Keys** in the sidebar.
* Locate your **Merchant ID (MID)**.** **In the image below, the Merchant ID is **K0123456.**
* Generate a new **Klarna API Key**.
Klarna will display a one-time dialog containing a **Username** (UUID) and a **Password** (starting with `klarna_test_api_` for sandbox or `klarna_live_api_` for production).
* Copy the **Username** and **Password**.
**The password is shown only once**. Once you close the dialog, Klarna cannot reveal it again. You would need to generate a new key. Save it before clicking **Close**.
**In Payrails**
* Paste the values into the **Klarna Account **section.
***
## Klarna's configuration
To enable your account to start working with Disputes via API, you need to interact with their API and reach out to your integration manager or technical point of contact, because they currently does not support the self-service for this whole process.
### Enrolling your merchant with Klarna Disputes API
Klarna's Disputes API (V4) requires merchants to be enrolled before they can receive dispute events or call the dispute endpoints.This is a one-time, per-environment action.
Call the **Self onboarding for merchants** endpoint using the credentials and merchant ID (MID) retrieved in the **API Credential **step.
More details in [Klarna's documentation page](https://docs.klarna.com/acquirer/klarna/api/disputes-api-v4/#tag/Payment-Dispute-API/operation/enrollMerchant).
```curl Production theme={null}
POST https://api.klarna.com/v4/payment/disputes/merchants/{merchant_id}/enroll
Authorization: Basic
Content-Type: application/json
```
```curl Playground theme={null}
POST https://api.playground.klarna.com/v4/payment/disputes/merchants/{merchant_id}/enroll
Authorization: Basic
Content-Type: application/json
```
### Ask Klarna to register the notification URL
Once enrolled, Klarna still needs to:
1. Register the Payrails notification URL as the destination for your webhooks.
2. Subscribe your account to the dispute webhook event types you need.
3. Issue an **HMAC signing key **that Payrails will use to verify the authenticity of inbound notifications.
#### What to send
Reach out to your Klarna integration manager or technical point of contact with:
* **Merchant account identifier** (your Klarna MID)
* **Environment** (Playground or Production)
* **Notification URL** copied from the Payrails Portal
* **Event subscriptions requested**:
* `payment.dispute.state-change.initiated`
* `payment.dispute.state-change.representment`
* `payment.dispute.state-change.pre-arbitration`
* `payment.dispute.state-change.arbitration`
* `payment.dispute.state-change.closed`
* `payment.dispute.updated`
* **Request**:
> Please register the above URL as our notification endpoint, subscribe us to the listed dispute events, and provide the HMAC signing key so we can verify inbound webhooks
***
### Setting up the HMAC signing key
Once you receive back the information requested in the step above, you will get back to your Payrails portal page and include the key in the corresponding field.
**In Payrails**
* Paste the **Signature key** into the **Signing keys** field.
***
### Enable the integration
**In Payrails**
* Select **Save account** to enable the integration.
The **Enabled** status indicates that the Klarna integration is active.
Your Klarna integration is now ready to process chargebacks.
***
### Next steps
1. Run a test payment triggering a chargeback using a supported Klarna [test account](https://docs.klarna.com/acquirer/klarna/after-payments/disputes/sample-customer-data/).
2. Verify that:
* The dispute appears in Klarna.
* The chargeback status updates correctly in Payrails.
3. Once verified in test mode, repeat the setup in live mode.
# Nuvei
Source: https://docs.payrails.com/docs/chargebacks/integrations/nuvei
Configure a Nuvei Disputes integration so Payrails processes your chargebacks, covering the Nuvei site and merchant site IDs.
**Who should use this guide**
This guide is intended for merchants who:
* Use **Payrails** as a chargeback
* Use **Nuvei Disputes**
***
## Create and configure a Nuvei integration in Payrails
1. Log in to the **Payrails** portal.
2. Go to **Settings** → **Integrations**.
3. Select **Add instance** to create a new Nuvei integration configuration.
4. Select the **workspaces** where this integration should be available.
**About workspaces**
Workspaces determine where this integration is available. They let you isolate provider setups by region or business line, or share the same configuration across multiple workspaces.
***
## Step 1: Choose the integration type
* Select **Dispute** and continue to the next step.
## Step 2: Choose the provider
* Select **Nuvei** and continue to the next step.
***
## Step 3: Configure your Nuvei integration
**What you’ll need from Nuvei**
Before you begin, make sure you have access to:
* Nuvei site ID
* Nuvei merchant site ID
* Nuvei processing entity ID
* Nuvei API key
* Nuvei Dispute Rest 2.0 API key
Log in to your Nuvei dashboard to complete this step.
### Integration instance name
**In Payrails**
* Enter an **Instance name** for your integration.
***
### Merchant Nuvei site ID and merchant site ID
**In Nuvei**
* Go to **Settings** → **Business**.
* On the **Account details** tab, copy the **client ID** and** site ID**.
**In Payrails**
* Paste the value into the** merchant site ID **and** site ID** fields.
***
### Processing entity ID
**In Nuvei**
1. With **Developer** role, locate **Settings** and select **Rest 2.0 API credentials**.
2. Copy the **entity ID**.
**In Payrails**
* Paste the value into the** Processing enity **field.
***
### API key and Dispute Rest 2.0 API key
Use a restricted API key to limit access scope and reduce blast radius if the key is compromised.
**In Nuvei**
1. With **Developer** role, locate **Settings** and select **Rest 2.0 API credentials**.
2. Generate the keys** API key **and** Dispute API key**.
3. Copy the generated **API key **and** Dispute API key**.
**In Payrails**
* Paste the value into the** API key** and **Dispute API key** fields.
***
### DMN
Payrails uses Nuvei DMN to receive asynchronous chargeback status updates.
**In Nuvei**
1. Go to **Settings** → **My Account** → **Events configuration** → **Client level configuration**.
2. Select the **Client**.
**In Payrails**
* Copy the **Notification URL** from the **Account configuration** section.
In **Nuvei**
1. Configure the destination:
1. Paste the Payrails **Notification URL** in **Chargebacks** events settings
2. Enable the **Chargebacks** events settings
***
## Enable the integration
**In Payrails**
* Select **Save account** to enable the integration.
The **Enabled** status indicates that the Nuvei integration is active.
Your Nuvei integration is now ready to process chargebacks.
***
## Next steps
1. Run a test payment triggering a chargeback using a Nuvei test card.
2. Verify that:
* The dispute appears in Nuvei.
* The chargeback status updates correctly in Payrails.
3. Once verified in test mode, repeat the setup in live mode.
# Stripe
Source: https://docs.payrails.com/docs/chargebacks/integrations/stripe
Configure a Stripe Disputes integration, with or without Stripe Connect, so Payrails processes your chargebacks.
**Who should use this guide**
This guide is intended for merchants who:
* Use **Payrails** as a chargeback
* Use **Stripe Disputes** (with or without Stripe Connect)
***
## Create and configure a Stripe integration in Payrails
1. Log in to the **Payrails** portal.
2. Go to **Settings** → **Integrations**.
3. Select **Add instance** to create a new Stripe integration configuration.
4. Select the **workspaces** where this integration should be available.
**About workspaces**
Workspaces determine where this integration is available. They let you isolate provider setups by region or business line, or share the same configuration across multiple workspaces.
***
## Step 1: Choose the integration type
* Select **Dispute** and continue to the next step.
## Step 2: Choose the provider
* Select **Stripe** and continue to the next step.
***
## Step 3: Configure your Stripe integration
**What you’ll need from Stripe**
Before you begin, make sure you have access to:
* (Optional) Service / platform account ID (Stripe Connect only)
* Stripe account ID
* Publishable API key
* Restricted secret API key
* Webhook signing secret
[Log in](https://dashboard.stripe.com/login) to your Stripe dashboard to complete this step.
### Integration instance name
**In Payrails**
* Enter an **Instance name** for your integration.
***
### Service account (Stripe Connect only)
Skip this section if you process disputes directly on your own Stripe account.
**In Stripe**
* Go to **Settings** → **Account details**.
* Copy the **Service account ID** (also called the platform account ID).
**In Payrails**
* Paste the value into the **Service account** field.
***
### Merchant Stripe account ID
**In Stripe**
* Go to **Settings** → **Business**.
* On the **Account details** tab, copy the **account ID** (`acct_` followed by a 16-character alphanumeric string).
**In Payrails**
* Paste the value into the **Merchant Stripe account ID** field.
***
### Secret API key
Use a restricted API key to limit access scope and reduce blast radius if the
key is compromised.
**In Stripe**
1. In **Developers**, locate **Restricted keys** and select **Create restricted key**.
2. Choose **Providing this key to another website**.
3. Grant the minimum required permissions:
* Charges (read/write)
* Disputes (read/write)
* Files (read/write)
* Payment Intents (read/write)
* Refunds (read/write)
* Webhooks (read)
4. Name the key and add `https://www.payrails.com` as the destination URL.
5. Copy the generated API key.
**In Payrails**
* Paste the value into the **Secret API key** field.
***
### HMAC key (webhook signing secret)
Payrails uses Stripe webhooks to receive asynchronous chargeback status
updates.
If you are using Stripe Connect, skip this part.
Create the webhook in the same mode (test or live) as your Payrails
integration.
**In Stripe**
1. Go to **Developers** → **Webhooks**.
2. Select **Add destination**.
3. Choose **Your account**, then **Selected events**, and enable only the following events:
4. Select **Webhook endpoint** and continue.
**In Payrails**
* Copy the **Notification URL** from the **Account configuration** section.
In **Stripe**
1. Configure the destination:
1. Give the destination a descriptive name.
2. Paste the Payrails **Notification URL** and
3. Select **Create destination**.
2. Copy the **Signing secret**.
In **Payrails**
* Paste the **Signing secret** into the **HMAC key** field.
***
## Enable the integration
**In Payrails**
* Select **Save account** to enable the integration.
The **Enabled** status indicates that the Stripe integration is active.
Your Stripe integration is now ready to process chargebacks.
***
## Next steps
1. Run a test payment triggering a chargeback using a Stripe [test card](https://docs.stripe.com/testing#disputes).
2. Verify that:
* The dispute appears in Stripe.
* The chargeback status updates correctly in Payrails.
3. Once verified in test mode, repeat the setup in live mode.
# Explore Payrails
Source: https://docs.payrails.com/docs/explore-payrails
Browse the Payrails product suite, from payment orchestration and checkout SDKs to Token Vault, chargebacks, and analytics.
## Products
***
## Integration Steps
From the start of the integration until the first transaction on live, there are **5 steps to complete**.
Our **onboarding team**, consisting of onboarding managers and technical solution engineers, will support you during all integration steps.
### 1. Explore the products and modules for your business needs
Payrails offers modular components that enable merchants to tailor their own payment infrastructure and needs, such as payment orchestration, PCI token vault, network tokenization, payment data analytics, in-person payments, fraud management, and alternative payment methods.
### 2. Set up your account and environments
The first step involves configuring your Payrails environment. This includes setting up your staging and production environments, configuring your settings, and performing tests to ensure integration is functioning as expected.
### 3. Integrate Payrails APIs
Depending on your desired setup, the APIs that you will need to integrate and the configuration to be set up will be defined during the integration phase. Payrails provides multiple ways to integrate with your existing systems. You can use our SDKs for a more seamless integration experience or build server-to-server integration.
[PSP integration setup guides](/docs/orchestration/integrations)
### 4. Configure your merchant settings
Once you've integrated Payrails, the next step is to customize your settings. Depending on the products you are using, you will navigate to our Portal or use the API reference to configure your setup flexibly.
### 5. Test on staging and going live
Once the environment setup, integration, and configurations are completed, you can use our testing features in our Portal, such as testing workflow configurations by simulating your customized payment requests, creating test payment instruments, and reviewing the API logs. After confirmation of all scenarios on staging, you can move to the production environment to go live.
# Fraud Score
Source: https://docs.payrails.com/docs/fraud-management/fraud-score
This document explains how you can request a Fraud Score/Check action to be performed within the authorization flow. Also how to get notified about Fraud Score results.
## Request format
Executing a `fraudScore` action does not require extra effort from your side, it gets automatically invoked with authorization and gets invoked based on Workflow Configuration(s). Even the invocation context (whether pre or post authorization) is controlled by Workflow Configuration(s). You hit Payrails authorize end point normally and the workflow when designed to invoke fraud actions, will take care of the rest and notify you with the results.
## Notification format
### Case: Allow
When the `fraudScore` action is executed in the workflow configuration, you'll receive the following notification in case of an allowed transaction by the defined risk management provider. This result can be expected for receipt on your side in both pre and post authorization contexts. Based on this result in pre-authorization you would typically proceed with a frictionless authorization attempt. When the context is post-authorization, you'd typically proceed with a capture.
```json theme={null}
{
"event": "executionActionCompleted",
"time": "2025-03-26T09:55:07.098502Z",
"details": {
"action": "fraudScore",
"actionId": "d3e91eda-816e-4afb-a0b3-17647dcf2a91",
"actionMeta": {
...
},
"execution": {
"holderId": "a135b581-8301-4694-be66-13135a9d5b69",
"holderReference": "a135b581-8301-4694-be66-13135a9d5b6a",
"id": "a135b581-8301-4694-be66-13135a9d5b6b",
"merchantReference": "a135b581-8301-4694-be66-13135a9d5b6c",
"meta": {
...
},
"workflowCode": "payment-acceptance"
},
"paymentComposition": [
{
"amount": { "currency": "USD", "value": "1.00" },
"decision": "Allow",
"instrumentId": "86d37847-4ddb-41f6-927f-bb0803eb2243",
"operationProviderReference": "3c604e69-45dd-46c6-b486-a233badc3d17",
"operationResult": "Success",
"operationType": "FraudScore",
"providerConfigId": "561a0b1f-131d-4f8b-b59c-c6f32db3a180",
"providerId": "b9f1ecec-bea3-4eca-99d5-33b507fb3865",
"providerReference": "3c604e69-45dd-46c6-b486-a233badc3d17",
"success": true
}
],
"success": true
},
"workspaceId": "7f9f1882-a103-408d-ac96-46a7021e537a",
"eventTrigger": "api"
}
```
### Case: Challenge
When the `fraudScore` action is executed in the workflow configuration, you'll receive the following notification in case of a challenged transaction by the defined risk management provider. In case the context is post-authorization, i.e you're executing the action after an authorize has been attempted, this notification would never be received on your end while a customer can no longer be challenged after authorization was attempted. In pre-authorization context, you'd typically proceed with authorization with a 3DS challenge, i.e authentication flow.
```json theme={null}
{
"event": "executionActionCompleted",
"time": "2025-03-26T09:55:07.098502Z",
"details": {
"action": "fraudScore",
"actionId": "d3e91eda-816e-4afb-a0b3-17647dcf2a91",
"actionMeta": {
...
},
"execution": {
"holderId": "a135b581-8301-4694-be66-13135a9d5b69",
"holderReference": "a135b581-8301-4694-be66-13135a9d5b6a",
"id": "a135b581-8301-4694-be66-13135a9d5b6b",
"merchantReference": "a135b581-8301-4694-be66-13135a9d5b6c",
"meta": {
...
},
"workflowCode": "payment-acceptance"
},
"paymentComposition": [
{
"amount": { "currency": "USD", "value": "1.00" },
"decision": "Challenge",
"instrumentId": "86d37847-4ddb-41f6-927f-bb0803eb2243",
"operationProviderReference": "3c604e69-45dd-46c6-b486-a233badc3d17",
"operationResult": "Success",
"operationType": "FraudScore",
"providerConfigId": "561a0b1f-131d-4f8b-b59c-c6f32db3a180",
"providerId": "b9f1ecec-bea3-4eca-99d5-33b507fb3865",
"providerReference": "3c604e69-45dd-46c6-b486-a233badc3d17",
"success": true
}
],
"success": true
},
"workspaceId": "7f9f1882-a103-408d-ac96-46a7021e537a",
"eventTrigger": "api"
}
```
### Case: Prevent
When the `fraudScore` action is executed in the workflow configuration, you'll receive the following notification in case of an allowed transaction by the defined risk management provider. This result can be expected for receipt on your side in both pre and post authorization contexts. Based on this result in pre-authorization you would typically not proceed an authorization attempt. When the context is post-authorization, you'd typically proceed with a refund in case of an instant capture or a cancel otherwise.
```json theme={null}
{
"event": "executionActionCompleted",
"time": "2025-03-26T09:55:07.098502Z",
"details": {
"action": "fraudScore",
"actionId": "d3e91eda-816e-4afb-a0b3-17647dcf2a91",
"actionMeta": {
...
},
"execution": {
"holderId": "a135b581-8301-4694-be66-13135a9d5b69",
"holderReference": "a135b581-8301-4694-be66-13135a9d5b6a",
"id": "a135b581-8301-4694-be66-13135a9d5b6b",
"merchantReference": "a135b581-8301-4694-be66-13135a9d5b6c",
"meta": {
...
},
"workflowCode": "payment-acceptance"
},
"paymentComposition": [
{
"amount": { "currency": "USD", "value": "1.00" },
"decision": "Prevent",
"instrumentId": "873a421f-4fb0-4bb1-8837-d0d07df96885",
"operationProviderReference": "92beb721-3528-42ff-aa7b-9199da687d0b",
"operationResult": "Success",
"operationType": "FraudScore",
"providerConfigId": "561a0b1f-131d-4f8b-b59c-c6f32db3a180",
"providerId": "b9f1ecec-bea3-4eca-99d5-33b507fb3865",
"providerReference": "92beb721-3528-42ff-aa7b-9199da687d0b",
"success": true
}
],
"success": true
},
"workspaceId": "7f9f1882-a103-408d-ac96-46a7021e537a",
"eventTrigger": "api"
}
```
# Alerts
Source: https://docs.payrails.com/docs/monitoring/configure-alerts
Stop checking the dashboard every hour. Let Payrails tell you when something dips.
Alerts turn your payment data into proactive, email-based notifications so you know the moment something changes — a drop in authorization rate or authorization failure rate. Instead of logging in to see how things are going, you go about your day and Payrails mails you when action is needed.
***
## Why use Alerts
Payment performance moves fast. A single misconfiguration at an acquirer, a fraud attack, or a 3DS challenge gone wrong can silently eat into revenue before anyone notices. Alerts close that gap by continuously monitoring your payment authorization rate and emailing the right people the moment a threshold is crossed.
**What you get:**
* **Faster detection** — catch degradations once they happen, not at end-of-day
* **Less monitoring toil** — no more "refresh the dashboard" routines
* **Shared awareness** — route alerts to the teammates (or Payrails contacts) who need them
* **Fewer false alarms** — threshold and alert configuration and traffic filters prevent alert fatigue
**Who it's for:** High-volume merchants running on Payrails Orchestrator who want to protect authorisation rates and catch incidents early.
***
## How Alerts work
At its core, an Alert is a rule: *"If this metric crosses this threshold during this time window, email these people."*
Each Alert is made up of five building blocks:
| Building block | What it controls | Possible values |
| - | - | - |
| **Metric** | The KPI being monitored | Authorization rate, Authorization failure result |
| **Dimensions** | The slice of traffic to watch | Provider, Provider config, Payment method, Workspace |
| **Threshold** | The condition that triggers the alert | Auth Rate \< 85% |
| **Trigger window** | The evaluation period and exceptions | Rolling 1 hour; skip low-traffic hours |
| **Notifications** | Who gets notified and how | Email to [ops@merchant.com](mailto:ops@merchant.com) |
Alerts are evaluated on an **hourly cadence**. When a rule fires, recipients receive an email and a corresponding event is written to the Alert Events log.
***
## Quick Start (takes about a minute)
### 1. Open the Alerts page
From the sidebar, go to **Settings → Alerts**. You'll see a list of all existing Alert configurations, with their status (enabled/disabled) and most recent events.
### 2. Create a new Alert
Click **+ New Alert** in the top-right corner.
### 3. Choose the scope
Select the **workspace** the Alert should apply to. Pick a specific workspace to target one business line, or choose **All Workspaces** to monitor across the board. Click **Continue**.
### 4. Fill in the Alert details
| Field | What to enter |
| - | - |
| **Alert Name** | A clear, descriptive title (e.g., *"Auth Rate drop — Card EU"*) |
| **Metric** | The KPI to monitor — e.g., Authorization rate |
| **Dimensions** | Optional filters to narrow the scope (provider, payment method, etc.) |
| **Threshold** | The trigger condition (e.g., *Auth Rate \< 85%*) |
| **Trigger** | When the rule is evaluated, plus any exception windows for low-traffic periods |
| **Notifications** | One or more email addresses to notify when the Alert fires |
| **Default status** | Enable or disable the Alert on save |
### 5. Save and enable
Save the Alert. If you enabled it, Payrails starts evaluating the rule on the next hourly cycle.
***
## Recommended starting point
If you're setting up your first Alert and unsure where to begin, use this **Standard Health Alert** as a baseline. It catches the most common revenue-impacting degradations with minimal tuning.
| Setting | Value |
| - | - |
| Metric | Authorisation Rate |
| Threshold | \< 85% |
| Trigger window | Rolling 1 hour |
| Dimensions | *(leave blank for account-wide)* |
| Notifications | Your payment ops distribution list |
From there, layer in more targeted Alerts as you learn which dimensions matter most to your business.
***
## Thresholds and trigger exceptions
Setting a threshold is **strongly recommended**. Without one, every small fluctuation becomes an alert — and your team starts ignoring them. A good threshold only fires when something is meaningfully wrong.
**Two controls to avoid noise:**
* **Minimum traffic threshold** — Only evaluate the rule when traffic is high enough to be statistically meaningful. This prevents false positives from a handful of transactions in the middle of the night.
* **Trigger exceptions** — Skip evaluation during known low-traffic windows (e.g., scheduled maintenance, off-hours for a regional business). Exceptions are defined as time ranges on the Alert configuration.
***
## Managing Alerts
Payrails gives you three views to stay on top of your Alert infrastructure:
### Alert List
The entry point — every Alert configuration in one place, with its enabled/disabled state, scope, and a quick summary of recent activity.
### Alert Details
Click into any Alert to see its full configuration, edit fields, pause or re-enable it, or add/remove recipients.
### Alert Events
The historical log — every time an Alert fired, the Alert name, and its necessary details.
***
## Best Practices
**1. Start simple, then specialise.** Begin with one account-wide Authorisation Rate alert. Once you see it in action, add targeted Alerts per payment method, country, or PSP.
**2. Set thresholds that reflect your normal.** An 85% Auth Rate floor makes sense for most card traffic, but APMs, BNPL, and high-risk geographies often operate at different baselines. Check your last 30 days before deciding.
**3. Use dimensions to reduce noise.** An account-wide alert can be masked by high-volume success in one region while another is failing. Dimension-scoped Alerts (e.g., *Card + US*) catch regional regressions that aggregate metrics miss.
**4. Route Alerts to the people who can act.** Add your payment ops distribution list — not individual mailboxes that may go unmonitored. Consider adding your Payrails contact for critical rules so we can help investigate.
**5. Document what each Alert means.** Give every Alert a descriptive name and, in your internal runbook, note the recommended first action when it fires. Future-you will thank present-you.
**6. Review and refine quarterly.** Traffic mixes change, PSPs get added, thresholds drift. A 15-minute review every quarter keeps the system trustworthy.
**7. Resolve events promptly.** Leaving events unresolved means you lose the signal the next time the same issue occurs.
***
## FAQ
Hourly. This cadence balances timely detection with evaluation cost.
Higher-frequency evaluation is on our roadmap.
It looks only at transactions where `IS_AUTHORIZED = 'FALSE'` — i.e., the
share of attempts that failed authorization. Non-authorization events are
excluded from the calculation.
The Alert will still save, but we strongly recommend always setting one.
Without a threshold, the rule can fire on minor fluctuations and cause alert
fatigue.
Today, email is the primary channel.
Yes — select **All Workspaces** when creating the Alert. Otherwise, scope it
to a single workspace.
No. Once an event is open it won't re-fire until you mark it as resolved.
***
## Next steps
* Set up your first Alert using the [Quick start](#quick-start-takes-about-a-minute) above
* Review the [Alert List](#alert-list), [Alert Details](#alert-details), and [Alert Events](#alert-events) views
* Reach out to your Payrails contact if you'd like help choosing the right metrics and thresholds for your traffic
# Payrails Checkout SDKs
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks
Compare the three Payrails SDK integration levels, Drop-in, Elements, and Secure Fields, across web, iOS, Android, and React Native.
Payrails SDKs provide building blocks to create a checkout experience for your customers.
They exist for iOS, Android, web, and React Native. Regardless of the platform, they follow the same principles.
You can integrate with the SDK in three ways.
* **[Drop-in](/docs/orchestration/checkout-sdks/web-v5-legacy/drop-in)**: an all-in-one payment form to accept payments on your website.
* **[Elements](/docs/orchestration/checkout-sdks/web-v5-legacy/elements)**: modular payment UI components you can assemble to build a modular payment form.
* **[Secure fields](/docs/orchestration/checkout-sdks/web-v5-legacy/secure-fields)**: secure input fields for PCI-compliant cardholder data collection.
The **Drop-in** contains one-to-many **Elements** (depending on how many active payment methods you have). Some Elements dealing with cardholder data collection include **Secure Fields**. All three options are included in our SDK.
## Get started with the SDK
Regardless of which SDK components you want, it starts with the same initialization process.
The Payrails SDK requires a secure context (HTTPS). If testing locally, ensure
your development server uses HTTPS.
### Include the SDK
You can install the SDK with your favorite package manager (e.g.,`npm`):
```shell JavaScript theme={null}
npm install @payrails/web-sdk
```
```swift iOS theme={null}
#Mentioning the below source will pick the podspec from Payrails repo
#Otherwise you can add cocoapod trunk as the source
#source 'https://github.com/payrailsapi/payrails-iOS-spec.git'
pod 'Payrails'
```
```shell React Native theme={null}
npm install @payrails/react-native-sdk
```
### Initialize the SDK
Use the `init()` method to initialize a Payrails client as shown below, with configuration:
```typescript JavaScript theme={null}
// With a Bundler (Webpack, Vite, Rollup, esbuild, etc.)
import { Payrails, PayrailsEnvironment } from "@payrails/web-sdk";
import "@payrails/web-sdk/payrails-styles.css";
// Without a Bundler (Native ESM in the Browser)
import { Payrails, PayrailsEnvironment } from "@payrails/web-sdk";
// CommonJS (Node.js / Legacy)
// For CommonJS environments, use require:
const { Payrails, PayrailsEnvironment } = require("@payrails/web-sdk");
import "@payrails/web-sdk/payrails-styles.css";
const config = await fetch("https://give.me.SDK.config.com");
const options = {
environment: "TEST", // or "PRODUCTION"
events: {
onClientInitialized: (execution: WorkflowExecution) => {
console.log("Payrails client initialized", execution);
},
},
returnInfo: {
success: "payrails.com/success",
cancel: "payrails.com/failure",
error: "payrails.com/error",
pending: "payrails.com/pending",
},
};
const payrailsClient = Payrails.init(config, options);
```
```swift iOS theme={null}
let config = Payrails.Configuration(
data: ,
options: Payrails.Options(
env?: Payrails.Env // optional, if not specified default is PROD
)
)
let payrails = try await Payrails.configure(
with: configuration
)
```
```typescript React Native theme={null}
import { PayrailsProvider } from "@payrails/react-native-sdk";
const App = () => {
const config = useFetch("https://give.me.sdk-config.com");
return (
// Other elements goes here.
);
};
```
Your backend should call the [Initialize a client SDK](/reference/clientinit) endpoint. You simply forward the response to SDK.
Once your SDK is initialized, it can interact directly with Payrails from your client.
### Example authorization flow
Here's a simple authorization flow with the surface covered by the SDK:
To ensure your frontend can communicate securely with Payrails, you must first fetch configurations from your **server-side** application. See the detailed endpoint reference to [Initialize a client SDK](/reference/clientinit).
#### Example for web
```html theme={null}
```
```javascript theme={null}
import "@payrails/web-sdk/payrails-styles.css";
import { Payrails } from "@payrails/web-sdk";
// 1. call your server-side app to get SDK configs from there
const config = await fetch("https://give.me.SDK.config.com");
// 2. Initialize Payrails client
const payrailsClient = Payrails.init(config, { environment: "TEST" });
// 3. Create a Dropin container: pass your styles, translations, configs and events)
const dropinConfig = {}; // sample dropin config
const dropin = payrailsClient.dropin(dropinConfig);
// 4. Mount the Dropin to your DOM
dropin.mount("#dropin-container");
```
### Learn more
After the SDK is initialized, you can leverage the SDK features to customize the user experience:
* custom field validation and errors
* subscribe and react to events happening inside the form
* customize the style with JSS
* save your tokenized card and get a payment instrument for future references
## Payment amount update
Our SDK supports the possibility of updating the amount. To achieve that, 2 things have to be done:
* Merchant backend must call the [lookup action](/reference/lookupaction) on the execution used for that payment session.
* Merchant client has to update the SDK with the new amount. Moreover, if installments are enabled, the merchant client must also update the installment configuration along with the amount.
Example:
```javascript JavaScript theme={null}
import '@payrails/web-sdk/payrails-styles.css';
import { Payrails } from "@payrails/web-sdk";
// 1. call your server-side app to get SDK configs from there.
const config = await fetch("https://give.me.SDK.config.com");
let executionId;
// 2. Initialize Payrails client
const payrailsClient = Payrails.init(config, {
events: {
onClientInitialized: async (workflow) => {
executionId = workflow.executionResponse.id
}
}
});
// 3. Initialize dropin
const dropinConfig = {} // sample dropin config
const dropin = payrailsClient.dropin(dropinConfig)
// 4. Update amount from the backend
await fetch(`https://do-lookup-with-new-amount-on-backend.com?executionId=${executionId}`)
// 5. Update the SDK Client
payrailsClient.update({
value: "1"
currency: "USD",
installmentConfig: {
defaultInstallment: 2,
installmentCountry: 'US',
installmentOptions: [],
}
})
```
In case of a mismatch between the authorized amount, which is set on execution on the backend, and what's set on the SDK client, requests will be rejected with `401` HTTP error.
## Refresh Client Session
Our SDK allows merchants to pass the function which contains their implementation of client init as part of the `onSessionExpired` event. This then can be used to automatically reinitialize the client when the client session is expired instead of merchants having to do this themselves.
Example:
```javascript theme={null}
import "@payrails/web-sdk/payrails-styles.css";
import { Payrails } from "@payrails/web-sdk";
// 1. call your server-side app to get SDK configs from there.
const config = await fetch("https://give.me.SDK.config.com");
let executionId;
// 2. Initialize Payrails client
const payrailsClient = Payrails.init(config, {
events: {
onClientInitialized: async (workflow) => {
executionId = workflow.executionResponse.id;
},
onSessionExpired: async () => {
const result = await initPayrailsClient(config);
return result?.res;
},
},
});
// 3. Initialize dropin
const dropinConfig = {}; // sample dropin config
const dropin = payrailsClient.dropin(dropinConfig);
```
## BIN lookup
Our SDK supports using our BIN lookup service to fetch card information. **This feature is not enabled by default, so please reach out to your account manager to enable.**
Example:
```javascript JavaScript theme={null}
import "@payrails/web-sdk/payrails-styles.css";
import { Payrails } from "@payrails/web-sdk";
// 1. call your server-side app to get SDK configs from there.
const config = await fetch("https://give.me.SDK.config.com");
// 2. Initialize Payrails client
const payrailsClient = Payrails.init(config);
// 3. Initialize dropin
const dropinConfig = {}; // sample dropin config
const dropin = payrailsClient.dropin(dropinConfig);
// 4. Perform BIN lookup
const result = await payrailsClient.binLookup();
```
In case complete BIN lookup is not enabled your environment, the API returns the first 8 digits of the PAN.
If the BIN Lookup is enabled then the complete API response is returned.
```json theme={null}
// Sample BIN Lookup Response
{
"bin": "411111",
"network": "visa",
"issuer": "JPMORGAN CHASE BANK, N.A.",
"issuerCountry": {
"code": "US",
"name": "UNITED STATES",
"iso3": "USA"
},
"type": "CREDIT"
}
```
### Example on how to allow only credit cards
```typescript theme={null}
import "@payrails/web-sdk/payrails-styles.css";
import { Payrails } from "@payrails/web-sdk";
// 1. call your server-side app to get SDK configs from there.
const config = await fetch("https://give.me.SDK.config.com");
// 2. Initialize Payrails client
const payrailsClient = Payrails.init(config);
const dropin = payrailsClient.dropin({
events: {
async onPaymentButtonClicked() {
const { type = "" } = await payrails.binLookup();
return type === "CREDIT";
},
},
});
```
## Installment payments for cards in MX and BR
In certain countries or regions, it's common for shoppers to pay for purchases in installments using credit cards. This allows them to receive their items right away while spreading the cost over multiple months. The installment payments are automatically charged to the shopper’s credit card every 30 days until the full balance is settled.
Our SDK supports collecting installment plan information from the end-user and passing it through to downstream processors. Please contact us if you would like to enable this feature.
### Config options for installments dropdown
```typescript theme={null}
payrails.cardForm({
installmentConfig: {
defaultInstallment: 2,
},
translations: {
labels: {
paymentInstallments: "Payment Installments",
},
},
styles: {
storeCardCheckbox: {
color: "red",
},
errorTextStyles: {
base: {
display: "block",
},
},
paymentInstallmentsDropdown: {
backgroundColor: "lightgray",
color: "black",
fontSize: "14px",
},
},
});
```
## Accept Customer Address
With dropin it is possible to show address component to the end user. You can use it for collecting billing address which may be needed to tax purposes. Please contact us if you would like to enable this feature.
### Example code for billing address config
```
const dropinConfig = {
"translations": {
"addressSelector": {
"labels": {
"countrySelector": "Select Country",
"postalCodeInput": "Zip Code"
},
"placeholders": {
"postalCodeInput": "12345",
"countrySelector": "Country"
}
}
},
"styles": {
"addressSelector": {
"wrapper": {
"display": "flex",
"flexDirection": "row",
"gap": "8px"
},
"countrySelector": {
"wrapper": {
"flexDirection": "column"
},
"element": {
"border": "1px solid #D3D3D3",
"borderRadius": "8px",
}
},
"postalCodeInput": {
"wrapper": {
"flexDirection": "column"
},
"element": {
"padding": "8px",
}
}
},
}
}
```
## Performance optimizations
* Preloading all card form assets before users reach the payment page is possible. This could improve the end-user experience, as secure fields will appear on the page instantly.
* The browser will de-prioritize these assets and not slow down the page load, which will not hurt the user experience.
* You can check how preload works on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload)
```typescript theme={null}
import { Payrails } from "@payrails/web-sdk";
// function will put a 's to with required assets for credit card form
Payrails.preloadCardForm();
```
## Return Info
You can pass custom return URLs to SDK configuration. This controls the redirect behavior after authorization. This is specifically useful for payment methods needing full-page redirection or 3DS processes.
| Property | Type | Description |
| :- | :- | :- |
| `cancel` | `string` | URL to redirect to if the customer canceled the flow. |
| `success` | `string` | URL to redirect to after a successful flow. |
| `error` | `string` | URL to redirect to after a failed flow. |
## Content security policy
If you have deployed a content security policy, please contact your account manager for the list of directives required by our SDKs.
## Report a Vulnerability
If you discover a potential security issue in this project, please get in touch with us at [security@payrails.com](mailto:security@payrails.com).
# Accept payments via API
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/accept-payments-via-api
Build your own checkout on the Payrails API, using Secure Fields or client-side encryption to tokenize card data.
Use our APIs and build your own payment experience if you want full control over the look and feel of your checkout page.
Building your own payment experience requires significantly more effort for
handling all alternative flows, while Payrails provides built-in components
that you can customize. So if you'd rather not build your own payment
experience, check our [Drop-in](/docs/orchestration/checkout-sdks/web-v5-legacy/drop-in) solution.
With our API-only solution, you have two options for accepting card payments:
* Use our [Secure fields](/docs/orchestration/checkout-sdks/web-v5-legacy/secure-fields) card tokenization to collect the card details. This helps you ensure PCI compliance, as you're only required to submit [SAQ A](https://listings.pcisecuritystandards.org/documents/SAQ_A_v3.pdf).
* Collect and pass raw card data. This requires you to assess your PCI compliance according to [SAQ D](https://listings.pcisecuritystandards.org/documents/PCI-DSS-v3_2_1-SAQ-D_Merchant.pdf), the most extensive form of self-certification.
On this page, we describe server-side integration and the client-side implementation that you have to build. Please note this is just an example of what Payrails can offer so that you picture the process better. If you have specific requirements on how you would prefer to accept payments, let us know and we'll guide you throw our API reference.
To get started, make sure you have an API key and are able to [Request an
access token](/reference/getoauthtoken).
## Fetch the payment options & initialize your checkout
To initialize your payment form with Payrails, you need to achieve 3 operations:
1. [Create a workflow execution](/docs/orchestration/payment-acceptance/create-a-workflow-execution). The execution is the equivalent of a customer payment session, so you want to save the `executionId` as a reference to perform the next steps.
2. [Lookup payment options](/docs/orchestration/payment-acceptance/lookup-payment-options) available for the customer in this context. Eventually, you want to bundle this operation with the workflow creation as an `initialAction` to save an API call (like in the example above).
3. [Initialize a client SDK](/reference/clientinit) to collect card information on the client side. Eventually, you want to perform this call in parallel to optimize your latency (like in the example above).
Beware that every time the payment data changes (e.g. applying a discount),
you want to [Lookup payment options](/reference/lookupaction) again because the
payment methods and instruments could be different!
## (Optional) Tokenize a new card
This step happens when the customer selects a new card as a payment instrument to authorize with. It is described in detail in the [Payrails SDK](/docs/orchestration/checkout-sdks) guide.
Make sure to save the tokenized card output provided by the SDK, you will need it for the next step.
## Authorize a payment
When the customer chooses a payment method and an instrument and then clicks the "Pay" button of your checkout form, it is time to [Authorize a payment](/docs/orchestration/payment-acceptance/authorize-a-payment)!
This process is asynchronous, after your server-side application sent the authorization request, it will immediately receive an HTTP `202 ACCEPTED` acknowledgment from Payrails.
It doesn't mean that your request was successful, but that it is in progress and Payrails will notify you about the success (or failure) later.
Meanwhile, you can decide to [Receive notifications](/docs/orchestration/payment-acceptance/receive-notifications) or long poll the execution to fetch the status with the [Get an execution by ID](/reference/getexecution) endpoint.
For understanding the possible results of payment authorization, you should check the [Result Codes](/docs/resources/payments/operation-results) page.
## Handle redirects for 3-D Secure
An authorization with cards may require a [3D Secure](https://www.emvco.com/emv-technologies/3-d-secure/) challenge to be resolved before the payment can complete. Since the challenge happens in a WebView (mobile) or a webpage (web), the user journey will leave your domain.
To handle this, redirect the customer to the `links.consumerWait` URL returned in the [authorize response](/docs/orchestration/payment-acceptance/authorize-a-payment#3-receive-the-response-to-the-authorization-request). Payrails takes care of the rest:
* If the authorization requires 3DS, the customer is taken through the challenge and returned to your `returnInfo.success` URL when complete.
* If no challenge is required, the customer is redirected straight to `returnInfo.success`.
* The same URL handles other authentication step-ups your PSP may require, so your integration doesn't need to change as you add payment methods.
Make sure your authorization request specifies a `returnInfo.success` URL - this is where Payrails returns the customer once the session is complete.
When the customer reaches your `returnInfo.success` URL, the authorization may still be in progress. Payrails will notify you of the final success or failure via webhook (similar to an authorization without 3DS).
**Want to handle the 3DS redirect yourself?** Some merchants prefer to detect
the 3DS step manually and redirect the customer to the challenge URL directly,
avoiding a Payrails-hosted intermediate page on non-3DS payments. See [3D
Secure](/docs/orchestration/payment-acceptance/3d-secure) for that integration
pattern.
## (Optional) Handle redirects for Alternative Payment Methods (APMs) with Hosted Payment Pages (HPP)
The APM redirect flow is similar to the 3-D Secure flow since both require a redirection.
Since the APM authorization happens in a WebView (mobile) or a webpage (web), the user journey will leave your domain. Therefore, you wanna make sure your authorization request specifies in the `returnInfo` which return URL the user shall be redirected to after the APM authorization is complete.
After the authorization request is placed, you will be informed by a notification or long poll of the execution to fetch the status if a APM redirect was requested.
```json theme={null}
"actionRequired": "redirect",
"links": {
"redirect": "https://api.payrails.io/public/redirect/merchant/..."
}
```
After fetching the redirect URL, your client will open a WebView (mobile) or redirect the user to a webpage (web) where the challenge will take place.
When the challenge is resolved, Payrails will take your client back to the address you specified in the `returnInfo`. It doesn't mean that your authorization was successful, but that it is in progress and Payrails will notify you about the success (or failure) later.
## (Optional) Apple Pay and Google Pay
For [Apple Pay](https://developer.apple.com/documentation/passkit/apple-pay) and [Google Pay](https://developers.google.com/pay/api/android/overview), you will have to install their vendor SDK and fetch their payment token directly on the Client.
Then you pass the payment token with your [Authorize a payment](/docs/orchestration/payment-acceptance/authorize-a-payment) endpoint call.
# Android SDK - Quick Start
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android
This tutorial walks you through integrating the Payrails Android SDK from scratch. By the end, you'll have a working card payment form in a Compose screen.
**Time:** \~15 minutes
**Prerequisites:** Android project with Jetpack Compose, JDK 17, `minSdk` 21, `compileSdk` 35
## Step 1: Add Dependencies
The SDK is distributed via [Maven Central](https://central.sonatype.com/).
In your app-level `build.gradle.kts`:
```kotlin theme={null}
dependencies {
implementation("com.payrails.android:checkout:")
implementation("com.payrails.android:cse:")
}
```
If Maven Central is not already in your project's repository list, add it to `settings.gradle.kts`:
```kotlin theme={null}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
```
Sync your project.
## Step 2: Fetch an Init Payload From Your Backend
The SDK requires an initialization payload from the Payrails API. Your backend should call the Payrails client-init endpoint and return the `version` and `data` fields.
**Kotlin**
```kotlin theme={null}
data class InitPayload(val version: String, val data: String)
suspend fun fetchInitPayloadFromBackend(): InitPayload {
// Call your backend → Payrails client-init endpoint
TODO("Implement your backend call")
}
```
**Java**
```java theme={null}
public class InitPayload {
public final String version;
public final String data;
public InitPayload(String version, String data) {
this.version = version;
this.data = data;
}
}
```
## Step 3: Initialize the SDK Session
Create a session before rendering any payment UI. This is typically done in your Activity or ViewModel.
**Kotlin**
```kotlin theme={null}
import com.payrails.sdk.Configuration
import com.payrails.sdk.InitData
import com.payrails.sdk.Options
import com.payrails.sdk.Payrails
suspend fun initializePayrails(): Session {
val payload = fetchInitPayloadFromBackend()
val configuration = Configuration(
initData = InitData(version = payload.version, data = payload.data),
option = Options()
)
return Payrails.createSession(configuration)
}
```
**Java**
```java theme={null}
import com.payrails.sdk.Configuration;
import com.payrails.sdk.InitData;
import com.payrails.sdk.Options;
import com.payrails.sdk.Payrails;
// Java — use the callback API instead of suspend
void initializePayrails(InitPayload payload) {
InitData initData = new InitData(payload.version, payload.data);
Configuration configuration = new Configuration(initData, new Options());
Payrails.createSession(configuration, result -> {
if (result.isSuccess()) {
Session session = result.getOrNull();
// Session ready — store and use it
} else {
// Initialization failed
}
return kotlin.Unit.INSTANCE;
});
}
```
## Step 4: Build the Card Payment Screen
**Kotlin/Compose only**
The payment UI elements use Jetpack Compose and require Kotlin. There is no Java equivalent for this step. Session initialization (Step 3) and delegate callbacks are Java-compatible.
Create a Composable that renders a card form and a pay button:
```kotlin theme={null}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.dp
import com.payrails.sdk.*
@Composable
fun CardPaymentScreen() {
// 1. Create the card form
val cardForm = remember {
Payrails.createCardForm(
config = CardFormConfig(
showCardHolderName = true,
showSingleExpiryDateField = true
)
)
}
// 2. Create the pay button
val payButton = remember {
Payrails.createCardPaymentButton(
translations = CardPaymenButtonTranslations(label = "Pay Now")
)
}
// 3. Set up payment callbacks
payButton.delegate = object : PayrailsCardPaymentButtonDelegate {
override fun onPaymentButtonClicked(button: CardPaymentButton) {
// Payment started
}
override fun onAuthorizeSuccess(button: CardPaymentButton) {
// Payment succeeded — show confirmation
}
override fun onThreeDSecureChallenge(button: CardPaymentButton) {
// 3DS challenge opened in browser — handled automatically
}
override fun onAuthorizeFailed(button: CardPaymentButton) {
// Payment failed — show error
}
}
// 4. Render the UI
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
cardForm.Render()
payButton.Render()
}
}
```
## Step 5: Handle Payment Results
The delegate callbacks tell you what happened:
| Callback | Meaning | What to do |
| - | - | - |
| `onPaymentButtonClicked` | User tapped "Pay Now" | Analytics and observability only — the SDK does not wait for it, so it cannot stop a payment. To gate one, see [How to Gate Payment Authorization](/docs/orchestration/how-to-gate-payment-authorization) |
| `onAuthorizeSuccess` | Payment authorized | Navigate to confirmation screen, verify on your backend |
| `onThreeDSecureChallenge` | 3DS challenge started | Browser opens automatically — no action needed |
| `onAuthorizeFailed` | Payment failed | Show error message, allow retry |
## Step 6: Test It
Use your Payrails sandbox environment and test card numbers to verify the integration. The card form validates input automatically — you'll see inline error messages for invalid card numbers, expiry dates, and CVV codes.
## What's Next?
Now that you have a basic card payment working:
* **[How to Tokenize a Card](/docs/orchestration/checkout-sdks/android/how-to-tokenize-card)** — Save a card to vault without charging it (for wallets, subscriptions, and save-for-later flows)
* **[How to Let Shoppers Choose a Card Network](/docs/orchestration/checkout-sdks/android/how-to-co-branded-cards)** — Co-branded (co-badged) cards: surface the brand selector and submit the preferred scheme
* **[How to Build a Custom Pay Button (Your Own UI)](/docs/orchestration/checkout-sdks/android/how-to-custom-pay-button)** — Use your own button and method selector while the SDK runs the payment flow
* **[How to Run a Payment Without an SDK Button (Headless)](/docs/orchestration/checkout-sdks/android/how-to-execute-payment-headless)** — Drive checkout from your own UI / `ViewModel` with `PayrailsPaymentLauncher`
* **[Styling Guide](/docs/orchestration/checkout-sdks/android/styling-guide)** — Customize colors, fonts, and layout to match your brand
* **[Troubleshooting](/docs/orchestration/checkout-sdks/android/troubleshooting)** — Diagnose integration and runtime issues
* **[SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts)** — Understand the architecture and mental model
* **[API Reference](/docs/orchestration/checkout-sdks/android/api-reference)** — Complete reference for all public APIs
# SDK API Reference
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/api-reference
Public API of the Payrails Android SDK — payment methods, configuration, styling, translations and event delegates.
> **Looking for guides?** [Quick Start](/docs/orchestration/checkout-sdks/android) | [Styling Guide](/docs/orchestration/checkout-sdks/android/styling-guide) | [Troubleshooting](/docs/orchestration/checkout-sdks/android/troubleshooting) | [SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts)
## Table of Contents
* Getting Started
* Custom UI Payment Launcher
* Core Concepts
* Logging
* Card Payments
* Card Tokenization
* Co-Branded Cards
* Google Pay
* Redirect Payments
* Stored Instruments
* Results and Errors
## Compatibility Requirements
* JDK 17
* Android `minSdk` 21
* Android `compileSdk` 35
## Public Artifact
* Distributed via [Maven Central](https://central.sonatype.com/)
* Maven coordinate: `com.payrails.android:checkout:`
* Additional dependency: `com.payrails.android:cse:`
## Getting Started
### Create a Session
Use a `Configuration` built from `InitData` plus optional `Options`.
**Kotlin**
```kotlin theme={null}
val initData = InitData(version = "...", data = "...")
val configuration = Configuration(initData = initData, option = Options())
// Suspend API
val session = Payrails.createSession(configuration)
// Callback API
Payrails.createSession(configuration) { result ->
result.onSuccess { /* session ready */ }
result.onFailure { /* initialization failed */ }
}
```
**Java**
```java theme={null}
// Java — use the callback API (suspend functions are not callable from Java)
InitData initData = new InitData("...", "...");
Configuration configuration = new Configuration(initData, new Options());
Payrails.createSession(configuration, result -> {
if (result.isSuccess()) {
Session session = result.getOrNull();
// session ready
} else {
// initialization failed
}
return kotlin.Unit.INSTANCE;
});
```
To enable automatic SDK-managed recovery when a redirect flow is abandoned or remains non-terminal, pass `onSessionExpired` in `Options.redirectSessionLifecycle`:
```kotlin theme={null}
val configuration = Configuration(
initData = initData,
option = Options(
redirectSessionLifecycle = RedirectSessionLifecycle(
onSessionExpired = {
// Merchant callback: run your client-init call and return fresh init payload
val refreshed = fetchInitPayloadFromBackend()
InitData(version = refreshed.version, data = refreshed.data)
}
)
)
)
```
### Session-Scoped Helpers
`Session` is the merchant's handle to all runtime SDK operations. Keep the reference returned by `createSession(...)` and call query/update/instrument/payment methods on it directly.
```kotlin theme={null}
val session = Payrails.createSession(configuration)
val instruments = session.getStoredInstruments()
val cardInstruments = session.getStoredInstruments(forType = PaymentMethod.card)
```
Breaking change: `Payrails.getCurrentSession()` is not part of the public API. Hold the `Session` returned by `createSession(...)` — methods like `query`, `update`, `getStoredInstruments`, `deleteInstrument`, `updateInstrument`, and `getPaymentMethodConfig` now live on `Session`. (Payment execution is not on `Session`; use [`PayrailsPaymentLauncher`](#payrailspaymentlauncher).)
## Querying Session Data
Use `Session.query()` to read data from the active session.
### `Session.query(key)`
```kotlin theme={null}
fun query(key: PayrailsQuery): T
```
Returns the value for `key` from the session, or `null` if the requested data is not present. Never throws.
| Parameter | Type | Description |
| - | - | - |
| `key` | `PayrailsQuery` | A typed query key from the `PayrailsQuery` sealed class. |
**Example**
```kotlin theme={null}
val executionId: String? = session.query(PayrailsQuery.ExecutionId)
val amount: PayrailsAmount? = session.query(PayrailsQuery.Amount)
val cardConfig: PayrailsPaymentMethodConfig? =
session.query(PayrailsQuery.PaymentMethodConfig("card"))
```
***
### `PayrailsQuery`
Sealed class of typed query keys for [`Session.query()`](#session-query-key), in package
`com.payrails.sdk`.
| Key | Return type | Description |
| - | - | - |
| `PayrailsQuery.ExecutionId` | `String?` | ID of the active payment execution. |
| `PayrailsQuery.HolderReference` | `String?` | Holder reference for the active session. |
| `PayrailsQuery.Amount` | `PayrailsAmount?` | Current checkout amount. |
| `PayrailsQuery.BinLookup` | `PayrailsLink?` | BIN lookup action link. |
| `PayrailsQuery.InstrumentDelete` | `PayrailsLink?` | Stored instrument delete action link. |
| `PayrailsQuery.InstrumentUpdate` | `PayrailsLink?` | Stored instrument update action link. |
| `PayrailsQuery.PaymentMethodConfig(code)` | `PayrailsPaymentMethodConfig?` | Config for the given payment method code. |
| `PayrailsQuery.PaymentMethodInstruments(code)` | `List?` | Stored instruments for the given payment method code. |
**Removed**
`PayrailsQuery.AvailableRedirectMethods`. Use
[`Session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)`](#session-getpaymentmethodconfig-filter)
instead.
***
### `PayrailsAmount`
```kotlin theme={null}
data class PayrailsAmount(
val value: String,
val currency: String
)
```
Represents the current checkout amount. `value` is the amount in wire format (e.g.
`"54.99"`), consistent with `AmountUpdate.value`. `currency` is the ISO 4217 code
(e.g. `"EUR"`).
***
### `PayrailsLink`
```kotlin theme={null}
data class PayrailsLink(
val href: String?,
val method: String?
)
```
A backend action link. `href` is the URL to call; `method` is the HTTP method (e.g.
`"POST"`, `"DELETE"`). Either field may be `null` if not provided by the backend.
***
### `PayrailsPaymentMethodConfig`
```kotlin theme={null}
data class PayrailsPaymentMethodConfig(
val paymentMethodCode: String,
val description: String?,
val displayName: String?,
val flow: String?,
val supportsSaveInstrument: Boolean?,
val supportsBillingInfo: Boolean?
)
```
Configuration for a payment method from the active session. All fields except
`paymentMethodCode` are optional and sourced from the session payload.
***
## Updating Session State
Use `Session.update()` to change the checkout amount after `createSession()` has
completed, without reinitialising the session.
### `Session.update(changes)`
```kotlin theme={null}
fun update(changes: PayrailsUpdate)
```
Updates in-memory session state. No network request is made. The updated values take
effect on the next `pay()` call.
| Parameter | Type | Description |
| - | - | - |
| `changes` | `PayrailsUpdate` | Container of optional updates. Fields set to `null` are ignored. |
**Throws**
| Exception | When |
| - | - |
| `IllegalStateException` | Called after the session is destroyed. |
| `IllegalArgumentException` | `changes.amount` is non-null and `AmountUpdate.value` is not a valid positive number string. |
**Example**
```kotlin theme={null}
session.update(
PayrailsUpdate(
amount = AmountUpdate(value = "54.98", currency = "EUR")
)
)
cardPaymentButton.pay() // uses the updated amount
```
***
### `PayrailsUpdate`
```kotlin theme={null}
data class PayrailsUpdate(
val amount: AmountUpdate? = null
)
```
Container for session updates passed to `Session.update()`, in package
`com.payrails.sdk`. Each field is optional —
`null` means "leave unchanged". Additional fields may be added in future SDK versions
without breaking existing call sites.
| Field | Type | Description |
| - | - | - |
| `amount` | `AmountUpdate?` | New checkout amount, or `null` to leave unchanged. |
***
## Session Payment APIs
`Session` exposes a Google Pay capability check. Payment **execution** is not on `Session` —
it is public only via [`PayrailsPaymentLauncher`](#payrailspaymentlauncher).
### `Session.isGooglePayAvailable(context)`
```kotlin theme={null}
suspend fun isGooglePayAvailable(context: Context): Boolean
```
Returns `true` when Google Pay is configured for the session and the device reports
`isReadyToPay() == true`. Does not require a `GooglePayButton` to be rendered.
### Executing a payment
`Session` exposes **no public payment trigger**. Payment execution is a public API only
through [`PayrailsPaymentLauncher.authorize(...)`](#payrailspaymentlauncher), which owns the
`Activity`, the Google Pay sheet, and the 3DS / redirect Custom Tab, and handles frictionless
and step-up (3DS) outcomes uniformly. The SDK button components and the launcher delegate into
`Session`'s `internal` networking; merchants do not call it directly.
The terminal outcome of any `authorize(...)` is the public [`ActionResult`](#actionresult) type.
#### `ActionResult`
```kotlin theme={null}
sealed class ActionResult {
data object Success : ActionResult()
data class Failed(val failure: AuthorizationFailure) : ActionResult()
}
```
| Case | Meaning |
| - | - |
| `Success` | Payment authorized successfully. |
| `Failed` | The payment did not authorize; inspect `failure` for the coded reason. |
#### `AuthorizationFailure`
```kotlin theme={null}
data class AuthorizationFailure(
val code: AuthorizationFailureReason,
val message: String,
val rawError: Throwable? = null,
)
enum class AuthorizationFailureReason(val code: String) {
AUTHORIZATION_ERROR, AUTHENTICATION_ERROR, USER_CANCELLED, UNKNOWN_ERROR, VALIDATION_FAILED
}
```
| Code | Meaning |
| - | - |
| `AUTHORIZATION_ERROR` | Rejected by the backend (issuer decline, 3DS, fraud); `message` carries the backend detail. |
| `AUTHENTICATION_ERROR` | Session token rejected (HTTP 401 / 403); re-initialise the session. |
| `USER_CANCELLED` | The user closed the redirect Custom Tab / web view. |
| `UNKNOWN_ERROR` | Network, decode, polling timeout, or other unexpected error; `rawError` carries the underlying throwable when present. |
| `VALIDATION_FAILED` | Your own `onRequestStart` handler stopped the payment before it started. No authorization request was sent, so this is not a decline. |
### `Session.getPaymentMethodConfig(filter)`
```kotlin theme={null}
fun getPaymentMethodConfig(
filter: PaymentMethodFilter = PaymentMethodFilter.All
): List
```
Returns the payment method options configured for the active session, filtered by the
supplied `PaymentMethodFilter`. Returns an empty list when no options match (never
`null`).
#### `PaymentMethodFilter`
```kotlin theme={null}
sealed class PaymentMethodFilter {
object All : PaymentMethodFilter()
object Redirect : PaymentMethodFilter()
data class Specific(val paymentMethodCode: String) : PaymentMethodFilter()
}
```
| Filter | Effect |
| - | - |
| `PaymentMethodFilter.All` | All configured payment options (default) |
| `PaymentMethodFilter.Redirect` | Only redirect-capable options (iDEAL, Bancontact, Sofort, etc.). Replaces the removed `PayrailsQuery.AvailableRedirectMethods`. |
| `PaymentMethodFilter.Specific(code)` | Options matching the given payment method code (e.g., `"card"`, `"googlePay"`) |
**Example**
```kotlin theme={null}
val redirects = session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)
redirects.forEach { option ->
// option.paymentMethodCode, option.displayName
}
```
***
### `AmountUpdate`
```kotlin theme={null}
data class AmountUpdate(
val value: String,
val currency: String
)
```
Represents a new checkout amount. Both fields are required when updating the amount.
| Field | Type | Description |
| - | - | - |
| `value` | `String` | New amount as a numeric string (e.g. `"49.99"`). Must be a valid positive number. |
| `currency` | `String` | ISO 4217 currency code (e.g. `"EUR"`, `"USD"`). Passed to the backend without format validation — invalid codes cause payment errors at execution time. |
**Note on `value` format**
The SDK validates that `value` is parseable as a positive
number but does not reformat it. Pass the string in the exact format expected by your
backend (for example, `"49.99"`, not `"4999"`).
***
## Custom UI Payment Launcher
`PayrailsPaymentLauncher` lets you run a complete payment flow from your **own** button
while the SDK owns the post-tap work — opening the Google Pay sheet, the 3DS / PayPal /
redirect Custom Tab, and the polling. It is the public "bring-your-own-button" entry point —
and the only public way to execute a payment.
### `PayrailsPaymentLauncher`
```kotlin theme={null}
class PayrailsPaymentLauncher {
fun authorize(
type: PaymentMethod,
paymentMethodCode: String? = null,
saveInstrument: Boolean = false
)
fun authorize(storedInstrument: StoredInstrument)
fun authorize(
encryptedCardData: String,
saveInstrument: Boolean = false
)
}
```
Construct it with one of the factory functions below — never directly. Each `pay(...)`
call resolves to exactly one terminal [`ActionResult`](#actionresult), delivered to the
`onResult` callback supplied at construction (user dismissal of the sheet or Custom Tab is
reported as `Failed` with `code == USER_CANCELLED`).
| Method | Behaviour |
| - | - |
| `pay(type, paymentMethodCode?, saveInstrument?)` | Runs the method's full flow. `googlePay` opens the Google Pay sheet; `payPal` / `genericRedirect` open a Custom Tab. |
| `pay(storedInstrument)` | Charges a saved instrument; opens the 3DS Custom Tab if the instrument requires a challenge. |
| `pay(encryptedCardData, saveInstrument?)` | Submits an **already-encrypted** card payload and presents 3DS in a Custom Tab when required. No raw-card or encryption API — producing the encrypted string is the integrator's responsibility. |
### `rememberPayrailsPaymentLauncher(session, onResult)`
```kotlin theme={null}
@Composable
fun rememberPayrailsPaymentLauncher(
session: Session,
onResult: (ActionResult) -> Unit
): PayrailsPaymentLauncher
```
Compose factory. Returns a launcher that is stable across recompositions and registers the
Google Pay Activity Result contract for the current activity. Call it during composition
(e.g. at the top of your checkout composable).
### `Payrails.createPaymentLauncher(activity, session, onResult)`
```kotlin theme={null}
fun Payrails.createPaymentLauncher(
activity: ComponentActivity,
session: Session,
onResult: (ActionResult) -> Unit
): PayrailsPaymentLauncher
```
View / Activity factory. **Must be called from `Activity.onCreate`** — it registers a
Google Pay Activity Result contract, and the Android Activity Result API forbids
registration after the activity is `STARTED` (calling it later throws).
| Parameter | Type | Description |
| - | - | - |
| `activity` | `ComponentActivity` | The host activity; its result registry and lifecycle bind the launcher. |
| `session` | `Session` | The active session the launcher delegates networking into. |
| `onResult` | `(ActionResult) -> Unit` | Invoked once per `pay(...)` with the terminal outcome. |
**Example (Compose)**
```kotlin theme={null}
@Composable
fun CheckoutScreen(session: Session) {
var status by remember { mutableStateOf(null) }
val launcher = rememberPayrailsPaymentLauncher(session) { status = it }
Button(onClick = { launcher.authorize(PaymentMethod.googlePay) }) { Text("Pay with Google Pay") }
}
```
See [How to Build a Custom Pay Button](/docs/orchestration/checkout-sdks/android/how-to-custom-pay-button) for a full
dropdown-plus-single-button walkthrough.
***
## Core Concepts
### Payment Methods
`PaymentMethod` supports:
* `PaymentMethod.card`
* `PaymentMethod.googlePay`
* `PaymentMethod.payPal`
* `PaymentMethod.genericRedirect`
Card, Google Pay, and generic redirect flows are creatable via `Payrails` factories.
### Options
`Options` configures SDK behavior:
* `env: Env` — `Env.PRODUCTION` (default) or `Env.TEST`
* `collectMetadata: Boolean` — whether the SDK collects client context metadata (default `true`)
* `redirectSessionLifecycle: RedirectSessionLifecycle` — configures automatic session recovery
* `onRequestStart: RequestStartHandler?` — optional gate awaited before every authorization (default `null`)
### Pre-authorization gate (`onRequestStart`)
Optional handler invoked once per payment attempt, before the authorization request is sent and before any provider UI (wallet sheet, redirect Custom Tab) is presented. `RequestStartDecision.Proceed` lets the attempt continue; `RequestStartDecision.Refuse` stops it.
For Google Pay the handler is consulted the moment the button is tapped, before the Google Pay sheet opens — so a refusal means the customer never sees the sheet, and the handler runs before they have chosen a card. A check that depends on the basket (voucher, wallet balance, loyalty points) is unaffected; one that depends on the chosen card is not possible here.
```kotlin theme={null}
typealias RequestStartHandler = suspend (RequestStartContext) -> RequestStartDecision
sealed interface RequestStartDecision {
data object Proceed : RequestStartDecision
data class Refuse(val message: String? = null) : RequestStartDecision
}
data class RequestStartContext(
val executionId: String?,
val paymentMethodCode: String,
val action: Action,
) {
enum class Action(val code: String) { AUTHORIZE, TOKENIZE }
}
```
`Refuse.message`, when supplied, becomes `AuthorizationFailure.message` on the delivered `VALIDATION_FAILED`. It is passed through verbatim and is not displayed by the SDK.
The gate fires for every payment method configured on the session. A handler that only gates one method must return `Proceed` on the other branches.
`Action.TOKENIZE` is reserved: the tokenization flow is not gated in this version, so `action` is always `AUTHORIZE` today.
| Handler behaviour | Result |
| - | - |
| returns `Proceed` | Authorization proceeds |
| returns `Refuse(message)` | Attempt stopped; delegate receives `VALIDATION_FAILED` with `message` |
| returns `Refuse()` | Attempt stopped; delegate receives `VALIDATION_FAILED` with a generic description |
| does not return within 10 seconds | Attempt stopped with a generic description; a warning is logged |
| throws | Attempt stopped with a generic description; a warning is logged |
The timeout and throw cases do not carry the SDK's diagnostic into `failure.message`: both describe an integration fault rather than something phrased for a customer, and an exception string may carry internals. Only a deliberate `Refuse(message)` travels outward.
When the attempt is stopped, no authorization request is sent, the initiating button returns to `ButtonState.ENABLED`, and its delegate receives `onAuthorizeFailed(button, failure)` with `failure.code == VALIDATION_FAILED`.
Invoked on a background dispatcher, not the main thread — a suspending handler can make its network call directly, and anything touching UI must switch context itself. The 10-second bound can only interrupt a handler that *suspends*; one that blocks its thread runs to completion regardless, since coroutine cancellation is cooperative. Omitting the handler leaves the payment path fully synchronous — the SDK skips the gate rather than taking an asynchronous detour.
A Kotlin-only API, like `onSessionExpired`: suspend functions are not implementable from Java.
See [How to gate payment authorization](/docs/orchestration/how-to-gate-payment-authorization).
### Redirect Session Lifecycle (`onSessionExpired`)
`onSessionExpired` is configured once through `Options.redirectSessionLifecycle` and is shared for redirect-capable flows (3DS now, other redirect methods in future). The callback should run the merchant client-init call and return fresh `InitData`.
Behavior:
* Triggered when redirect reconciliation is classified as abandoned or stuck non-terminal.
* If callback is configured and succeeds, SDK refreshes active session/execution state automatically.
* If callback is missing/fails/returns invalid data, SDK does not continue the stale execution.
* Current payment attempt still resolves through standard failure handling (the `Failed` path).
### Client Context Metadata
The SDK collects client context and attaches it to authorize requests under `meta.clientContext` by default.
Collected fields:
* `osType` (android)
* `userAgent`
* `acceptHeader`
* `language`
* `screenHeight`
* `screenWidth`
* `colorDepth`
* `timeZoneOffset`
* `javaEnabled`
* `javaScriptEnabled`
Opt-out:
```kotlin theme={null}
val configuration = Configuration(
initData = initData,
option = Options(collectMetadata = false)
)
```
### Request Headers
The SDK attaches the following headers to every API request:
| Header | Value | Description |
| - | - | - |
| `x-client-version` | SDK version (e.g. `2.1.0`) | Automatically set from the version defined in `gradle.properties` |
| `x-client-type` | `android-sdk` | Identifies the SDK platform |
| `x-idempotency-key` | UUID | Unique per request, prevents duplicate processing |
| `Authorization` | `Bearer ` | Session token from init data |
The `x-client-version` value is generated at build time from `gradle.properties` (`sdk.version`) and compiled into the SDK as an internal constant. Merchants do not need to configure it.
## Logging
The SDK uses an internal logging system with two output channels:
* **In-memory log store** — Always active. The SDK maintains an in-memory buffer of the last 500 log entries (with timestamps). This is used by the built-in debug viewer (`DebugManager`).
* **Logcat** — Suppressed by default. Logcat output under the tag `PayrailsSDK` is gated behind `Log.isLoggable()`, so it produces no Logcat output unless explicitly enabled.
### Enabling Logcat Output
To enable SDK debug logs in Logcat, run:
```bash theme={null}
adb shell setprop log.tag.PayrailsSDK DEBUG
```
This persists until the device reboots. After enabling, filter Logcat by the `PayrailsSDK` tag:
```bash theme={null}
adb logcat -s PayrailsSDK:D
```
To disable again:
```bash theme={null}
adb shell setprop log.tag.PayrailsSDK INFO
```
### Default State
| Channel | Default | When active |
| - | - | - |
| In-memory (`LogStore`) | **Always on** | Used by debug viewer; last 500 entries |
| Logcat (`PayrailsSDK` tag) | **Off** | Only when `Log.isLoggable("PayrailsSDK", Log.DEBUG)` returns `true` |
### Security
The SDK never logs raw card data, tokens, or PII through its logging system. Log messages contain only operational information (e.g., payment flow steps, error descriptions).
## Card Payments
### CardForm + CardPaymentButton
Create a `CardForm`, then a `CardPaymentButton`. Elements are decoupled and composed explicitly.
**Kotlin/Compose only**
Element creation and `Render()` calls require Kotlin and Jetpack Compose. The delegate callbacks below are Java-compatible.
**Kotlin**
```kotlin theme={null}
val cardForm = Payrails.createCardForm(
config = CardFormConfig(
showCardHolderName = true,
showStoreInstrumentCheckbox = true
)
)
val payButton = Payrails.createCardPaymentButton(
translations = CardPaymenButtonTranslations(label = "Pay Now")
)
payButton.delegate = object : PayrailsCardPaymentButtonDelegate {
override fun onPaymentButtonClicked(button: CardPaymentButton) {}
override fun onAuthorizeSuccess(button: CardPaymentButton) {}
override fun onThreeDSecureChallenge(button: CardPaymentButton) {}
override fun onAuthorizeFailed(button: CardPaymentButton) {}
}
// In Compose
cardForm.Render()
payButton.Render()
```
**Java** — setting the delegate
```java theme={null}
payButton.setDelegate(new PayrailsCardPaymentButtonDelegate() {
@Override
public void onPaymentButtonClicked(CardPaymentButton button) {}
@Override
public void onAuthorizeSuccess(CardPaymentButton button) {}
@Override
public void onThreeDSecureChallenge(CardPaymentButton button) {}
@Override
public void onAuthorizeFailed(CardPaymentButton button) {}
});
```
Order does not matter. If `CardPaymentButton` is created before `CardForm`, creating `CardForm` later will auto-attach it to the current button (web-sdk-aligned behavior).
Behavior model (aligned with web-sdk):
* Form mode: if no stored instrument is selected, the button validates/collects card form data, then authorizes.
* Stored-instrument mode: if a stored instrument is selected, the button authorizes with that instrument and bypasses card form collection/validation.
* Form interaction reset: if the user focuses/edits card fields, the selected stored instrument is cleared and button behavior returns to form mode.
* Guardrail: if no stored instrument is selected and no card form is attached, payment does not proceed and the button emits authorization failure.
3DS redirect behavior:
* The SDK opens 3DS URLs in a Custom Tab when available, falling back to the system browser.
* On app return, the SDK polls execution status until terminal success/failure or expiry classification.
* If status remains non-terminal through reconciliation window, SDK emits authorization failure and runs `onSessionExpired` callback (if configured).
* On terminal success/failure, SDK performs best-effort Custom Tab close/return-to-app handling.
* Avoid using WebView for 3DS challenge pages (issuer/ACS compatibility issues, reduced isolation, unreliable deep-link returns).
#### CardForm Configuration
`CardFormConfig` controls visibility and behavior:
* `showCardHolderName: Boolean`
* `showStoreInstrumentCheckbox: Boolean`
* `showSingleExpiryDateField: Boolean`
* `layout: List>?`
* `alwaysStoreInstrument: Boolean`
* `defaultStoreInstrumentState: DefaultStoreInstrumentState` (`checked` | `unchecked`)
* `events: CardFormEvents?` (`onFocus`, `onChange`, `onReady`, `onSaveInstrumentCheckboxChanged`)
* `showCardIcon: Boolean` (renders built-in Payrails network icons and default field icons)
* `cardIconAlignment: CardIconAlignment` (`left` or `right`)
* `cardFieldIcons: CardFieldIcons?` (custom icon URLs per field; overrides defaults when provided)
* `styles: CardFormStylesConfig?`
* `translations: CardTranslations?`
Use `CardFormConfig.defaultConfig` as a baseline.
`Payrails.createCardForm(...)` reads save-instrument visibility only from `CardFormConfig.showStoreInstrumentCheckbox`; there is no separate standalone visibility argument on `createCardForm(...)`.
Migration rules:
* `defaultStoreInstrumentState` defaults to `unchecked`. Visibility no longer implies a checked state.
* `alwaysStoreInstrument = true` forces submission behavior to store the instrument regardless of toggle state.
#### Card Form Layout Options
`CardFormConfig` also supports label placement and field variant:
* `labelPlacement: LabelPlacement` — `FLOATING` (default) or `ABOVE`. When `ABOVE`, labels render as separate `Text` composables above each field instead of floating inside the text field.
* `labelSpacing: Dp?` — spacing between label and field when `labelPlacement = ABOVE`. Default: `4.dp`.
* `fieldVariant: FieldVariant` — `OUTLINED` (default) or `FILLED`. `OUTLINED` uses `OutlinedTextField` (full border); `FILLED` uses `TextField` (background fill with bottom indicator).
#### Card Form Styles
`CardFormStylesConfig` lets you style the wrapper, fields, labels, and errors. Key types:
* `CardWrapperStyle`
* `CardFieldSpecificStyles`
* `CardStyle` (alias of `Style`)
#### Card Form Spacing Tokens
`CardFormStylesConfig` exposes spacing tokens that replace previously hardcoded layout values:
| Property | Type | Default | Description |
| - | - | - | - |
| `rowSpacing` | `Dp?` | `12.dp` | Vertical spacing between form rows |
| `fieldSpacing` | `Dp?` | `12.dp` | Horizontal spacing between fields in a row |
| `errorSpacing` | `Dp?` | `4.dp` | Spacing between a field and its error message (only rendered when an error is present) |
| `contentPadding` | `PaddingValues?` | `PaddingValues(16.dp)` | Outer padding of the card form |
| `fieldHeight` | `Dp?` | wrap content | Explicit height for text fields |
All tokens are nullable. When `null`, the hardcoded default is used.
#### Card Translations
`CardTranslations` provides:
* `placeholders: CardTranslations.Placeholders`
* `labels: CardTranslations.Labels`
* `error: CardTranslations.ErrorMessages`
Each map is keyed by `CardFieldType` (alias of `ElementType`).
#### Network-Aware State
The card form exposes network-aware metadata derived from the card number:
* `cardNetwork: CardNetwork` (visa, mastercard, amex, discover, unknown)
* `bin: String` (policy-driven: first 6 or 8 digits based on card network and PAN length; empty until sufficient digits are entered)
* `cvvMaxLength: Int`
Field accessory behavior:
* **Card number**: when `showCardIcon` is enabled, the SDK shows the detected network icon (e.g., Visa, Mastercard). When no network is detected, a default card icon is shown (`ic-card.png`). Card number does not switch to the clear affordance.
* **CVV**: when `showCardIcon` is enabled, the field shows a default CVV icon (`ic-cvv.png`) while empty. When populated, it shows a clear affordance regardless of `showCardIcon`.
* **Expiry date**: when `showCardIcon` is enabled, the field shows a default expiration icon (`ic-expiration.png`) while empty. When populated, it shows a clear affordance regardless of `showCardIcon`.
* **Split expiry month/year**: when `showCardIcon` is enabled, both fields show the default expiration icon while empty. When populated, each field switches to a clear affordance independently regardless of `showCardIcon`.
* **Cardholder name**: has no default empty-state icon, but still shows a clear affordance while populated regardless of `showCardIcon`. Merchants can add a custom empty-state icon via `cardFieldIcons.cardholderName` when `showCardIcon` is enabled.
All default icons are loaded from the Payrails assets CDN. Merchants can override any default icon — or add a cardholder name icon — by providing custom URLs via `cardFieldIcons`:
```kotlin theme={null}
CardFormConfig(
showCardIcon = true,
cardFieldIcons = CardFieldIcons(
cardholderName = "https://example.com/my-name.png", // adds a custom cardholder icon (no default)
cardNumber = "https://example.com/my-card.png", // replaces default card icon (network icons still show when detected)
cvv = "https://example.com/my-cvv.png", // replaces default CVV icon
expiryDate = "https://example.com/my-expiry.png" // replaces default expiry icon
)
)
```
`CardFieldIcons` fields:
* `cardholderName: String?` — custom icon URL for the empty cardholder name field when `showCardIcon` is enabled. No default icon exists; when populated, the field shows the clear affordance instead.
* `cardNumber: String?` — custom icon URL for the card number field when no network is detected. Once a network is detected, the network icon takes precedence.
* `cvv: String?` — custom empty-state icon URL for the CVV field when `showCardIcon` is enabled.
* `expiryDate: String?` — custom empty-state icon URL for the expiration date field(s), including split expiry month/year fields, when `showCardIcon` is enabled.
Icon placement follows `cardIconAlignment` (`left` or `right`).
BIN length policy (PCI SSC FAQ 1091 aligned):
* 15-digit PANs (Amex): 6-digit BIN
* 16-digit PANs (Visa/Mastercard/Discover): 8-digit BIN
* 17–19 digit PANs or unknown networks: 6-digit BIN
Migration note: if your integration assumes a fixed 8-digit BIN, update routing/lookup logic to accept 6-digit BINs when provided. Do not infer or pad BIN length; rely on the SDK's `bin` value.
#### Card Payment Button Translations
`CardPaymenButtonTranslations(label: String?)` sets the pay button text.
#### Card Payment Button Styling
Button appearance is configured on the button component (for example via `createCardPaymentButton(buttonStyle = ...)`). `CardFormStylesConfig` no longer includes button styling.
Migration note: If you previously set `CardFormStylesConfig.buttonStyle`, move that styling to the button API (`createCardPaymentButton(buttonStyle = ...)`).
Migration note: `CardPaymentButton` no longer exposes instrument-specific style/translation buckets. Keep one button settings surface and configure stored-instrument list/item UI in `StoredInstrumentsStyle` / `StoredInstrumentsTranslations`.
`CardButtonStyle` is applied in `CardPaymentButton.Render` for:
* `backgroundColor`, `textColor`
* `cornerRadius`
* `borderWidth`, `borderColor`
* `contentPadding`
* `font`
* `height`, `fillMaxWidth`
* `disabledStyle`, `loadingStyle` (state-variant styles)
* `loadingIndicatorColor`
Additional design tokens:
| Property | Type | Default | Description |
| - | - | - | - |
| `loadingIndicatorSize` | `Dp?` | `20.dp` | Diameter of the loading spinner |
| `loadingIndicatorStrokeWidth` | `Dp?` | `2.dp` | Stroke width of the loading spinner |
| `elevation` | `Dp?` | Material3 default | Button elevation (shadow depth) |
| `opacity` | `Float?` | `1.0f` | Button opacity (0.0 = transparent, 1.0 = opaque) |
| `minHeight` | `Dp?` | Material3 default (`40.dp`) | Minimum button height |
These tokens participate in state-variant merging (`disabledStyle`, `loadingStyle`).
### Stored Instrument Card Button
Single instrument mapping:
```kotlin theme={null}
val cardForm = Payrails.createCardForm()
val button = Payrails.createCardPaymentButton(
translations = CardPaymenButtonTranslations("Pay")
)
button.setStoredInstrument(instrument)
```
### Coupled Wrappers (Breaking Change)
* `CardPaymentForm` has been removed from the element API.
* `StoredInstrumentPaymentButton` has been removed. Use `CardPaymentButton` + `setStoredInstrument(...)`.
* The `createCardPaymentButton(storedInstrument = ...)` overload has been removed. Create the button first, then call `setStoredInstrument(...)`.
* `createStoredInstruments()` and `createStoredInstrumentView()` have been removed. Use `session.getStoredInstruments()` to retrieve instruments and `CardPaymentButton.setStoredInstrument(...)` to pay with them.
* Migrate to explicit composition with `CardForm` + `CardPaymentButton`.
## Card Tokenization
Save a card to the Payrails vault without initiating a payment. The tokenize flow encrypts card data using CSE, POSTs it to the vault endpoint from `vaultConfiguration.links.saveInstrument`, and returns a `SaveInstrumentResponse` containing an instrument ID for future use.
**Requires vault configuration**
The session init payload must include `vaultConfiguration` with a `saveInstrument` link. Contact Payrails to enable this for your merchant account.
### `Session.tokenize(request, options?)` — suspend
Saves a reusable payment instrument from a payment method without running a payment. Mirrors the iOS SDK's `session.tokenize(...)`.
```kotlin theme={null}
suspend fun Session.tokenize(
request: TokenizationRequest,
options: TokenizeOptions = TokenizeOptions(),
): SaveInstrumentResponse
```
**Parameters**
| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `request` | `TokenizationRequest` | Yes | — | Selects what to tokenize (e.g. `TokenizationRequest.Card(cardForm)`) |
| `options` | `TokenizeOptions` | No | `TokenizeOptions()` | Controls storage behavior and signals intended future use to the card network |
**Returns**: `SaveInstrumentResponse` — the saved instrument record including `id`, `status`, and card metadata.
**Throws**
* `PayrailsError.invalidCardData` — card form validation failed; field errors are shown automatically on the form
* `PayrailsError.missingData("Vault configuration with providerConfigId is required...")` — session init payload is missing `vaultConfiguration`
* `PayrailsError.missingData("holderReference is required...")` — session init payload is missing `holderReference`
> Defined as a `Session` extension in the `checkout` (UI) module. Import it from `com.payrails.sdk`; the call site is `session.tokenize(...)`.
**Example**
```kotlin theme={null}
val response = session.tokenize(
TokenizationRequest.Card(cardForm),
TokenizeOptions(storeInstrument = true, futureUsage = FutureUsage.CardOnFile),
)
val instrumentId = response.id
```
***
### `Session.tokenize(request, options?, onSuccess, onFailed, onCancelled)` — callback
Callback variant for callers not using coroutines (and Java callers). Exactly one of the three callbacks is invoked, exactly once, on the main thread.
```kotlin theme={null}
fun Session.tokenize(
request: TokenizationRequest,
options: TokenizeOptions = TokenizeOptions(),
onSuccess: (SaveInstrumentResponse) -> Unit,
onFailed: (PayrailsError) -> Unit,
onCancelled: () -> Unit,
)
```
**Callbacks**
| Name | Invoked when |
| - | - |
| `onSuccess(SaveInstrumentResponse)` | The instrument was saved |
| `onFailed(PayrailsError)` | Tokenization failed; receives a `PayrailsError` (non-`PayrailsError` causes are wrapped in `PayrailsError.unknown`) |
| `onCancelled()` | The operation was cancelled (coroutine/session-scope cancellation). The card path has no user-cancellable UI step, so `onCancelled` fires only on cancellation — not on validation or network failures |
**Example**
```kotlin theme={null}
session.tokenize(
request = TokenizationRequest.Card(cardForm),
onSuccess = { instrument -> /* instrument.id */ },
onFailed = { error -> /* show error */ },
onCancelled = { /* cancelled */ },
)
```
***
### `TokenizationRequest`
Sealed type selecting what to tokenize. Card and Google Pay are supported (mirroring the iOS SDK's `card` / `applePay` cases).
```kotlin theme={null}
sealed interface TokenizationRequest {
data class Card(val cardForm: CardForm) : TokenizationRequest
data class GooglePay(val presenter: GooglePayPresenter) : TokenizationRequest
}
```
| Case | Description |
| - | - |
| `Card(cardForm)` | Tokenize the card currently entered in `cardForm` |
| `GooglePay(presenter)` | Tokenize via Google Pay. `presenter` is obtained from `rememberGooglePayPresenter(session)` |
***
### `GooglePayPresenter` / `rememberGooglePayPresenter(session)`
`GooglePayPresenter` is an opaque handle that drives the Google Pay sheet for `tokenize(TokenizationRequest.GooglePay(...))`. It is obtained inside a Composable, which registers the Google Pay Activity-result launcher before the host reaches STARTED (Android requires this — a Google Pay token cannot be acquired from a plain suspend call).
```kotlin theme={null}
interface GooglePayPresenter
@Composable
fun rememberGooglePayPresenter(session: Session): GooglePayPresenter
```
**Returns**: a `GooglePayPresenter` bound to the current composition.
**Example**
```kotlin theme={null}
val presenter = rememberGooglePayPresenter(session)
// later, from a coroutine:
val response = session.tokenize(
TokenizationRequest.GooglePay(presenter),
TokenizeOptions(storeInstrument = true),
)
```
***
### `GooglePayTokenizeButton(...)`
A compliant, Google-branded button that tokenizes via Google Pay (save, no charge). Wraps `rememberGooglePayPresenter` + `Session.tokenize`. Renders **only when Google Pay is available** for the session (config present and `isReadyToPay` succeeds). Exactly one of `onSuccess`/`onFailed`/`onCancelled` fires per tap, on the main thread.
```kotlin theme={null}
@Composable
fun GooglePayTokenizeButton(
session: Session,
options: TokenizeOptions = TokenizeOptions(),
style: GooglePayButtonStyle = GooglePayButtonStyle(),
modifier: Modifier = Modifier,
onSuccess: (SaveInstrumentResponse) -> Unit,
onFailed: (PayrailsError) -> Unit,
onCancelled: () -> Unit,
)
```
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `session` | `Session` | Yes | — | Active session |
| `options` | `TokenizeOptions` | No | `TokenizeOptions()` | Storage behavior + future-usage signal |
| `style` | `GooglePayButtonStyle` | No | `GooglePayButtonStyle()` | Button theme/type/corner styling |
| `modifier` | `Modifier` | No | `Modifier` | Compose modifier |
| `onSuccess` | `(SaveInstrumentResponse) -> Unit` | Yes | — | Instrument saved |
| `onFailed` | `(PayrailsError) -> Unit` | Yes | — | Tokenization failed |
| `onCancelled` | `() -> Unit` | Yes | — | User dismissed the sheet / cancelled |
**Example**
```kotlin theme={null}
GooglePayTokenizeButton(
session = session,
options = TokenizeOptions(storeInstrument = true),
onSuccess = { instrument -> /* instrument.id */ },
onFailed = { error -> /* show error */ },
onCancelled = { /* dismissed */ },
)
```
***
### `CardForm.tokenize(options?)`
Convenience wrapper that tokenizes this form via `Session.tokenize`. Equivalent to `session.tokenize(TokenizationRequest.Card(cardForm), options)`.
```kotlin theme={null}
suspend fun tokenize(options: TokenizeOptions = TokenizeOptions()): SaveInstrumentResponse
```
**Parameters**
| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `options` | `TokenizeOptions` | No | `TokenizeOptions()` | Controls storage behavior and signals intended future use to the card network |
**Returns**: `SaveInstrumentResponse` — the saved instrument record including `id`, `status`, and card metadata.
**Throws**
* `PayrailsError.invalidCardData` — card form validation failed; field errors are shown automatically on the form
* `PayrailsError.missingData("Session is required for tokenization.")` — `CardForm` was created without an active session
* `PayrailsError.missingData("Vault configuration with providerConfigId is required...")` — session init payload is missing `vaultConfiguration`
* `PayrailsError.missingData("holderReference is required...")` — session init payload is missing `holderReference`
**Example**
```kotlin theme={null}
val response = cardForm.tokenize(
options = TokenizeOptions(
storeInstrument = true,
futureUsage = FutureUsage.CardOnFile
)
)
val instrumentId = response.id
```
***
### `TokenizeOptions`
Options controlling card vault behavior during tokenization.
```kotlin theme={null}
data class TokenizeOptions(
val storeInstrument: Boolean = false,
val futureUsage: FutureUsage = FutureUsage.CardOnFile
)
```
| Property | Type | Default | Description |
| - | - | - | - |
| `storeInstrument` | `Boolean` | `false` | Whether to persist the instrument for repeated future use after tokenization |
| `futureUsage` | `FutureUsage` | `FutureUsage.CardOnFile` | Signals the intended future use of this card to the card network |
***
### `FutureUsage`
Enum signaling intended future card use to the card network. Sent as part of the vault request.
```kotlin theme={null}
enum class FutureUsage(val value: String) {
CardOnFile("CardOnFile"),
Subscription("Subscription"),
UnscheduledCardOnFile("UnscheduledCardOnFile")
}
```
| Value | Description |
| - | - |
| `CardOnFile` | Customer-initiated payments where the cardholder is present at checkout |
| `Subscription` | Recurring charges on a fixed schedule authorized by the cardholder |
| `UnscheduledCardOnFile` | Merchant-initiated charges with no fixed schedule (e.g., top-ups, threshold billing) |
***
### `SaveInstrumentResponse`
Response returned by `tokenize()`. Represents the saved instrument record from the vault.
```kotlin theme={null}
@Serializable
data class SaveInstrumentResponse(
val id: String,
val createdAt: String,
val holderId: String,
val paymentMethod: String,
val status: String,
val data: InstrumentResponseData,
val fingerprint: String? = null,
val futureUsage: String? = null
)
```
| Property | Type | Nullable | Description |
| - | - | - | - |
| `id` | `String` | No | Instrument identifier; use for future payments and instrument management |
| `createdAt` | `String` | No | ISO 8601 timestamp of instrument creation |
| `holderId` | `String` | No | Payrails holder ID associated with this instrument |
| `paymentMethod` | `String` | No | Always `"card"` for card tokenization |
| `status` | `String` | No | Instrument lifecycle status (e.g., `"active"`) |
| `data` | `InstrumentResponseData` | No | Card presentation data (BIN, suffix, network, etc.) |
| `fingerprint` | `String?` | Yes | Card fingerprint for deduplication across instruments |
| `futureUsage` | `String?` | Yes | Future use value as recorded by the vault |
#### `SaveInstrumentResponse.InstrumentResponseData`
| Property | Type | Nullable | Description |
| - | - | - | - |
| `bin` | `String?` | Yes | Card BIN (first digits, per PCI SSC BIN length policy) |
| `binLookup` | `BinLookup?` | Yes | Enriched BIN metadata including network, issuer, and issuer country when available |
| `suffix` | `String?` | Yes | Card last digits |
| `network` | `String?` | Yes | Card network identifier (e.g., `"visa"`, `"mastercard"`) |
| `holderName` | `String?` | Yes | Cardholder name as entered in the card form |
| `expiryMonth` | `String?` | Yes | Two-digit expiry month |
| `expiryYear` | `String?` | Yes | Two or four-digit expiry year |
***
### `InstrumentAPIResponse.Save`
The `Save` variant of the `InstrumentAPIResponse` sealed class, returned when using lower-level instrument API calls. `CardForm.tokenize()` returns `SaveInstrumentResponse` directly rather than wrapping it.
```kotlin theme={null}
sealed class InstrumentAPIResponse {
data class Delete(val response: DeleteInstrumentResponse) : InstrumentAPIResponse()
data class Update(val response: UpdateInstrumentResponse) : InstrumentAPIResponse()
data class Save(val response: SaveInstrumentResponse) : InstrumentAPIResponse()
}
```
***
## Google Pay
### GooglePayButton
Create a `GooglePayButton` after initializing a session. The button handles Google Pay availability checks, payment sheet presentation, and authorization automatically.
**Kotlin/Compose only**
Element creation and `Render()` require Kotlin and Jetpack Compose. The delegate callbacks below are Java-compatible.
**Kotlin**
```kotlin theme={null}
val googlePayButton = Payrails.createGooglePayButton(
style = GooglePayButtonStyle(),
translations = GooglePayButtonTranslations(),
showStoreInstrumentCheckbox = false
)
googlePayButton.delegate = object : PayrailsGooglePayButtonDelegate {
override fun onGooglePayAvailable(button: GooglePayButton) {
// Google Pay is supported on this device
}
override fun onPaymentButtonClicked(button: GooglePayButton) {
// User tapped the Google Pay button
}
override fun onAuthorizeSuccess(button: GooglePayButton) {
// Payment succeeded
}
override fun onThreeDSecureChallenge(button: GooglePayButton) {
// 3DS challenge started
}
override fun onAuthorizeFailed(button: GooglePayButton) {
// Payment failed
}
}
// In Compose
googlePayButton.Render()
```
**Java** — setting the delegate
```java theme={null}
googlePayButton.setDelegate(new PayrailsGooglePayButtonDelegate() {
@Override
public void onGooglePayAvailable(GooglePayButton button) {}
@Override
public void onPaymentButtonClicked(GooglePayButton button) {}
@Override
public void onAuthorizeSuccess(GooglePayButton button) {}
@Override
public void onThreeDSecureChallenge(GooglePayButton button) {}
@Override
public void onAuthorizeFailed(GooglePayButton button) {}
});
```
The button is hidden by default and becomes visible only after `isReadyToPay()` succeeds. The `onGooglePayAvailable` callback fires when the button becomes visible.
#### Google Pay Configuration
Google Pay configuration is provided by the Payrails backend through `paymentCompositionOptions`. The SDK reads `clientConfig.additionalConfig` for the Google Pay payment option, which contains:
* `apiVersion` / `apiVersionMinor` — Google Pay API version (defaults to `2` / `0`)
* `allowedPaymentMethods` — Payment methods array including `tokenizationSpecification` (gateway configuration)
* `merchantInfo` — Merchant identifier and name (required in production)
* `emailRequired` — Whether to request the payer's email
* `shippingAddressRequired` / `shippingAddressParameters` — Shipping address collection
The `countryCode` for `transactionInfo` is extracted from `meta.order.billingAddress.country.code` in the execution metadata.
No client-side configuration of gateway credentials is needed. The `PAYMENT_GATEWAY`, `gateway`, and `gatewayMerchantId` values are provided by the backend.
#### GooglePayButtonStyle
`GooglePayButtonStyle` controls the button appearance:
| Property | Type | Default | Description |
| - | - | - | - |
| `buttonTheme` | `ButtonTheme` | `Dark` | Google Pay button theme (`Dark` or `Light`) |
| `buttonType` | `ButtonType` | `Pay` | Button label type (`Buy`, `Pay`, `Checkout`, `Donate`, `Order`, `Plain`, `Subscribe`) |
| `cornerRadius` | `Dp` | `100.dp` | Button corner radius |
| `containerPadding` | `PaddingValues?` | `null` | Padding around the button container |
| `containerBackgroundColor` | `Color?` | `null` | Background color behind the button |
#### GooglePayButtonTranslations
`GooglePayButtonTranslations` provides:
* `storeInstrument: String?` — Checkbox label text when `showStoreInstrumentCheckbox` is enabled. Defaults to `"Save this payment method"`.
#### PayrailsGooglePayButtonDelegate
| Callback | When it fires |
| - | - |
| `onGooglePayAvailable(button)` | `isReadyToPay()` succeeded; button is now visible |
| `onPaymentButtonClicked(button)` | User tapped the button; payment sheet is about to open |
| `onAuthorizeSuccess(button)` | Payment authorized successfully |
| `onThreeDSecureChallenge(button)` | 3DS challenge started (Custom Tab opened) |
| `onAuthorizeFailed(button, failure)` | Payment failed or was rejected |
| `onStateChanged(button, state)` | Button state changed (`ENABLED`, `DISABLED`, `LOADING`). Optional — default no-op. |
#### Store Instrument Checkbox
Pass `showStoreInstrumentCheckbox = true` to `createGooglePayButton(...)` to render a checkbox below the Google Pay button. When checked, the payment token is stored as an instrument for future use.
```kotlin theme={null}
val googlePayButton = Payrails.createGooglePayButton(
showStoreInstrumentCheckbox = true,
translations = GooglePayButtonTranslations(storeInstrument = "Save for next time")
)
```
#### 3DS for Google Pay
If the authorize response requires 3DS verification, the SDK opens the challenge URL in a Custom Tab and polls for the terminal result, following the same 3DS flow used for card payments. The `onThreeDSecureChallenge` delegate callback fires when this happens.
## PayPal
### PayPalButton
Create a `PayPalButton` after initializing a session. The button handles PayPal redirect presentation, polling for results, and authorization automatically.
**Kotlin/Compose only**
Element creation and `Render()` require Kotlin and Jetpack Compose. The delegate callbacks below are Java-compatible.
**Kotlin**
```kotlin theme={null}
val payPalButton = Payrails.createPayPalButton(
style = PayPalButtonStyle(height = 45.dp, width = 200.dp),
translations = PayPalButtonTranslations()
)
payPalButton.delegate = object : PayrailsPayPalButtonDelegate {
override fun onPaymentButtonClicked(button: PayPalButton) {
// User tapped the PayPal button
}
override fun onAuthorizeSuccess(button: PayPalButton) {
// Payment succeeded
}
override fun onAuthorizeFailed(button: PayPalButton) {
// Payment failed or was rejected
}
override fun onPaymentSessionExpired(button: PayPalButton) {
// Session is no longer usable — fetch a new init payload and call Payrails.createSession()
}
}
// In Compose
payPalButton.Render()
```
#### PayPalButtonStyle
| Property | Type | Default | Description |
| - | - | - | - |
| `height` | `Dp` | `45.dp` | Button height. Clamped to the PayPal-sanctioned range of 30–55 dp. |
| `width` | `Dp?` | `null` | Fixed button width. When `null`, width is controlled by the `modifier` passed to `Render()` (e.g. `Modifier.fillMaxWidth()`). |
#### PayPalButtonTranslations
| Property | Type | Default | Description |
| - | - | - | - |
| `contentDescription` | `String?` | `"PayPal"` | Accessibility content description for the button, read by TalkBack. |
#### PayrailsPayPalButtonDelegate
| Callback | Required | When it fires |
| - | - | - |
| `onPaymentButtonClicked(button)` | No | User tapped the button; payment flow is starting. Default no-op. |
| `onAuthorizeSuccess(button)` | No | Payment authorized successfully. Default no-op. |
| `onAuthorizeFailed(button, failure)` | No | Payment failed, was rejected, or an error occurred. Default no-op. |
| `onPaymentSessionExpired(button)` | **Yes** | Session is no longer usable — fires after cancel, and also after `authorizeFailed` when the session expired during the payment attempt. Must fetch fresh init data and call `Payrails.createSession()`. |
| `onCancelled(button)` | No | User explicitly cancelled in the PayPal browser flow. Fires before `onPaymentSessionExpired` on explicit cancellation. Default no-op. |
| `onStateChanged(button, state)` | No | Button state changed (`LOADING` when payment starts, `ENABLED` when complete). Default no-op. |
Only `onPaymentSessionExpired` has no default implementation and must be provided — the session cannot be reused after a PayPal cancellation or expiry, so the merchant must handle this to allow retries.
## Redirect Payments
### GenericRedirectButton
Create a `GenericRedirectButton` for redirect-based payment methods (iDEAL, Bancontact, Sofort, etc.). Use `Session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)` to discover which methods are configured.
**Kotlin/Compose only**
Element creation and `Render()` require Kotlin and Jetpack Compose. The delegate callbacks below are Java-compatible.
### Discovering Available Redirect Methods
```kotlin theme={null}
val redirectMethods: List =
session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)
```
Returns an empty list if no redirect methods are configured. See
[`Session.getPaymentMethodConfig(filter)`](#session-getpaymentmethodconfig-filter) for the
full filter API.
#### PayrailsPaymentOption
| Property | Type | Nullable | Description |
| - | - | - | - |
| `paymentMethodCode` | `String` | No | Payment method identifier (e.g., `"iDeal"`, `"bancontact"`) |
| `displayName` | `String?` | Yes | Human-readable label from the backend |
### Creating a Redirect Button
```kotlin theme={null}
val button = Payrails.createGenericRedirectButton(
translations = CardPaymenButtonTranslations(label = "Pay with iDEAL"),
paymentMethodCode = "iDeal",
buttonStyle = CardButtonStyle(/* optional styling */)
)
button.delegate = object : GenericRedirectPaymentButtonDelegate {
override fun onPaymentButtonClicked(button: GenericRedirectButton) {}
override fun onAuthorizeSuccess(button: GenericRedirectButton) {}
override fun onAuthorizeFailed(button: GenericRedirectButton) {}
override fun onPaymentSessionExpired(button: GenericRedirectButton) {}
override fun onStateChanged(button: GenericRedirectButton, state: ButtonState) {}
}
// In Compose
button.Render()
```
**Java** — setting the delegate
```java theme={null}
button.setDelegate(new GenericRedirectPaymentButtonDelegate() {
@Override
public void onPaymentButtonClicked(GenericRedirectButton button) {}
@Override
public void onAuthorizeSuccess(GenericRedirectButton button) {}
@Override
public void onAuthorizeFailed(GenericRedirectButton button) {}
// onPaymentSessionExpired and onStateChanged have default no-ops
});
```
#### GenericRedirectPaymentButtonDelegate
| Callback | When it fires |
| - | - |
| `onPaymentButtonClicked(button)` | User tapped the button; redirect flow is starting |
| `onAuthorizeSuccess(button)` | Payment authorized successfully |
| `onAuthorizeFailed(button, failure)` | Payment failed, was rejected, or an error occurred |
| `onCancelled(button)` | User explicitly cancelled the redirect (e.g., pressed back in the browser). Optional — default no-op. |
| `onPaymentSessionExpired(button)` | Redirect was abandoned without reaching a terminal status, triggering session recovery. Optional — default no-op. |
| `onStateChanged(button, state)` | Button state changed (`LOADING` when payment starts, `ENABLED` when complete). Optional — default no-op. |
#### Redirect Flow Behavior
When the user taps the button, the SDK:
1. Opens the payment provider's redirect URL in a Custom Tab (falls back to system browser)
2. Polls the execution status in the background until terminal success/failure
3. Delivers the result through delegate callbacks
The redirect flow shares the same polling and session recovery infrastructure as card 3DS and Google Pay 3DS redirects. If the redirect is abandoned or remains non-terminal, or the provider reports an authentication/session-expiry error, the `onPaymentSessionExpired` callback (if implemented) is invoked for automatic recovery.
#### Button Styling
`GenericRedirectButton` accepts the same `CardButtonStyle` and `CardPaymenButtonTranslations` as `CardPaymentButton`. See [Card Payment Button Styling](#card-payment-button-styling) for available properties.
## Stored Instruments
### StoredInstrument Interface
Each `StoredInstrument` exposes:
| Property | Type | Description |
| - | - | - |
| `id` | `String` | Instrument identifier |
| `email` | `String?` | Email (PayPal instruments) |
| `description` | `String?` | Legacy display string (`"bin***suffix"` for cards, email for PayPal) |
| `type` | `PaymentMethod` | Instrument payment method |
| `displayName` | `String?` | Server-provided display name (cards: custom name; PayPal: email) |
| `isDefault` | `Boolean` | Whether this is the holder's default instrument |
| `cardMetadata` | `CardInstrumentMetadata?` | Typed card details (non-null for cards, null for other types) |
The SDK maps the backend instrument field `default` to `StoredInstrument.isDefault`.
#### CardInstrumentMetadata
Public data class with typed card presentation data:
| Property | Type | Description |
| - | - | - |
| `bin` | `String?` | Card BIN (first digits) |
| `suffix` | `String?` | Card last digits |
| `network` | `String?` | Card network (e.g., `"visa"`, `"mastercard"`) |
| `issuerCountry` | `String?` | Issuing country code |
Migration note: prefer `displayName` and `cardMetadata` over parsing the `description` string. `description` remains available for backwards compatibility.
### Retrieving Stored Instruments
Use `session.getStoredInstruments()` to retrieve available instruments, then set one on the button:
**Kotlin**
```kotlin theme={null}
val instruments = session.getStoredInstruments()
val cardInstruments = session.getStoredInstruments(forType = PaymentMethod.card)
// Set a stored instrument on the card payment button
payButton.setStoredInstrument(cardInstruments.first())
```
**Java**
```java theme={null}
List instruments = session.getStoredInstruments(null);
List cardInstruments = session.getStoredInstruments(PaymentMethod.card);
payButton.setStoredInstrument(cardInstruments.get(0));
```
### Instrument Management APIs
Use typed methods on `Session` to delete or update stored instruments:
```kotlin theme={null}
// Delete an instrument
val deleteResult: InstrumentAPIResponse.Delete = session.deleteInstrument(instrumentId)
// Update an instrument
val updateResult: InstrumentAPIResponse.Update = session.updateInstrument(
instrumentId,
UpdateInstrumentBody(default = true)
)
```
The return type is `InstrumentAPIResponse`, a sealed class with `Delete`, `Update`, and `Save` variants (capitalized). See [Card Tokenization](#card-tokenization) for the `Save` variant and the `tokenize()` method.
## Results and Errors
Payment results are surfaced through delegate interfaces:
**Card payments** — `PayrailsCardPaymentButtonDelegate`:
* `onAuthorizeSuccess` — payment succeeded
* `onAuthorizeFailed` — payment failed (includes session-expiry outcomes from redirect reconciliation)
* `onThreeDSecureChallenge` — 3DS challenge started
* `onPaymentButtonClicked` — button was tapped
**Google Pay** — `PayrailsGooglePayButtonDelegate`:
* `onAuthorizeSuccess` — payment succeeded
* `onAuthorizeFailed` — payment failed
* `onThreeDSecureChallenge` — 3DS challenge started
* `onPaymentButtonClicked` — button was tapped
* `onGooglePayAvailable` — Google Pay is ready on this device
**PayPal** — `PayrailsPayPalButtonDelegate`:
* `onAuthorizeSuccess` — payment authorized
* `onAuthorizeFailed` — payment failed or was rejected
* `onCancelled` — user explicitly cancelled in the browser
* `onPaymentSessionExpired` — session expired and must be refreshed before retry (**required**)
* `onPaymentButtonClicked` — button was tapped
**Generic Redirect** — `GenericRedirectPaymentButtonDelegate`:
* `onAuthorizeSuccess` — payment authorized
* `onAuthorizeFailed` — payment failed or was rejected
* `onCancelled` — user explicitly cancelled the redirect
* `onPaymentSessionExpired` — redirect was abandoned or session expired
* `onPaymentButtonClicked` — button was tapped
`Failed` includes session-expiry outcomes from redirect reconciliation (for example browser abandoned/non-terminal timeout), including cases where automatic `onSessionExpired` recovery is attempted.
Errors are represented by `PayrailsError` (e.g., `authenticationError`, `missingData`).
***
## Co-Branded Cards
Support for cards carrying two networks (a local scheme like `mada` plus an international one like `mastercard`). Activates only when the session is co-branded-enabled (backend `featureConfig.coBrandedCardsRollout` flag + a `binLookup` link). See the [how-to](/docs/orchestration/checkout-sdks/android/how-to-co-branded-cards) and the [concept](/docs/orchestration/checkout-sdks/android/sdk-concepts#co-branded-cards).
### `Session.binLookup(bin)`
```kotlin theme={null}
suspend fun binLookup(bin: String): BinLookupResponse?
```
One-shot BIN lookup against the session's `binLookup` endpoint. **Non-throwing**: returns `null` on invalid input (not 6-8 digits), an inactive session, a missing endpoint, or a network error (coroutine cancellation propagates). No throttling or caching — the card form's internal as-you-type lookup handles that.
```kotlin theme={null}
val result = session.binLookup("529741")
if (result?.localNetwork != null) { /* co-branded */ }
```
### `BinLookupResponse`
| Field | Type | Notes |
| - | - | - |
| `bin` | `String` | The looked-up BIN |
| `network` | `String` | Canonical international scheme, e.g. `"mastercard"` |
| `localNetwork` | `String?` | Co-branded local scheme, e.g. `"mada"` |
| `issuer` | `String?` | Issuing bank |
| `issuerCountry` | `IssuerCountry?` | Issuer country |
| `type` | `String?` | e.g. `"credit"` / `"debit"` |
### `IssuerCountry`
| Field | Type |
| - | - |
| `code` | `String` |
| `name` | `String?` |
| `iso3` | `String?` |
### `CardFormEvents.onPreferredSchemeChanged`
```kotlin theme={null}
val onPreferredSchemeChanged: ((PreferredSchemeChange) -> Unit)? = null
```
Invoked when the shopper's preferred scheme changes: on resolution to a co-branded card (default selection), on a selection change, and once with an empty payload when co-branded state clears. De-duplicated.
### `PreferredSchemeChange`
| Field | Type | Notes |
| - | - | - |
| `preferredScheme` | `String?` | Selected canonical code; `null` when not co-branded |
| `cardSchemes` | `List` | Detected schemes (empty when not co-branded) |
### `CardScheme`
| Field | Type | Notes |
| - | - | - |
| `code` | `String` | Canonical scheme code, e.g. `"mada"` |
| `name` | `String` | Display label |
| `logoUrl` | `String?` | Brand logo URL |
| `selected` | `Boolean` | Whether this scheme is selected |
### Selector UI, styling, and translations
The Card Brand selector renders automatically inside `CardForm` for co-branded cards. Customize:
* **Text** — `CardTranslations.Labels.cardBrandSelectorTitle` / `cardBrandSelectorSubtitle` (defaults `"Card Brand"` / `"Select your preferred network"`).
* **Look** — `CardFormStylesConfig.cardBrandSelector: CardBrandSelectorStyle?`; see the [Card Brand Selector styling reference](/docs/orchestration/checkout-sdks/android/styling-guide#card-brand-selector).
### Payment
When a co-branded card is paid, the selected scheme is submitted as `paymentInstrumentData.preferredScheme` on the authorize request; it is omitted for single-brand cards.
***
## Further Reading
* **[Quick Start](/docs/orchestration/checkout-sdks/android)** — Get a card payment working in 15 minutes
* **[How to Tokenize a Card](/docs/orchestration/checkout-sdks/android/how-to-tokenize-card)** — Save a card without charging the customer
* **[How to Accept Redirect Payments](/docs/orchestration/checkout-sdks/android/how-to-accept-redirect-payments)** — Accept iDEAL, Bancontact, Sofort, and other redirect-based methods
* **[How to Update the Checkout Amount](/docs/orchestration/checkout-sdks/android/update-checkout-amount)** — Apply tips, discounts, or shipping costs after session creation
* **[Styling Guide](/docs/orchestration/checkout-sdks/android/styling-guide)** — Customize form and button appearance
* **[SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts)** — Architecture, payment flows, 3DS, security model
* **[Troubleshooting](/docs/orchestration/checkout-sdks/android/troubleshooting)** — Common issues and solutions
# How to Accept PayPal Payments
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/how-to-accept-paypal-payments
Accept PayPal payments in your Android app using `PayPalButton`. This guide covers new payments and charging previously saved PayPal accounts.
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android))
* A session init payload that includes a PayPal payment option (`paymentMethodCode: "payPal"`)
* Kotlin + Jetpack Compose for rendering the button
## New PayPal Payment
### 1. Initialize a session
```kotlin theme={null}
val configuration = Configuration(
initData = InitData(version = payload.version, data = payload.data),
option = Options(
redirectSessionLifecycle = RedirectSessionLifecycle(
onSessionExpired = {
// Fetch a fresh init payload from your backend after cancel/timeout
val refreshed = fetchInitPayloadFromBackend()
InitData(version = refreshed.version, data = refreshed.data)
}
)
)
)
val session = Payrails.createSession(configuration)
```
> `redirectSessionLifecycle.onSessionExpired` is optional here — `PayrailsPayPalButtonDelegate.onPaymentSessionExpired` is the primary hook for PayPal session refresh. Configure `onSessionExpired` if you also use 3DS or generic redirect payment methods in the same session.
### 2. Create the PayPal button
```kotlin theme={null}
val payPalButton = Payrails.createPayPalButton(
style = PayPalButtonStyle(height = 48.dp)
)
```
### 3. Set the delegate
```kotlin theme={null}
payPalButton.delegate = object : PayrailsPayPalButtonDelegate {
override fun onPaymentButtonClicked(button: PayPalButton) {
// Payment is starting — optionally show a loading overlay
}
override fun onAuthorizeSuccess(button: PayPalButton) {
navigateToConfirmation()
}
override fun onAuthorizeFailed(button: PayPalButton) {
showPaymentFailedMessage()
}
override fun onPaymentSessionExpired(button: PayPalButton) {
// The session is no longer usable after the user cancelled or a timeout occurred.
// Fetch a new init payload from your backend and reinitialize the session.
val refreshed = fetchInitPayloadFromBackend()
Payrails.createSession(
Configuration(initData = InitData(version = refreshed.version, data = refreshed.data))
)
}
override fun onCancelled(button: PayPalButton) {
// Optional — fires before onPaymentSessionExpired when the user explicitly cancels
showCancelMessage()
}
}
```
`onPaymentSessionExpired` is **required**. After a PayPal cancellation or timeout the session cannot be reused — you must reinitialize before the user can attempt payment again.
### 4. Render the button
```kotlin theme={null}
@Composable
fun CheckoutScreen() {
// ...
payPalButton.Render(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp))
}
```
The button renders in PayPal's branded gold (`#ffc439`) with the PayPal wordmark. No additional styling configuration is required.
### 5. Handle the payment result
When the user taps the button, the SDK opens the PayPal authorization page in a Chrome Custom Tab. After the user approves or cancels, the Activity resumes and the SDK polls for the final execution status. Your delegate receives `onAuthorizeSuccess`, `onAuthorizeFailed`, or `onCancelled` + `onPaymentSessionExpired` accordingly.
***
## Storing a PayPal Account for Future Use
To request that the PayPal account is saved for future payments, set `saveInstrument = true` before the user taps:
```kotlin theme={null}
payPalButton.saveInstrument = true
payPalButton.pay() // or let the user tap the rendered button
```
After a successful payment with `saveInstrument = true`, the account appears in `session.getStoredInstruments(forType = PaymentMethod.payPal)`.
***
## Charging a Stored PayPal Account
Stored PayPal accounts are charged directly without opening a browser. No user interaction in the browser is required.
### 1. Retrieve stored instruments
```kotlin theme={null}
val storedPayPalAccounts = session.getStoredInstruments(forType = PaymentMethod.payPal)
```
Each `StoredInstrument` exposes:
* `id` — the instrument identifier
* `email` — the PayPal account email
* `displayName` — server-provided display name (typically the email)
* `isDefault` — whether this is the holder's default instrument
### 2. Build your instrument picker
The SDK does not provide a pre-built instrument picker UI. Build your own and let the user select an account.
### 3. Trigger payment with the stored instrument
```kotlin theme={null}
val selectedInstrument = storedPayPalAccounts.first()
payPalButton.pay(selectedInstrument.id)
```
The result is delivered through the same delegate: `onAuthorizeSuccess` or `onAuthorizeFailed`. `onCancelled` and `onPaymentSessionExpired` do not fire for stored instrument payments — there is no browser session to cancel.
***
## Troubleshooting
**Button taps do nothing after a cancelled payment**
The Payrails session is expired after a PayPal cancellation. Make sure your `onPaymentSessionExpired` implementation fetches fresh init data and calls `Payrails.createSession()` before the user retries.
**`onAuthorizeFailed` fires immediately after the Custom Tab closes**
If the user closes the Custom Tab before completing the PayPal flow (without pressing the PayPal cancel button), the SDK's grace reconciliation window applies. The polling resolves as `authorizeFailed` after a brief delay. This is expected — the session is still valid in this case and the user can retry without refreshing.
**`IllegalStateException: session not initialized` on `createPayPalButton()`**
`Payrails.createPayPalButton()` must be called after `Payrails.createSession()` completes successfully.
## See Also
* [SDK Concepts — PayPal Payment](/docs/orchestration/checkout-sdks/android/sdk-concepts#paypal-payment) — How the PayPal redirect flow works
* [API Reference — PayPalButton](/docs/orchestration/checkout-sdks/android/api-reference#paypalbutton) — Complete `PayPalButton` API
* [Quick Start](/docs/orchestration/checkout-sdks/android) — Session initialization
# How to Accept Redirect Payments
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/how-to-accept-redirect-payments
Accept payments through redirect-based payment methods (iDEAL, Bancontact, Sofort, etc.) using the Payrails SDK.
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android))
* A session init payload with redirect-capable payment methods configured in the Payrails backend
* Kotlin + Jetpack Compose for rendering the redirect buttons
## Steps
### 1. Initialize a session
```kotlin theme={null}
val configuration = Configuration(
initData = InitData(version = payload.version, data = payload.data),
option = Options()
)
val session = Payrails.createSession(configuration)
```
### 2. Discover available redirect methods
Ask the session for redirect payment methods configured by the backend:
```kotlin theme={null}
val redirectMethods: List =
session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)
```
Returns an empty list if no redirect methods are configured.
Each `PayrailsPaymentOption` contains:
* `paymentMethodCode` — the identifier used to create the button (e.g., `"ideal"`, `"bancontact"`)
* `displayName` — a human-readable label from the backend (nullable)
### 3. Create a redirect button for each method
```kotlin theme={null}
redirectMethods.forEach { method ->
val button = Payrails.createGenericRedirectButton(
translations = CardPaymenButtonTranslations(
label = method.displayName ?: method.paymentMethodCode
),
paymentMethodCode = method.paymentMethodCode
)
button.delegate = object : GenericRedirectPaymentButtonDelegate {
override fun onPaymentButtonClicked(button: GenericRedirectButton) {
// Payment flow started — show loading indicator if desired
}
override fun onAuthorizeSuccess(button: GenericRedirectButton) {
// Payment succeeded — navigate to success screen
}
override fun onAuthorizeFailed(button: GenericRedirectButton) {
// Payment failed — show error UI
}
override fun onPaymentSessionExpired(button: GenericRedirectButton) {
// Session expired during redirect — prompt user to retry
}
override fun onStateChanged(button: GenericRedirectButton, state: ButtonState) {
// LOADING when payment starts, ENABLED when it completes
}
}
// In Compose
button.Render(modifier = Modifier.padding(horizontal = 24.dp))
}
```
### 4. Handle the redirect flow
When the user taps the button, the SDK:
1. Opens the payment provider's page in a Custom Tab (or system browser as fallback)
2. Polls the execution status in the background
3. Returns the result through the delegate callbacks
No additional handling is needed — the SDK manages the redirect lifecycle, polling, and result delivery automatically.
## Session recovery
If the redirect flow is abandoned or remains non-terminal, the SDK follows the same session recovery path as card 3DS redirects. Configure `onSessionExpired` in `Options.redirectSessionLifecycle` to enable automatic recovery:
```kotlin theme={null}
val configuration = Configuration(
initData = initData,
option = Options(
redirectSessionLifecycle = RedirectSessionLifecycle(
onSessionExpired = {
val refreshed = fetchInitPayloadFromBackend()
InitData(version = refreshed.version, data = refreshed.data)
}
)
)
)
```
## Related
* [How to Accept PayPal Payments](/docs/orchestration/checkout-sdks/android/how-to-accept-paypal-payments) — PayPal uses the same redirect infrastructure
* [SDK API Reference](/docs/orchestration/checkout-sdks/android/api-reference) — Full API details for redirect payments
* [Quick Start](/docs/orchestration/checkout-sdks/android) — Get started with the SDK
* [SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts) — Payment flow architecture
# How to Support Co-Branded Cards
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/how-to-co-branded-cards
Let shoppers choose which network processes a co-branded card in the Android SDK, and submit that choice with the payment.
Some cards carry two networks — a local/domestic scheme (e.g. `mada`, `cartesbancaires`) and an international one (e.g. `visa`, `mastercard`). This guide shows how to let the shopper pick which network the payment routes over and submit that choice. Also known as **co-badged cards** or **card brand choice**.
> Background: [Co-Branded Cards](/docs/orchestration/checkout-sdks/android/sdk-concepts#co-branded-cards).
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android)) rendering a `CardForm` + `CardPaymentButton`.
* Co-branded handling **enabled for the session**: the init response must include the `binLookup` link **and** `featureConfig.coBrandedCardsRollout` greater than 0 (configured by the Payrails backend — contact Payrails to enable it for your merchant account).
* A genuinely co-branded test card (e.g. a mada/Mastercard dual-scheme card).
## Steps
### 1. Render the card form as usual
The Card Brand selector appears **automatically** inside `CardForm` when a co-branded card is detected (two or more schemes). You do not add a separate component.
### 2. Observe the shopper's choice
```kotlin theme={null}
val config = CardFormConfig(
events = CardFormEvents(
onPreferredSchemeChanged = { change ->
// change.preferredScheme — selected canonical code (null when not co-branded)
// change.cardSchemes — all detected schemes, each with .selected
}
)
)
val cardForm = Payrails.createCardForm(config)
```
`onPreferredSchemeChanged` fires when a co-branded card is detected (with the default selection), when the shopper taps a different tile, and once with an empty payload if the card stops being co-branded. It is de-duplicated, so recompositions do not re-fire it.
### 3. (Optional) Localize and style the selector
```kotlin theme={null}
val config = CardFormConfig(
translations = CardTranslations().apply {
labels.cardBrandSelectorTitle = "Choose your network"
labels.cardBrandSelectorSubtitle = "This card supports two networks"
},
styles = CardFormStylesConfig(
cardBrandSelector = CardBrandSelectorStyle(
selectedTileBorderColor = 0xFF0F63BD.toInt(),
selectedCheckColor = 0xFF0F63BD.toInt(),
tileCornerRadius = 12, // dp
)
),
)
```
See the [Card Brand Selector styling reference](/docs/orchestration/checkout-sdks/android/styling-guide#card-brand-selector) for all tokens.
### 4. Pay
No extra work. When the shopper pays a co-branded card, the SDK submits the selected scheme as `paymentInstrumentData.preferredScheme` on the authorize request. For a single-brand card, no selector shows and no `preferredScheme` is sent.
## (Optional) Look up a BIN directly
To inspect card metadata yourself (without the form), call `Session.binLookup`:
```kotlin theme={null}
val result = session.binLookup("529741") // suspend; returns null on failure
if (result?.localNetwork != null) {
// co-branded card: result.network + result.localNetwork
}
```
See [`Session.binLookup`](/docs/orchestration/checkout-sdks/android/api-reference#co-branded-cards) for the full contract.
## Related
* [Co-Branded Cards (concept)](/docs/orchestration/checkout-sdks/android/sdk-concepts#co-branded-cards)
* [API Reference — Co-Branded Cards](/docs/orchestration/checkout-sdks/android/api-reference#co-branded-cards)
* [Styling Guide — Card Brand Selector](/docs/orchestration/checkout-sdks/android/styling-guide#card-brand-selector)
# How to Build a Custom Pay Button (Your Own UI)
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/how-to-custom-pay-button
Drive checkout from your own Android button while the SDK handles the Google Pay sheet, the 3D Secure and redirect tabs, and polling.
Drive checkout from your **own** button and method selector while the SDK handles the
post-tap work — the Google Pay sheet, the 3DS / PayPal / redirect Custom Tab, and polling.
Use this when you want full control of styling, placement, and state but don't want to
reimplement payment orchestration. The handle that makes this possible is
[`PayrailsPaymentLauncher`](/docs/orchestration/checkout-sdks/android/api-reference#payrailspaymentlauncher).
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android))
* The `Session` reference returned by `Payrails.createSession(...)`
* The `com.payrails.android:checkout` dependency (see [Quick Start](/docs/orchestration/checkout-sdks/android))
* For the encrypted-card path: a card payload **already encrypted** by your client-side
encryption library (the SDK exposes no encryption API — see
[Why no encryption API](/docs/orchestration/checkout-sdks/android/sdk-concepts#why-no-encryption-api))
## Steps
### 1. Create the launcher early
The launcher registers a Google Pay Activity Result contract, so it must be created before
the host reaches the `STARTED` state. In Compose, call the factory during composition:
```kotlin theme={null}
@Composable
fun CheckoutScreen(session: Session) {
var status by remember { mutableStateOf(null) }
val launcher = rememberPayrailsPaymentLauncher(session) { result ->
status = result
}
// ... your UI below
}
```
From a View / Activity, create it in `onCreate`:
```kotlin theme={null}
class CheckoutActivity : ComponentActivity() {
private lateinit var launcher: PayrailsPaymentLauncher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launcher = Payrails.createPaymentLauncher(this, session) { result ->
render(result)
}
}
}
```
### 2. Draw your own method selector and Pay button
This is your UI — style it however you like. Track the selected method in your own state:
```kotlin theme={null}
var selected by remember { mutableStateOf(PaymentMethod.googlePay) }
DropdownMenuForYourMethods(
selected = selected,
onSelect = { selected = it }
)
Button(onClick = { launcher.authorize(selected) }) {
Text("Pay")
}
```
### 3. Trigger the payment
Call the matching `authorize(...)` overload from your button. The SDK opens whatever UI the method
needs:
```kotlin theme={null}
// By method type — SDK opens the Google Pay sheet / Custom Tab as required
launcher.authorize(PaymentMethod.googlePay)
launcher.authorize(PaymentMethod.payPal, saveInstrument = true)
launcher.authorize(PaymentMethod.genericRedirect, paymentMethodCode = "klarna")
// A previously saved instrument
launcher.authorize(storedInstrument = savedCard)
// An already-encrypted card — 3DS handled in a Custom Tab when required
launcher.authorize(encryptedCardData = encrypted, saveInstrument = true)
```
### 4. Handle the result
Each `authorize(...)` resolves to exactly one [`ActionResult`](/docs/orchestration/checkout-sdks/android/api-reference#actionresult),
delivered to the callback you supplied in step 1:
```kotlin theme={null}
when (val result = status) {
ActionResult.Success -> showReceipt()
is ActionResult.Failed -> when (result.failure.code) {
AuthorizationFailureReason.USER_CANCELLED -> { /* user dismissed the sheet / tab — no action */ }
AuthorizationFailureReason.VALIDATION_FAILED -> showBlocked(result.failure.message) /* your own gate refused it — not a decline */
else -> showDeclined(result.failure.message)
}
null -> { /* no payment attempted yet */ }
}
```
## Full example — custom dropdown + single Pay button
```kotlin theme={null}
@Composable
fun CustomCheckout(session: Session) {
var selected by remember { mutableStateOf(PaymentMethod.googlePay) }
var encrypted by remember { mutableStateOf("") }
var status by remember { mutableStateOf(null) }
val launcher = rememberPayrailsPaymentLauncher(session) { result ->
status = when (result) {
ActionResult.Success -> "Approved"
is ActionResult.Failed -> "Declined: ${result.failure.code}"
}
}
Column {
// Your own method selector
PaymentMethodDropdown(selected) { selected = it }
// Optional: your own card field that yields an ENCRYPTED string
OutlinedTextField(
value = encrypted,
onValueChange = { encrypted = it },
label = { Text("Encrypted card payload") }
)
Button(
onClick = {
if (encrypted.isNotBlank()) launcher.authorize(encryptedCardData = encrypted)
else launcher.authorize(selected)
}
) { Text("Pay") }
status?.let { Text(it) }
}
}
```
## Troubleshooting
**Problem**: The launcher throws when created
**Solution**: `Payrails.createPaymentLauncher(...)` must be called from `Activity.onCreate`.
Registering the Google Pay Activity Result contract after the activity is `STARTED` is not
allowed by the Android Activity Result API.
**Problem**: A card that needs 3DS does not complete
**Solution**: 3DS is handled automatically by the launcher (it owns the Custom Tab) — you do
not branch on it. The terminal `ActionResult` arrives after the challenge resolves. There is no
separate "headless" card call to choose; `launcher.authorize(encryptedCardData = …)` covers
both frictionless and 3DS.
**Problem**: `ActionResult.Failed` with `code == UNKNOWN_ERROR` (rawError `unsupportedPayment`)
**Solution**: The selected method is not configured on the session. Confirm availability with
[`session.getPaymentMethodConfig()`](/docs/orchestration/checkout-sdks/android/api-reference#session-getpaymentmethodconfig-filter)
before calling `authorize(...)`.
**Problem**: Google Pay never opens
**Solution**: Check device capability with `session.isGooglePayAvailable(context)` and confirm
Google Pay is configured on the session.
## See also
[`PayrailsPaymentLauncher` API Reference](/docs/orchestration/checkout-sdks/android/api-reference#payrailspaymentlauncher) — every method and factory
* [Custom UI: Launcher vs Client](/docs/orchestration/checkout-sdks/android/sdk-concepts#custom-ui-the-launcher-and-the-internal-client) — how the launcher and the internal networking client relate
* [How to Execute a Payment Without an SDK Button (Headless)](/docs/orchestration/checkout-sdks/android/how-to-execute-payment-headless) — the same launcher flow framed for headless/ViewModel-driven checkouts
[`ActionResult` API Reference](/docs/orchestration/checkout-sdks/android/api-reference#actionresult) — every terminal payment outcome
# How to Run a Payment Without an SDK Button (Headless)
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/how-to-execute-payment-headless
Run a payment from your own Android UI with PayrailsPaymentLauncher, without rendering a prebuilt SDK button component.
Drive checkout from your **own** UI — a custom layout, a `ViewModel`-driven flow, your own
"Pay" button — instead of rendering a prebuilt SDK button component. The single public
payment trigger is [`PayrailsPaymentLauncher`](/docs/orchestration/checkout-sdks/android/api-reference#payrailspaymentlauncher):
you draw the controls, the SDK owns the post-tap work (Google Pay sheet, 3DS / PayPal /
redirect Custom Tab, polling).
> There is no public `Session.authorize(...)`. Payment execution is launcher-only, so a
> single `authorize(...)` call handles **both** frictionless and 3DS outcomes — you never
> branch on whether a charge will need a step-up. See
> [Custom UI: the launcher and the internal client](/docs/orchestration/checkout-sdks/android/sdk-concepts#custom-ui-the-launcher-and-the-internal-client).
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android)) — hold the `Session`
returned by `Payrails.createSession(...)`.
* A `ComponentActivity` host. The launcher is lifecycle-bound; it cannot live in a bare
`ViewModel` (it registers a Google Pay Activity Result contract), but your `ViewModel` can
drive *when* it fires.
* For stored-instrument payments: at least one saved instrument
(see [How to Tokenize a Card](/docs/orchestration/checkout-sdks/android/how-to-tokenize-card)).
## Steps
### 1. Create the launcher early
Construct it in `onCreate` (or a Compose `remember`) so its Activity Result contract registers
before the host is `STARTED`:
```kotlin theme={null}
val launcher = rememberPayrailsPaymentLauncher(session) { result ->
// single terminal result for every authorize(...) call
}
```
### 2. Charge a stored instrument from your own UI
The most common headless case — no SDK button, your own list:
```kotlin theme={null}
val savedCards = session.getStoredInstruments(forType = PaymentMethod.card)
// from your own row's onClick:
launcher.authorize(storedInstrument = savedCards.first())
```
A frictionless charge completes with no UI; if the instrument requires 3DS, the launcher
opens the Custom Tab and still resolves to one terminal result — you write the same code
either way.
### 3. Charge a fresh card you encrypted yourself
If you collect and encrypt card data with the client-side encryption library, pass the
**already-encrypted** string. The SDK exposes no encryption API, and raw card fields never
cross this call:
```kotlin theme={null}
// `encrypted` produced by your client-side encryption library — your responsibility.
launcher.authorize(encryptedCardData = encrypted, saveInstrument = false)
```
### 4. Trigger other methods by type
```kotlin theme={null}
launcher.authorize(PaymentMethod.googlePay)
launcher.authorize(PaymentMethod.payPal)
launcher.authorize(PaymentMethod.genericRedirect, paymentMethodCode = "klarna")
```
### 5. Handle the result
Every `authorize(...)` resolves to exactly one
[`ActionResult`](/docs/orchestration/checkout-sdks/android/api-reference#actionresult), delivered to the callback from step 1:
```kotlin theme={null}
when (result) {
ActionResult.Success -> showReceipt()
is ActionResult.Failed -> when (result.failure.code) {
AuthorizationFailureReason.USER_CANCELLED -> { /* user dismissed the sheet / tab — no action */ }
AuthorizationFailureReason.VALIDATION_FAILED -> showBlocked(result.failure.message) /* your own gate refused it — not a decline */
else -> showDeclined(result.failure.message)
}
}
```
## Full example — stored-instrument checkout driven from a ViewModel
The `ViewModel` owns selection and state; the activity owns the launcher and fires it:
```kotlin theme={null}
class CheckoutViewModel(private val session: Session) : ViewModel() {
val savedCards = session.getStoredInstruments(forType = PaymentMethod.card)
var status by mutableStateOf(null)
private set
fun onResult(result: ActionResult) {
status = when (result) {
ActionResult.Success -> "Payment approved"
is ActionResult.Failed -> "Payment declined: ${result.failure.code}"
}
}
}
@Composable
fun CheckoutScreen(vm: CheckoutViewModel, session: Session) {
val launcher = rememberPayrailsPaymentLauncher(session, vm::onResult)
Column {
vm.savedCards.forEach { card ->
Button(onClick = { launcher.authorize(storedInstrument = card) }) {
Text(card.displayName ?: "Saved card")
}
}
vm.status?.let { Text(it) }
}
}
```
## Troubleshooting
**Problem**: The launcher throws when created
**Solution**: `Payrails.createPaymentLauncher(...)` must be called from `Activity.onCreate`
(or `rememberPayrailsPaymentLauncher` during composition). Registering the Google Pay Activity
Result contract after the activity is `STARTED` is not allowed by the Android Activity Result API.
**Problem**: `ActionResult.Failed` with `code == UNKNOWN_ERROR` (rawError `sdkNotInitialized`)
**Solution**: The session was destroyed or replaced (for example a newer `createSession()`
superseded it). Obtain a fresh `Session` from `Payrails.createSession()`.
**Problem**: `ActionResult.Failed` with `code == UNKNOWN_ERROR` (rawError `unsupportedPayment`)
**Solution**: The payment method is not configured on the session. Call
[`session.getPaymentMethodConfig()`](/docs/orchestration/checkout-sdks/android/api-reference#session-getpaymentmethodconfig-filter)
to confirm what is available before triggering.
## See also
* [How to Build a Custom Pay Button](/docs/orchestration/checkout-sdks/android/how-to-custom-pay-button) — the launcher with a method picker + single button
* [`PayrailsPaymentLauncher` API Reference](/docs/orchestration/checkout-sdks/android/api-reference#payrailspaymentlauncher) — all factories, `authorize(...)` overloads, and parameters
* [`ActionResult` API Reference](/docs/orchestration/checkout-sdks/android/api-reference#actionresult) — every terminal payment outcome
* [How to Tokenize a Card](/docs/orchestration/checkout-sdks/android/how-to-tokenize-card) — save a card, then charge it from your own UI
* [Custom UI: the launcher and the internal client](/docs/orchestration/checkout-sdks/android/sdk-concepts#custom-ui-the-launcher-and-the-internal-client) — why payment execution is launcher-only
# Query Session Data
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/how-to-query-session-data-at-runtime
Read session information — such as the current amount, execution ID, or payment method configuration — from a Payrails `Session` at any point after `createSession()` completes.
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android))
* The `Session` reference returned by `Payrails.createSession(...)`
## Query session data with `session.query()`
Hold on to the `Session` returned by `createSession(...)` and call `query()` on it with
a typed key from `PayrailsQuery`:
```kotlin theme={null}
val session = Payrails.createSession(configuration)
val executionId: String? = session.query(PayrailsQuery.ExecutionId)
val amount: PayrailsAmount? = session.query(PayrailsQuery.Amount)
```
All keys return `null` when the queried data is not present. No exception is thrown.
## Available keys
### `PayrailsQuery.ExecutionId`
The unique identifier for the active payment execution.
```kotlin theme={null}
val executionId: String? = session.query(PayrailsQuery.ExecutionId)
```
Use this to tell your backend which execution to act on — for example, before calling
the Payrails lookup action to update the checkout amount.
***
### `PayrailsQuery.HolderReference`
The holder reference for the active session.
```kotlin theme={null}
val holderReference: String? = session.query(PayrailsQuery.HolderReference)
```
***
### `PayrailsQuery.Amount`
The current checkout amount as a [`PayrailsAmount`](/docs/orchestration/checkout-sdks/android/api-reference#payrailsamount).
```kotlin theme={null}
val amount: PayrailsAmount? = session.query(PayrailsQuery.Amount)
// amount?.value → "54.99"
// amount?.currency → "EUR"
```
***
### `PayrailsQuery.BinLookup`
The BIN lookup action link from the session config, as a
[`PayrailsLink`](/docs/orchestration/checkout-sdks/android/api-reference#payrailslink).
```kotlin theme={null}
val binLookupLink: PayrailsLink? = session.query(PayrailsQuery.BinLookup)
// binLookupLink?.href → "https://..."
// binLookupLink?.method → "POST"
```
***
### `PayrailsQuery.InstrumentDelete`
The stored instrument delete action link.
```kotlin theme={null}
val deleteLink: PayrailsLink? = session.query(PayrailsQuery.InstrumentDelete)
```
***
### `PayrailsQuery.InstrumentUpdate`
The stored instrument update action link.
```kotlin theme={null}
val updateLink: PayrailsLink? = session.query(PayrailsQuery.InstrumentUpdate)
```
***
### `PayrailsQuery.PaymentMethodConfig`
Configuration for a specific payment method, as a
[`PayrailsPaymentMethodConfig`](/docs/orchestration/checkout-sdks/android/api-reference#payrailspaymentmethodconfig).
```kotlin theme={null}
val cardConfig: PayrailsPaymentMethodConfig? =
session.query(PayrailsQuery.PaymentMethodConfig("card"))
// cardConfig?.displayName → "Credit or Debit Card"
// cardConfig?.flow → "inline"
// cardConfig?.supportsSaveInstrument → true
```
Pass the payment method code (e.g. `"card"`, `"paypal"`) as the parameter. Returns
`null` if the code is not present in the session config.
***
### `PayrailsQuery.PaymentMethodInstruments`
Stored instruments for a specific payment method.
```kotlin theme={null}
val instruments: List? =
session.query(PayrailsQuery.PaymentMethodInstruments("card"))
```
Returns `null` if the payment method code is unrecognised or has no stored instruments.
Never returns an empty list — use `null` as the "nothing available" signal.
***
## Discovering redirect-capable payment methods
`PayrailsQuery.AvailableRedirectMethods` has been removed. To list redirect-based
payment methods configured for the session, use
[`session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)`](/docs/orchestration/checkout-sdks/android/api-reference#session-getpaymentmethodconfig-filter):
```kotlin theme={null}
val redirectMethods = session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)
redirectMethods.forEach { option ->
// option.paymentMethodCode — e.g. "ideal", "bancontact"
// option.displayName — backend-provided label, or null
}
```
Returns an empty list (not `null`) when no redirect methods are configured. Use this to
dynamically build your redirect payment UI rather than hardcoding method codes.
See [How to Accept Redirect Payments](/docs/orchestration/checkout-sdks/android/how-to-accept-redirect-payments) for the full
integration guide.
## Example: passing the execution ID to your backend
```kotlin theme={null}
Payrails.createSession(configuration) { result ->
result.onSuccess { session ->
val executionId = session.query(PayrailsQuery.ExecutionId)
myApiClient.updateExecutionAmount(
executionId = executionId,
amount = newTotal,
currency = "EUR"
)
session.update(PayrailsUpdate(amount = AmountUpdate(newTotal, "EUR")))
}
}
```
## See also
* [How to update the checkout amount after session creation](/docs/orchestration/checkout-sdks/android/update-checkout-amount)
* [`Session.query()` API reference](/docs/orchestration/checkout-sdks/android/api-reference#session-query-key)
* [`Session.getPaymentMethodConfig()` API reference](/docs/orchestration/checkout-sdks/android/api-reference#session-getpaymentmethodconfig-filter)
# How to Tokenize a Card Without Charging It
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/how-to-tokenize-card
Save a card to the Payrails vault for future use without initiating a payment.
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android))
* A session init payload with `vaultConfiguration` and a `saveInstrument` link (provided by the Payrails backend — contact Payrails to enable vault configuration for your merchant account)
* Kotlin + Jetpack Compose for rendering the card form
## Steps
### 1. Initialize a session
```kotlin theme={null}
val configuration = Configuration(
initData = InitData(version = payload.version, data = payload.data),
option = Options()
)
val session = Payrails.createSession(configuration)
```
### 2. Create a card form (no payment button required)
```kotlin theme={null}
val cardForm = Payrails.createCardForm(
config = CardFormConfig(showCardHolderName = true)
)
```
A `CardPaymentButton` is not needed for tokenization. The card form collects and validates card fields independently.
### 3. Render the card form and a save button
```kotlin theme={null}
@Composable
fun TokenizeScreen(scope: CoroutineScope) {
val cardForm = remember {
Payrails.createCardForm(
config = CardFormConfig(showCardHolderName = true)
)
}
var instrumentId by remember { mutableStateOf(null) }
var error by remember { mutableStateOf(null) }
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
cardForm.Render()
Button(onClick = {
scope.launch {
try {
val response = session.tokenize(
TokenizationRequest.Card(cardForm),
TokenizeOptions(
storeInstrument = true,
futureUsage = FutureUsage.CardOnFile
)
)
instrumentId = response.id
error = null
} catch (e: PayrailsError) {
error = e.message
}
}
}) {
Text("Save Card")
}
error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
instrumentId?.let { Text("Saved: $it") }
}
}
```
`session.tokenize(...)` is the primary tokenization API (mirroring the iOS SDK). `TokenizationRequest.Card(cardForm)` selects the card currently entered in the form.
#### Callback variant (without coroutines)
If you are not calling from a coroutine, use the callback overload. Exactly one of the three callbacks fires once, on the main thread:
```kotlin theme={null}
session.tokenize(
request = TokenizationRequest.Card(cardForm),
options = TokenizeOptions(storeInstrument = true),
onSuccess = { response -> instrumentId = response.id },
onFailed = { e -> error = e.message },
onCancelled = { /* tokenization was cancelled */ },
)
```
`onCancelled` fires only on cancellation (coroutine/session-scope) — card validation and network failures are delivered to `onFailed`.
**Shortcut**
`cardForm.tokenize(options)` is a convenience wrapper equivalent to `session.tokenize(TokenizationRequest.Card(cardForm), options)`. Use whichever reads better in your code.
### 4. Choose a `FutureUsage` value
| Value | When to use |
| - | - |
| `FutureUsage.CardOnFile` | Customer-initiated purchases where the cardholder is present at checkout (default) |
| `FutureUsage.Subscription` | Recurring charges on a fixed schedule authorized by the cardholder |
| `FutureUsage.UnscheduledCardOnFile` | Merchant-initiated charges with no fixed schedule (e.g., top-ups, threshold billing) |
### 5. Use the returned instrument ID
The `response.id` is the instrument identifier. Pass it to `setStoredInstrument` for future payments, or use it with the instrument management API:
```kotlin theme={null}
// Pay with the saved instrument
payButton.setStoredInstrument(savedInstrument)
// Or manage the instrument via the session
session.deleteInstrument(response.id)
session.updateInstrument(response.id, UpdateInstrumentBody(default = true))
```
## Tokenize with Google Pay
Google Pay tokenization uses the same `session.tokenize(...)` API with a `TokenizationRequest.GooglePay` case. Because a Google Pay token can only be obtained through an Activity-result launcher that Android requires to be registered up front, you create a **presenter** in your Composable and pass it to `tokenize`:
```kotlin theme={null}
@Composable
fun SaveGooglePayScreen(session: Session, scope: CoroutineScope) {
val presenter = rememberGooglePayPresenter(session) // registers the launcher up front
Button(onClick = {
scope.launch {
try {
val response = session.tokenize(
TokenizationRequest.GooglePay(presenter),
TokenizeOptions(storeInstrument = true),
)
// response.id is the saved instrument
} catch (e: PayrailsError) {
// show error
}
}
}) { Text("Save with Google Pay") }
}
```
Tapping the button opens the Google Pay sheet; on authorization the SDK saves the instrument and returns a `SaveInstrumentResponse`. User dismissal cancels (the callback overload's `onCancelled`, or a `CancellationException` from the suspend variant).
For a compliant, Google-branded button with no wiring, use `GooglePayTokenizeButton` instead — it renders only when Google Pay is available for the session:
```kotlin theme={null}
GooglePayTokenizeButton(
session = session,
options = TokenizeOptions(storeInstrument = true),
onSuccess = { response -> /* response.id */ },
onFailed = { error -> /* show error */ },
onCancelled = { /* dismissed */ },
)
```
Google's brand guidelines require the official Google Pay button to launch the Google Pay flow. Prefer `GooglePayTokenizeButton` (or a Google Pay–branded button) over a fully custom button.
## Verify it worked
Check that the response contains a valid ID and `"active"` status:
```kotlin theme={null}
val response = session.tokenize(
TokenizationRequest.Card(cardForm),
TokenizeOptions(storeInstrument = true),
)
check(response.id.isNotBlank()) { "Tokenization returned no instrument ID" }
check(response.status == "active") { "Unexpected status: ${response.status}" }
```
## Troubleshooting
**Problem**: `PayrailsError.invalidCardData` is thrown
**Solution**: Card form validation failed. The form automatically shows inline field errors — the user needs to correct the highlighted fields before retrying. No manual error display is needed.
**Problem**: `PayrailsError.missingData("Vault configuration with providerConfigId is required...")`
**Solution**: The session init payload does not include `vaultConfiguration`. Contact Payrails to enable vault configuration for your merchant account.
**Problem**: `PayrailsError.missingData("holderReference is required...")`
**Solution**: The session init payload does not include `holderReference`. Ensure your client-init request includes a holder reference.
## Alternative: pay directly with the saved instrument
Once a card is tokenized, you can charge it through the button flow shown above
(`payButton.setStoredInstrument(...)` + `payButton.Render()`), or skip the button entirely
and charge it from your own UI with `PayrailsPaymentLauncher`:
```kotlin theme={null}
// Pay with the just-saved instrument
val storedInstrument = session.getStoredInstruments(forType = PaymentMethod.card)
.first { it.id == response.id }
launcher.authorize(storedInstrument = storedInstrument)
```
`launcher.authorize(storedInstrument = ...)` is the way to charge an *existing* stored
instrument from a custom layout or `ViewModel`-driven flow — and, because the launcher holds
a presenter, it transparently handles a 3DS step-up if the issuer requires one. See
[How to Run a Payment Without an SDK Button](/docs/orchestration/checkout-sdks/android/how-to-execute-payment-headless).
## See also
* [Understanding Card Tokenization](/docs/orchestration/checkout-sdks/android/sdk-concepts#card-tokenization) — why tokenization and payment are separate flows, and what `storeInstrument` and `FutureUsage` mean
* [Card Tokenization API Reference](/docs/orchestration/checkout-sdks/android/api-reference#card-tokenization) — complete reference for `TokenizeOptions`, `FutureUsage`, and `SaveInstrumentResponse`
* [Stored Instruments](/docs/orchestration/checkout-sdks/android/api-reference#stored-instruments) — how to retrieve and pay with saved instruments
* [`PayrailsPaymentLauncher`](/docs/orchestration/checkout-sdks/android/api-reference#payrailspaymentlauncher) — the public payment-execution API
# SDK Concepts
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/sdk-concepts
This page explains *why* the Payrails Android SDK is designed the way it is. Understanding these concepts helps you integrate correctly and debug issues faster.
## Mental Model
The SDK is built around three layers:
```
┌──────────────────────────────────────────────────────────────┐
│ Your App (Compose) │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ │
│ │ CardForm │ │ CardPayment │ │ GooglePay │ │ PayPal │ │
│ │ │ │ Button │ │ Button │ │ Button │ │
│ └────┬─────┘ └──────┬──────┘ └───────┬───────┘ └────┬─────┘ │
│ │ │ │ │
│ └────────┬───────┴───────────────────┴──────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Session │ │
│ └──────┬──────┘ │
│ │ │
├─────────────────┼────────────────────────────────────────────┤
│ Payrails API │
└──────────────────────────────────────────────────────────────┘
```
### Session
A `Session` is the foundation and the merchant's handle to all runtime SDK operations. It holds your initialization data, manages communication with the Payrails API, and tracks payment state. You create one session per checkout flow.
```kotlin theme={null}
val session = Payrails.createSession(configuration)
```
The returned `Session` is a public class — keep the reference for the lifetime of the
checkout. All runtime operations are called on it:
```kotlin theme={null}
session.query(PayrailsQuery.ExecutionId)
session.update(PayrailsUpdate(amount = AmountUpdate("54.99", "EUR")))
session.getStoredInstruments(forType = PaymentMethod.card)
session.deleteInstrument(instrumentId)
session.getPaymentMethodConfig(PaymentMethodFilter.Redirect)
```
`Session` handles queries and configuration. To **execute** a payment you use
`PayrailsPaymentLauncher` (see [Custom UI: the launcher and the internal client](#custom-ui-the-launcher-and-the-internal-client)).
The `Payrails` object now exposes only `createSession` and UI factory methods
(`createCardForm`, `createCardPaymentButton`, `createPayPalButton`,
`createGooglePayButton`, `createGenericRedirectButton`). Most runtime operations — query,
update, stored instruments, instrument management — live on the `Session`. Payment
**execution** is the exception: it is public only through `PayrailsPaymentLauncher`.
All UI elements (`CardForm`, `CardPaymentButton`, `GooglePayButton`, `PayPalButton`) are created *after* the session and operate within its context. If the session doesn't exist, element creation throws an error.
### Elements Are Decoupled
`CardForm`, `CardPaymentButton`, `GooglePayButton`, and `PayPalButton` are independent composable elements. They don't need to be created together or rendered in the same container. The SDK links card elements automatically:
```
CardForm ◄─── auto-linked ───► CardPaymentButton
GooglePayButton (standalone — no form linkage needed)
PayPalButton (standalone — no form linkage needed)
```
This design lets you:
* Place the form and button in different parts of your layout
* Create the button before the form (or vice versa)
* Reuse the same button for both card form payments and stored instrument payments
* Add Google Pay or PayPal alongside card payments as separate buttons
### Delegates (Event Callbacks)
The SDK uses the delegate pattern for event callbacks. Each element type has a delegate interface:
| Element | Delegate | Key callbacks |
| - | - | - |
| `CardPaymentButton` | `PayrailsCardPaymentButtonDelegate` | `onAuthorizeSuccess`, `onAuthorizeFailed`, `onThreeDSecureChallenge`, `onStateChanged` |
| `GooglePayButton` | `PayrailsGooglePayButtonDelegate` | `onGooglePayAvailable`, `onAuthorizeSuccess`, `onAuthorizeFailed`, `onThreeDSecureChallenge`, `onStateChanged` |
| `PayPalButton` | `PayrailsPayPalButtonDelegate` | `onAuthorizeSuccess`, `onAuthorizeFailed`, `onCancelled`, `onPaymentSessionExpired` (required), `onStateChanged` |
| `GenericRedirectButton` | `GenericRedirectPaymentButtonDelegate` | `onAuthorizeSuccess`, `onAuthorizeFailed`, `onCancelled`, `onPaymentSessionExpired`, `onStateChanged` |
Delegates are set on the element instance, not passed as constructor parameters. This keeps the creation API clean and allows you to change handlers at runtime.
**Stored instruments**
The SDK does not provide a pre-built stored-instruments UI. Retrieve saved cards with `session.getStoredInstruments()`, build your own picker, and call `cardPaymentButton.setStoredInstrument(instrument)` to pay with one.
## Payment Flow
### Card Payment (Happy Path)
```
User fills card form
│
▼
User taps "Pay Now"
│
▼
SDK validates card data ──── Invalid? ──► Show field errors
│ (auto-clear on fix)
│ Valid
▼
SDK encrypts card data (via CSE)
│
▼
SDK calls authorize API
│
├──── Success ──────────────► onAuthorizeSuccess
│
├──── 3DS Required ─────────► Open browser (Custom Tab)
│ │
│ User completes 3DS
│ │
│ SDK polls for result
│ │
│ ├──── Success ──► onAuthorizeSuccess
│ └──── Failure ──► onAuthorizeFailed
│
└──── Failure ──────────────► onAuthorizeFailed
```
### Stored Instrument Payment
When a stored instrument is selected, the button bypasses card form validation entirely:
```
User selects stored instrument
│
▼
CardPaymentButton enters "stored instrument mode"
│
▼
User taps "Pay Now"
│
▼
SDK authorizes with stored instrument
│
├──── Success ──► onAuthorizeSuccess
└──── Failure ──► onAuthorizeFailed
```
If the user starts typing in the card form, the stored instrument selection is automatically cleared and the button returns to "form mode."
### Google Pay Payment
Google Pay uses a separate `GooglePayButton` element with its own self-contained flow:
```
App creates GooglePayButton
│
▼
SDK checks isReadyToPay() ─── Not ready? ──► Button stays hidden
│
│ Ready
▼
Button becomes visible
onGooglePayAvailable fires
│
▼
User taps Google Pay button
│
▼
SDK opens Google Pay payment sheet
│
├──── User selects payment method ──► SDK receives token
│ │
│ SDK calls authorize API
│ │
│ ├──── Success ──► onAuthorizeSuccess
│ │
│ ├──── 3DS Required ──► Custom Tab
│ │ (same as card 3DS)
│ │
│ └──── Failure ──► onAuthorizeFailed
│
└──── User cancels ──────────────► Button returns to enabled state
```
Google Pay configuration (gateway, merchant info, allowed card networks) is provided entirely by the Payrails backend. The SDK reads it from `clientConfig.additionalConfig` and passes it through unchanged to the Google Pay API.
### PayPal Payment
PayPal uses a separate `PayPalButton` element that routes through the same redirect infrastructure as 3DS:
```
App creates PayPalButton
│
▼
User taps PayPal button
│
▼
SDK calls authorize API
│
▼
SDK opens PayPal in Custom Tab (or external browser fallback)
│
├──── User approves in PayPal ──► Activity resumes
│ │
│ SDK polls for result
│ │
│ ├──── Success ──► onAuthorizeSuccess
│ └──── Failure ──► onAuthorizeFailed
│
├──── User cancels ──────────────► SDK polls → canceled status
│ │
│ onCancelled fires
│ │
│ onPaymentSessionExpired fires
│ (session is no longer usable)
│
└──── Authorization fails ──────► onAuthorizeFailed fires
│
onPaymentSessionExpired fires
(if session expired during attempt)
```
**Session expiry:** Unlike Google Pay, when a user cancels a PayPal payment the Payrails session is expired and cannot be reused — `onPaymentSessionExpired` fires unconditionally after cancel. It also fires after `onAuthorizeFailed` when the payment attempt itself triggered session expiry (for example a failed redirect that left the session in an unusable state). The `onPaymentSessionExpired` callback is required — the merchant must fetch fresh init data from their backend before the user can attempt payment again.
**Stored instruments:** Saved PayPal accounts are charged directly via the prebuilt button, or `launcher.authorize(storedInstrument = storedInstrument)` for a custom-UI flow. A frictionless charge opens no browser — the request goes straight to the API and returns `onAuthorizeSuccess` or `onAuthorizeFailed`.
## 3DS (3D Secure)
3DS is a security protocol that adds an extra verification step for card payments. Here's what happens under the hood:
### The Flow
1. **SDK requests authorization** — The Payrails API may respond with a 3DS challenge URL
2. **Browser opens** — The SDK opens the URL in a Chrome Custom Tab (preferred) or the system browser
3. **User completes challenge** — The user interacts with their bank's 3DS page
4. **User returns to app** — The Custom Tab closes or the user navigates back
5. **SDK polls for result** — The SDK checks the execution status until it reaches a terminal state
6. **Result delivered** — `onAuthorizeSuccess` or `onAuthorizeFailed` fires
### Why Custom Tabs (Not WebView)?
The SDK uses Chrome Custom Tabs instead of WebView for 3DS because:
* **Bank compatibility** — Some issuer/ACS pages block or behave incorrectly in embedded WebViews
* **Security** — Custom Tabs provide better isolation than WebViews, reducing compliance risk
* **User experience** — Deep-link returns are more reliable from Custom Tabs; users are less likely to get stuck
### Background Handling
When a user backgrounds your app during 3DS (e.g., switches to their banking app for OTP), the SDK uses a foreground service to keep polling alive. This ensures the payment result is captured even if the user takes time to complete the challenge.
### Session Recovery
If the 3DS flow is abandoned (user closes the browser without completing) or remains non-terminal past the reconciliation window:
1. The `onAuthorizeFailed` delegate callback fires
2. If `onSessionExpired` is configured, the SDK calls it to get fresh init data
3. The SDK refreshes the session state, allowing the user to retry
Configure this in your session setup:
```kotlin theme={null}
val configuration = Configuration(
initData = initData,
option = Options(
redirectSessionLifecycle = RedirectSessionLifecycle(
onSessionExpired = {
val refreshed = fetchInitPayloadFromBackend()
InitData(version = refreshed.version, data = refreshed.data)
}
)
)
)
```
## Button Modes
`CardPaymentButton` operates in two mutually exclusive modes:
```
┌─────────────────────────────────────────────────────┐
│ CardPaymentButton │
│ │
│ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ Form Mode │ │ Stored Instrument Mode│ │
│ │ │ │ │ │
│ │ Validates card │ │ Skips card form │ │
│ │ form, encrypts, │◄──►│ Uses stored data │ │
│ │ then authorizes │ │ directly │ │
│ └──────────────────┘ └───────────────────────┘ │
│ │
│ Switching: │
│ • setStoredInstrument() → enters stored mode │
│ • User types in card form → returns to form mode │
│ • clearStoredInstrument() → returns to form mode │
└─────────────────────────────────────────────────────┘
```
This means a single `CardPaymentButton` instance handles both new card payments and stored instrument payments. You don't need separate buttons.
## Custom UI: the launcher and the internal client
There is **one** public way to execute a payment — `PayrailsPaymentLauncher` — built on an
internal networking client inside `Session`:
```
You own the button & layout SDK owns everything post-tap
┌────────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ PayrailsPaymentLauncher (public) │ │ Google Pay sheet · 3DS / PayPal / │
│ your Button → launcher.authorize(...) │─►│ redirect Custom Tab · polling │
└────────────────────────────────────────┘ └──────────────────────────────────────┘
│ delegates networking
▼
┌────────────────────────────────────────┐
│ Session (internal authorize) │ stateless networking —
│ not a public payment trigger │ no Activity, no UI.
└────────────────────────────────────────┘
```
* **`PayrailsPaymentLauncher`** is the public handle. You draw your own button; the launcher
owns the post-tap UI hand-off. It is lifecycle-bound — construct it early
(`Activity.onCreate` or a Compose `remember`) so it can register the Google Pay Activity
Result contract before the host is `STARTED`. Because it always holds an `Activity` (and
therefore a presenter), a single `authorize(...)` call handles **both** frictionless and
3DS/step-up outcomes — you never predict which path a charge will take.
* **`Session`** holds the stateless networking (no `Activity`, no UI). Its
`authorize(...)` overloads are **`internal`** — the launcher and the prebuilt buttons
delegate into them, but they are **not** a public payment trigger. `Session`'s public
surface is queries and configuration (`query`, `update`, `getStoredInstruments`,
`getPaymentMethodConfig`, `isGooglePayAvailable`).
This mirrors the idiomatic Android "launcher owns the UI, client does the networking" split
(e.g. Stripe's `PaymentLauncher`, Braintree's `Launcher` + `Client`) — with a single public
trigger.
### Why no encryption API
The launcher's card path — `authorize(encryptedCardData: String)` — accepts an
**already-encrypted** string only. The SDK deliberately exposes no client-side encryption API
and no raw-card API on this surface: raw card fields never cross the public boundary, and
producing the encrypted payload (via the client-side encryption library) stays the
integrator's responsibility. This keeps the PCI-sensitive surface area minimal and matches how
the prebuilt `CardForm` already feeds an encrypted payload into the same internal flow (see
[Security Model](#security-model)).
### Why there's no public headless trigger
`Session` deliberately exposes no public `authorize`. Whether a charge needs a 3DS step-up is
decided by the issuer at authorization time — not knowable up front — and
[`ActionResult`](/docs/orchestration/checkout-sdks/android/api-reference#actionresult) is terminal, with no "pending
challenge" variant a presenter-less caller could resume. A public headless trigger would
therefore fail unpredictably on 3DS. Instead, `PayrailsPaymentLauncher` (which always has a
presenter) is the single public path and completes the challenge for you.
## The Pre-Authorization Gate
Every element — card form, card button, PayPal, Google Pay, generic redirect, stored instrument and
the headless launcher — starts its payment inside `Session`, not inside the element. That is what
lets one merchant-supplied gate cover all of them rather than each element carrying its own hook:
the gate lives on `Session`'s authorization entry points, so an element cannot accidentally skip it
and a new element inherits it.
```mermaid theme={null}
flowchart TD
Element["Any element (card · PayPal · wallet · redirect)"] --> Session["Payrails Session"]
Session --> Gate{"onRequestStart registered?"}
Gate -- "no" --> Authorize["POST authorize"]
Gate -- "yes" --> Ask["Merchant handler answers"]
Ask -- "Proceed" --> Authorize
Ask -- "Refuse · throws · timeout" --> Blocked["Stopped VALIDATION_FAILED"]
Authorize --> Provider["Provider UI (sheet · Custom Tab)"]
Provider --> Confirm["POST confirm"]
```
The gate sits **before** the authorization request and before any provider UI — including the
Google Pay sheet, which the UI layer opens itself, so the SDK consults the gate there rather than
when the token comes back. That position is the whole point: a merchant revalidating a voucher, wallet balance or loyalty points needs the answer
while the customer is still on the checkout screen, not after they have approved a payment in
PayPal. Validating when the element is first drawn would answer against a basket the customer can
still change; the further the tap drifts from the check, the staler the answer.
Two design consequences follow.
**Silence is a refusal, not a pass.** If the handler never returns, the SDK stops the payment after
ten seconds; if it throws, the SDK stops it too. A gate whose failure mode is "authorize anyway"
gives no guarantee at all, and the alternative — a button spinning indefinitely because a merchant
endpoint hung — is worse than a refused payment the customer can retry.
**A block is not a decline.** It surfaces as `AuthorizationFailureReason.VALIDATION_FAILED`, distinct
from `AUTHORIZATION_ERROR`, so your own decision never lands in your analytics as an issuer
rejection. Nothing reached the backend, so there is no payment attempt to reconcile.
**The refusal carries its own reason.** `RequestStartDecision.Refuse(message)` rather than a bare
`false`, because only the merchant knows *why* they refused — an expired voucher reads differently
to a changed basket — and only they can phrase it for their customer. The message arrives as
`AuthorizationFailure.message`, the same place all other failure text comes from, so it needs no
separate channel and no correlation by `executionId`. The SDK's own diagnostics for a silent or
throwing handler are deliberately *not* delivered this way: those describe an integration fault, and
an exception string may carry internals no customer should see.
The handler receives `paymentMethodCode` and can therefore gate one method while leaving the rest
untouched. It is opt-in: sessions created without it keep a fully synchronous payment path.
### Why not `onPaymentButtonClicked`?
The two hooks look adjacent but answer different questions, and conflating them is the mistake worth
avoiding:
| | `onPaymentButtonClicked` | `onRequestStart` |
| - | - | - |
| Purpose | The customer tapped | May this payment proceed? |
| Returns | `Unit` | `RequestStartDecision` |
| SDK waits for it | No | Yes |
| Can stop the payment | No | Yes |
| Use for | Analytics, observability | Any check the payment depends on |
`onPaymentButtonClicked` is deliberately a notification. It cannot gate anything, because the SDK
never inspects it and does not wait — work started inside it races the authorization rather than
preceding it. The Web SDK draws the same line between its `buttonClicked` and `requestStart` events.
See [How to gate payment authorization](/docs/orchestration/how-to-gate-payment-authorization).
## Security Model
### Card Data
Card data never leaves the device in plain text. The flow is:
1. User enters card details into `CardForm` (Compose text fields)
2. On payment, the SDK uses the CSE (Client-Side Encryption) library to encrypt card data
3. Only the encrypted payload is sent to the Payrails API
4. The SDK never stores raw card data beyond the current form state
### Logging
The SDK has two logging channels:
* **In-memory buffer** — Always active. Stores the last 500 timestamped log entries for the built-in debug viewer.
* **Logcat** — Off by default. Gated behind `Log.isLoggable("PayrailsSDK", Log.DEBUG)`, so no log output appears in production unless explicitly enabled via `adb shell setprop log.tag.PayrailsSDK DEBUG`.
The SDK never logs raw card data, tokens, or PII through either channel. See [Troubleshooting](/docs/orchestration/checkout-sdks/android/troubleshooting#enabling-sdk-debug-logs) for how to enable Logcat output during development.
### Client Context
By default, the SDK collects device metadata (OS type, screen size, timezone, language) and attaches it to authorization requests. This data helps with fraud detection and 3DS risk assessment.
You can opt out:
```kotlin theme={null}
val configuration = Configuration(
initData = initData,
option = Options(collectMetadata = false)
)
```
## Card Tokenization
Tokenization and payment are distinct operations that share the same card entry UI but diverge at the point of submission.
### Two paths, one form
When a user fills in the `CardForm`, the SDK can do one of two things with the data:
* **Payment path** — the `CardPaymentButton` triggers form collection, encrypts card data, and calls the Payrails authorize endpoint. A charge is attempted.
* **Tokenization path** — `session.tokenize(TokenizationRequest.Card(cardForm))` encrypts card data and calls the Payrails vault endpoint. No charge is attempted. The result is a saved instrument with an ID. (`CardForm.tokenize()` is a convenience wrapper over this same path.)
The encryption step is identical in both paths. What changes is the endpoint and the intent.
```
CardForm (collects + validates card data)
│
├── CardPaymentButton.Render() → authorize endpoint → charge
│
└── session.tokenize(Card(cardForm)) → vault endpoint → saved instrument
```
### Why separate operations?
Card vaulting without payment is a common need in commerce: onboarding flows that save a card before the first transaction, subscription setups where billing happens later, in-app wallets where the user manages saved payment methods explicitly. Bundling vaulting into the payment flow would force merchants to make a charge to save a card, which is the wrong user experience in these cases.
The SDK exposes tokenization as a first-class standalone method rather than a flag on the payment call to make this separation explicit and avoid ambiguity about what a given call will do.
### Card and Google Pay
Tokenization supports two payment methods — **card** and **Google Pay** — mirroring the iOS SDK's `card` and Apple Pay cases. Both go through the same `session.tokenize(request, options)` call and converge on one save-instrument request; only the source of the sensitive data differs: the card path encrypts the live card fields client-side, while the Google Pay path drives the Google Pay sheet (via a presenter) and saves the returned wallet token. Other payment methods are out of scope for tokenization.
### What `storeInstrument` means
`TokenizeOptions.storeInstrument` controls whether the vault persists the instrument for repeated use after the tokenization call:
* `storeInstrument = false` (default) — the card data is encrypted and vaulted for a single use. The instrument may be usable once, then expired.
* `storeInstrument = true` — the instrument is retained in the holder's vault and can be retrieved via `session.getStoredInstruments()` for future payments.
For save-card-for-later flows, always set `storeInstrument = true`.
### What `FutureUsage` signals
`FutureUsage` is a signal to the card network about how the merchant intends to use the stored credential. Card networks use this to apply appropriate authorization rules:
* `CardOnFile` — the cardholder is present and initiating the payment (the common case for checkout)
* `Subscription` — a recurring, scheduled charge that the cardholder pre-authorized
* `UnscheduledCardOnFile` — a merchant-initiated charge with no fixed schedule
Setting the right value helps reduce authorization declines and chargeback risk. It is not merely cosmetic — it affects how the issuer processes subsequent charges made with the stored credential.
### Instrument lifecycle after tokenization
A tokenized card becomes a `StoredInstrument` with a stable `id`. That ID can be used to:
* Pay: `payButton.setStoredInstrument(instrument)` or `launcher.authorize(storedInstrument = instrument)`
* Delete: `session.deleteInstrument(instrumentId)`
* Update (e.g., set as default): `session.updateInstrument(instrumentId, UpdateInstrumentBody(default = true))`
The `SaveInstrumentResponse` returned by `tokenize()` is the snapshot at creation time. Retrieve the live instrument list from `session.getStoredInstruments()`.
## Element Lifecycle
Elements are tied to the session lifecycle:
```
createSession() ──► Session active
│
├── createCardForm()
├── createCardPaymentButton()
├── createGooglePayButton()
├── createPayPalButton()
│
▼
Activity destroyed (isFinishing = true)
│
▼
session.destroy() called automatically
│
▼
All element references become stale
```
The SDK registers an `ActivityLifecycleCallbacks` listener internally. When the bound Activity is destroyed (and `isFinishing` is true), the SDK automatically cleans up the session and element state.
## Co-Branded Cards
Some cards are **co-branded** (also called **co-badged**, or **card brand choice**): they carry two payment networks — a local/domestic scheme (e.g. `mada`, `cartesbancaires`) and an international one (e.g. `visa`, `mastercard`). The shopper — or merchant preference — decides which network the payment routes over, which has real cost and acceptance implications.
### How it works
1. As the shopper types, the SDK looks up the BIN (first 6–8 digits) against the `binLookup` endpoint — throttled (\~500 ms), cached, and de-duplicated, with stale responses discarded so a slow lookup can never overwrite a newer card number.
2. The detected schemes resolve into an ordered list of canonical codes (merchant `preferredSchemes` first, else local-first) with a default selection.
3. When two or more schemes are detected, the **Card Brand selector** renders inside `CardForm`; the shopper can change the selection.
4. The selected scheme is submitted as `paymentInstrumentData.preferredScheme` on the payment.
### When it activates (gating)
Co-branded handling runs only when **both** hold: the backend `featureConfig.coBrandedCardsRollout` flag is active for the session (evaluated once, then cached) **and** the init response includes the `binLookup` link. Otherwise the form behaves exactly as a single-brand card form — no selector, no extra payload.
### Design notes
* **Uniform tiles.** All scheme tiles are styled identically — the only visual difference is the check mark on the selected one — satisfying the EU IFR 2015/751 visual-balance requirement. There is no per-scheme styling.
* **Canonical string codes.** Schemes are plain strings (`mada`, `mastercard`, …), not a closed enum, so new networks never require an SDK release.
* **Resilient.** A failed or unavailable BIN lookup degrades silently to a non-co-branded card; the public `Session.binLookup` returns `null` rather than throwing (matching the iOS and Web SDKs).
## Further Reading
* **[Quick Start](/docs/orchestration/checkout-sdks/android)** — Get a card payment working in 15 minutes
* **[How to Accept PayPal Payments](/docs/orchestration/checkout-sdks/android/how-to-accept-paypal-payments)** — PayPal redirect flow and stored instruments
* **[How to Accept Redirect Payments](/docs/orchestration/checkout-sdks/android/how-to-accept-redirect-payments)** — iDEAL, Bancontact, Sofort, and other redirect methods
* **[How to Tokenize a Card](/docs/orchestration/checkout-sdks/android/how-to-tokenize-card)** — Save a card to vault without charging it
* **[Troubleshooting](/docs/orchestration/checkout-sdks/android/troubleshooting)** — Common issues and solutions
* **[Styling Guide](/docs/orchestration/checkout-sdks/android/styling-guide)** — Customize the look and feel
* **[API Reference](/docs/orchestration/checkout-sdks/android/api-reference)** — Complete public API documentation
# Styling Guide
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/styling-guide
This guide shows how to customize the appearance of Payrails SDK components to match your brand.
## Overview
The SDK has two main styling surfaces:
| Component | Styling class | What it controls |
| - | - | - |
| Card form | `CardFormStylesConfig` | Wrapper, input fields, labels, errors, checkbox |
| Pay button | `CardButtonStyle` | Background, text, border, corner radius, loading/disabled states |
Styles are set at creation time and use Compose-native types (`Color`, `Dp`, `TextStyle`, `PaddingValues`).
## Card Form Styling
### Style Hierarchy
Card form styles cascade from general to specific:
```
CardFormStylesConfig
├── wrapperStyle → outer container (background, border, padding)
├── baseStyle → applies to all input fields as a baseline
├── allInputFieldStyles → field states (base, focus, completed, invalid)
├── inputFieldStyles → per-field overrides (keyed by CardFieldType)
├── labelStyles → per-field label overrides
├── errorTextStyle → error message appearance
├── storeInstrumentCheckboxStyle → checkbox wrapper
└── storeInstrumentCheckboxLabelStyle → checkbox label text
```
### Basic Example
```kotlin theme={null}
val cardForm = Payrails.createCardForm(
config = CardFormConfig(
showCardHolderName = true,
showSingleExpiryDateField = true,
styles = CardFormStylesConfig(
wrapperStyle = CardWrapperStyle(
backgroundColor = Color(0xFFF5F5F5),
cornerRadius = 12.dp,
padding = PaddingValues(16.dp)
),
allInputFieldStyles = CardFieldSpecificStyles(
base = CardStyle(
borderColor = Color(0xFFE0E0E0),
borderWidth = 1.dp,
cornerRadius = 8.dp,
textColor = Color(0xFF212121)
),
focus = CardStyle(
borderColor = Color(0xFF1976D2),
cursorColor = Color(0xFF1976D2)
),
invalid = CardStyle(
borderColor = Color(0xFFD32F2F)
)
),
errorTextStyle = CardStyle(
textColor = Color(0xFFD32F2F)
)
)
)
)
```
### Per-Field Styling
Override styles for specific fields:
```kotlin theme={null}
styles = CardFormStylesConfig(
allInputFieldStyles = CardFieldSpecificStyles(
base = CardStyle(borderColor = Color.Gray, cornerRadius = 8.dp)
),
inputFieldStyles = mapOf(
CardFieldType.CARD_NUMBER to CardFieldSpecificStyles(
base = CardStyle(borderColor = Color.Black, borderWidth = 2.dp)
)
),
labelStyles = mapOf(
CardFieldType.CVV to CardStyle(textColor = Color.DarkGray)
)
)
```
### Default Values
The SDK applies these defaults if you don't provide styles:
| Property | Default |
| - | - |
| Field border color | Inherited from theme (no explicit default) |
| Field border width | `1.dp` |
| Field corner radius | `2.dp` |
| Field text color | Inherited from theme (no explicit default) |
| Focus border color | `#D63D00` (Payrails orange) |
| Focus cursor color | `#D63D00` |
| Completed border color | `Color.Green` |
| Invalid border color | `Color.Red` |
| Error text color | `Color.Red` |
| `fieldContentPadding` (OUTLINED) | `start = 16.dp`, other sides from Material3 |
| `fieldContentPadding` (FILLED) | `start = 0.dp`, other sides from Material3 |
Use `CardFormStylesConfig.defaultConfig` as a baseline and override only what you need. Custom styles are merged over defaults — you only need to specify properties you want to change.
### Spacing Tokens
Control form-level spacing with tokens on `CardFormStylesConfig`:
```kotlin theme={null}
styles = CardFormStylesConfig(
rowSpacing = 16.dp, // vertical gap between form rows (default: 12.dp)
fieldSpacing = 8.dp, // horizontal gap between fields in a row (default: 12.dp)
errorSpacing = 2.dp, // gap between field and error text, only when error is shown (default: 4.dp)
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), // outer padding (default: 16.dp all)
fieldHeight = 48.dp, // explicit height for text fields (default: wrap content)
fieldContentPadding = PaddingValues(start = 12.dp, top = 8.dp, end = 8.dp, bottom = 8.dp) // inner padding of each text field (see below)
)
```
All spacing tokens are nullable. When `null`, the hardcoded default is used. These tokens are merged the same way as other style properties — a non-null value wins, null falls through to the fallback. Error spacing is only applied when a field has an active error — fields without errors have no reserved space below them.
### Field Content Padding
`fieldContentPadding` controls the inner padding of every text field — the distance between the field border and the text cursor. When not set, the SDK applies variant-specific defaults:
| Variant | Default `start` | Other sides |
| - | - | - |
| `OUTLINED` | `16.dp` | Material3 defaults |
| `FILLED` | `0.dp` | Material3 defaults |
The `OUTLINED` default (`16.dp`) matches Material3's standard inset so text sits visibly away from the border. The `FILLED` default (`0.dp`) lets text align flush with any decoration you've applied to the wrapper.
To override for both variants at once:
```kotlin theme={null}
styles = CardFormStylesConfig(
fieldContentPadding = PaddingValues(start = 12.dp, top = 8.dp, end = 8.dp, bottom = 8.dp)
)
```
To use variant-specific padding, create two separate `CardFormStylesConfig` instances and pass the appropriate one based on your chosen `FieldVariant`.
### Label Placement
Choose where field labels appear:
```kotlin theme={null}
config = CardFormConfig(
labelPlacement = LabelPlacement.ABOVE, // labels above field (default: FLOATING)
labelSpacing = 6.dp // gap between label and field (default: 4.dp)
)
```
* `LabelPlacement.FLOATING` (default) — labels render as Material3 floating labels inside the text field.
* `LabelPlacement.ABOVE` — labels render as separate `Text` composables above each field, with a configurable gap.
When using `LabelPlacement.ABOVE`, use `fieldContentPadding` on `CardFormStylesConfig` to control the inner padding of the text field (the distance from the field edges to the text cursor). The SDK default is `start = 16.dp` for `OUTLINED` and `start = 0.dp` for `FILLED`; all other sides fall through to Material3 defaults. See [Field Content Padding](#field-content-padding) for details.
### Field Variant
Choose the Material3 text field style:
```kotlin theme={null}
config = CardFormConfig(
fieldVariant = FieldVariant.FILLED // (default: OUTLINED)
)
```
* `FieldVariant.OUTLINED` (default) — `OutlinedTextField` with a full border.
* `FieldVariant.FILLED` — `TextField` with a background fill and bottom indicator.
### CardStyle Properties
`CardStyle` (alias of `Style`) supports these properties:
| Property | Type | Description |
| - | - | - |
| `textColor` | `Color?` | Text color |
| `backgroundColor` | `Color?` | Background fill |
| `borderColor` | `Color?` | Border color |
| `borderWidth` | `Dp?` | Border width |
| `cornerRadius` | `Dp?` | Corner radius |
| `font` | `TextStyle?` | Font/text style (size, weight, family) |
| `padding` | `PaddingValues?` | Content padding |
| `width` | `Dp?` | Fixed width |
| `height` | `Dp?` | Fixed height |
| `minWidth` | `Dp?` | Minimum width |
| `maxWidth` | `Dp?` | Maximum width |
| `minHeight` | `Dp?` | Minimum height |
| `maxHeight` | `Dp?` | Maximum height |
| `placeholderColor` | `Color?` | Placeholder text color |
| `cursorColor` | `Color?` | Input cursor color |
## Pay Button Styling
Button styling is configured on `CardPaymentButton`, not on the card form.
### Basic Example
```kotlin theme={null}
val payButton = Payrails.createCardPaymentButton(
translations = CardPaymenButtonTranslations(label = "Pay Now"),
buttonStyle = CardButtonStyle(
backgroundColor = Color(0xFF1976D2),
textColor = Color.White,
cornerRadius = 8.dp,
height = 48.dp,
fillMaxWidth = true
)
)
```
### Button States
The button supports three visual states. Configure each independently:
```kotlin theme={null}
val payButton = Payrails.createCardPaymentButton(
translations = CardPaymenButtonTranslations(label = "Pay Now"),
buttonStyle = CardButtonStyle(
// Default (enabled) state
backgroundColor = Color(0xFF1976D2),
textColor = Color.White,
cornerRadius = 8.dp,
// Disabled state (shown when disabledByDefault=true and form is invalid)
disabledStyle = CardButtonStyle(
backgroundColor = Color(0xFFE0E0E0),
textColor = Color(0xFF9E9E9E),
borderColor = Color(0xFFBDBDBD),
borderWidth = 1.dp
),
// Loading state (shown during payment processing)
loadingStyle = CardButtonStyle(
backgroundColor = Color(0xFF1565C0)
),
loadingIndicatorColor = Color.White
)
)
```
State styles are merged over the base style — you only need to specify properties that differ from the enabled state.
### CardButtonStyle Properties
| Property | Type | Default | Description |
| - | - | - | - |
| `backgroundColor` | `Color?` | `#1976D2` (blue) | Button background |
| `textColor` | `Color?` | `Color.White` | Button label color |
| `font` | `TextStyle?` | System default | Label text style |
| `cornerRadius` | `Dp?` | `8.dp` | Corner radius |
| `borderWidth` | `Dp?` | None | Border width |
| `borderColor` | `Color?` | None | Border color |
| `contentPadding` | `PaddingValues?` | Material default | Content padding |
| `height` | `Dp?` | Wrap content | Fixed height |
| `fillMaxWidth` | `Boolean` | `true` | Stretch to fill width |
| `disabledStyle` | `CardButtonStyle?` | Gray (#E0E0E0) | Override for disabled state |
| `loadingStyle` | `CardButtonStyle?` | None | Override for loading state |
| `loadingIndicatorColor` | `Color?` | `Color.White` | Spinner color |
| `loadingIndicatorSize` | `Dp?` | `20.dp` | Spinner diameter |
| `loadingIndicatorStrokeWidth` | `Dp?` | `2.dp` | Spinner stroke width |
| `elevation` | `Dp?` | Material3 default | Button shadow depth |
| `opacity` | `Float?` | `1.0f` | Button opacity (0.0–1.0) |
| `minHeight` | `Dp?` | Material3 default | Minimum button height |
All new tokens participate in state-variant merging — set them on `disabledStyle` or `loadingStyle` to vary by state.
### Disable-Until-Valid Pattern
To keep the button disabled until the card form is valid:
```kotlin theme={null}
val payButton = Payrails.createCardPaymentButton(
translations = CardPaymenButtonTranslations(label = "Pay Now"),
disabledByDefault = true,
buttonStyle = CardButtonStyle(
backgroundColor = Color(0xFF1976D2),
textColor = Color.White,
disabledStyle = CardButtonStyle(
backgroundColor = Color(0xFFE0E0E0),
textColor = Color(0xFF9E9E9E)
)
)
)
```
The button automatically enables when the card form becomes valid (all fields pass validation).
## Card Form Layout
Customize which fields appear and how they're arranged:
```kotlin theme={null}
val cardForm = Payrails.createCardForm(
config = CardFormConfig(
showCardHolderName = true,
showSingleExpiryDateField = true,
layout = listOf(
listOf(CardFieldType.CARDHOLDER_NAME), // Row 1: full width
listOf(CardFieldType.CARD_NUMBER), // Row 2: full width
listOf(CardFieldType.EXPIRATION_DATE, CardFieldType.CVV) // Row 3: side by side
)
)
)
```
### Layout Rules
* Each inner list is a row; fields in the same row share space equally
* `EXPIRATION_DATE` is a combined MM/YY field (use with `showSingleExpiryDateField = true`)
* `EXPIRATION_MONTH` + `EXPIRATION_YEAR` are separate fields (use without `showSingleExpiryDateField`)
* Cannot mix `EXPIRATION_DATE` with `EXPIRATION_MONTH`/`EXPIRATION_YEAR`
* No duplicate fields allowed
### Card Network Icons
Show detected card network icons in the card number field:
```kotlin theme={null}
val cardForm = Payrails.createCardForm(
config = CardFormConfig(
showCardIcon = true,
cardIconAlignment = CardIconAlignment.right // or .left
)
)
```
Icons update automatically as the user types and the SDK detects the card network (Visa, Mastercard, Amex, Discover).
## Translations
Customize all user-facing text:
```kotlin theme={null}
val cardForm = Payrails.createCardForm(
config = CardFormConfig(
translations = CardTranslations(
placeholders = CardTranslations.Placeholders(mutableMapOf(
CardFieldType.CARDHOLDER_NAME to "Name on card",
CardFieldType.CARD_NUMBER to "1234 5678 9012 3456",
CardFieldType.EXPIRATION_DATE to "MM/YY",
CardFieldType.CVV to "123"
)),
labels = CardTranslations.Labels(
values = mutableMapOf(
CardFieldType.CARDHOLDER_NAME to "Name",
CardFieldType.CARD_NUMBER to "Card number",
CardFieldType.EXPIRATION_DATE to "Expiry",
CardFieldType.CVV to "Security code"
),
storeInstrument = "Save this card"
),
error = CardTranslations.ErrorMessages(mutableMapOf(
CardFieldType.CARD_NUMBER to "Invalid card number"
))
)
)
)
```
### Save Instrument Label Priority
The checkbox label resolves in this order:
1. `labels.storeInstrument` (if set)
2. `labels.saveInstrument` (if set)
3. `labels.saveCreditCard` (if set)
4. `"Save instrument"` (default)
## Stored Instruments
The SDK does not provide a pre-built stored-instruments UI. Retrieve saved cards with `session.getStoredInstruments()` (called on the `Session` returned by `Payrails.createSession(...)`) and build your own picker using standard Compose components. When the user selects an instrument, call `cardPaymentButton.setStoredInstrument(instrument)` to pay with it.
## Card Brand Selector
For [co-branded cards](/docs/orchestration/checkout-sdks/android/sdk-concepts#co-branded-cards), the Card Brand selector renders automatically inside the card form. Style it via `CardFormStylesConfig.cardBrandSelector`; localize the title/subtitle via `CardTranslations`.
```kotlin theme={null}
CardFormStylesConfig(
cardBrandSelector = CardBrandSelectorStyle(
titleColor = Color.Black.toArgb(),
subtitleColor = 0xFF6B7280.toInt(),
tileBackgroundColor = Color.White.toArgb(),
tileBorderColor = 0xFFD0D5DD.toInt(),
tileTitleColor = Color.Black.toArgb(),
selectedTileBorderColor = 0xFF0F63BD.toInt(),
selectedCheckColor = 0xFF0F63BD.toInt(),
tileCornerRadius = 12, // dp
)
)
```
### CardBrandSelectorStyle Properties
All fields are optional. Color fields are `@ColorInt` (ARGB); an unset color falls back to the current `MaterialTheme.colorScheme`, so the selector adapts to light/dark themes automatically.
| Property | Type | Effect (unset → theme default) |
| - | - | - |
| `titleColor` | `Int?` | Selector title color (→ `onSurface`) |
| `subtitleColor` | `Int?` | Subtitle color (→ `onSurfaceVariant`) |
| `tileTitleColor` | `Int?` | Brand-name color on a tile (→ `onSurface`) |
| `tileBackgroundColor` | `Int?` | Tile background (→ `surface`) |
| `tileBorderColor` | `Int?` | Unselected tile border + indicator ring (→ `outline`) |
| `selectedTileBorderColor` | `Int?` | Selected tile border + check badge (→ `primary`) |
| `selectedCheckColor` | `Int?` | Check-badge color (→ selected border color) |
| `tileCornerRadius` | `Int?` | Tile corner radius, in dp |
Tiles are styled uniformly (no per-scheme styling) per the EU IFR 2015/751 visual-balance requirement — the only per-tile difference is the selected check mark.
## Further Reading
* **[Quick Start](/docs/orchestration/checkout-sdks/android)** — Get a basic integration working first
* **[API Reference](/docs/orchestration/checkout-sdks/android/api-reference)** — Full property reference for all style classes
* **[SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts)** — Understand element composition and button modes
# Troubleshooting
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/troubleshooting
Common issues and their solutions when integrating the Payrails Android SDK.
## Enabling SDK Debug Logs
The SDK's Logcat output is **off by default** — it produces no Logcat messages in production. To enable debug logging:
```bash theme={null}
adb shell setprop log.tag.PayrailsSDK DEBUG
```
Then filter Logcat:
```bash theme={null}
adb logcat -s PayrailsSDK:D
```
This shows SDK lifecycle events, payment flow steps, and error details. The setting persists until reboot.
To turn logging off again:
```bash theme={null}
adb shell setprop log.tag.PayrailsSDK INFO
```
The SDK also maintains an in-memory log buffer (last 500 entries) that stays active regardless of the Logcat setting. This buffer and the associated debug viewer are internal tooling used during SDK development and are not exposed as part of the public SDK surface. Logcat logging is the supported opt-in channel for merchant development and debugging.
**Security**
The SDK never logs raw card data, tokens, or PII. Log messages contain only operational information.
## Build Issues
### "Payrails session must be initialized"
**Error:** `IllegalStateException: Payrails session must be initialized`
**Cause:** You called `createCardForm()` or `createCardPaymentButton()` before `Payrails.createSession()`.
**Fix:** Ensure `createSession()` completes successfully before creating any UI elements:
```kotlin theme={null}
// Correct order
val session = Payrails.createSession(configuration) // must complete first
val payButton = Payrails.createCardPaymentButton(...) // now safe
val cardForm = Payrails.createCardForm() // now safe
```
### Dependency Resolution Fails
**Error:** `Could not find com.payrails.android:checkout:`
**Fixes:**
* Verify the SDK is published to the repository you're using (Maven Central or Maven Local)
* For local development, run `./gradlew publishSdkToMavenLocal` first
* Ensure `mavenLocal()` is listed before `mavenCentral()` in your settings if using local artifacts
* Clear stale local artifacts:
```bash theme={null}
rm -rf ~/.m2/repository/com/payrails/android/checkout
./gradlew --refresh-dependencies
```
### Missing CSE Dependency
**Error:** Runtime crash or `NoClassDefFoundError` related to card encryption
**Fix:** Add both dependencies:
```kotlin theme={null}
dependencies {
implementation("com.payrails.android:checkout:")
implementation("com.payrails.android:cse:") // required for card payments
}
```
## Card Form Issues
### Card Form Not Validating
**Symptom:** User can submit without validation, or errors don't show.
**Checks:**
* The `CardPaymentButton` validates the form automatically when clicked. You don't need to call `validate()` manually.
* Validation errors appear when a field loses focus (blur validation) or when the user taps "Pay Now". For split expiry fields, the year field also validates the combined expiry date on blur — if the month/year are individually valid but the date is expired, the error appears on the year field.
* If you're using `onChange` events, check `event.isValid` for real-time validation state.
* Error messages only occupy vertical space when an error is present. Fields shift to fill the space when errors are cleared.
### Card Network Not Detected
**Symptom:** `cardNetwork` stays `UNKNOWN`, card icon doesn't update.
**Checks:**
* Network detection requires at least 1–2 digits. Enter a full card number to see detection.
* Supported networks: Visa, Mastercard, Amex, Discover. Other networks show as `UNKNOWN`.
* Ensure `showCardIcon = true` in `CardFormConfig` if you expect to see icons.
### Layout Crashes at Creation
**Error:** `IllegalArgumentException: CardForm layout contains unsupported fields` or similar.
**Fix:** Check your `layout` configuration:
* Don't mix `EXPIRATION_DATE` with `EXPIRATION_MONTH`/`EXPIRATION_YEAR`
* Don't duplicate fields across rows
* Supported fields: `CARDHOLDER_NAME`, `CARD_NUMBER`, `EXPIRATION_DATE`, `EXPIRATION_MONTH`, `EXPIRATION_YEAR`, `CVV`
## Google Pay Issues
### Google Pay Button Not Appearing
**Symptom:** `GooglePayButton.Render()` is called but the button is never visible. `onGooglePayAvailable` is not called.
**Explanation:** The button is hidden by default and only becomes visible after `isReadyToPay()` succeeds. This check can fail for several reasons.
**Checks:**
* **Emulator:** Google Pay is not available on most emulators. Test on a physical device with the Google Pay app installed and a card added.
* **Google Pay app:** Ensure the Google Pay app is installed and set up on the device with at least one payment method.
* **Merchant configuration:** Verify that Google Pay is enabled as a payment method in your Payrails dashboard. The SDK reads Google Pay config from `paymentCompositionOptions` — if the backend doesn't include it, `isReadyToPay()` will fail.
* **Environment:** In `Env.TEST` mode, Google Pay uses the test environment which has different availability. Switch to a production-configured device for full testing.
### Google Pay Payment Sheet Not Opening
**Symptom:** User taps the Google Pay button, `onPaymentButtonClicked` fires, but the payment sheet doesn't appear.
**Checks:**
* Ensure the Activity is valid and not finishing when the button is tapped
* Check that the Google Pay API version in your backend config matches what's expected (`apiVersion: 2`, `apiVersionMinor: 0`)
* Verify `merchantInfo` is configured in production (required by Google)
### Google Pay Authorization Fails Immediately
**Symptom:** `onAuthorizeFailed` fires right after the user selects a payment method in the sheet.
**Checks:**
* Verify your Payrails dashboard has the correct gateway credentials for Google Pay
* Check the `PayrailsError` details in the delegate callback
* If 3DS is triggered (`onThreeDSecureChallenge`), follow the same 3DS troubleshooting as card payments below
## Payment Issues
### 3DS Browser Doesn't Open
**Symptom:** Payment hangs after authorization, no browser opens.
**Checks:**
* The SDK opens 3DS challenges automatically via Chrome Custom Tabs (or the system browser as fallback)
* Ensure `Render()` has been composed before `pay()` is called — the button creates its internal presenter during composition
* The device must have a browser installed (Chrome Custom Tabs preferred)
* Check that the Activity reference is still valid (not destroyed)
### 3DS Succeeds But onAuthorizeSuccess Not Called
**Symptom:** User completes 3DS in browser, returns to app, but delegate isn't called.
**Explanation:** This is expected behavior. The SDK polls the execution status after the user returns. The delegate callback fires only when polling reaches a terminal state (success or failure).
**Possible causes for delay:**
* Slow backend processing — the SDK continues polling
* Network connectivity issues — polling retries with backoff
* If polling times out, `onAuthorizeFailed` fires instead (the SDK triggers session recovery if `onSessionExpired` is configured)
### Payment Always Returns Failed
**Checks:**
1. Verify your init payload is correct (version and data from your backend)
2. Check that your Payrails dashboard has the payment method configured
3. Ensure your test card numbers are valid for your sandbox environment
4. Check the `AuthorizationFailure` (its `code` and `message`) passed to your `onAuthorizeFailed` delegate callback for specific details (e.g., `AUTHORIZATION_ERROR`, `AUTHENTICATION_ERROR`, `UNKNOWN_ERROR`)
### Stored Instrument Payment Fails
**Checks:**
* Ensure the stored instrument is still valid (not expired or deleted)
* Verify the instrument was retrieved from the current session: `session.getStoredInstruments()`
## Pre-Authorization Gate Issues
### Payments are blocked and nothing reaches the backend
Your `onRequestStart` handler returned `RequestStartDecision.Refuse`, threw, or never returned. Confirm by checking
`failure.code == AuthorizationFailureReason.VALIDATION_FAILED` in `onAuthorizeFailed` — a block is
always reported with that code and never as `AUTHORIZATION_ERROR`.
The most common cause is a handler that only answers on the branch it cares about. The gate fires
for **every** payment method on the session, so any branch that does not return `Proceed` blocks
that method:
```kotlin theme={null}
onRequestStart = { context ->
if (context.paymentMethodCode != "payPal") {
RequestStartDecision.Proceed // ← omitting this blocks card, wallets, everything else
} else {
val check = myBackend.validate(context.executionId)
if (check.ok) RequestStartDecision.Proceed else RequestStartDecision.Refuse(check.reason)
}
}
```
If `failure.message` reads *"The payment was blocked before authorization by the onRequestStart
handler"* rather than your own text, the refusal came from a `Refuse()` with no message, a timeout,
or a throw — not from a `Refuse(message)` you wrote. That distinction is usually enough to tell an
intentional refusal from a broken handler.
### A payment is blocked roughly ten seconds after tapping
The handler never returned. The SDK stops the attempt rather than leaving the element spinning, and
logs:
```
onRequestStart did not answer within 10s for ; the payment was blocked.
```
Enable debug logs (see above) to see it. Check that every path through the handler returns —
including error branches and any early exits.
If the handler *does* return but takes longer than ten seconds, note that the bound can only
interrupt a handler that **suspends**. A handler that blocks its thread — a synchronous HTTP client,
`Thread.sleep`, a blocking database read — runs to completion regardless, because coroutine
cancellation is cooperative. Use a suspending client, or wrap the blocking call so it can be
cancelled.
### The handler throws and the payment is refused
That is intended. A handler that throws blocks the payment rather than letting it proceed, because a
gate that fails open is not a gate. If you would rather allow payments when your own service is
unreachable, catch the exception and return `RequestStartDecision.Proceed` explicitly.
Note that the exception's own message is not passed to the customer: `failure.message` gets the
SDK's generic description instead, since a crash string can carry internals. To show your own text,
catch the exception and return `RequestStartDecision.Refuse("…")`.
### `onPaymentButtonClicked` does not stop the payment
It cannot. It returns `Unit` and the SDK does not wait for it — it is a notification for analytics
and observability. Move anything that decides whether a payment should happen into `onRequestStart`.
***
## Lifecycle Issues
### Session Lost After Configuration Change
**Symptom:** Payment fails after screen rotation or configuration change.
**Explanation:** The SDK ties the session to the Activity lifecycle. When the Activity is destroyed (including for configuration changes), the session is cleaned up if `isFinishing` is true.
**Fix:** For configuration changes (rotation), the standard Compose `remember` pattern preserves element references across recomposition. However, if the Activity is recreated, you'll need to re-initialize the session.
Consider using a `ViewModel` to hold the session across configuration changes:
```kotlin theme={null}
class PaymentViewModel : ViewModel() {
var session: Session? = null
private set
suspend fun initializeSession(configuration: Configuration) {
session = Payrails.createSession(configuration)
}
}
```
### Memory Leak Warnings
**Symptom:** LeakCanary reports a leak from `Payrails` or `BrowserPaymentPresenterImpl`.
**Checks:**
* The SDK holds a `WeakReference` to the Activity — this should not cause leaks
* Ensure you're not holding strong references to `CardForm` or `CardPaymentButton` in long-lived scopes (e.g., a singleton)
* The SDK registers `ActivityLifecycleCallbacks` once and cleans up when the bound Activity is destroyed
## ProGuard / R8
If you're using code shrinking, the SDK's public API should work without additional rules. If you encounter issues with serialization or reflection:
```proguard theme={null}
-keep class com.payrails.sdk.** { *; }
```
This is a broad keep rule. In most cases, the SDK works without any ProGuard configuration. Only add rules if you see specific obfuscation-related crashes.
## Getting Help
If your issue isn't covered here:
1. Check the [API Reference](/docs/orchestration/checkout-sdks/android/api-reference#results-and-errors) for specific error types and delegate behavior
2. Review the [SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts) to verify your integration pattern
3. Check the [API Reference](/docs/orchestration/checkout-sdks/android/api-reference) for correct method signatures
4. Contact Payrails support with:
* SDK version
* Android API level
* The specific `PayrailsError` message (if applicable)
* Steps to reproduce
# Update Checkout Amount
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/android/update-checkout-amount
Change the checkout amount and currency at runtime in the Android SDK, for a tip, a discount code, or shipping added after address entry.
## Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/android))
* The `Session` reference returned by `Payrails.createSession(...)`
* At least one payment button already created (`CardPaymentButton` or `GooglePayButton`)
* Your backend can call the Payrails **lookup action** on an execution
## How amount updates work
Updating the checkout amount requires **two coordinated steps** — one on your backend and
one on the client. Both must happen before the payment is triggered, and they must agree
on the same amount.
1. **Backend**: Call the Payrails lookup action on the execution with the new amount. This
authorizes the updated amount on the Payrails side.
2. **Client**: Call `session.update()` with the same new amount. This tells the SDK what
amount to send in the next payment request.
**Important**
If the amount authorized by your backend (via lookup) and the amount
sent by the SDK (via `update()`) do not match, Payrails will reject the payment with
a `401` HTTP error. Keep the two in sync.
## Steps
### 1. Calculate the new amount in your UI logic
Compute the final amount before calling your backend. The value must be a numeric string
matching the format your backend expects.
```kotlin theme={null}
val originalAmount = 45.99
val tipPercent = 0.15
val totalAmount = originalAmount + (originalAmount * tipPercent) // 52.89
val totalAmountString = "%.2f".format(totalAmount) // "52.89"
```
### 2. Call your backend to update the execution via the lookup action
Your backend must call the Payrails lookup action with the new amount before the client
updates the SDK. Pass the `executionId` from the active session so your backend knows
which execution to update.
```kotlin theme={null}
// Get the execution ID from the active session
val executionId = session.query(PayrailsQuery.ExecutionId)
// Call your own backend endpoint, which in turn calls the Payrails lookup action
myApiClient.updateExecutionAmount(
executionId = executionId,
amount = totalAmountString,
currency = "EUR"
)
```
Your backend endpoint should call the Payrails lookup action:
```
POST /v1/merchant/executions/{executionId}/lookup
{
"amount": {
"value": "52.89",
"currency": "EUR"
}
}
```
Wait for your backend to confirm success before proceeding to step 3.
### 3. Call `session.update()` with the same new amount
Once your backend has successfully updated the execution, update the SDK with the
matching amount. The value and currency must be identical to what was sent to the
backend.
```kotlin theme={null}
session.update(
PayrailsUpdate(
amount = AmountUpdate(
value = totalAmountString,
currency = "EUR"
)
)
)
```
`update()` is synchronous and requires no `suspend` context.
### 4. Trigger payment as normal
The updated amount is applied automatically on the next `pay()` call. No additional
configuration is required.
```kotlin theme={null}
cardPaymentButton.pay()
```
## Full example — tip selection before payment
```kotlin theme={null}
@Composable
fun CheckoutScreen(session: Session, myApiClient: MyApiClient) {
val scope = rememberCoroutineScope()
var selectedTipPercent by remember { mutableStateOf(0.0) }
var isUpdatingAmount by remember { mutableStateOf(false) }
val baseAmount = 45.99
val executionId = remember { session.query(PayrailsQuery.ExecutionId) }
val cardPaymentButton = remember {
Payrails.createCardPaymentButton(
translations = CardPaymenButtonTranslations(label = "Pay")
)
}
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
// Tip selector
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf(0.0, 0.10, 0.15, 0.20).forEach { tip ->
Button(
enabled = !isUpdatingAmount,
onClick = {
scope.launch {
isUpdatingAmount = true
val total = baseAmount + (baseAmount * tip)
val totalString = "%.2f".format(total)
// Step 1: update the execution on your backend first
myApiClient.updateExecutionAmount(
executionId = executionId,
amount = totalString,
currency = "EUR"
)
// Step 2: update the SDK with the same amount
session.update(
PayrailsUpdate(
amount = AmountUpdate(
value = totalString,
currency = "EUR"
)
)
)
selectedTipPercent = tip
isUpdatingAmount = false
}
}
) {
Text(if (tip == 0.0) "No tip" else "${(tip * 100).toInt()}%")
}
}
}
cardPaymentButton.Render()
}
}
```
## Important: update() is reset on redirect session recovery
If the session is refreshed during a redirect flow (via `onSessionExpired`), the amount
override is cleared and the value from the new session init payload takes effect. If your
checkout flow involves redirects, re-call `session.update()` after session recovery if a
merchant-side amount override is still needed.
## Troubleshooting
**Problem**: Payment fails with a `401` HTTP error after calling `update()`
**Solution**: The amount set on the SDK and the amount authorized on the backend via the
lookup action do not match. Ensure both use exactly the same `value` and `currency`
strings, and that your backend lookup call succeeded before `session.update()` was
called.
**Problem**: `IllegalStateException` — "No active Payrails session"
**Solution**: `session.update()` was called before `createSession()` completed. Ensure
the session is fully initialized before calling `update()`.
**Problem**: `IllegalArgumentException` — "AmountUpdate.value must be a valid positive number"
**Solution**: The `value` string is not a valid positive number (it may be empty, zero,
negative, or contain non-numeric characters). Validate your amount calculation before
passing it to `AmountUpdate`.
## See also
* [`session.update()` API Reference](/docs/orchestration/checkout-sdks/android/api-reference#session-update-changes): full parameter and exception reference
* [`PayrailsUpdate` API Reference](/docs/orchestration/checkout-sdks/android/api-reference#payrailsupdate) — extensible update container
* [`AmountUpdate` API Reference](/docs/orchestration/checkout-sdks/android/api-reference#amountupdate) — amount and currency fields
# iOS SDK - Quick start
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/ios
Install the Payrails iOS SDK with Swift Package Manager, create a session, and render a working card payment screen.
## Quick Start
Get up and running with the Payrails iOS SDK in about 15 minutes. By the end you will have a working card payment screen in your app.
### Prerequisites
* Xcode 16.4+. Each release is built with Xcode 16.4. Swift's module interface format is not backward compatible across toolchains, so an older Xcode cannot load the prebuilt framework.
* iOS 14.0+ deployment target.
* Swift 5.0+.
* A Payrails merchant account and a backend that can fetch an init payload.
***
### Step 1: Install the SDK
The SDK ships as a prebuilt, signed XCFramework. Swift Package Manager is the only supported installation method.
In Xcode, click **File → Add Package Dependencies** and enter the package URL:
```
https://github.com/payrails/ios-sdk.git
```
Select the `Payrails` product and add it to your app target.
To declare the dependency in a `Package.swift` instead:
```swift theme={null}
dependencies: [
.package(url: "https://github.com/payrails/ios-sdk.git", from: "3.0.0")
]
```
Swift Package Manager downloads the framework and verifies it against the checksum published in the package manifest. Resolution also pulls in `PayrailsCSE` and `PayPalCheckout`, so three packages appear in your project, not one.
***
### Step 2: Enable Apple Pay capability (optional)
If you plan to use Apple Pay, add the **Apple Pay** capability in Xcode under **Signing & Capabilities** and provide your merchant identifier.
***
### Step 3: Fetch the init payload from your backend
The SDK requires an init payload that your backend fetches from the Payrails API. This payload is a base64-encoded JSON string along with a version string.
```swift theme={null}
// Your backend call — implementation depends on your networking layer
func fetchInitPayload() async throws -> (data: String, version: String) {
// Call your backend, which calls the Payrails /checkout/initialize endpoint
// Returns the `data` and `version` fields from the Payrails response
}
```
***
### Step 4: Initialize the SDK session
#### Async/await
```swift theme={null}
import Payrails
let initData = Payrails.InitData(
version: "2", // version returned from your backend
data: "" // data returned from your backend
)
let configuration = Payrails.Configuration(
initData: initData,
option: Payrails.Options(env: .production) // use .test for sandbox
)
do {
let session = try await Payrails.createSession(
with: configuration,
onSessionExpired: { completion in
// Re-run your backend's init flow (authentication + initSdk) and hand
// the fresh InitData back. The SDK swaps its internal config in place —
// your `session` reference and any cached buttons / forms keep working.
fetchFreshPayrailsInitData { result in
completion(result)
}
}
)
// session is ready; store it or proceed to build your UI
} catch {
print("SDK initialization failed:", error.localizedDescription)
}
```
**Recommended**
supply `onSessionExpired` at `createSession` time. The SDK invokes it when it detects the current execution is no longer reusable (most commonly: the user abandoned a 3DS challenge). Without it, the next payment attempt against the poisoned `Session` fails naturally and the SDK logs a warning at init.
#### Callback
```swift theme={null}
Payrails.createSession(with: configuration) { result in
switch result {
case .success(let session):
// session is ready
case .failure(let error):
print("SDK initialization failed:", error.localizedDescription)
}
}
```
***
### Step 5: Build a card payment screen
Payrails provides UIKit-based elements. Add them to your view hierarchy:
```swift theme={null}
import UIKit
import Payrails
class CheckoutViewController: UIViewController, PaymentPresenter {
var encryptedCardData: String?
override func viewDidLoad() {
super.viewDidLoad()
// 1. Create the card form
let cardForm = Payrails.createCardForm()
// 2. Create the pay button
let payButton = Payrails.createCardPaymentButton(
translations: CardPaymenButtonTranslations(label: "Pay now")
)
payButton.delegate = self
payButton.presenter = self // PaymentPresenter: required to present 3DS challenges
// 3. Add to view hierarchy
view.addSubview(cardForm)
view.addSubview(payButton)
// 4. Layout (use Auto Layout or frames as preferred)
cardForm.translatesAutoresizingMaskIntoConstraints = false
payButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cardForm.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
cardForm.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
cardForm.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
payButton.topAnchor.constraint(equalTo: cardForm.bottomAnchor, constant: 16),
payButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
payButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
])
}
// PaymentPresenter: called when a view controller must be presented (e.g. 3DS)
func presentPayment(_ viewController: UIViewController) {
present(viewController, animated: true)
}
}
```
***
### Step 6: Handle payment results
Conform to `PayrailsCardPaymentButtonDelegate`:
```swift theme={null}
extension CheckoutViewController: PayrailsCardPaymentButtonDelegate {
func onPaymentButtonClicked(_ button: Payrails.CardPaymentButton) {
// Notification only — the SDK does not wait for this. Use it for analytics or a
// loading indicator. To make the payment conditional on your own check, see
// How to Run a Merchant Check Before Authorization.
}
func onAuthorizeSuccess(_ button: Payrails.CardPaymentButton) {
// Payment succeeded — navigate to confirmation screen
}
func onAuthorizeFailed(_ button: Payrails.CardPaymentButton, failure: AuthorizationFailure) {
switch failure.code {
case .userCancelled:
// User dismissed the 3DS sheet or otherwise abandoned the flow.
// The SDK has already triggered the `onSessionExpired` refresh closure
// (supplied at `createSession`) so retries continue to work against
// the same `Session` reference.
statusLabel.text = "Payment cancelled."
case .authorizationError:
// Issuer declined / 3DS rejected. `failure.message` is the backend's
// extracted failure reason (e.g. "Insufficient funds") with a generic
// fallback — never nil.
statusLabel.text = "Declined: \(failure.message)"
case .authenticationError:
// Session token rejected (401 / 403). The SDK fires `onSessionExpired`
// in the background; ask the user to retry.
statusLabel.text = "Session expired. Please retry."
case .unknownError:
// Network, SDK, or other unexpected error. `failure.rawError` carries
// the underlying error when one exists.
statusLabel.text = "Payment failed: \(failure.rawError?.localizedDescription ?? failure.message)."
}
}
func onThreeDSecureChallenge(_ button: Payrails.CardPaymentButton) {
// 3DS challenge is being presented — optional hook
}
}
```
***
### Step 7: Test with the sandbox environment
Change the environment to `.test` when initializing the session:
```swift theme={null}
let configuration = Payrails.Configuration(
initData: initData,
option: Payrails.Options(env: .test)
)
```
Use Payrails sandbox card numbers to test different payment outcomes.
***
## What's next
* [Concepts](/docs/orchestration/checkout-sdks/ios/sdk-concepts) — understand the Session, Elements, and Delegates mental model
* [Styling Guide](/docs/orchestration/checkout-sdks/ios/styling-guide) — customise card form and button appearance
* [How to Tokenize a Card](/docs/orchestration/checkout-sdks/ios/sdk-concepts#how-to-tokenize-a-card) — save a card without immediate payment
* [How to Query Session Data](/docs/orchestration/checkout-sdks/ios/sdk-api-reference#how-to-query-session-data) — read execution ID, amount, and more
* [How to Run a Merchant Check Before Authorization](/docs/orchestration/checkout-sdks/ios/sdk-concepts#how-to-run-a-merchant-check-before-authorization) — approve a payment against your own backend first
* [How to Support Co-branded Cards](/docs/orchestration/checkout-sdks/ios/sdk-concepts#how-to-support-co-branded-cards) — let the shopper choose the payment network
* [SDK API Reference](/docs/orchestration/checkout-sdks/ios/sdk-api-reference) — complete API documentation
* [Troubleshooting](/docs/orchestration/checkout-sdks/ios/troubleshooting) — common issues and fixes
# SDK API Reference
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/ios/sdk-api-reference
Reference for the Payrails iOS SDK: installation, the init payload, session creation, card form and button components, and delegates.
**Current version:** 3.0.0
**Minimum deployment target:** iOS 14.0
**Swift version:** 5.0+
**Distribution:** Swift Package Manager (signed XCFramework)
## Installation
Swift Package Manager, using the package URL:
```
https://github.com/payrails/ios-sdk.git
```
```swift theme={null}
dependencies: [
.package(url: "https://github.com/payrails/ios-sdk.git", from: "3.0.0")
]
```
The package resolves a prebuilt, signed `Payrails.xcframework` and verifies it against the checksum in the package manifest. `PayrailsCSE` and `PayPalCheckout` resolve as separate package dependencies.
***
## Getting started
### `Payrails.InitData`
Holds the init payload returned by your backend after calling the Payrails initialization endpoint.
```swift theme={null}
public struct Payrails.InitData: Codable {
public init(version: String, data: String)
public let version: String // Version string from backend response
public let data: String // Base64-encoded JSON payload from backend response
}
```
### `Payrails.Options`
Runtime options passed to `Configuration`.
```swift theme={null}
public struct Payrails.Options {
public init(env: Payrails.Env = .production)
public let env: Payrails.Env
}
public enum Payrails.Env: String {
case production
case test
}
```
### `Payrails.Configuration`
Wraps `InitData` and `Options` as the input to `createSession`.
```swift theme={null}
public struct Payrails.Configuration {
public init(initData: Payrails.InitData, option: Payrails.Options)
public let initData: Payrails.InitData
public let option: Payrails.Options
}
```
### `Payrails.createSession(with:onSessionExpired:onRequestStart:)`
Creates and stores a session. All factory methods use the most recently created session.
```swift theme={null}
// Async/await
public static func createSession(
with configuration: Payrails.Configuration,
onSessionExpired: SessionExpiredHandler? = nil,
onRequestStart: RequestStartHandler? = nil
) async throws -> Payrails.Session
// Callback
public static func createSession(
with configuration: Payrails.Configuration,
onSessionExpired: SessionExpiredHandler? = nil,
onRequestStart: RequestStartHandler? = nil,
onInit: OnInitCallback
)
public typealias OnInitCallback = (Result) -> Void
public typealias SessionExpiredHandler = (
@escaping (Result) -> Void
) -> Void
public typealias RequestStartHandler = (
Payrails.RequestStartContext,
@escaping (Payrails.RequestStartDecision) -> Void
) -> Void
```
The `onSessionExpired` closure lets the SDK self-heal when the underlying Payrails execution becomes unreusable (typically: user abandoned a 3DS challenge and the backend execution stayed in `authorizePending`). The merchant supplies a closure that fetches fresh `InitData` from their backend; the SDK swaps its internal config in place — the merchant's `Session` reference and cached buttons / forms keep working unchanged.
```swift theme={null}
let session = try await Payrails.createSession(
with: Payrails.Configuration(initData: initData, option: .init(env: .production)),
onSessionExpired: { completion in
myBackend.fetchPayrailsInit { result in
completion(result) // .success(Payrails.InitData) or .failure(Error)
}
}
)
```
If the closure is omitted, the SDK logs a warning at init time and cannot recover from a poisoned execution — the next payment attempt against that `Session` will fail naturally.
#### `onRequestStart`
Optional gate invoked once per payment attempt, before the authorization request is sent and before any provider UI (wallet sheet, PayPal sheet, redirect) is presented. Calling `completion(.proceed)` lets the attempt continue; `completion(.refuse())` stops it.
The gate fires for every payment method configured on the session. A handler that only gates one method must call `completion(.proceed)` on the other branches.
```swift theme={null}
public extension Payrails {
enum RequestStartDecision {
case proceed
case refuse(message: String?)
/// Refuse without a reason.
public static func refuse() -> RequestStartDecision
}
struct RequestStartContext {
/// The Payrails execution this attempt runs against, when one is known.
public let executionId: String?
/// `"card"`, `"payPal"`, `"applePay"`, or any other configured code.
public let paymentMethodCode: String
/// Whether the SDK is about to authorize or tokenize.
public let action: Action
public enum Action: String {
case authorize = "AUTHORIZE"
case tokenize = "TOKENIZE"
}
}
}
```
`Action.tokenize` is reserved. The tokenization flow is not gated in this version, so `action` is always `.authorize` today.
`refuse(message:)`'s message, when supplied, becomes `AuthorizationFailure.message` on the delivered `.validationFailed`. It is passed through verbatim and is not displayed by the SDK.
| Handler behaviour | Result |
| - | - |
| `completion(.proceed)` | Authorization proceeds |
| `completion(.refuse(message:))` | Attempt stopped; delegate receives `.validationFailed` carrying `message` |
| `completion(.refuse())` | Attempt stopped; delegate receives `.validationFailed` with a generic description |
| `completion` not called within 10 seconds | Attempt stopped with a generic description; a warning is logged |
| `completion` called more than once | First answer decides; later calls ignored |
The timeout case does not carry the SDK's diagnostic into `failure.message`: it describes an integration fault rather than something phrased for a customer. Only a deliberate `.refuse(message:)` travels outward.
When the attempt is stopped, no authorization request is sent, `isPaymentInProgress` returns to `false`, and the initiating element's delegate receives `onAuthorizeFailed(_:failure:)` with `failure.code == .validationFailed`.
Omitting the handler leaves the payment path fully synchronous — the SDK skips the gate rather than taking an asynchronous detour.
***
## Session
`Payrails.Session` is returned from `createSession` and is the single typed API surface for headless integrations.
**When to use `query(_:)` vs session methods directly:**
* `query(_:)` — stateless reads of session metadata (holder reference, amount, execution ID, API links, payment method config, stored instruments). Single unified accessor returning a `PayrailsQueryResult` enum.
* **Session methods** — actions and mutations (`executePayment`, `tokenize`, `deleteInstrument`, `updateInstrument`, `update`), device-capability checks (`isApplePayAvailable`), or typed reads where merchants prefer concrete return types over an enum (`getPaymentMethodConfig(_:)`).
Rule of thumb: `query(_:)` reads data; session methods do things, check the device, or return typed values.
```swift theme={null}
public class Payrails.Session {
// Availability — device capability only
// Compose with getPaymentMethodConfig(.specific("apple_pay")) for
// the combined "configured and device capable" signal.
public var isApplePayAvailable: Bool { get }
// Payment method configuration
public func getPaymentMethodConfig(_ filter: PaymentMethodFilter = .all) -> [PayrailsPaymentOption]
// Payment execution — callback variants
public func executePayment(
with type: PaymentType,
paymentMethodCode: String?,
saveInstrument: Bool,
presenter: PaymentPresenter?,
onResult: @escaping OnPayCallback
)
public func executePayment(
withStoredInstrument instrument: StoredInstrument,
presenter: PaymentPresenter?,
onResult: @escaping OnPayCallback
)
// Payment execution — async variants
@MainActor public func executePayment(
with type: Payrails.PaymentType,
paymentMethodCode: String?,
saveInstrument: Bool,
presenter: PaymentPresenter?
) async -> OnPayResult
@MainActor public func executePayment(
withStoredInstrument instrument: StoredInstrument,
presenter: PaymentPresenter?
) async -> OnPayResult
// Tokenization — save an instrument without paying (async + callback)
public func tokenize(
_ request: Payrails.TokenizationRequest,
options: TokenizeOptions = TokenizeOptions()
) async throws -> SaveInstrumentResponse
public func tokenize(
_ request: Payrails.TokenizationRequest,
options: TokenizeOptions = TokenizeOptions(),
onSuccess: @escaping (SaveInstrumentResponse) -> Void,
onFailed: @escaping (PayrailsError) -> Void,
onCancelled: @escaping () -> Void
)
// Instrument management
public func deleteInstrument(instrumentId: String) async throws -> DeleteInstrumentResponse
public func updateInstrument(instrumentId: String, body: UpdateInstrumentBody) async throws -> UpdateInstrumentResponse
// Session state
public func query(_ key: PayrailsQueryKey) -> PayrailsQueryResult?
public func update(_ options: UpdateOptions)
}
```
***
## Payment types
```swift theme={null}
public enum Payrails.PaymentType: String {
case card
case applePay
case payPal
case genericRedirect
}
```
***
## Factory methods
All factory methods are static methods on `Payrails` and require an active session.
### Card form
```swift theme={null}
public static func createCardForm(
config: CardFormConfig? = nil,
showSaveInstrument: Bool = false
) -> Payrails.CardForm
```
`Payrails.CardForm` is a `UIStackView` subclass. Add it to your view hierarchy and constrain with Auto Layout.
### Card payment button
```swift theme={null}
// Card form mode — requires prior createCardForm()
public static func createCardPaymentButton(
buttonStyle: CardButtonStyle? = nil,
translations: CardPaymenButtonTranslations
) -> Payrails.CardPaymentButton
// Stored instrument mode
public static func createCardPaymentButton(
storedInstrument: StoredInstrument,
buttonStyle: StoredInstrumentButtonStyle? = nil,
translations: CardPaymenButtonTranslations,
storedInstrumentTranslations: StoredInstrumentButtonTranslations? = nil
) -> Payrails.CardPaymentButton
```
### Apple Pay button
```swift theme={null}
public static func createApplePayButton(
type: PKPaymentButtonType,
style: PKPaymentButtonStyle,
showSaveInstrument: Bool = false
) -> ApplePayElement
```
### PayPal button
```swift theme={null}
public static func createPayPalButton(showSaveInstrument: Bool = false) -> PaypalElement
```
### Generic redirect button
```swift theme={null}
public static func createGenericRedirectButton(
buttonStyle: CardButtonStyle? = nil,
translations: CardPaymenButtonTranslations,
paymentMethodCode: String
) -> Payrails.GenericRedirectButton
```
### Stored instruments
```swift theme={null}
public static func createStoredInstruments(
style: StoredInstrumentsStyle? = nil,
translations: StoredInstrumentsTranslations? = nil,
showDeleteButton: Bool = false,
showUpdateButton: Bool = false,
showPayButton: Bool = false
) -> Payrails.StoredInstruments
```
***
## Static helpers
```swift theme={null}
// Returns all stored instruments (card + PayPal)
public static func getStoredInstruments() -> [StoredInstrument]
// Returns stored instruments for a specific payment type
public static func getStoredInstruments(for type: Payrails.PaymentType) -> [StoredInstrument]
// Runtime session state update
public static func update(_ options: UpdateOptions)
// Query session state
public static func query(_ key: PayrailsQueryKey) -> PayrailsQueryResult?
```
> Instrument management (delete/update) moved to typed Session methods in 1.28.0.
> Use `session.deleteInstrument(instrumentId:)` and `session.updateInstrument(instrumentId:body:)` directly — see the Session block above.
***
## Query API
### `PayrailsQueryKey`
```swift theme={null}
public enum PayrailsQueryKey {
case executionId
case holderReference
case amount
case binLookup
case instrumentDelete
case instrumentUpdate
case paymentMethodConfig(PaymentMethodFilter)
case paymentMethodInstruments(type: Payrails.PaymentType)
}
```
### `PaymentMethodFilter`
```swift theme={null}
public enum PaymentMethodFilter {
case all
case redirect
case specific(String) // paymentMethodCode
}
```
### `PayrailsQueryResult`
```swift theme={null}
public enum PayrailsQueryResult {
case string(String)
case amount(PayrailsAmount)
case link(PayrailsLink)
case paymentOptions([PayrailsPaymentOption])
case storedInstruments([StoredInstrument])
}
```
### Supporting types
```swift theme={null}
public struct PayrailsAmount {
public let value: String
public let currency: String
}
public struct PayrailsLink {
public let method: String?
public let href: String?
}
public struct PayrailsPaymentOption {
public let paymentMethodCode: String
public let description: String?
public let integrationType: String
public let clientConfig: ClientConfig?
public struct ClientConfig {
public let displayName: String?
public let flow: String?
public let supportsSaveInstrument: Bool?
public let supportsBillingInfo: Bool?
}
}
```
### How to Query Session Data
`Payrails.query(_:)` provides read-only access to the current session's configuration and state. Use it to retrieve the execution ID, payment amount, stored instruments, API links, and more — without reaching into internal session state.
#### When to use `query(_:)` vs Session methods
* **Use `query(_:)`** for stateless reads of session metadata: `.holderReference`, `.amount`, `.executionId`, `.binLookup`, `.paymentMethodConfig(...)`, `.paymentMethodInstruments(...)`.
* **Use Session methods directly** for actions (`executePayment`, `deleteInstrument`, `updateInstrument`, `update`), device-capability checks (`isApplePayAvailable`), or typed reads where merchants prefer concrete return types over an enum (`getPaymentMethodConfig(_:)`).
Rule of thumb: `query(_:)` reads data. Session methods do things, check the device, or return typed values where an enum would add friction.
#### Prerequisites
An active session must exist (created via `Payrails.createSession(with:)`). All queries return `nil` when no session is active.
***
#### Calling `Payrails.query`
```swift theme={null}
let result: PayrailsQueryResult? = Payrails.query(.amount)
```
The return type is `PayrailsQueryResult?`, a Swift enum. Switch on it to extract the typed value:
```swift theme={null}
switch Payrails.query(.amount) {
case .amount(let amount):
print("Amount:", amount.value, amount.currency)
case .none:
print("No active session")
default:
break
}
```
***
#### Available query keys
##### `.executionId`
The execution ID for the current checkout. Pass this to your backend for order correlation.
```swift theme={null}
if case .string(let executionId) = Payrails.query(.executionId) {
print("Execution ID:", executionId)
// myBackend.attachExecutionId(executionId)
}
```
##### `.holderReference`
The holder reference (customer identifier) associated with this session.
```swift theme={null}
if case .string(let ref) = Payrails.query(.holderReference) {
print("Holder reference:", ref)
}
```
##### `.amount`
The payment amount and currency for the current execution.
```swift theme={null}
if case .amount(let payrailsAmount) = Payrails.query(.amount) {
let display = "\(payrailsAmount.currency) \(payrailsAmount.value)"
amountLabel.text = display
}
```
##### `.binLookup`
The API link for BIN lookup. Use this to call the lookup endpoint and determine card network, country, and 3DS requirements before payment.
```swift theme={null}
if case .link(let link) = Payrails.query(.binLookup) {
print("BIN lookup URL:", link.href ?? "")
print("Method:", link.method ?? "")
}
```
##### `.instrumentDelete`
The API link for deleting a stored instrument.
```swift theme={null}
if case .link(let link) = Payrails.query(.instrumentDelete) {
// Use link.href and link.method to build the request in your networking layer
}
```
##### `.instrumentUpdate`
The API link for updating a stored instrument (e.g. setting as default).
```swift theme={null}
if case .link(let link) = Payrails.query(.instrumentUpdate) {
// Use link.href and link.method to build the request
}
```
##### `.paymentMethodConfig(filter:)`
Configuration for available payment methods, filtered by a `PaymentMethodFilter`.
```swift theme={null}
// All payment methods
if case .paymentOptions(let options) = Payrails.query(.paymentMethodConfig(.all)) {
for option in options {
print(option.paymentMethodCode, option.clientConfig?.displayName ?? "")
}
}
// Redirect-based methods only
if case .paymentOptions(let options) = Payrails.query(.paymentMethodConfig(.redirect)) {
// Build redirect payment buttons dynamically
}
// A specific method by code
if case .paymentOptions(let options) = Payrails.query(.paymentMethodConfig(.specific("ideal"))) {
let ideal = options.first
print("iDEAL display name:", ideal?.clientConfig?.displayName ?? "")
}
```
##### `.paymentMethodInstruments(type:)`
The stored instruments for a given payment type.
```swift theme={null}
// Card instruments
if case .storedInstruments(let cards) = Payrails.query(.paymentMethodInstruments(type: .card)) {
print("Saved cards:", cards.count)
for card in cards {
print(" -", card.id, card.type.rawValue)
}
}
// PayPal instruments
if case .storedInstruments(let paypals) = Payrails.query(.paymentMethodInstruments(type: .payPal)) {
print("Saved PayPal accounts:", paypals.count)
}
```
***
#### Summary table
| Key | Returns | Description |
| - | - | - |
| `.executionId` | `.string` | Current execution ID |
| `.holderReference` | `.string` | Customer holder reference |
| `.amount` | `.amount(PayrailsAmount)` | Payment amount and currency |
| `.binLookup` | `.link(PayrailsLink)` | BIN lookup API link |
| `.instrumentDelete` | `.link(PayrailsLink)` | Instrument delete API link |
| `.instrumentUpdate` | `.link(PayrailsLink)` | Instrument update API link |
| `.paymentMethodConfig(.all)` | `.paymentOptions([PayrailsPaymentOption])` | All payment method configs |
| `.paymentMethodConfig(.redirect)` | `.paymentOptions([PayrailsPaymentOption])` | Redirect-only methods |
| `.paymentMethodConfig(.specific(code))` | `.paymentOptions([PayrailsPaymentOption])` | Single method config |
| `.paymentMethodInstruments(type:)` | `.storedInstruments([StoredInstrument])` | Saved instruments by type |
***
## Updating session state
```swift theme={null}
public struct UpdateOptions {
public var amount: PayrailsAmount?
public init(amount: PayrailsAmount? = nil)
}
// Usage
Payrails.update(UpdateOptions(amount: PayrailsAmount(value: "57.49", currency: "USD")))
```
### How to Update the Checkout Amount
The checkout amount is set when the session is initialized from the init payload. If the amount changes after initialization — for example, the user adds a tip, chooses express shipping, or applies a discount code — you must update both your backend and the SDK in lockstep.
**Important**
The SDK amount and the amount recorded in the Payrails execution must match. A mismatch causes the payment to be rejected with a 401 error.
***
#### How amount updates work
Updating the amount is a two-step process:
```
1. Recalculate amount in your UI (e.g. user selects a tip)
2. Call your backend to update the execution amount in Payrails
3. Call Payrails.update(options:) on the client to sync the SDK
4. User taps the pay button — amount is now consistent
```
Both steps must complete before the user initiates payment.
***
#### Step 1: Recalculate the amount
```swift theme={null}
let subtotal = 49.99
let tipPercentage = 0.15
let total = subtotal + (subtotal * tipPercentage)
let formattedTotal = String(format: "%.2f", total) // "57.49"
let currency = "USD"
```
***
#### Step 2: Update the amount on your backend
Call your backend, which calls the Payrails API to update the execution amount. The exact endpoint and request shape are defined by your backend implementation.
```swift theme={null}
func updateExecutionAmount(value: String, currency: String) async throws {
// Your backend call — POST /executions/{id}/update or similar
// This MUST complete before calling Payrails.update()
try await myBackendClient.updateCheckoutAmount(value: value, currency: currency)
}
```
***
#### Step 3: Update the SDK amount
After the backend confirms the update, sync the SDK:
```swift theme={null}
let newAmount = PayrailsAmount(value: formattedTotal, currency: currency)
Payrails.update(UpdateOptions(amount: newAmount))
```
***
#### Complete example: tip selection
```swift theme={null}
class CheckoutViewController: UIViewController {
private var selectedTipRate: Double = 0.0
@IBAction func tipButtonTapped(_ sender: UIButton) {
let tipRate = tipRate(for: sender.tag)
selectTip(rate: tipRate)
}
private func selectTip(rate: Double) {
selectedTipRate = rate
updateAmountDisplay()
Task {
await applyTipToPayment(rate: rate)
}
}
private func updateAmountDisplay() {
let total = calculateTotal(tipRate: selectedTipRate)
amountLabel.text = formatAmount(total)
}
private func applyTipToPayment(rate: Double) async {
let total = calculateTotal(tipRate: rate)
let formatted = String(format: "%.2f", total)
do {
// Step 1: Update the backend execution
try await myBackend.updateExecutionAmount(value: formatted, currency: "USD")
// Step 2: Sync the SDK
let newAmount = PayrailsAmount(value: formatted, currency: "USD")
Payrails.update(UpdateOptions(amount: newAmount))
} catch {
showError("Failed to apply tip: \(error.localizedDescription)")
}
}
private func calculateTotal(tipRate: Double) -> Double {
let subtotal = 49.99
return subtotal + (subtotal * tipRate)
}
}
```
***
#### After a redirect session recovery
If the user's payment involved a redirect (e.g. PayPal, generic redirect) and the app returned from the background, the session may be restored from the original init payload. In this case:
* Any in-memory amount updates made via `Payrails.update()` are **reset** to the original init payload amount.
* If you need to preserve the updated amount after a redirect, you must re-apply `Payrails.update()` once the session is restored.
***
#### Troubleshooting
**Payment rejected with 401 / authorization error**
The SDK amount does not match the Payrails execution amount. Verify that your backend update completed successfully before calling `Payrails.update()`.
**`Payrails.update()` appears to have no effect**
If there is no active session, the call is silently dropped. Ensure `Payrails.createSession()` has completed successfully before calling `update()`. Check for any `No active Payrails session` log messages.
**Amount label does not update**
`Payrails.update()` updates the internal SDK state; it does not automatically refresh any UI element. Update your amount label independently after recalculating.
***
## Callbacks and results
Payment outcomes are delivered to your `CardPaymentButton` / `CardPaymentForm` / `StoredInstrumentPaymentButton` / `GenericRedirectButton` delegate. Failures arrive as an `AuthorizationFailure` struct — a flat `{ code, message, rawError }` shape that mirrors the Web SDK's `onFailed` payload:
```swift theme={null}
/// Payload passed to `onAuthorizeFailed(_:failure:)` on every delegate protocol.
public struct AuthorizationFailure {
/// Discriminating code — switch on this to give each outcome the right UX.
public let code: AuthorizationFailureReason
/// Human-readable detail. Backend's `errors[0].reason.result` for `.authorizationError`,
/// generic fallback for the other cases. Never nil.
public let message: String
/// Underlying error when one exists (typically populated for `.unknownError`).
public let rawError: Error?
}
/// String-raw-valued discriminator. Raw values match the Web SDK's
/// `AuthorizationFailureReasons` 1:1.
public enum AuthorizationFailureReason: String {
/// Authorization rejected — issuer declined, 3DS rejected, fraud blocked, etc.
case authorizationError = "AUTHORIZATION_ERROR"
/// Session token was rejected (HTTP 401 / 403). The merchant must
/// re-initialise the session; the SDK also fires its `onSessionExpired`
/// refresh in the background.
case authenticationError = "AUTHENTICATION_ERROR"
/// The user intentionally abandoned the flow (e.g. swiped the 3DS sheet away).
case userCancelled = "USER_CANCELLED"
/// Network failure, decode error, encryption failure, polling timeout, or
/// any other unexpected error. `rawError` carries the underlying error.
case unknownError = "UNKNOWN_ERROR"
/// The merchant's `onRequestStart` handler stopped the payment before it
/// started — it answered `false`, or did not answer within the timeout.
/// No authorization request was sent, so this is not a decline.
case validationFailed = "VALIDATION_FAILED"
}
```
> Client-side *input* validation never reaches this path: an element early-returns on an invalid form rather than emitting a failure. `.validationFailed` is reserved for an `onRequestStart` handler blocking the attempt.
### Delegate callbacks fired
| Outcome | Delegate method fired |
| - | - |
| Authorization succeeded | `onAuthorizeSuccess(self)` |
| Backend declined / rejected authorization | `onAuthorizeFailed(self, failure: .authorizationError(message:))` |
| Session token expired / rejected | `onAuthorizeFailed(self, failure: .authenticationError)` |
| User dismissed 3DS sheet (no backend terminal confirmed) | `onAuthorizeFailed(self, failure: .userCancelled)` |
| Network / SDK error | `onAuthorizeFailed(self, failure: .unknownError(_))` |
| `onRequestStart` refused the attempt | `onAuthorizeFailed(self, failure: .validationFailed(message:))` |
| Backend execution pending with no action required | `onAuthorizePending(self)` |
When the user dismisses the 3DS sheet (or any other path that leaves the Payrails execution in `authorizePending`), the SDK additionally invokes the merchant's `onSessionExpired` closure (supplied at `createSession`) in the background to rebuild its internal config in place — the merchant's `Session` reference keeps working. If the closure was not supplied, the SDK logs a warning at `createSession` time and the next payment attempt against the `Session` will fail naturally against the dead execution.
***
## Payment presenter protocol
Required to present view controllers during payment (e.g. 3DS challenges):
```swift theme={null}
public protocol PaymentPresenter: AnyObject {
func presentPayment(_ viewController: UIViewController)
var encryptedCardData: String? { get set }
}
```
Typically conformed to by a `UIViewController`:
```swift theme={null}
extension MyCheckoutViewController: PaymentPresenter {
func presentPayment(_ viewController: UIViewController) {
present(viewController, animated: true)
}
}
```
***
## Delegate protocols
> `onPaymentButtonClicked` is a notification, not a gate. It tells you the customer tapped, for
> analytics, observability or showing a spinner. It returns `Void` and the SDK does not wait for it,
> so it cannot stop or defer a payment. To make the payment conditional on your own check, use
> [`onRequestStart`](#onrequeststart) — the SDK awaits that one and honours its answer.
### `PayrailsCardPaymentButtonDelegate`
```swift theme={null}
public protocol PayrailsCardPaymentButtonDelegate: AnyObject {
func onPaymentButtonClicked(_ button: Payrails.CardPaymentButton)
func onAuthorizeSuccess(_ button: Payrails.CardPaymentButton)
func onAuthorizePending(_ button: Payrails.CardPaymentButton)
func onThreeDSecureChallenge(_ button: Payrails.CardPaymentButton)
/// Fires for every terminal failure of an authorization attempt. The `failure`
/// payload's `.code` discriminates between issuer decline, authentication
/// failure, user cancellation, and other errors — see `AuthorizationFailure`.
func onAuthorizeFailed(_ button: Payrails.CardPaymentButton, failure: AuthorizationFailure)
/// Optional — default no-op.
func onStoredInstrumentChanged(_ button: Payrails.CardPaymentButton, instrument: StoredInstrument?)
}
```
**Breaking change in ONB-739**
The pre-ONB-739 signature was
`onAuthorizeFailed(_ button: Payrails.CardPaymentButton)` with no payload.
Merchants migrating from earlier versions must update their delegate
conformance to take `failure: AuthorizationFailure` and (if they relied on
session-expiry signaling) supply the `onSessionExpired` closure to
`createSession`. See the card-payment-flow documentation for a migration example.
### `PayrailsCardFormDelegate`
```swift theme={null}
public protocol PayrailsCardFormDelegate: AnyObject {
func cardForm(_ view: Payrails.CardForm, didCollectCardData data: String)
func cardForm(_ view: Payrails.CardForm, didFailWithError error: Error)
func cardForm(_ view: Payrails.CardForm, didChangePreferredScheme change: PreferredSchemeChange)
}
```
`didChangePreferredScheme` is optional — a protocol-extension default provides a no-op, so implementations that predate co-branded support continue to compile. See [Co-branded cards](#co-branded-cards).
### Co-branded cards
Emitted by the card form when a card carries two payment networks. Requires `featureConfig.coBrandedCardsRollout` and a `links.binLookup` entry in the session init response; absent either, none of this is produced.
```swift theme={null}
public struct PreferredSchemeChange: Equatable {
public let preferredScheme: String?
public let cardSchemes: [CardScheme]
}
public struct CardScheme: Equatable {
public let code: String
public let name: String
public let logoUrl: URL?
public let selected: Bool
}
```
| Field | Type | Notes |
| - | - | - |
| `preferredScheme` | `String?` | Scheme code sent with the authorization. `nil` when the card is not co-branded |
| `cardSchemes` | `[CardScheme]` | Both available schemes. Empty when the card is not co-branded |
| `code` | `String` | `"visa"`, `"cartesBancaires"`, `"mada"`, … |
| `name` | `String` | Display name |
| `logoUrl` | `URL?` | Brand logo, when the scheme has one |
| `selected` | `Bool` | Whether this scheme is the current choice |
`cardForm(_:didChangePreferredScheme:)` fires when a co-branded BIN resolves, when the shopper selects a different brand, and when the card number changes such that co-branded state clears. The clearing case delivers an empty `cardSchemes` and a `nil` `preferredScheme`.
The selected scheme is carried into the authorization request by the SDK. Callers do not pass it to any payment method.
Styling is `CardFormStyle.cardBrandSelector: CardBrandSelectorStyle?`; `nil` keeps the SDK default. The selector's heading and subheading are `CardTranslations.Labels.cardBrandSelectorTitle` and `.cardBrandSelectorSubtitle`; each falls back to an SDK default when `nil`.
### `PayrailsApplePayButtonDelegate`
```swift theme={null}
public protocol PayrailsApplePayButtonDelegate: AnyObject {
func onPaymentButtonClicked(_ button: Payrails.ApplePayButton)
func onAuthorizeSuccess(_ button: Payrails.ApplePayButton)
func onAuthorizeFailed(_ button: Payrails.ApplePayButton, failure: AuthorizationFailure)
func onPaymentSessionExpired(_ button: Payrails.ApplePayButton)
}
```
**Deprecated**
`onAuthorizeFailed(_ button: Payrails.ApplePayButton)` with no payload. A default
implementation forwards to it, so existing integrations keep receiving failures, but only the
`failure:` variant carries the discriminating `code` — the sole way to distinguish an
`onRequestStart` block from an issuer decline.
### `PayrailsPayPalButtonDelegate`
```swift theme={null}
public protocol PayrailsPayPalButtonDelegate: AnyObject {
func onPaymentButtonClicked(_ button: Payrails.PayPalButton)
func onAuthorizeSuccess(_ button: Payrails.PayPalButton)
func onAuthorizeFailed(_ button: Payrails.PayPalButton, failure: AuthorizationFailure)
func onPaymentSessionExpired(_ button: Payrails.PayPalButton)
}
```
**Deprecated**
`onAuthorizeFailed(_ button: Payrails.PayPalButton)` with no payload. Same
forwarding default and same reasoning as Apple Pay above.
### `PayrailsStoredInstrumentsDelegate`
```swift theme={null}
public protocol PayrailsStoredInstrumentsDelegate: AnyObject {
func storedInstruments(_ view: Payrails.StoredInstruments, didSelectInstrument instrument: StoredInstrument)
func storedInstruments(_ view: Payrails.StoredInstruments, didCompletePaymentForInstrument instrument: StoredInstrument)
func storedInstruments(_ view: Payrails.StoredInstruments, didFailPaymentForInstrument instrument: StoredInstrument, error: PayrailsError)
func storedInstruments(_ view: Payrails.StoredInstruments, didRequestDeleteInstrument instrument: StoredInstrument)
func storedInstruments(_ view: Payrails.StoredInstruments, didRequestUpdateInstrument instrument: StoredInstrument)
}
```
***
## Error handling
### `PayrailsError`
```swift theme={null}
public enum PayrailsError: Error, LocalizedError {
case authenticationError
case sdkNotInitialized
case missingData(String?)
case invalidDataFormat
case invalidCardData
case unknown(error: Error?)
case unsupportedPayment(type: Payrails.PaymentType)
case incorrectPaymentSetup(type: Payrails.PaymentType)
case pollingFailed(String)
case failedToDerivePaymentStatus(String)
case finalStatusNotFoundAfterLongPoll(String)
case longPollingFailed(underlyingError: Error?)
}
```
`PayrailsError` conforms to `LocalizedError`; use `error.errorDescription` for a human-readable message.
***
## Tokenization
Saves a payment method as a reusable instrument without charging the customer. `tokenize` is unified across methods: the `TokenizationRequest` case selects which instrument to tokenize and carries what that method needs. Both overloads live on `Payrails.Session` (see [Session](#session)), alongside `executePayment`.
```swift theme={null}
public enum Payrails.TokenizationRequest {
case applePay(presenter: PaymentPresenter) // SDK presents the Apple Pay sheet
case card(CardForm) // SDK reads + encrypts the embedded card form
}
public struct TokenizeOptions {
public let storeInstrument: Bool // default false
public let futureUsage: FutureUsage // default .cardOnFile
}
public enum FutureUsage: String {
case cardOnFile
case subscription
case unscheduledCardOnFile
}
public struct SaveInstrumentResponse: Decodable {
public let id: String // stable Payrails instrument id
public let createdAt: String
public let holderId: String
public let paymentMethod: String // e.g. "applePay", "card"
public let status: String
public let data: InstrumentData
public let fingerprint: String?
public let futureUsage: String?
}
```
```swift theme={null}
// Apple Pay — async; `self` conforms to PaymentPresenter
let response = try await session.tokenize(
.applePay(presenter: self),
options: TokenizeOptions(storeInstrument: true)
)
let instrumentId = response.id
```
***
## Instrument management
```swift theme={null}
public struct UpdateInstrumentBody: Codable {
// Fields depend on the update operation (e.g. isDefault)
}
public struct DeleteInstrumentResponse: Codable {
public let success: Bool
}
public struct UpdateInstrumentResponse: Codable {
// Returned data depends on the operation
}
```
Call the typed Session methods to manage instruments:
```swift theme={null}
let delete = try await session.deleteInstrument(instrumentId: id)
let update = try await session.updateInstrument(
instrumentId: id,
body: UpdateInstrumentBody(default: true)
)
```
***
## Card form configuration
See [Styling Guide](/docs/orchestration/checkout-sdks/ios/styling-guide) for full details.
```swift theme={null}
public struct CardFormConfig {
public init(
showNameField: Bool = false,
showSaveInstrument: Bool = false,
showCardIcon: Bool = false,
showRequiredAsterisk: Bool = true,
cardIconAlignment: CardIconAlignment = .left,
fieldVariant: FieldVariant = .outlined,
layout: CardLayoutConfig? = nil,
styles: CardFormStylesConfig? = nil,
translations: CardTranslations? = nil
)
}
public enum FieldVariant {
case outlined
case filled
}
public enum CardIconAlignment {
case left
case right
}
```
***
## Debug
```swift theme={null}
public extension Payrails {
struct Debug {
// Returns a SwiftUI view displaying the parsed SDK config and logs
public static func configViewer() -> some View
}
}
// Logs a message to both the Xcode console and the on-screen LogStore
public static func log(_ items: Any..., separator: String, terminator: String)
```
# SDK Concepts
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/ios/sdk-concepts
This page explains the mental model behind the Payrails iOS SDK so you can reason about integration decisions confidently.
## The three building blocks
### 1. Session
The **Session** (`Payrails.Session`) is the single source of truth for a checkout. It holds:
* The parsed init payload (amounts, payment method configurations, vault settings)
* The active execution ID
* The holder reference
* Links to backend API actions (BIN lookup, instrument management)
You create a session once per checkout by calling `Payrails.createSession(with:)`. All elements and factory methods draw their configuration from the current session — there is no need to pass it around explicitly.
```
App Backend ──init payload──► Payrails.createSession() ──► Session (ready)
```
A session does not persist across app launches. When the user starts a new checkout, create a new session.
### 2. Elements
Elements are UIKit views that the SDK manages. You obtain them via factory methods on `Payrails`:
| Factory method | Element type | Description |
| - | - | - |
| `Payrails.createCardForm()` | `Payrails.CardForm` | Card input form (number, expiry, CVV, optional name) |
| `Payrails.createCardPaymentButton(translations:)` | `Payrails.CardPaymentButton` | Submit button for card form or stored instrument |
| `Payrails.createApplePayButton(type:style:)` | `ApplePayElement` | Apple Pay button wrapping `PKPaymentButton` |
| `Payrails.createPayPalButton()` | `PaypalElement` | PayPal checkout button |
| `Payrails.createGenericRedirectButton(translations:paymentMethodCode:)` | `Payrails.GenericRedirectButton` | Button for redirect-based methods (for example, iDEAL) |
| `Payrails.createStoredInstruments()` | `Payrails.StoredInstruments` | List of previously saved payment methods |
All elements are UIView subclasses; add them to your view hierarchy with Auto Layout or frames.
`Payrails.createCardPaymentButton` requires that `createCardForm` has been called first. The form and button are linked automatically.
### 3. Delegates
Delegates are protocols your view controller (or any object) conforms to in order to receive payment lifecycle events. Each element type has a corresponding delegate:
| Element | Delegate protocol |
| - | - |
| `CardPaymentButton` | `PayrailsCardPaymentButtonDelegate` |
| `ApplePayButton` | `PayrailsApplePayButtonDelegate` |
| `PayPalButton` | `PayrailsPayPalButtonDelegate` |
| `GenericRedirectButton` | `GenericRedirectPaymentButtonDelegate` |
| `StoredInstruments` | `PayrailsStoredInstrumentsDelegate` |
| `StoredInstrumentView` | `PayrailsStoredInstrumentViewDelegate` |
Assign the delegate before adding the element to the window.
***
## Payment flows
### Card payment (new card)
```
1. createCardForm() — card fields appear
2. createCardPaymentButton() — pay button appears
3. User fills fields, taps button
4. SDK encrypts card data (PayrailsCSE)
5. SDK calls Payrails payment API
6. ┌─ 3DS required ──► presentPayment(_:) called on PaymentPresenter
│ ──► user completes challenge in SFSafariViewController
│ ──► SDK polls for final status
└─ no 3DS ──► result delivered immediately
7. delegate callback fires:
- success → delegate.onAuthorizeSuccess(_:)
- failure → delegate.onAuthorizeFailed(_:failure:)
(failure.code discriminates: .userCancelled / .authorizationError /
.authenticationError / .unknownError)
- pending → delegate.onAuthorizePending(_:)
In parallel, if the execution is left in `authorizePending` (e.g. the user
abandoned 3DS), the SDK fires the `onSessionExpired` closure supplied at
`createSession` time to swap the internal config in place — the merchant's
`Session` reference and cached buttons keep working.
```
### Stored instrument payment
```
1. createStoredInstruments() or createCardPaymentButton(storedInstrument:)
2. User selects instrument, taps button
3. SDK calls Payrails payment API with instrument ID
4. Result via delegate callback
```
### Apple Pay
```
1. createApplePayButton(type:style:)
2. User taps button
3. Apple Pay sheet presented by the SDK
4. User authorises with Face ID / Touch ID
5. SDK processes payment token
6. Result via PayrailsApplePayButtonDelegate
```
### PayPal
```
1. createPayPalButton()
2. User taps button
3. PayPal checkout web flow presented
4. SDK confirms payment and polls for status
5. Result via PayrailsPayPalButtonDelegate
```
### Generic redirect
```
1. createGenericRedirectButton(translations:paymentMethodCode:)
2. User taps button
3. Browser opens redirect URL (SFSafariViewController)
4. User completes flow on payment provider website
5. App returns to foreground — success is reported immediately
```
***
## Tokenization
Tokenization saves a payment method as a reusable Payrails **instrument** without charging the customer. It returns a stable instrument `id` that identifies the saved method for later use.
This exists to support a **two-step model**: tokenize first, run the resulting instrument through an external decision — a saved-card list, a subscription setup — and only then, if at all, charge it with `executePayment`. Charging is a separate, deliberate action, never a side effect of tokenizing.
Tokenization is **unified across payment methods**. The same `session.tokenize` call handles Apple Pay (the SDK presents the Apple Pay sheet) and cards (the SDK encrypts the embedded card form), selected by the `TokenizationRequest` case, and both return the same `SaveInstrumentResponse`. Adding a method later does not change how the call is made.
Tokenization is distinct from **pay-and-save**: pay-and-save (the `storeInstrument` toggle on a payment) charges the customer and vaults the method in one step, whereas tokenization vaults without any charge.
### How to tokenize a card
Tokenization saves a card to the Payrails vault without triggering an immediate payment. Use this when you want to store a card for future purchases (subscriptions, one-click checkout, etc.).
#### Prerequisites
* An active Payrails session (see [Quick Start](/docs/orchestration/checkout-sdks/ios))
* Vault configuration present in the init payload (`providerConfigId`)
* A `holderReference` in the session config (required to associate the card with a customer)
***
#### How tokenization works
The card form collects and encrypts the card fields client-side using PayrailsCSE. The encrypted blob is sent to the Payrails vault, which returns an instrument ID. Your backend can then use that instrument ID for future payments without ever handling raw card data.
There are two paths:
| Path | When to use |
| - | - |
| **Tokenize only** (`storeInstrument: true`, no immediate payment) | Save the card for later, no charge now |
| **Pay and save** (`showSaveInstrument: true` on card form) | Charge the card and save it simultaneously |
***
#### Path 1: Tokenize without payment
##### Step 1: Create the card form with save toggle
```swift theme={null}
let cardForm = Payrails.createCardForm(
config: CardFormConfig(
showNameField: true,
showSaveInstrument: false // hide the toggle; saving is always-on here
)
)
```
##### Step 2: Collect the card and tokenize
The card form collects and encrypts the data. You drive tokenization by calling `tokenize` on the session directly after collecting:
```swift theme={null}
// Implement PaymentPresenter on your view controller
// When the user taps your custom "Save card" button, call collectFields() on the form.
// The form will call its delegate when data is ready.
extension MyViewController: PayrailsCardFormDelegate {
func cardForm(_ view: Payrails.CardForm, didCollectCardData encryptedData: String) {
Task {
do {
let options = TokenizeOptions(
storeInstrument: true,
futureUsage: .cardOnFile
)
let response = try await session.tokenize(
encryptedData: encryptedData,
options: options
)
// response.instrumentId is your saved card token
print("Card saved with instrument ID:", response.instrumentId)
} catch {
print("Tokenization failed:", error.localizedDescription)
}
}
}
func cardForm(_ view: Payrails.CardForm, didFailWithError error: Error) {
print("Card collection error:", error.localizedDescription)
}
}
```
##### Step 3: Choose a `FutureUsage`
`FutureUsage` tells the vault how the instrument will be used for network-mandated storage rules:
| Value | Meaning |
| - | - |
| `.cardOnFile` | Customer-initiated future payments (default) |
| `.subscription` | Merchant-initiated recurring charges |
| `.unscheduledCardOnFile` | Merchant-initiated, non-recurring (for example, top-up) |
```swift theme={null}
let options = TokenizeOptions(
storeInstrument: true,
futureUsage: .subscription
)
```
***
#### Path 2: Pay and save simultaneously
Enable the save toggle on the card form. The user checks the toggle and taps Pay; the SDK performs the payment and vaults the card in one call.
```swift theme={null}
let cardForm = Payrails.createCardForm(
showSaveInstrument: true // renders a "Save card" checkbox
)
let payButton = Payrails.createCardPaymentButton(
translations: CardPaymenButtonTranslations(label: "Pay")
)
payButton.delegate = self
payButton.presenter = self
```
The SDK automatically includes `storeInstrument: true` in the payment request when the toggle is checked. No additional code is needed.
***
#### Using the saved instrument ID
After tokenization, the `SaveInstrumentResponse` contains the instrument ID:
```swift theme={null}
let instrumentId = response.instrumentId
// Pass it to your backend to associate with the customer
// Or use it immediately with the SDK:
let storedInstruments = Payrails.getStoredInstruments(for: .card)
// storedInstruments will include the newly saved card after a session refresh
```
***
#### Verification checklist
* [ ] `providerConfigId` is present in the init payload
* [ ] `holderReference` is present in the init payload
* [ ] `TokenizeOptions.storeInstrument` is `true`
* [ ] `FutureUsage` matches the intended use case
* [ ] Your backend associates the returned instrument ID with the customer record
***
#### Troubleshooting
**"Vault configuration with providerConfigId is required"**
The init payload does not include vault configuration. Ensure your backend passes the correct Payrails environment and merchant configuration.
**"holderReference is required for tokenization"**
The holder reference is missing from the init payload. Contact your Payrails integration engineer to verify the checkout initialization call.
**Card form delegate not being called**
Make sure `cardForm.delegate = self` is set before the user triggers collection.
***
## 3D Secure
When a card payment requires a 3DS challenge, the SDK presents an `SFSafariViewController`. Your view controller must conform to `PaymentPresenter` and implement `presentPayment(_:)`:
```swift theme={null}
func presentPayment(_ viewController: UIViewController) {
present(viewController, animated: true)
}
```
The SDK handles the rest: it polls the Payrails API until a final status is received, then calls the appropriate delegate callback.
> Set `payButton.presenter = self` before the user taps the button.
***
## CardPaymentButton modes
`Payrails.CardPaymentButton` operates in two modes:
| Mode | How it's created | Behaviour on tap |
| - | - | - |
| **Card form mode** | `createCardPaymentButton(translations:)` (requires prior `createCardForm()`) | Collects and encrypts card fields, then executes payment |
| **Stored instrument mode** | `createCardPaymentButton(storedInstrument:translations:)` | Executes payment immediately with the stored instrument |
You can switch between modes at runtime using `setStoredInstrument(_:)` and `clearStoredInstrument()`.
***
## Stored instruments and `bindCardPaymentButton`
`Payrails.StoredInstruments` can be bound to a single `CardPaymentButton`:
```swift theme={null}
let storedInstrumentsView = Payrails.createStoredInstruments()
let payButton = Payrails.createCardPaymentButton(translations: translations)
storedInstrumentsView.bindCardPaymentButton(payButton)
```
When a user selects an instrument from the list, the button automatically switches to stored instrument mode. When deselected, it reverts to card form mode. This pattern lets you render one card form and one pay button that handles both flows without conditional logic in your view controller.
***
## The pre-authorization gate
Every element — card form, card button, Apple Pay, PayPal, generic redirect, stored instrument —
routes its payment through a single `Session` method. That convergence is what makes one
merchant-supplied gate able to cover all of them, present and future, rather than each element
carrying its own hook.
```mermaid theme={null}
flowchart TD
Element["Any element (card · PayPal · wallet · redirect)"] --> Session["Payrails Session"]
Session --> Gate{"onRequestStart registered?"}
Gate -- "no" --> Authorize["POST authorize"]
Gate -- "yes" --> Ask["Merchant handler answers"]
Ask -- "proceed" --> Authorize
Ask -- "refuse · timeout" --> Blocked["Stopped VALIDATION_FAILED"]
Authorize --> Provider["Provider UI (sheet · redirect)"]
Provider --> Confirm["POST confirm"]
```
The gate sits **before** the authorization request and before any provider UI. That position is the
whole point: a merchant revalidating a voucher, wallet balance or loyalty points needs the answer to
arrive while the customer is still on the checkout screen, not after they have approved a payment in
PayPal. Validating when the element is first drawn would answer against a basket the customer can
still change; the further the tap drifts from the check, the staler the answer.
Two design consequences follow.
**Silence is a block, not a pass.** If the handler never answers, the SDK stops the payment after
ten seconds rather than proceeding. A gate whose failure mode is "authorize anyway" gives no
guarantee at all, and the alternative — an element spinning indefinitely because a merchant endpoint
hung — is worse than a refused payment the customer can retry.
**A block is not a decline.** It surfaces as `AuthorizationFailureReason.validationFailed`, distinct
from `authorizationError`, so a merchant's own decision never lands in their analytics as an issuer
rejection. Nothing reached the backend, so there is no payment attempt to reconcile.
**The refusal carries its own reason.** `.refuse(message:)` rather than a bare `false`, because only
the merchant knows *why* they refused — an expired voucher reads differently to a changed basket —
and only they can phrase it for their customer. The message arrives as `AuthorizationFailure.message`,
the same place all other failure text comes from, so it needs no separate channel and no correlation
by `executionId`. The SDK's own timeout diagnostic is deliberately *not* delivered this way: it
describes an integration fault, not something a customer should read.
The handler receives the payment method code and can therefore gate one method while leaving the
rest untouched. It is opt-in: sessions created without it keep a fully synchronous payment path.
### Why not `onPaymentButtonClicked`?
The two hooks look adjacent but answer different questions, and conflating them is the mistake worth
avoiding:
| | `onPaymentButtonClicked` | `onRequestStart` |
| - | - | - |
| Purpose | The customer tapped | May this payment proceed? |
| Returns | `Void` | A `Bool`, via its completion |
| SDK waits for it | No | Yes |
| Can stop the payment | No | Yes |
| Use for | Analytics, observability, spinners | Any check the payment depends on |
`onPaymentButtonClicked` is deliberately a notification. It cannot gate anything, because the SDK
never looks at it and does not wait — work started inside it races the authorization rather than
preceding it. The Web SDK draws the same line between its `buttonClicked` and `requestStart` events.
### How to run a merchant check before authorization
Use `onRequestStart` when your backend has to approve a payment before Payrails authorizes it —
revalidating a voucher, confirming wallet balance, or re-checking loyalty points at the moment the
customer commits.
For why the gate sits where it does, see [The pre-authorization gate](#the-pre-authorization-gate).
```mermaid theme={null}
sequenceDiagram
participant Customer
participant App as Merchant App
participant SDK as Payrails SDK
participant Backend as Merchant Backend
participant API as Payrails API
Customer->>App: Taps pay
App->>SDK: Element starts the payment
SDK->>App: onRequestStart(context, completion)
App->>Backend: POST /pre-payment
Backend-->>App: valid or invalid
App-->>SDK: completion(.proceed) or completion(.refuse)
alt completion(.proceed)
SDK->>API: POST authorize
API-->>SDK: Authorized
else completion(.refuse)
SDK->>App: onAuthorizeFailed(failure: .validationFailed)
end
```
#### 1. Register the handler
Supply it at `createSession` time, alongside `onSessionExpired`:
```swift theme={null}
let session = try await Payrails.createSession(
with: Payrails.Configuration(initData: initData, option: .init(env: .production)),
onSessionExpired: { completion in
myBackend.fetchPayrailsInit { completion($0) }
},
onRequestStart: { context, completion in
myBackend.validatePrePayment(executionId: context.executionId) { isValid in
completion(isValid ? .proceed : .refuse(message: "Your basket is no longer valid."))
}
}
)
```
#### 2. Gate only the methods you care about
The handler fires for every payment method on the session. Call `completion(.proceed)` on the
branches you are not gating, or those payments will be blocked too:
```swift theme={null}
onRequestStart: { context, completion in
guard context.paymentMethodCode == "payPal" else {
completion(.proceed)
return
}
myBackend.validatePrePayment(executionId: context.executionId) { isValid in
completion(isValid)
}
}
```
#### 3. Answer explicitly, even on failure
The SDK waits ten seconds, then blocks the payment and logs a warning. Answer explicitly on your
error paths so the outcome is your decision rather than a timeout:
```swift theme={null}
onRequestStart: { context, completion in
myBackend.validatePrePayment(executionId: context.executionId) { result in
switch result {
case .success(let check):
completion(check.isValid ? .proceed : .refuse(message: check.reason))
case .failure:
// Your service is unreachable. Decide deliberately: .refuse stops the payment,
// .proceed accepts the risk of an unvalidated basket.
completion(.refuse(message: "We couldn't confirm your basket. Please try again."))
}
}
}
```
#### 4. Handle the block in your delegate
A blocked payment arrives as `onAuthorizeFailed(_:failure:)` with `failure.code == .validationFailed`.
Separate it from a decline — nothing reached the backend, so there is no failed payment to explain:
```swift theme={null}
extension CheckoutViewController: PayrailsPayPalButtonDelegate {
func onPaymentButtonClicked(_ button: Payrails.PayPalButton) {}
func onAuthorizeSuccess(_ button: Payrails.PayPalButton) {
showReceipt()
}
func onPaymentSessionExpired(_ button: Payrails.PayPalButton) {
showRetry()
}
func onAuthorizeFailed(_ button: Payrails.PayPalButton, failure: AuthorizationFailure) {
switch failure.code {
case .validationFailed:
// Your own check refused it. failure.message is the reason you passed to
// .refuse(message:), so you can show it directly instead of mapping a code.
showBasketChangedMessage(failure.message)
case .authorizationError:
showDeclineMessage() // the issuer refused it
default:
showGenericError(failure.message)
}
}
}
```
#### Reference
| Context field | Type | Notes |
| - | - | - |
| `executionId` | `String?` | The Payrails execution, when known |
| `paymentMethodCode` | `String` | `"card"`, `"payPal"`, `"applePay"`, … |
| `action` | `Action` | `.authorize` in this version |
| Handler behaviour | Result |
| - | - |
| `completion(.proceed)` | Authorization proceeds |
| `completion(.refuse(message:))` | Stopped as `.validationFailed`, carrying your `message` |
| `completion(.refuse())` | Stopped as `.validationFailed` with a generic description |
| No answer within 10 seconds | Stopped with a generic description, warning logged |
| `completion` called twice | First answer wins |
Only a deliberate `.refuse(message:)` reaches `failure.message`. The timeout logs its diagnostic
instead of surfacing it — it describes an integration fault rather than anything phrased for a
customer.
Refusing never sends an authorization request and returns the element to its idle state.
***
## Co-branded cards
Some cards carry two payment networks at once — a domestic scheme such as `cartesBancaires`, `mada` or `dankort` alongside `visa` or `mastercard`. The same card can be routed either way, and the two routes differ in cost and in which rules apply. EU regulation puts that choice with the shopper rather than the merchant, which is why the SDK surfaces it rather than resolving it silently.
```mermaid theme={null}
flowchart TD
Typed["Shopper types card number"] --> Lookup["BIN lookup on the first 8 digits"]
Lookup -- "one scheme" --> Plain["Normal card form, no selector"]
Lookup -- "two schemes" --> Selector["Card Brand selector appears one scheme preselected"]
Selector --> Notify["didChangePreferredScheme"]
Selector --> Authorize["Authorize carries preferredScheme"]
Plain --> Authorize
```
The lookup runs on the BIN — the first eight digits — not the full number, so it happens while the shopper is still typing and before anything sensitive is complete.
Three consequences are worth understanding.
**The shopper chooses, and the SDK carries it.** When two schemes resolve, the form shows a selector with one preselected. Whatever is selected travels with the authorization request automatically. Your integration never sets the scheme on a payment; `didChangePreferredScheme` exists so your own UI and analytics can follow along, not so you can forward the value.
**Clearing is a state change too.** If the shopper edits the number so it no longer resolves to a co-branded BIN, the callback fires again with an empty `cardSchemes` and a `nil` `preferredScheme`. That is the signal to undo whatever the earlier call made you draw. An integration that reads `preferredScheme` without checking `cardSchemes` leaves stale UI behind — the most common mistake with this callback.
**It is off unless the backend turns it on.** Two prerequisites, both in the session init response: a `featureConfig.coBrandedCardsRollout` percentage, and a `links.binLookup` entry for the form to call. Absent either, the card form behaves exactly as it did before — no selector, no callback, no BIN lookup. Nothing in your app switches this on.
The delegate method is optional, with a protocol-extension default, so adding it was source-compatible for integrations that implement only the original two `PayrailsCardFormDelegate` callbacks.
### How to support co-branded cards
A co-branded card carries two payment networks — a domestic scheme (`cartesBancaires`, `mada`, `dankort`) alongside an international one (`visa`, `mastercard`). EU regulation requires the shopper to choose which one the payment runs on. The SDK handles the choice; this guide covers what your integration has to do around it.
For why the SDK works this way, see [Co-branded cards](#co-branded-cards) above.
```mermaid theme={null}
sequenceDiagram
participant Shopper
participant Form as Payrails Card Form
participant API as Payrails API
participant App as Merchant App
Shopper->>Form: Types card number
Form->>API: BIN lookup (first 8 digits)
API-->>Form: Two schemes, one default
Form->>Shopper: Shows Card Brand selector
Form->>App: didChangePreferredScheme(change)
Shopper->>Form: Picks a scheme
Form->>App: didChangePreferredScheme(change)
App->>Form: Pay
Form->>API: Authorize with preferredScheme
```
#### 1. Confirm the feature is switched on
Co-branded support is gated on two things, both outside your app:
* `featureConfig.coBrandedCardsRollout` in the session init response
* a `links.binLookup` entry in the same response
Without both, the card form behaves exactly as before — no selector, no callback. Ask your Payrails contact to enable the rollout for your workspace.
#### 2. Handle the scheme change
The callback is optional: a protocol-extension default means existing `PayrailsCardFormDelegate` implementations keep compiling. Implement it when you want to react to the choice.
```swift theme={null}
extension CheckoutViewController: PayrailsCardFormDelegate {
func cardForm(_ view: Payrails.CardForm, didChangePreferredScheme change: PreferredSchemeChange) {
guard !change.cardSchemes.isEmpty else {
// Not a co-branded card, or the number changed and the state cleared.
hideSchemeHint()
return
}
// change.preferredScheme is the code that will be sent with the payment.
showSchemeHint(for: change.preferredScheme)
}
}
```
It fires in three situations, and the third is the one integrations forget:
| When | `cardSchemes` | `preferredScheme` |
| - | - | - |
| A co-branded BIN resolves | both schemes | the default |
| The shopper picks a different brand | both schemes | their choice |
| The card number changes and co-branded state clears | empty | `nil` |
Treat the third as "this is no longer a co-branded card" and reset any UI you drew from an earlier call. Reading `change.preferredScheme` without checking `cardSchemes` leaves stale hints on screen.
#### 3. Style the selector
The selector renders with the SDK's defaults, so existing styling is unaffected. Override it through `CardFormStyle`:
```swift theme={null}
let style = CardFormStyle(
cardBrandSelector: CardBrandSelectorStyle(
// your tokens
)
)
```
A `nil` value leaves the default in place — see the [styling guide](/docs/orchestration/checkout-sdks/ios/styling-guide) for the full token set.
#### 4. Localize the title
The selector's heading and subheading come from `CardTranslations.Labels`. Supply localized strings rather than relying on the built-in English defaults (`"Card Brand"` and its subtitle):
```swift theme={null}
let translations = CardTranslations(
labels: CardTranslations.Labels(
cardBrandSelectorTitle: NSLocalizedString("checkout.card_brand", comment: ""),
cardBrandSelectorSubtitle: NSLocalizedString("checkout.card_brand_hint", comment: "")
)
)
```
Either may be left `nil` to keep the SDK default for that line.
#### What you do not have to do
**You do not send the scheme yourself.** The form carries the shopper's choice into the authorization request. `didChangePreferredScheme` is for your UI and analytics — the payment already carries it.
**You do not build the selector.** It is part of the card form.
#### Reference
| Symbol | Notes |
| - | - |
| `cardForm(_:didChangePreferredScheme:)` | Optional delegate callback; default no-op |
| `PreferredSchemeChange` | `preferredScheme: String?`, `cardSchemes: [CardScheme]` |
| `CardScheme` | `code`, `name`, `logoUrl`, `selected` |
| `CardFormStyle.cardBrandSelector` | `CardBrandSelectorStyle?`; `nil` keeps the default |
| `CardTranslations.Labels.cardBrandSelectorTitle` | Selector heading; `nil` uses `"Card Brand"` |
| `CardTranslations.Labels.cardBrandSelectorSubtitle` | Selector subheading; `nil` uses the SDK default |
***
## Security model
* **Card data is never exposed in plaintext.** The SDK encrypts card fields using PayrailsCSE (a Skyflow vault client) before they leave the device.
* **The Session token is short-lived.** Tokens are fetched by your backend and passed to the SDK; they are not stored persistently.
* **Logging is off by default.** The debug overlay and `Payrails.log` output are only visible when explicitly enabled. See [Troubleshooting](/docs/orchestration/checkout-sdks/ios/troubleshooting) for details.
***
## Element lifecycle
Elements hold a weak reference to the session. They are safe to create in `viewDidLoad` and will be deallocated with the view controller. You do not need to manually tear them down.
If the user navigates away during a payment, the in-flight `Task` is cancelled in `deinit` of `CardPaymentButton`, preventing dangling callbacks.
***
## Next steps
* [Quick Start](/docs/orchestration/checkout-sdks/ios) — get to a running integration in 15 minutes
* [SDK API Reference](/docs/orchestration/checkout-sdks/ios/sdk-api-reference) — complete API surface
* [Styling Guide](/docs/orchestration/checkout-sdks/ios/styling-guide) — customise the UI
# Styling Guide
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/ios/styling-guide
Style the iOS SDK card form, inputs, labels, error text, and payment buttons, including field variants and layout presets.
## Table of Contents
1. [Overview](#overview)
2. [How Styling Works](#how-styling-works)
3. [Card Form Styling](#card-form-styling)
* [Wrapper Style](#wrapper-style)
* [Input Field Styles](#input-field-styles)
* [Per-Field Overrides](#per-field-overrides)
* [Label Styles](#label-styles)
* [Error Text Style](#error-text-style)
* [Spacing](#spacing)
4. [Field Variants](#field-variants)
5. [Card Icon Alignment](#card-icon-alignment)
6. [Layout Presets](#layout-presets)
7. [Card Payment Button Styling](#card-payment-button-styling)
8. [Generic Redirect Button Styling](#generic-redirect-button-styling)
9. [Stored Instruments Styling](#stored-instruments-styling)
* [List Style](#list-style)
* [Pay Button Style](#pay-button-style)
* [Delete Button Style](#delete-button-style)
* [Update Button Style](#update-button-style)
10. [Apple Pay Button Styling](#apple-pay-button-styling)
11. [Translations & Labels](#translations-labels)
* [Card Form Translations](#card-form-translations)
* [Button Translations](#button-translations)
* [Stored Instruments Translations](#stored-instruments-translations)
12. [Using with SwiftUI](#using-with-swiftui)
13. [Full Example: Themed Checkout](#full-example-themed-checkout)
14. [Style Properties Reference](#style-properties-reference)
***
## Overview
The Payrails iOS SDK ships with sensible default styles for every UI component. You can override **any** property you want — only the values you provide will replace the defaults, everything else stays intact.
**Key components you can style:**
| Component | Description |
| - | - |
| **Card Form** | Secure card-number, CVV, expiry, and cardholder-name fields |
| **Card Payment Button** | "Pay" button that submits the card form |
| **Generic Redirect Button** | Button for redirect-based methods (iDEAL, Sofort, etc.) |
| **Stored Instruments List** | Displays saved cards / PayPal accounts for returning customers |
| **Apple Pay Button** | Native Apple Pay button (limited to Apple's built-in styles) |
***
## How Styling Works
The SDK uses a **merge-over-defaults** pattern:
1. Every style struct has a `.defaultStyle` preset with production-ready defaults.
2. You create a style struct and set **only the properties you want to change** — leave everything else `nil`.
3. The SDK automatically merges your customizations over the defaults.
```swift theme={null}
// You only need to set what you want to change:
let myButtonStyle = CardButtonStyle(
backgroundColor: .black,
cornerRadius: 12
// Everything else (textColor, font, height…) keeps its default value
)
```
This means you **never** need to provide a complete style — just your overrides.
***
## Card Form Styling
The card form is created via `Payrails.createCardForm(config:)`. All visual customization goes into `CardFormConfig.styles`, which is a `CardFormStylesConfig`.
### Quick Start
```swift theme={null}
let config = CardFormConfig(
styles: CardFormStylesConfig(
wrapperStyle: CardWrapperStyle(cornerRadius: 12),
allInputFieldStyles: CardFieldSpecificStyles(
base: CardStyle(
borderColor: .systemGray3,
cornerRadius: 8,
borderWidth: 1,
font: .systemFont(ofSize: 16),
textColor: .label
),
focus: CardStyle(borderColor: .systemBlue),
completed: CardStyle(borderColor: .systemGreen),
invalid: CardStyle(borderColor: .systemRed)
)
)
)
let cardForm = session.createCardForm(config: config)
```
***
### Wrapper Style
`CardWrapperStyle` controls the **outer container** that wraps all fields.
```swift theme={null}
CardWrapperStyle(
backgroundColor: UIColor?, // Container background (default: nil / clear)
borderColor: UIColor?, // Border color (default: .separator)
borderWidth: CGFloat?, // Border width (default: 1.0)
cornerRadius: CGFloat?, // Corner radius (default: 8.0)
padding: UIEdgeInsets? // Inner padding (default: 16 on all sides)
)
```
**Example — card-like wrapper with shadow feel:**
```swift theme={null}
let wrapper = CardWrapperStyle(
backgroundColor: .systemBackground,
borderColor: .systemGray4,
borderWidth: 1,
cornerRadius: 16,
padding: UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
)
```
***
### Input Field Styles
`CardFieldSpecificStyles` lets you style text fields for **each state**:
| State | When Applied |
| - | - |
| `base` | Default / idle state |
| `focus` | Field is actively being edited |
| `completed` | Field has valid, complete input |
| `invalid` | Validation failed |
Each state is a `CardStyle` (alias for `Style`) with these properties:
```swift theme={null}
CardStyle(
borderColor: UIColor?, // Field border color
cornerRadius: CGFloat?, // Field corner radius (default: 2)
padding: UIEdgeInsets?, // Inner text padding (default: 10 on all sides)
borderWidth: CGFloat?, // Border width (default: 1)
font: UIFont?, // Text font
textAlignment: NSTextAlignment?, // Text alignment (default: .left)
textColor: UIColor?, // Text color (default: .label)
backgroundColor: UIColor?, // Field background color
cursorColor: UIColor?, // Cursor / tint color
placeholderColor: UIColor?, // Placeholder text color
width: CGFloat?, // Explicit width (overrides fieldInsets)
height: CGFloat?, // Explicit height
minWidth: CGFloat?, // Minimum width constraint
maxWidth: CGFloat?, // Maximum width constraint
minHeight: CGFloat?, // Minimum height constraint
maxHeight: CGFloat?, // Maximum height constraint
cardIconAlignment: CardIconAlignment?, // Card brand icon position (.left / .right)
fieldInsets: UIEdgeInsets? // Field-to-container spacing (default: 0,6,0,6)
)
```
**Example — modern rounded fields:**
```swift theme={null}
let fieldStyles = CardFieldSpecificStyles(
base: CardStyle(
borderColor: .systemGray4,
cornerRadius: 10,
padding: UIEdgeInsets(top: 12, left: 14, bottom: 12, right: 14),
borderWidth: 1.5,
font: .systemFont(ofSize: 16),
textColor: .label,
backgroundColor: .secondarySystemBackground,
placeholderColor: .tertiaryLabel
),
focus: CardStyle(
borderColor: UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0),
backgroundColor: .systemBackground
),
completed: CardStyle(
borderColor: .systemGreen
),
invalid: CardStyle(
borderColor: .systemRed,
backgroundColor: UIColor.systemRed.withAlphaComponent(0.05)
)
)
```
You only need to set properties that *differ* from the `base` state. For example, if the `focus` state should only change the border color, just set `borderColor` in `focus`.
***
### Per-Field Overrides
Need a specific field to look different? Use `inputFieldStyles` to target individual fields by type:
```swift theme={null}
let styles = CardFormStylesConfig(
// Applies to ALL fields:
allInputFieldStyles: CardFieldSpecificStyles(
base: CardStyle(cornerRadius: 8, borderWidth: 1)
),
// Override just the card number field:
inputFieldStyles: [
.CARD_NUMBER: CardFieldSpecificStyles(
base: CardStyle(
font: .monospacedDigitSystemFont(ofSize: 18, weight: .medium),
height: 52
)
),
.CVV: CardFieldSpecificStyles(
base: CardStyle(
maxWidth: 100
)
)
]
)
```
**Available field types:**
| `CardFieldType` | Description |
| - | - |
| `.CARDHOLDER_NAME` | Cardholder name field |
| `.CARD_NUMBER` | Card number field |
| `.EXPIRATION_DATE` | Combined expiry field (MM/YY) |
| `.EXPIRATION_MONTH` | Expiry month (separate) |
| `.EXPIRATION_YEAR` | Expiry year (separate) |
| `.CVV` | Security code field |
> Per-field styles are **merged over** `allInputFieldStyles`, so you only need to specify the differences.
***
### Label Styles
Customize the label above each field:
```swift theme={null}
let styles = CardFormStylesConfig(
labelStyles: [
.CARD_NUMBER: CardStyle(
font: .systemFont(ofSize: 13, weight: .semibold),
textColor: .secondaryLabel
),
.CVV: CardStyle(
font: .systemFont(ofSize: 13, weight: .semibold),
textColor: .secondaryLabel
),
.EXPIRATION_MONTH: CardStyle(
font: .systemFont(ofSize: 13, weight: .semibold),
textColor: .secondaryLabel
),
.EXPIRATION_YEAR: CardStyle(
font: .systemFont(ofSize: 13, weight: .semibold),
textColor: .secondaryLabel
),
.CARDHOLDER_NAME: CardStyle(
font: .systemFont(ofSize: 13, weight: .semibold),
textColor: .secondaryLabel
)
]
)
```
***
### Error Text Style
Control how validation error messages look below fields:
```swift theme={null}
let styles = CardFormStylesConfig(
errorTextStyle: CardStyle(
font: .systemFont(ofSize: 12),
textColor: .systemRed
)
)
```
***
### Spacing
Fine-tune the spacing between fields and sections:
```swift theme={null}
let styles = CardFormStylesConfig(
fieldSpacing: 12, // Vertical gap between fields (default: 10)
sectionSpacing: 20 // Gap between row groups (default: 16)
)
```
***
### Field Insets
`fieldInsets` controls the spacing between a field and its container edge — **independent** of `padding`, which controls text inset inside the field.
```
┌─────────────── container ───────────────┐
│ │
│ fieldInsets.left ┌──────────┐ fieldInsets.right
│ ◄────────────────►│ Field │◄────────────────►│
│ │ padding │ │
│ │ ◄──────► │ │
│ │ (text) │ │
│ └──────────┘ │
└──────────────────────────────────────────────────┘
```
Use the convenience method `.fieldInsets(top:left:bottom:right:)` — defaults are `(0, 6, 0, 6)`:
```swift theme={null}
let styles = CardFormStylesConfig(
allInputFieldStyles: CardFieldSpecificStyles(
base: CardStyle(
padding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), // text inset
fieldInsets: .fieldInsets(left: 24, right: 24) // container spacing
)
)
)
```
Only specify the sides you want to change:
```swift theme={null}
.fieldInsets(top: 8) // only change top
.fieldInsets(left: 24, right: 24) // only change horizontal
.zero // edge-to-edge
```
Per-field overrides work via `inputFieldStyles`:
```swift theme={null}
inputFieldStyles: [
.CARD_NUMBER: CardFieldSpecificStyles(
base: CardStyle(fieldInsets: .fieldInsets(left: 32, right: 32))
)
]
```
When an explicit `width` is set, `fieldInsets` is ignored—the field uses a fixed-width constraint instead.
***
## Field Variants
Choose between two visual styles for input fields:
| Variant | Description |
| - | - |
| `.outlined` | *(Default)* — bordered rectangle with transparent background |
| `.filled` | Filled background with no border |
```swift theme={null}
let config = CardFormConfig(
fieldVariant: .filled
)
```
You can further customize each variant using `CardFieldSpecificStyles` as described above.
***
## Card Icon Alignment
Display the detected card brand icon (Visa, Mastercard, etc.) inside the card number field:
```swift theme={null}
let config = CardFormConfig(
showCardIcon: true,
cardIconAlignment: .right // .left (default) or .right
)
```
***
## Layout Presets
Control the arrangement of fields using built-in presets or a custom layout:
### Built-in Presets
```swift theme={null}
// Standard (default):
// Row 1: [Card Number]
// Row 2: [CVV] [Expiry Month] [Expiry Year]
let config = CardFormConfig(layout: .standard)
// Compact:
// Row 1: [Card Number]
// Row 2: [Expiry Month] [Expiry Year] [CVV]
let config = CardFormConfig(layout: .compact)
// Minimal (no name field, compact layout):
// Row 1: [Card Number]
// Row 2: [Expiry Month] [Expiry Year] [CVV]
let config = CardFormConfig(layout: .minimal)
```
### Combined Expiry Date Field
Replace separate month/year fields with a single MM/YY field:
```swift theme={null}
let config = CardFormConfig(
layout: .preset(.standard, useCombinedExpiryDateField: true)
)
// Row 1: [Card Number]
// Row 2: [CVV] [MM/YY]
```
### Custom Layout
Define exactly which fields appear on each row:
```swift theme={null}
let config = CardFormConfig(
showNameField: true,
layout: .custom(
[
[.CARDHOLDER_NAME], // Row 1
[.CARD_NUMBER], // Row 2
[.EXPIRATION_DATE, .CVV] // Row 3
],
useCombinedExpiryDateField: true
)
)
```
Custom layouts must include `.CARD_NUMBER`, `.CVV`, and expiry fields (either `.EXPIRATION_DATE` or both `.EXPIRATION_MONTH` / `.EXPIRATION_YEAR`). If the layout is invalid, the SDK falls back to the standard preset.
***
## Card Payment Button Styling
Created via `session.createCardPaymentButton(buttonStyle:translations:)`.
### Properties
```swift theme={null}
CardButtonStyle(
backgroundColor: UIColor?, // Button background (default: .systemBlue)
textColor: UIColor?, // Title text color (default: .white)
font: UIFont?, // Title font (default: system)
cornerRadius: CGFloat?, // Corner radius (default: 8)
borderWidth: CGFloat?, // Border width (default: none)
borderColor: UIColor?, // Border color (default: none)
contentEdgeInsets: UIEdgeInsets?, // Padding inside button
height: CGFloat? // Button height (default: 44)
)
```
### Examples
```swift theme={null}
// Pill-shaped dark button
let payButton = session.createCardPaymentButton(
buttonStyle: CardButtonStyle(
backgroundColor: .black,
textColor: .white,
font: .systemFont(ofSize: 18, weight: .bold),
cornerRadius: 22,
height: 48
)
)
// Outlined / ghost button
let payButton = session.createCardPaymentButton(
buttonStyle: CardButtonStyle(
backgroundColor: .clear,
textColor: .systemBlue,
font: .systemFont(ofSize: 16, weight: .semibold),
cornerRadius: 8,
borderWidth: 2,
borderColor: .systemBlue,
height: 44
)
)
```
> The button automatically shows a **loading spinner** during payment processing — no extra code needed.
***
## Generic Redirect Button Styling
For redirect-based payment methods (iDEAL, Sofort, Klarna, etc.). Uses the same `CardButtonStyle`:
```swift theme={null}
let idealButton = session.createGenericRedirectButton(
buttonStyle: CardButtonStyle(
backgroundColor: UIColor(red: 0.8, green: 0, blue: 0.4, alpha: 1),
textColor: .white,
cornerRadius: 10,
height: 48
),
paymentMethodCode: "ideal"
)
```
***
## Stored Instruments Styling
When returning customers have saved cards or PayPal accounts, the `StoredInstruments` view displays them as a selectable list.
### List Style
```swift theme={null}
StoredInstrumentsStyle(
backgroundColor: UIColor, // List background (default: .clear)
itemBackgroundColor: UIColor, // Unselected item background (default: .systemBackground)
selectedItemBackgroundColor: UIColor, // Selected item background (default: .systemGray6)
labelTextColor: UIColor, // Instrument label color (default: .label)
labelFont: UIFont, // Instrument label font (default: system 16)
itemCornerRadius: CGFloat, // Item corner radius (default: 8)
itemSpacing: CGFloat, // Gap between items (default: 8)
itemPadding: UIEdgeInsets, // Padding inside each item (default: 12/16/12/16)
buttonStyle: StoredInstrumentButtonStyle, // Pay button
deleteButtonStyle: DeleteButtonStyle, // Delete icon/button
updateButtonStyle: UpdateButtonStyle // Update icon/button
)
```
### Pay Button Style
```swift theme={null}
StoredInstrumentButtonStyle(
backgroundColor: UIColor, // Default: .systemBlue
textColor: UIColor, // Default: .white
font: UIFont, // Default: system 16 medium
cornerRadius: CGFloat, // Default: 8
height: CGFloat, // Default: 44
borderWidth: CGFloat, // Default: 0
borderColor: UIColor, // Default: .clear
contentEdgeInsets: UIEdgeInsets // Default: 8/16/8/16
)
```
### Delete Button Style
```swift theme={null}
DeleteButtonStyle(
backgroundColor: UIColor, // Default: .systemRed
textColor: UIColor, // Default: .white
font: UIFont, // Default: system 14
cornerRadius: CGFloat, // Default: 4
size: CGSize // Default: 32×32
)
```
### Update Button Style
```swift theme={null}
UpdateButtonStyle(
backgroundColor: UIColor, // Default: .systemBlue
textColor: UIColor, // Default: .white
font: UIFont, // Default: system 14
cornerRadius: CGFloat, // Default: 4
size: CGSize // Default: 32×32
)
```
### Full Example
```swift theme={null}
let storedInstruments = session.createStoredInstruments(
style: StoredInstrumentsStyle(
backgroundColor: .clear,
itemBackgroundColor: .secondarySystemBackground,
selectedItemBackgroundColor: UIColor.systemBlue.withAlphaComponent(0.1),
labelTextColor: .label,
labelFont: .systemFont(ofSize: 15),
itemCornerRadius: 12,
itemSpacing: 10,
itemPadding: UIEdgeInsets(top: 14, left: 18, bottom: 14, right: 18),
buttonStyle: StoredInstrumentButtonStyle(
backgroundColor: .black,
textColor: .white,
font: .systemFont(ofSize: 16, weight: .bold),
cornerRadius: 22,
height: 48
),
deleteButtonStyle: DeleteButtonStyle(
backgroundColor: UIColor.systemRed.withAlphaComponent(0.1),
textColor: .systemRed,
cornerRadius: 8,
size: CGSize(width: 36, height: 36)
),
updateButtonStyle: UpdateButtonStyle(
backgroundColor: UIColor.systemBlue.withAlphaComponent(0.1),
textColor: .systemBlue,
cornerRadius: 8,
size: CGSize(width: 36, height: 36)
)
)
)
```
***
## Apple Pay Button Styling
Apple restricts customization of the Apple Pay button to its own set of styles and types:
```swift theme={null}
let applePayButton = session.createApplePayButton(
type: .buy, // .plain, .buy, .setUp, .inStore, .donate, .checkout, .book, .subscribe
style: .black // .black, .white, .whiteOutline, .automatic
)
```
> You cannot apply custom fonts, colors, or corner radii to the Apple Pay button — Apple enforces its Human Interface Guidelines.
***
## Translations & Labels
### Card Form Translations
Override placeholder text, labels, and error messages:
```swift theme={null}
let translations = CardTranslations(
placeholders: CardTranslations.Placeholders(values: [
.CARD_NUMBER: "Card number",
.CARDHOLDER_NAME: "Name on card",
.CVV: "CVV",
.EXPIRATION_DATE: "MM/YY",
.EXPIRATION_MONTH: "MM",
.EXPIRATION_YEAR: "YY"
]),
labels: CardTranslations.Labels(
values: [
.CARD_NUMBER: "Card Number",
.CARDHOLDER_NAME: "Cardholder Name",
.CVV: "Security Code",
.EXPIRATION_DATE: "Expiry Date",
.EXPIRATION_MONTH: "Expiry Month",
.EXPIRATION_YEAR: "Expiry Year"
],
saveInstrument: "Save card for future payments",
storeInstrument: "Store this card"
),
error: CardTranslations.ErrorMessages(values: [
.CARD_NUMBER: "Please enter a valid card number",
.CARDHOLDER_NAME: "Cardholder name is required",
.CVV: "Invalid security code",
.EXPIRATION_DATE: "Invalid expiry date",
.EXPIRATION_MONTH: "Invalid month",
.EXPIRATION_YEAR: "Invalid year"
])
)
let config = CardFormConfig(
translations: translations
)
```
### Button Translations
```swift theme={null}
// Card payment button
let payButton = session.createCardPaymentButton(
translations: CardPaymenButtonTranslations(label: "Complete Purchase")
)
// Stored instrument pay button
let payButton = session.createCardPaymentButton(
storedInstrument: instrument,
translations: StoredInstrumentButtonTranslations(
label: "Pay Now",
processingLabel: "Processing..."
)
)
```
### Stored Instruments Translations
```swift theme={null}
let storedInstruments = session.createStoredInstruments(
translations: StoredInstrumentsTranslations(
cardPrefix: "Card ending in",
paypalPrefix: "PayPal",
buttonTranslations: StoredInstrumentButtonTranslations(
label: "Pay",
processingLabel: "Processing..."
)
)
)
```
***
## Using with SwiftUI
The SDK's UI components are built with UIKit. To embed them in SwiftUI, use `UIViewRepresentable`:
```swift theme={null}
import SwiftUI
import Payrails
struct CardFormView: UIViewRepresentable {
let session: PayrailsSession
func makeUIView(context: Context) -> UIView {
let config = CardFormConfig(
showNameField: true,
showCardIcon: true,
styles: CardFormStylesConfig(
wrapperStyle: CardWrapperStyle(cornerRadius: 16),
allInputFieldStyles: CardFieldSpecificStyles(
base: CardStyle(
cornerRadius: 10,
font: .systemFont(ofSize: 16),
textColor: .label
),
focus: CardStyle(borderColor: .systemBlue)
)
)
)
return session.createCardForm(config: config)
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
struct PayButtonView: UIViewRepresentable {
let session: PayrailsSession
func makeUIView(context: Context) -> UIView {
return session.createCardPaymentButton(
buttonStyle: CardButtonStyle(
backgroundColor: .black,
cornerRadius: 12,
height: 50
)
)
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
// Usage in a SwiftUI view:
struct CheckoutView: View {
let session: PayrailsSession
var body: some View {
VStack(spacing: 20) {
CardFormView(session: session)
.frame(height: 300)
PayButtonView(session: session)
.frame(height: 50)
}
.padding()
}
}
```
***
## Full Example: Themed Checkout
Here's a complete example that demonstrates a dark-themed, brand-customized checkout:
```swift theme={null}
import UIKit
import Payrails
class CheckoutViewController: UIViewController {
var session: PayrailsSession!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.05, green: 0.05, blue: 0.1, alpha: 1)
setupUI()
}
private func setupUI() {
let brandBlue = UIColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 1.0)
// 1. Card Form
let cardForm = session.createCardForm(config: CardFormConfig(
showNameField: true,
showCardIcon: true,
cardIconAlignment: .right,
fieldVariant: .outlined,
layout: .preset(.standard, useCombinedExpiryDateField: true),
styles: CardFormStylesConfig(
wrapperStyle: CardWrapperStyle(
backgroundColor: UIColor(white: 0.12, alpha: 1),
borderColor: UIColor(white: 0.25, alpha: 1),
borderWidth: 1,
cornerRadius: 16,
padding: UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
),
errorTextStyle: CardStyle(
font: .systemFont(ofSize: 12),
textColor: .systemPink
),
allInputFieldStyles: CardFieldSpecificStyles(
base: CardStyle(
borderColor: UIColor(white: 0.3, alpha: 1),
cornerRadius: 10,
padding: UIEdgeInsets(top: 14, left: 14, bottom: 14, right: 14),
borderWidth: 1,
font: .monospacedDigitSystemFont(ofSize: 16, weight: .regular),
textColor: .white,
backgroundColor: UIColor(white: 0.15, alpha: 1),
cursorColor: brandBlue,
placeholderColor: UIColor(white: 0.5, alpha: 1)
),
focus: CardStyle(
borderColor: brandBlue,
backgroundColor: UIColor(white: 0.18, alpha: 1)
),
completed: CardStyle(
borderColor: .systemGreen
),
invalid: CardStyle(
borderColor: .systemPink,
backgroundColor: UIColor.systemPink.withAlphaComponent(0.08)
)
),
fieldSpacing: 12,
sectionSpacing: 20
),
translations: CardTranslations(
placeholders: CardTranslations.Placeholders(values: [
.CARD_NUMBER: "1234 5678 9012 3456",
.CARDHOLDER_NAME: "John Doe",
.CVV: "123",
.EXPIRATION_DATE: "MM/YY"
]),
labels: CardTranslations.Labels(values: [
.CARD_NUMBER: "CARD NUMBER",
.CARDHOLDER_NAME: "NAME ON CARD",
.CVV: "CVV",
.EXPIRATION_DATE: "EXPIRY"
])
)
))
// 2. Pay Button
let payButton = session.createCardPaymentButton(
buttonStyle: CardButtonStyle(
backgroundColor: brandBlue,
textColor: .white,
font: .systemFont(ofSize: 18, weight: .bold),
cornerRadius: 14,
height: 52
),
translations: CardPaymenButtonTranslations(label: "Pay $49.99")
)
// 3. Layout
let stack = UIStackView(arrangedSubviews: [cardForm, payButton])
stack.axis = .vertical
stack.spacing = 24
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
}
```
***
## Style Properties Reference
### CardStyle (alias: `Style`)
| Property | Type | Default |
| - | - | - |
| `borderColor` | `UIColor?` | `nil` |
| `cornerRadius` | `CGFloat?` | `2` |
| `padding` | `UIEdgeInsets?` | `(10, 10, 10, 10)` |
| `borderWidth` | `CGFloat?` | `1` |
| `font` | `UIFont?` | System default |
| `textAlignment` | `NSTextAlignment?` | `.left` |
| `textColor` | `UIColor?` | `.label` |
| `backgroundColor` | `UIColor?` | `nil` |
| `cursorColor` | `UIColor?` | `nil` |
| `placeholderColor` | `UIColor?` | `nil` |
| `width` | `CGFloat?` | `nil` |
| `height` | `CGFloat?` | `nil` |
| `minWidth` | `CGFloat?` | `nil` |
| `maxWidth` | `CGFloat?` | `nil` |
| `minHeight` | `CGFloat?` | `nil` |
| `maxHeight` | `CGFloat?` | `nil` |
| `cardIconAlignment` | `CardIconAlignment?` | `.left` |
| `fieldInsets` | `UIEdgeInsets?` | `nil` (defaults to `(0, 6, 0, 6)`) |
### CardFieldSpecificStyles
| Property | Type | Description |
| - | - | - |
| `base` | `CardStyle?` | Default / idle state |
| `focus` | `CardStyle?` | Active editing state |
| `completed` | `CardStyle?` | Valid input state |
| `invalid` | `CardStyle?` | Validation error state |
### CardButtonStyle
| Property | Type | Default |
| - | - | - |
| `backgroundColor` | `UIColor?` | `.systemBlue` |
| `textColor` | `UIColor?` | `.white` |
| `font` | `UIFont?` | System default |
| `cornerRadius` | `CGFloat?` | `8` |
| `borderWidth` | `CGFloat?` | `nil` |
| `borderColor` | `UIColor?` | `nil` |
| `contentEdgeInsets` | `UIEdgeInsets?` | `nil` |
| `height` | `CGFloat?` | `44` |
### CardWrapperStyle
| Property | Type | Default |
| - | - | - |
| `backgroundColor` | `UIColor?` | `nil` |
| `borderColor` | `UIColor?` | `.separator` |
| `borderWidth` | `CGFloat?` | `1` |
| `cornerRadius` | `CGFloat?` | `8` |
| `padding` | `UIEdgeInsets?` | `(16, 16, 16, 16)` |
### CardFormStylesConfig
| Property | Type | Default |
| - | - | - |
| `wrapperStyle` | `CardWrapperStyle?` | `.defaultStyle` |
| `errorTextStyle` | `CardStyle?` | `textColor: .systemRed` |
| `allInputFieldStyles` | `CardFieldSpecificStyles?` | `.defaultStyle` |
| `inputFieldStyles` | `[CardFieldType: CardFieldSpecificStyles]?` | `nil` |
| `labelStyles` | `[CardFieldType: CardStyle]?` | `textColor: .secondaryLabel` |
| `fieldSpacing` | `CGFloat?` | `10` |
| `sectionSpacing` | `CGFloat?` | `16` |
### StoredInstrumentsStyle
| Property | Type | Default |
| - | - | - |
| `backgroundColor` | `UIColor` | `.clear` |
| `itemBackgroundColor` | `UIColor` | `.systemBackground` |
| `selectedItemBackgroundColor` | `UIColor` | `.systemGray6` |
| `labelTextColor` | `UIColor` | `.label` |
| `labelFont` | `UIFont` | `.systemFont(ofSize: 16)` |
| `itemCornerRadius` | `CGFloat` | `8` |
| `itemSpacing` | `CGFloat` | `8` |
| `itemPadding` | `UIEdgeInsets` | `(12, 16, 12, 16)` |
| `buttonStyle` | `StoredInstrumentButtonStyle` | `.defaultStyle` |
| `deleteButtonStyle` | `DeleteButtonStyle` | `.defaultStyle` |
| `updateButtonStyle` | `UpdateButtonStyle` | `.defaultStyle` |
### StoredInstrumentButtonStyle
| Property | Type | Default |
| - | - | - |
| `backgroundColor` | `UIColor` | `.systemBlue` |
| `textColor` | `UIColor` | `.white` |
| `font` | `UIFont` | `.systemFont(ofSize: 16, weight: .medium)` |
| `cornerRadius` | `CGFloat` | `8` |
| `height` | `CGFloat` | `44` |
| `borderWidth` | `CGFloat` | `0` |
| `borderColor` | `UIColor` | `.clear` |
| `contentEdgeInsets` | `UIEdgeInsets` | `(8, 16, 8, 16)` |
### DeleteButtonStyle
| Property | Type | Default |
| - | - | - |
| `backgroundColor` | `UIColor` | `.systemRed` |
| `textColor` | `UIColor` | `.white` |
| `font` | `UIFont` | `.systemFont(ofSize: 14)` |
| `cornerRadius` | `CGFloat` | `4` |
| `size` | `CGSize` | `(32, 32)` |
### UpdateButtonStyle
| Property | Type | Default |
| - | - | - |
| `backgroundColor` | `UIColor` | `.systemBlue` |
| `textColor` | `UIColor` | `.white` |
| `font` | `UIFont` | `.systemFont(ofSize: 14)` |
| `cornerRadius` | `CGFloat` | `4` |
| `size` | `CGSize` | `(32, 32)` |
***
**Need help?** Check the [SDK API Reference](/docs/orchestration/checkout-sdks/ios/sdk-api-reference) or the [Troubleshooting Guide](/docs/orchestration/checkout-sdks/ios/troubleshooting) for common issues.
# Troubleshooting
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/ios/troubleshooting
Common issues and how to fix them.
## Enabling debug logs
The SDK provides two mechanisms for debugging.
### On-screen log overlay (SwiftUI)
Add the debug config viewer to any SwiftUI view:
```swift theme={null}
import SwiftUI
import Payrails
struct DebugView: View {
var body: some View {
Payrails.Debug.configViewer()
}
}
```
This renders the parsed SDK configuration and recent log entries on screen. Requires an active session.
### Console logging
The SDK writes to `LogStore.shared` and also calls `Swift.print`. To see logs in the Xcode console, ensure the scheme is not suppressing standard output.
***
## Installation issues
**Package resolution fails**
* Confirm the package URL is exactly `https://github.com/payrails/ios-sdk.git`.
* Click **File → Packages → Reset Package Caches** in Xcode, then resolve again.
**"the checksum of the downloaded artifact does not match"**
* Swift Package Manager verifies the downloaded `Payrails.xcframework.zip` against the checksum in the package manifest, and the two disagree.
* Almost always a stale cache. Click **File → Packages → Reset Package Caches**, clear the Swift Package Manager cache at `~/Library/Caches/org.swift.swiftpm`, then resolve again.
* Payrails never re-uploads a published zip, so a mismatch that survives a cleared cache is worth a support ticket with the SDK version.
**`Package.resolved` still pins a source revision after upgrading to 3.0.0**
* 3.0.0 replaces the source target with a binary target, and `Package.resolved` pins the revision that was resolved before the upgrade.
* Click **File → Packages → Reset Package Caches**, then **File → Packages → Update to Latest Package Versions**. Resolving again rewrites the pin.
**Linker error: PayrailsCSE not found**
* `PayrailsCSE` is linked statically into the framework binary, so this is not a missing-code problem. The generated `.swiftinterface` carries `import PayrailsCSE`, so the Swift compiler needs that module present to build against the binary. That holds even though no public Payrails API exposes a CSE type.
* Confirm the `ios-cse` package appears in the project's resolved packages. If it is missing, resolve the `Payrails` package again. Its manifest declares both `ios-cse` and `paypalcheckout-ios`.
* The CSE code is therefore present twice: once absorbed into `Payrails.framework`, once as the separately resolved `ios-cse` package. Inside the framework that is 176 exported `PayrailsCSE` symbols, plus 626 from JOSESwift, which arrives transitively through `ios-cse`. That matters when you measure binary size or integrate `PayrailsCSE` directly. `PayPalCheckout` is the opposite case: it ships as a prebuilt binary XCFramework, so it stays a genuine external dynamic dependency.
**Compiler cannot load the framework's module interface**
* Each release is built with Xcode 16.4. Swift's `.swiftinterface` format is not backward compatible across toolchains, so an older Xcode cannot read the module. Xcode 16.4 is the minimum, not a recommendation.
* Upgrade Xcode, or open a support ticket with the Xcode version.
***
## SDK initialization issues
**"Provided configuration data is invalid and cannot be parsed"**
* The init payload `data` field is not valid base64-encoded JSON
* Ensure your backend passes the exact string returned by the Payrails `POST /checkout/initialize` endpoint without modification
**"SDK has not been properly initialized"**
* `Payrails.createCardForm()` or another factory was called before `Payrails.createSession()` completed
* These calls trigger a `precondition` failure if called without an active session — always `await` session creation first
**Session init works in debug but fails in release**
* Check that your backend call succeeds in the production environment
* Verify that the `env` option matches your backend environment (`.production` vs `.test`)
***
## Card form issues
**Card form appears but cannot submit — payment button stays unresponsive**
* Ensure `Payrails.createCardForm()` is called before `Payrails.createCardPaymentButton()`
* The button holds a strong reference to the form at creation time; order matters
**Validation errors not shown**
* Check that the card form has enough vertical space — error labels require height to render
* Use `CardFormConfig(showRequiredAsterisk: true)` to make required fields explicit
**Card icon not appearing**
* Pass `showCardIcon: true` in `CardFormConfig`.
* Icons load from the asset catalog inside the framework bundle, which the package resolves automatically. If icons are missing after a clean resolve, open a support ticket with the SDK version.
**Layout falls back to default unexpectedly (console logs "falling back to default layout")**
* Your custom `CardLayoutConfig` is missing required fields (`.CARD_NUMBER`, `.CVV`, and at least one expiry field)
* All three are required for a valid submission
***
## Apple Pay issues
**Apple Pay button is not visible**
* `session.isApplePayAvailable` is a **device-capability check only** (returns `false` when `PKPaymentAuthorizationController.canMakePayments()` is false). It does not inspect the merchant config.
* For a combined "configured and device capable" check, compose:
```swift theme={null}
let canShow = session.isApplePayAvailable
&& !session.getPaymentMethodConfig(.specific("apple_pay")).isEmpty
```
* The `PKPaymentButton` hides itself when `PKPaymentAuthorizationViewController.canMakePayments()` returns false.
**Apple Pay sheet dismisses immediately**
* The merchant identifier in your app's entitlements does not match the one in the Payrails merchant configuration
* Verify the capability is enabled under **Signing & Capabilities** in Xcode
**"incorrectPaymentSetup" error for Apple Pay**
* The init payload does not include an Apple Pay payment option configuration
* Contact your Payrails integration engineer to enable Apple Pay in your merchant account
***
## PayPal issues
**PayPal button tap does nothing**
* Ensure the `PayPalCheckout` SDK is properly linked (check the build phases)
* The PayPal SDK requires a client ID in the init payload config; verify the payload includes it
**PayPal checkout WebView dismissed with no result**
* The user cancelled — `onAuthorizeFailed(_:failure:)` fires with `failure.code == .userCancelled`. In parallel, the SDK invokes the `onSessionExpired` closure supplied at `createSession` time to refresh the underlying execution; the merchant's cached `Session` reference keeps working.
***
## Payment issues
**Payment failed — how do I know why?**
* `onAuthorizeFailed(_ button:, failure:)` carries an `AuthorizationFailure` struct. Switch on `failure.code` to give each case the right UX, and read `failure.message` / `failure.rawError` for detail:
* `.userCancelled` — user dismissed the 3DS sheet. Show neutral copy, no error banner.
* `.authorizationError` — authorization rejected (issuer decline, 3DS rejected, fraud blocked, etc.). `failure.message` is the backend's `errors[0].reason.result`, falling back to "Authorization failed" when no detail is provided.
* `.authenticationError` — session token rejected (HTTP 401 / 403). The SDK also fires its `onSessionExpired` refresh in the background.
* `.unknownError` — network, SDK, or other unexpected failure. Inspect `failure.rawError` for the underlying error.
**My retry on the same card doesn't work after a failure**
* Payrails executions are single-use. Once a payment terminates (success or failure), the same execution cannot be retried. Supply the `onSessionExpired` closure to `Payrails.createSession(with:onSessionExpired:)` so the SDK can mint a fresh execution in-place when needed; the merchant's `Session` reference and cached buttons keep working unchanged.
**3DS challenge never appears / `presentPayment(_:)` not called**
* Confirm that `payButton.presenter = self` is set on the `CardPaymentButton`
* Confirm your view controller conforms to both `PaymentPresenter` and `PayrailsCardPaymentFormDelegate` if needed
**Long-polling timeout errors**
* `PayrailsError.finalStatusNotFoundAfterLongPoll` — the payment status was not confirmed within the polling window
* This is typically a transient network or backend issue; prompt the user to check their payment status through your order history
***
## Pre-authorization gate issues
### Payments are blocked and no request reaches the backend
The `onRequestStart` handler supplied at `createSession` answered `false`, or did not answer at all.
Check `failure.code == .validationFailed` in `onAuthorizeFailed(_:failure:)` to confirm — a block is
reported with that code and never as `.authorizationError`.
The most common cause is a handler that only answers on the branch it cares about. The gate fires
for **every** payment method on the session, so any branch that does not call `completion(.proceed)`
blocks that method:
```swift theme={null}
onRequestStart: { context, completion in
guard context.paymentMethodCode == "payPal" else {
completion(.proceed) // ← omitting this blocks card, wallets, everything else
return
}
myBackend.validate { completion($0) }
}
```
### A payment is blocked roughly ten seconds after tapping
The handler never called its completion. The SDK stops the attempt rather than leaving the element
spinning, and logs:
```
⚠️ onRequestStart did not answer within 10s for ; the payment was blocked.
```
Enable debug logs (see above) to see it. Check every path through the handler — including error and
early-return branches — calls `completion` exactly once.
### Only PayPal or Apple Pay report a block without a reason
Those two delegates still expose a deprecated `onAuthorizeFailed(_ button:)` with no payload, and a
default implementation forwards to it for backwards compatibility. Implement
`onAuthorizeFailed(_ button:, failure:)` instead — it is the only variant carrying the code that
distinguishes a gate block from an issuer decline.
***
## Stored instruments issues
**`Payrails.getStoredInstruments()` returns an empty array**
* The init payload does not include any stored instruments for this holder reference
* Instruments with status other than `"enabled"` or `"created"` are filtered out
**StoredInstruments view renders nothing**
* Same as above — the list silently renders nothing when there are no eligible instruments
* Optionally check the count before adding the view: `Payrails.getStoredInstruments().isEmpty`
***
## Crash report issues
**Payrails frames arrive as raw addresses**
* The SDK ships as a binary, so a crash reporter needs the SDK's debug symbols to name those frames.
* Payrails builds `Payrails.dSYMs.zip` for every release. Request the file for the version you shipped through a support ticket, then upload it to your crash reporter alongside your app's own dSYMs.
* Symbols only resolve for the SDK version in the build. dSYMs from another version do not match.
***
## Getting help
1. Enable debug logs and capture the output
2. Reproduce the issue with `env: .test` to rule out production-only configuration issues
3. Check the `PayrailsError.errorDescription` for the specific failure reason
4. Open a support ticket with your `executionId` (from `Payrails.query(.executionId)`) and the full error description
# Payrails Web Fraud SDK
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/payrails-web-fraud-sdk
Initialize the Payrails fraud SDK to collect device signals from several fraud providers behind one interface, for the web SDK.
## Payrails Fraud SDK provides abstraction on different fraud client SDKs
The SDK should be initialized as soon as possible to enable data collection as soon as possible. The SDK will then pass all the information needed to the Payrails Web SDK without additional work on the merchant client.
## Including the SDK
```typescript theme={null}
import { FraudSDK } from "@payrails/fraud-sdk";
```
## Loading specific fraud providers
The load function accepts an array of providers with merchant specific configurations.
```typescript theme={null}
import { FraudSDK } from "@payrails/fraud-sdk";
// Pass an array of providers
FraudSDK.load([
{
provider: "provider name",
// specific provider options
},
]);
```
## Supported providers
### Cybersource
To initialize the Cybersource provider, `merchantId` and `orgId` needs to be passed.
```typescript theme={null}
import { FraudSDK, FraudProvider } from "@payrails/fraud-sdk";
// Pass an array of providers
FraudSDK.load([
{
provider: FraudProvider.CYBERSOURCE,
merchantId: "merchantId",
orgId: "orgId",
},
]);
```
### Forter
To initialize the Forter provider, `siteId` needs to be passed.
```typescript theme={null}
import { FraudSDK } from "@payrails/fraud-sdk";
// Pass an array of providers
FraudSDK.load([
{
provider: FraudProvider.FORTER,
siteId: "siteId",
},
]);
```
### Ravelin
To initialize the Ravelin provider, `key` needs to be passed.
```typescript theme={null}
import { FraudSDK } from "@payrails/fraud-sdk";
// Pass an array of providers
FraudSDK.load([
{
provider: FraudProvider.RAVELIN,
key: "ravelinPublicKey",
},
]);
```
### Airwallex
Requires `environment` parameter. `orderSessionId` is optional and auto-generated if omitted:
```typescript theme={null}
import { FraudSDK, FraudProvider } from "@payrails/fraud-sdk";
FraudSDK.load([
{
provider: FraudProvider.AIRWALLEX,
environment: "LIVE", // use 'TEST' for sandbox
orderSessionId: "orderSessionId", // optional
},
]);
```
### Stripe Radar
To initialize the Stripe Radar provider, `publishableKey` needs to be passed:
```typescript theme={null}
import { FraudSDK, FraudProvider } from "@payrails/fraud-sdk";
FraudSDK.load([
{
provider: FraudProvider.STRIPE_RADAR,
publishableKey: "pk_test_xxx",
},
]);
```
#### Multiple Stripe accounts
Requires `@payrails/fraud-sdk` 1.9.0 and `@payrails/web-sdk` 6.1.2 or later.
A Radar session is only valid for the Stripe account whose publishable key created it, and Payrails selects the account when the payment is routed, which happens after the session is created. If you process through more than one Stripe account, configure one entry per account, each with that account's own `publishableKey` and the `providerConfigId` of the matching Payrails provider config:
```typescript theme={null}
FraudSDK.load([
{
provider: FraudProvider.STRIPE_RADAR,
publishableKey: "pk_live_accountA_xxx",
providerConfigId: "11111111-1111-4111-8111-111111111111",
},
{
provider: FraudProvider.STRIPE_RADAR,
publishableKey: "pk_live_accountB_xxx",
providerConfigId: "22222222-2222-4222-8222-222222222222",
},
]);
```
`stripe.js` loads once regardless of how many entries you configure, and one Radar session is created per entry. The Web SDK sends them all when the payment is authorized, and Payrails uses the one matching the account it routed to.
Each entry's publishable key must belong to the account its `providerConfigId` points at. A mismatched pair is not rejected up front and instead surfaces as Stripe declining the payment with `No such radar session`.
With a single Stripe account, omit `providerConfigId` and nothing changes.
### Signifyd
To initialize the Signifyd provider, no required fields are needed. Optionally, pass `orderSessionId` — a merchant-generated session id (fewer than 128 characters, \[a-zA-Z0-9\_-]). If omitted, a UUID is auto-generated. This value is sent to the backend as `device.sessionID`.
```typescript theme={null}
FraudSDK.load([
{
provider: FraudProvider.SIGNIFYD,
orderSessionId: "optional-merchant-session-id", // optional, auto-generated if omitted
},
]);
```
### Riskified
To initialize the Riskified provider, `storeDomain` needs to be passed (your Riskified shop domain, matching the `X-RISKIFIED-SHOP-DOMAIN` header on backend API calls). Riskified's Beacon auto-generates the session id, which is sent to the backend as the order's cart\_token.
```typescript theme={null}
FraudSDK.load([
{
provider: FraudProvider.RISKIFIED,
storeDomain: "my-store.myshopify.com",
},
]);
```
### HiPay
To initialize the HiPay provider, username, password, and environment need to be passed. username/password are the same public HiPay SDK credentials configured for the HiPay provider in the Payrails portal. lang is optional and defaults to 'en'. HiPay's JS SDK collects a device fingerprint synchronously once loaded; this value is sent to the backend as `device_fingerprint` on the HiPay Order/Authorize API request.
```typescript theme={null}
FraudSDK.load([
{
provider: FraudProvider.HIPAY,
username: "public-username",
password: "public-password",
environment: "stage", // or 'production'
},
]);
```
Requires `@payrails/fraud-sdk` 1.10.0 or later.
### Checkout.com
Requires `@payrails/fraud-sdk` 1.11.0 or later.
To initialize the Checkout.com provider, `publicKey` and `environment` need to be passed. The SDK loads Checkout.com Risk.js on the page where the shopper pays and creates a device session. With the Payrails Web SDK, the session is sent automatically when the shopper pays, and Payrails passes it to Checkout.com as the risk device session ID.
```typescript theme={null}
import { FraudSDK, FraudProvider } from "@payrails/fraud-sdk";
FraudSDK.load([
{
provider: FraudProvider.CHECKOUT,
publicKey: "pk_sbox_xxx",
environment: "sandbox", // or 'production'
providerConfigId: "11111111-1111-4111-8111-111111111111", // optional
timeoutMs: 1500, // optional
},
]);
```
| Option | Status | Description |
| :- | :- | :- |
| `publicKey` | Required | The public key of your Checkout.com account. |
| `environment` | Required | `sandbox` or `production`. Must match the public key. |
| `providerConfigId` | Optional | The ID of the matching Checkout.com provider config in Payrails. |
| `timeoutMs` | Optional | How long Risk.js waits to collect device data, in milliseconds. Defaults to 1000. |
Load one Checkout.com entry per page, for a single Checkout.com account. If a second entry with a different `publicKey` or `providerConfigId` loads on the same page, the SDK rejects it and keeps the first account's session.
The SDK reuses a Risk.js device session on the same page for 15 minutes. After that, the next load creates a new session.
#### API-only integrations
If you build your own frontend, load Checkout.com Risk.js yourself and send the device session ID in `meta.risk.sessions` on the [authorize request](/reference/authorizeaction):
```json theme={null}
{
"meta": {
"risk": {
"sessions": [{ "provider": "checkout", "sessionId": "dsid_xxx" }]
}
}
}
```
# React Native SDK
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/react-native
The Payrails React Native SDK provides pre-built payment components for accepting payments in mobile apps.
## Get started with the SDK
### Include the SDK
You can install the SDK with your favorite package manager (e.g.,`npm`, `yarn`):
```typescript React Native theme={null}
npm install @payrails/react-native-sdk
or
yarn add @payrails/react-native-sdk
```
### Initialize the SDK
Initialize a Payrails client as shown below, with configuration:
```typescript React Native theme={null}
import { PayrailsProvider } from "@payrails/react-native-sdk";
const App = () => {
const config = useFetch("https://give.me.sdk-config.com");
return (
// Other elements goes here.
);
};
```
Your backend should call the [Initialize a client SDK](/reference/clientinit) endpoint, then forward the response to the SDK.
#### Optional: custom return URLs for redirect payments
If your integration uses redirect-based payment methods such as Lean or Generic Redirect, you can also pass `returnInfo` to `PayrailsProvider`.
```typescript React Native theme={null}
{/* Other elements go here. */}
```
* If `returnInfo` is omitted, the SDK uses its default hosted return URLs instead.
* The base return URLs must also be configured in the Payrails Admin Portal. Contact the Payrails team for that setup.
* `returnInfo` defines where the payment provider redirects the user. Redirection does not emit terminal SDK callbacks such as `onAuthorizeSuccess`, `onAuthorizeFailure`, or `onAuthorizeCancel`.
# API References
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/react-native/api-references
Reference for the PayrailsDropin React Native component: every property, callback, and configuration object, with types and defaults.
## Drop-in API Reference
### `PayrailsDropin`
#### Properties
| Property | Type | Required | Description |
| :- | :- | :- | :- |
| `config` | `InitData` | Yes | Init payload `{ data: string; version: string}` from the `/client/init` response. |
| `returnInfo` | `{ success?: string; cancel?: string; error?: string; pending?: string }` | No | Optional deep link or universal link targets sent with redirect-based authorize requests. Missing fields fall back to SDK defaults. |
| `paymentMethodsConfiguration` | `PaymentMethodsConfiguration` | No | Per-method display and behaviour options. |
| `events` | `DropinEvents` | Yes | Payment lifecycle callbacks. |
| `styles` | `DropinStyles` | No | Visual styling for the Drop-in container and all child elements. |
| `translations` | `Translations` | No | Localised labels for all Drop-in UI strings. |
***
#### Events
| Property | Type | Description |
| :- | :- | :- |
| `onRequestProgressChange` | `(inProgress: boolean) => void` | Fired when payment processing starts or ends. |
| `onAuthorizeStart` | `() => boolean` | Called before authorization begins. Return `true` to proceed. |
| `onAuthorizeSuccess` | `(result: AuthorizeResult) => void` | Called when the payment is successfully authorized. |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | Called when authorization fails. |
| `onAuthorizeRequestStart` | `() => void` | Called when the authorization request begins for Apple Pay and Google Pay. |
| `onRequestStart` | `() => Promise` | Called after the shopper approves in Apple Pay or Google Pay. Resolve `false` to stop the payment. |
| `onPaymentOptionSelected` | `(option: PaymentOption) => void` | Called when the user selects a payment method. |
| `onBillingAddressChanged` | `(address: BillingAddress) => void` | Called when the user changes country code or postal code. Only fires when address selection is enabled. |
***
#### Style
Pass a `DropinStyles` object to the `styles` property.
```tsx theme={null}
type DropinStyles = {
container?: {
base?: ViewStyle;
};
element?: {
neutral?: {
container?: ViewStyle;
radioOuter?: ViewStyle;
label?: TextStyle;
logo?: ImageStyle;
};
selected?: {
container?: ViewStyle;
radioOuter?: ViewStyle;
radioInner?: ViewStyle;
label?: TextStyle;
logo?: ImageStyle;
};
};
leanButton?: {
styles?: {
base?: ViewStyle;
loading?: ViewStyle;
disabled?: ViewStyle;
};
textStyles?: {
base?: TextStyle;
loading?: TextStyle;
disabled?: TextStyle;
};
customization?: {
themeColor?: string;
buttonTextColor?: string;
buttonBorderRadius?: string;
linkColor?: string;
overlayColor?: string;
};
};
googlePayButton?: {
type?:
| "pay"
| "buy"
| "book"
| "checkout"
| "donate"
| "order"
| "subscribe"
| "mark";
style?: ViewStyle;
};
paypalButton?: {
color?: "gold" | "blue" | "silver" | "white" | "black";
height?: number;
label?:
| "paypal"
| "checkout"
| "buynow"
| "pay"
| "installment"
| "subscribe"
| "donate";
shape?: "rect" | "pill";
tagline?: boolean;
};
applePayButton?: {
type?:
| "plain"
| "buy"
| "setUp"
| "inStore"
| "donate"
| "checkout"
| "book"
| "subscribe"
| "reload"
| "addMoney"
| "topUp"
| "order"
| "rent"
| "support"
| "contribute"
| "tip"
| "continue";
buttonStyle?: "white" | "white-outline" | "black";
};
cardForm?: {
inputFields?: InputStyles;
errorTextStyles?: {
base: TextStyle;
};
cardNumberInput?: InputStyles;
expiryDateInput?: InputStyles;
cvvInput?: InputStyles;
cardholderNameInput?: InputStyles;
};
cardPaymentButton?: {
base?: Record;
disabled?: Record;
text?: Record;
};
storeInstrumentSwitch?: {
style?: ViewStyle;
thumbColor?: string;
trackColor?: {
false?: string;
true?: string;
};
};
addressInput?: {
wrapper?: ViewStyle | TextStyle;
countrySelector?: {
wrapper?: ViewStyle | TextStyle;
element?: ViewStyle | TextStyle;
label?: TextStyle;
error?: ViewStyle | TextStyle;
};
postalCodeInput?: {
wrapper?: ViewStyle | TextStyle;
element?: ViewStyle | TextStyle;
label?: TextStyle;
error?: ViewStyle | TextStyle;
};
};
};
```
***
#### Payment Methods Configuration
Controls how individual payment methods appear and behave within the Drop-in.
```tsx theme={null}
paymentMethodsConfiguration: {
preselectFirstPaymentOption?: boolean;
cards?: {
showCardHolderName?: boolean;
showExistingCards?: boolean;
showPaymentMethodLogo?: boolean;
};
payPal?: {
showPaymentMethodLogo?: boolean;
};
googlePay?: {
showPaymentMethodLogo?: boolean;
};
applePay?: {
showPaymentMethodLogo?: boolean;
};
mercadoPago?: {
showPaymentMethodLogo?: boolean;
};
}
```
***
#### Translations
All UI strings are localised through the `translations` prop.
```tsx theme={null}
export enum ElementType {
CARD_NUMBER = "CARD_NUMBER",
CARDHOLDER_NAME = "CARDHOLDER_NAME",
CVV = "CVV",
EXPIRATION_MONTH = "EXPIRATION_MONTH",
EXPIRATION_YEAR = "EXPIRATION_YEAR",
EXPIRATION_DATE = "EXPIRATION_DATE",
}
export interface Translations {
googlePayButton?: {
labels?: {
storeInstrument?: string;
};
};
cardPaymentButton?: {
labels?: {
neutral?: string;
processing?: string;
};
};
cardForm?: {
placeholders?: {
[key in ElementType]?: string;
};
labels?: {
[key in ElementType]?: string;
} & {
label?: string;
storeInstrument?: string;
};
};
leanButton?: {
labels?: {
loading?: string;
// Note: the button label is fixed to "Pay by bank with Al Tareq." and cannot be overridden.
};
};
paymentResult?: {
success?: string;
fail?: string;
pending?: string;
};
paypalButton?: {
labels?: {
storeInstrument?: string;
};
};
applePayButton?: {
labels?: {
storeInstrument?: string;
};
};
}
```
***
## Elements API Reference
All Elements must be rendered inside a `PayrailsProvider`. For usage examples, see [How to Use Elements](/docs/orchestration/checkout-sdks/react-native/how-to-integrate#how-to-use-elements).
***
### `PayWithCardButton`
Renders a payment button for a stored card instrument.
| Prop | Type | Required | Description |
| - | - | - | - |
| `instrument` | `StoredCardInstrument` | Yes | The stored card instrument to charge |
| `title` | `string` | No | Custom button label |
| `onAuthorizeSuccess` | `() => void` | No | Called when payment is authorized |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | No | Called when authorization fails |
| `onRequestProgressChange` | `(inProgress: boolean) => void` | No | Called when request state changes |
The button displays the card network icon and masked card number automatically. It is disabled while a payment request is in progress.
**Hooks**
| Hook | Returns | Description |
| - | - | - |
| `useStoredCardInstruments` | `StoredCardInstrument[]` | Retrieves saved card instruments for the current session |
***
### `PayWithNewCardButton`
Renders the submit button for a new card payment. Must be placed inside a `TokenizationForm`.
| Prop | Type | Required | Default | Description |
| - | - | - | - | - |
| `title` | `string` | No | `"Confirm payment"` | Button label |
| `shouldSaveCard` | `boolean` | No | — | Whether to store the card for future use |
| `dropinStyles` | `CustomStylingProps` | No | — | Custom button styles |
| `translations` | `Translations` | No | — | Localised labels |
| `isAddressEnabled` | `boolean` | No | — | Enables billing address validation |
| `onAuthorizeSuccess` | `() => void` | No | — | Called when payment is authorized |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | No | — | Called when authorization fails |
The button is automatically disabled until all card fields pass validation.
**Related hooks**
| Hook | Returns | Description |
| - | - | - |
| `useBinLookup` | `BinLookupResult` | Identifies card network from BIN as user types |
| `useTokenize` | `TokenizeFunction` | Tokenizes card data without executing a payment |
***
### `ApplePay`
Renders the native Apple Pay button on iOS. Returns `null` on Android and when Apple Pay is unavailable.
| Prop | Type | Required | Description |
| - | - | - | - |
| `buttonType` | `ButtonType` | No | Button label variant: `"plain"`, `"buy"`, `"setUp"`, `"inStore"`, `"donate"`, `"checkout"`, `"book"`, `"subscribe"`, `"reload"`, `"addMoney"`, `"topUp"`, `"order"`, `"rent"`, `"support"`, `"contribute"`, `"tip"`, `"continue"` |
| `buttonStyle` | `ButtonStyle` | No | Button appearance: `"white"`, `"whiteOutline"`, `"black"` |
| `width` | `number` | No | Button width in pixels |
| `height` | `number` | No | Button height in pixels |
| `cornerRadius` | `number` | No | Button corner radius |
| `storeInstrument` | `boolean` | No | Whether to save the instrument for future use |
| `label` | `string` | No | Payment label shown in the Apple Pay sheet |
| `onAuthorizeSuccess` | `() => void` | No | Called when Apple Pay payment is authorized |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | No | Called when authorization fails |
| `onAuthorizeCancel` | `() => void` | No | Called when the user cancels the Apple Pay sheet |
| `onRequestStart` | `() => Promise` | No | Called after the shopper approves, before authorization. Resolve `false` to stop the payment |
**Hooks**
| Hook | Returns | Description |
| - | - | - |
| `useApplePayAvailable` | `boolean` | `true` when Apple Pay is configured and available on the device |
***
### `GooglePay`
Renders the native Google Pay button on Android. Returns `null` on iOS and when Google Pay is unavailable.
| Prop | Type | Required | Description |
| - | - | - | - |
| `merchantInfo` | `MerchantInfo` | Yes | `{ merchantId: string, merchantName?: string, merchantOrigin?: string }` |
| `buttonType` | `ButtonType` | No | Button label variant: `"pay"`, `"buy"`, `"book"`, `"checkout"`, `"donate"`, `"order"`, `"subscribe"`, `"mark"` |
| `storeInstrument` | `boolean` | No | Whether to save the instrument for future use |
| `style` | `Record` | No | Custom button styles |
| `onAuthorizeSuccess` | `() => void` | No | Called when Google Pay payment is authorized |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | No | Called when authorization fails |
| `onAuthorizeCancel` | `() => void` | No | Called when the user cancels Google Pay |
| `onRequestStart` | `() => Promise` | No | Called after the shopper approves, before authorization. Resolve `false` to stop the payment |
**Hooks**
| Hook | Returns | Description |
| - | - | - |
| `useGooglePayAvailable` | `boolean` | `true` when Google Pay is available on the device |
***
### `PayPal`
Renders a PayPal payment button. Works on both iOS and Android.
| Prop | Type | Required | Description |
| - | - | - | - |
| `storeInstrument` | `boolean` | No | Whether to save the PayPal account for future use |
| `paymentInstrumentId` | `string` | No | Stored PayPal instrument ID; charges the saved account directly without opening a redirect |
| `style` | `PayPalButtonStyleConfig` | No | Button style. Supports `height` (number, 30–55px) |
| `onAuthorizeSuccess` | `() => void` | No | Called when PayPal payment is authorized |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | No | Called when authorization fails |
| `onAuthorizeCancel` | `() => void` | No | Called when the user cancels the PayPal flow |
| `onSessionExpired` | `() => Promise<{ data: string; version: string } \| void>` | No | Called when the session expires; return a refreshed config |
**Hooks**
| Hook | Returns | Description |
| - | - | - |
| `useStoredPayPalInstruments` | `StoredPayPalInstrument[]` | Retrieves saved PayPal accounts for the current session |
***
### `GenericRedirect`
Renders a redirect-based payment button for methods such as iDEAL, Pix, Yape, MercadoPago, and others.
| Prop | Type | Required | Description |
| - | - | - | - |
| `paymentMethodCode` | `string` | Yes | Payment method identifier: `"ideal"`, `"pix"`, `"yape"`, `"mercadopago"`, `"bankaccount"`, `"revolutpay"` |
| `redirectMode` | `RedirectModes` | No | How to open the redirect URL. Default: `RedirectModes.customTab`. Options: `customTab`, `defaultBrowser` |
| `translations` | `{ label?: string; loading?: string }` | No | Button text overrides |
| `styles` | `{ base?, loading?, pressed?, disabled? }` | No | `ViewStyle` per button state |
| `textStyles` | `{ base?, loading?, pressed?, disabled? }` | No | `TextStyle` per text state |
| `onAuthorizeSuccess` | `() => void` | No | Called when payment is authorized |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | No | Called when authorization fails |
| `onAuthorizeCancel` | `() => void` | No | Called when the user cancels |
| `onSessionExpired` | `() => Promise<{ data: string; version: string } \| void>` | No | Called when the session expires; return a refreshed config |
> `paymentMethodCode` must match the payment method code from your configuration exactly, including case.
**`RedirectModes`**
| Value | Behavior |
| - | - |
| `RedirectModes.customTab` | Opens in Chrome Custom Tab (Android) / SFSafariViewController (iOS). Recommended. Falls back to default browser if custom tabs are unavailable. |
| `RedirectModes.defaultBrowser` | Opens the system browser. The user must manually return to the app. The SDK detects app foregrounding to resume. |
***
### `Lean`
Renders a bank transfer payment button for the UAE, powered by the Lean SDK checkout dialog.
**Prerequisites**
* Lean requires the peer dependency `lean-react-native` with version `4.2.0`. Earlier versions may not be compatible.
* Also install `react-native-webview` if it is not already installed in your project.
```typescript React Native theme={null}
yarn add lean-react-native@4.2.0 react-native-webview
// OR
npm install lean-react-native@4.2.0 react-native-webview
```
The button label is fixed to **"Pay by bank with Al Tareq."** and cannot be overridden via `translations`.
| Prop | Type | Required | Description |
| - | - | - | - |
| `translations` | `LeanTranslations` | No | `{ loading?: string }` — only the loading text is configurable |
| `styles` | `LeanStyles` | No | `ViewStyle` per button state: `base`, `loading`, `disabled` |
| `textStyles` | `LeanTextStyles` | No | `TextStyle` per text state: `base`, `loading`, `disabled` |
| `customization` | `LeanCustomization` | No | Lean checkout dialog theming (see below) |
| `onAuthorizeSuccess` | `() => void` | No | Called when the bank transfer is authorized |
| `onAuthorizeFailure` | `(failure: AuthorizeFailure) => void` | No | Called when authorization fails |
| `onAuthorizeCancel` | `() => void` | No | Called when the user dismisses the Lean dialog |
| `onSessionExpired` | `() => Promise<{ data: string; version: string } \| void>` | No | Called when the session expires; return a refreshed config |
**`LeanCustomization`**
| Property | Type | Description |
| - | - | - |
| `themeColor` | `string` | Primary color for the Lean checkout dialog UI |
| `buttonTextColor` | `string` | Text color on dialog buttons |
| `buttonBorderRadius` | `string` | Border radius of dialog buttons (e.g. `"8"`) |
| `linkColor` | `string` | Color of links inside the dialog |
| `overlayColor` | `string` | Overlay backdrop color |
For payment completion options, see [Payment Completion Modes](/docs/orchestration/checkout-sdks/react-native/sdk-concepts#payment-completion-modes).
***
### Event Callbacks
All Elements share the same callback contract. See the [React Native event glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary#react-native-event-glossary) for full type definitions.
#### Stop a payment after wallet approval
For Apple Pay and Google Pay, `onRequestStart` runs after the shopper approves in the sheet and before the SDK sends the authorization request. Resolve `false` to stop the payment. A callback that rejects stops it too. The SDK then skips the authorization request and calls `onAuthorizeFailure` with `VALIDATION_FAILED`. Resolve `true` to continue. The SDK waits for the promise with no timeout.
`onRequestStart` also runs for stored Apple Pay and Google Pay instruments, generic redirect and Lean payments, right after the button tap and before the authorization request.
`onAuthorizeRequestStart` is a notification that fires once the authorization request starts, so it doesn't fire when `onRequestStart` stops the payment.
```tsx theme={null}
const payWithApplePay = useExecuteApplePayPayment({
onRequestStart: async () => {
const reserved = await reserveOrder();
return reserved;
},
onAuthorizeFailure: (failure) => showError(failure),
});
```
Pass the callback to the hook itself. `useExecuteApplePayPayment` and `useExecuteGooglePayPayment` don't read callbacks set on `PayrailsProvider`. The `ApplePay` and `GooglePay` components accept it as a prop or from `PayrailsProvider`, and stop at the first handler that returns `false`.
PayPal and card payments don't call `onRequestStart`. Use `onAuthorizeStart` to gate those.
On iOS, the Apple Pay sheet shows a success state once the shopper approves, before the SDK calls `onRequestStart`. If you stop the payment, show your own error.
# How to Integrate
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/react-native/how-to-integrate
How to integrate Drop-in and use different Elements.
## How to Integrate Drop-in
Drop-in is a pre-built UI that bundles all your payment methods into a single, ready-to-mount component. No per method integration required.
### Prerequisites
* `@payrails/react-native-sdk` installed.
* A backend endpoint that calls `/client/init` and returns the session response.
* See the [Getting Started](/docs/orchestration/checkout-sdks/react-native) guide for provider setup.
### Steps
#### 1. Mount the Drop-in
Pass the full `/client/init` response to `config`. The Drop-in will not render without a valid config.
```typescript React Native theme={null}
import { PayrailsDropin } from '@payrails/react-native-sdk';
console.log('Success'),
onAuthorizeFailure: (failure) => console.log('Payment failed', failure),
}}
/>
```
#### 2. Configure return URLs
* If you use `returnInfo`, the base return URLs must also be configured in the Payrails Admin Portal. Contact the Payrails team for that setup.
* `returnInfo` defines where the payment provider redirects the user after a redirect-based payment flow.
#### 3. Handle payment outcomes
Use `events.onAuthorizeSuccess` and `events.onAuthorizeFailure` to navigate or show feedback. Always confirm the final payment status server-side via webhook — do not rely solely on client callbacks.
### Customise the Drop-in
* **Styles**: pass a `DropinStyles` object to the `styles` property. See the [Drop-in API Reference](/docs/orchestration/checkout-sdks/react-native/api-references#style) for the full type.
* **Payment method visibility**: use `paymentMethodsConfiguration` to show or hide logos, stored instruments, and cardholder name. See the [Drop-in API Reference](/docs/orchestration/checkout-sdks/react-native/api-references#payment-methods-configuration).
* **Translations**: pass a `Translations` object to localise all labels. See the [Drop-in API Reference](/docs/orchestration/checkout-sdks/react-native/api-references#translations).
***
## How to Use Elements
Each Element handles a single payment method — drop it anywhere in your UI for full control over layout and placement.
* Elements must be rendered inside a `PayrailsProvider`. See the [Getting Started](/docs/orchestration/checkout-sdks/react-native#get-started-with-the-sdk) guide for provider setup.
* For configuration options for each element, see the [Elements API Reference](/docs/orchestration/checkout-sdks/react-native/api-references#elements-api-reference).
### How to Pay with a Stored Card
```typescript React Native theme={null}
import React, { useState } from 'react';
import {
PayWithCardButton,
useStoredCardInstruments,
PayrailsProvider,
} from '@payrails/react-native-sdk';
export function StoredCardsExample() {
const [loading, setLoading] = useState(false);
const storedCards = useStoredCardInstruments();
return (
{storedCards.map((card) => (
console.log('Payment succeeded')}
onAuthorizeFailure={(failure) =>
console.log('Payment failed', failure)
}
onRequestProgressChange={(inProgress) => setLoading(inProgress)}
/>
))}
);
}
```
***
### How to Pay with a New Card
```typescript React Native theme={null}
import React from 'react';
import {
TokenizationForm,
CardNumberElement,
CardHolderNameElement,
ExpirationDateElement,
CvvElement,
PayWithNewCardButton,
PayrailsProvider,
} from '@payrails/react-native-sdk';
export function NewCardPaymentExample() {
return (
console.log('Payment succeeded')}
onAuthorizeFailure={(failure) =>
console.log('Payment failed', failure)
}
/>
);
}
```
***
### How to Add Apple Pay
Apple Pay renders a native button on iOS and returns `null` on Android.
**Prerequisites**: Apple Pay must be configured in your Apple Developer account, and the device must have at least one card added.
```typescript React Native theme={null}
import { ApplePay, useApplePayAvailable } from '@payrails/react-native-sdk';
const isAvailable = useApplePayAvailable();
{isAvailable && (
console.log('Apple Pay succeeded')}
onAuthorizeFailure={(failure) => console.log('Apple Pay failed', failure)}
onAuthorizeCancel={() => console.log('User cancelled Apple Pay')}
/>
)}
```
***
### How to Add Google Pay
Google Pay renders a native button on Android and returns `null` on iOS.
```typescript React Native theme={null}
import { GooglePay, useGooglePayAvailable } from '@payrails/react-native-sdk';
const isAvailable = useGooglePayAvailable();
{isAvailable && (
console.log('Google Pay succeeded')}
onAuthorizeFailure={(failure) => console.log('Google Pay failed', failure)}
onAuthorizeCancel={() => console.log('User cancelled Google Pay')}
/>
)}
```
***
### How to Add PayPal
```typescript React Native theme={null}
import { PayPal } from '@payrails/react-native-sdk';
console.log('PayPal payment succeeded')}
onAuthorizeFailure={(failure) => console.log('PayPal payment failed', failure)}
onAuthorizeCancel={() => console.log('User cancelled PayPal')}
onSessionExpired={async () => {
const newConfig = await refreshSession();
return { data: newConfig.data, version: newConfig.version };
}}
/>
```
#### How to Pay with a Stored PayPal Account
```typescript React Native theme={null}
import { useStoredPayPalInstruments, PayPal } from '@payrails/react-native-sdk';
const storedPayPalAccounts = useStoredPayPalInstruments();
console.log('Stored PayPal payment succeeded')}
onAuthorizeFailure={(failure) => console.log('Failed', failure)}
/>
```
***
### How to Add Generic Redirect (iDeal, pix, yape, etc.)
Pass `returnInfo` on `PayrailsProvider` so the payment provider redirects the user back to your app via deep link or universal link after payment.
```typescript React Native theme={null}
import {
PayrailsProvider,
GenericRedirect,
RedirectModes,
} from '@payrails/react-native-sdk';
console.log('iDEAL payment succeeded')}
onAuthorizeFailure={(failure) => console.log('Failed', failure)}
onAuthorizeCancel={() => console.log('User cancelled')}
onSessionExpired={async () => {
const newConfig = await refreshSession();
return { data: newConfig.data, version: newConfig.version };
}}
/>
```
***
### How to Add Lean (Bank Transfer, UAE)
**Prerequisites**
* Lean requires the peer dependency `lean-react-native` with version `4.2.0`. Earlier versions may not be compatible.
* Also install `react-native-webview` if it is not already installed in your project.
```typescript React Native theme={null}
yarn add lean-react-native@4.2.0 react-native-webview
// OR
npm install lean-react-native@4.2.0 react-native-webview
```
The button label is fixed to **"Pay by bank with Al Tareq."** and cannot be overridden.
Pass `returnInfo` on `PayrailsProvider` to redirect the user back to your app after the bank transfer completes.
```typescript React Native theme={null}
import { PayrailsProvider, Lean } from '@payrails/react-native-sdk';
console.log('Success')}
onAuthorizeFailure={(failure) => console.log('Failed', failure)}
onAuthorizeCancel={() => console.log('Cancelled')}
onSessionExpired={async () => {
const newConfig = await refreshSession();
return { data: newConfig.data, version: newConfig.version };
}}
/>
```
For payment completion mode options (polling, background polling, return URLs), see [Payment Completion Modes](/docs/orchestration/checkout-sdks/react-native/sdk-concepts#payment-completion-modes).
# SDK Concepts
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/react-native/sdk-concepts
How the Payrails React Native Drop-in works, and which parts of an integration run on your server, in your app, and over webhooks.
## How Drop-in Works
Payrails Drop-in for React Native is a pre-built UI solution that renders all configured payment methods as a single list. It handles tokenization, 3D Secure authentication, and redirect flows without requiring additional integration work beyond mounting the component.
### Integration Architecture
A Drop-in integration has three parts:
* **Server-side**: a single API call to `/client/init` that creates the payment session and returns the configuration object the Drop-in needs.
* **Client-side**: the `PayrailsDropin` React Native component, which consumes the session data to render the payment method list, execute payment requests, and handle follow-up actions such as 3D Secure challenges and external redirects.
* **Webhook server**: receives asynchronous payment outcome notifications from Payrails, independent of the client session.
### What the Drop-in Manages Automatically
* Rendering all payment methods enabled for the merchant in a single UI block.
* Disabling the payment button during in-flight requests.
* Launching 3D Secure authentication flows when required by the card issuer.
* Opening redirect-based payment flows (e.g. PayPal, bank redirects) and detecting the result when the user returns.
* Showing and hiding stored instruments for returning customers.
### What the Drop-in Does Not Manage
* Session refresh — if the session expires, the host app is responsible for obtaining a new one and re-mounting the provider. See `onSessionExpired` in the event callback reference.
* Layout positioning — the Drop-in renders its own container; the host app controls where it appears in the screen hierarchy.
* Backend webhook handling — payment outcomes must be confirmed server-side via webhook, not solely from client callbacks.
***
## Payment Completion Modes
After a user initiates a payment through the Lean checkout dialog or a Generic Redirect flow, the SDK must determine the final payment result. Because redirect-based payments take the user outside the app, the SDK cannot rely on a simple synchronous response. Three mechanisms exist to handle this.
### Foreground Polling (Default)
The SDK polls the Payrails execution endpoint on a timer while the application is in the foreground. Polling stops automatically when the app moves to the background and resumes when the app returns to the foreground. On resumption, the SDK fetches the latest payment status.
This is the default behaviour and requires no additional configuration. It covers most integrations where the user completes payment quickly and returns to the app.
**Limitation**: if the payment provider redirects the user into an external banking app for an extended period, polling is suspended for that entire duration and the SDK only learns the result when the user manually returns.
### Continuous Background Polling (Optional)
Background polling keeps the execution endpoint poll running even while the app is in the background. This reduces the delay between payment completion and result delivery for flows that redirect into external banking apps.
Requires the optional dependency:
```typescript theme={null}
npm install react-native-background-actions
```
The SDK detects the presence of this library at runtime and activates background polling automatically when it is installed.
**Trade-offs**: background tasks consume battery and are subject to OS-level restrictions on both iOS and Android. Use this option only when faster payment result detection is a product requirement.
### Custom Return URLs
Return URLs are sent to the payment provider as part of the authorization request. After the user completes or cancels the payment, the provider redirects them to the corresponding URL, which the OS maps back to the app via a deep link or universal link.
The SDK detects this incoming link and resolves the payment result without waiting for a polling cycle.
```typescript React Native theme={null}
{}} />
{}} />
```
Supported link types:
* **Deep links** — custom URI schemes (e.g. `myapp://payment/success`)
* **Universal links / App Links** — HTTPS URLs mapped to the app (e.g. `https://yourapp.com/payment/success`)
Important notes:
* The base return URLs must also be configured in the Payrails Admin Portal. Contact the Payrails team to set this up.
* `returnInfo` only tells the payment provider where to redirect the user. Redirection to these URLs does not emit `onAuthorizeSuccess`, `onAuthorizeFailure`, or `onAuthorizeCancel`.
* Your app must already be configured for the corresponding deep link or universal link handling.
### Which Mode to Use
| Situation | Recommended mode |
| - | - |
| Standard redirect flow, user returns quickly | Foreground polling (default) |
| External banking app redirect, fast result needed | Background polling |
| Provider supports return URLs, deep link setup is feasible | Custom return URLs |
# Web SDK - Quick Start
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web
Install the Payrails web SDK, initialize a client from your server's init payload, and mount a working checkout on your page.
Payrails Web provides the building blocks to create a checkout experience for your customers. This page gets you to a first mounted checkout and is the hub for the rest of the documentation.
You can integrate at three levels, from quickest to most control:
* **Drop-in** — all-in-one checkout UI; the quickest way to accept payments.
* **Elements** — one component per payment method (card form, wallet buttons,
and more) for a fully customizable checkout.
* **Secure fields** — the lowest-level option: mount individual PCI-compliant
card input iframes and build your own card-form markup around them. Most
control, most work.
## Quick start
Install the SDK, initialize it once, then pick the integration that fits your checkout.
### 1. Install the SDK
```bash theme={null}
npm install @payrails/web-sdk
```
### 2. Initialize with your client init response
Call your backend, which calls the Payrails `/merchant/client/init` API, and pass the response to `Payrails.init`.
`init` is asynchronous - it loads the SDK bundle (and its styles) for the version your session was created with, so `await` it:
```typescript theme={null}
import { Payrails } from "@payrails/web-sdk";
const payrails = await Payrails.init(clientInitResponse, {
events: {
onClientInitialized: () => console.log("SDK ready"),
},
});
```
You now have a `payrails` client. Handle payment results with the instance-level `.on(...)` API - the same events fire for **any** integration below, so you wire them once:
```typescript theme={null}
payrails.on("success", (event) =>
console.log("Payment success", event.paymentMethodCode),
);
payrails.on("failed", (event) =>
console.log("Payment failed", event.data?.code),
);
```
### 3. Choose your integration
Pick one of the following and mount it into a container on your page. Each example is self-contained and assumes the `payrails` client and the `.on(...)`result handlers from step 2.
#### Drop-in
The drop-in renders every payment method enabled for the current session - you do not list methods yourself.
```html theme={null}
```
```typescript theme={null}
const dropin = payrails.dropin({});
dropin.mount("#dropin");
```
#### Elements
Compose your own layout from individual components. Here a card form and a payment button, created from the same client — the client keeps the button disabled until the form is valid and starts the payment on click.
```html theme={null}
```
```typescript theme={null}
const cardForm = payrails.cardForm({ showCardHolderName: true });
cardForm.mount("#card-form");
const payButton = payrails.paymentButton({
translations: { label: "Pay now" },
});
payButton.mount("#pay-button");
```
#### Secure fields
Build a card form field by field with a collect container. Sensitive data stays inside Payrails-hosted iframes and never touches your page.
```html theme={null}
```
```typescript theme={null}
import { ElementType } from "@payrails/web-sdk";
const container = payrails.collectContainer({});
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
});
cardNumber.mount("#card-number");
// create and mount CVV and expiry the same way, then container.collect()
```
You should now see your chosen integration render the payment methods configured for your merchant account. From here, pick a guide that matches your task.
## Reporting a Vulnerability
If you discover a potential security issue in this project, please reach out to us at [security@payrails.com](mailto:security@payrails.com).
# Drop-in
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web-v5-legacy/drop-in
Mount the prebuilt Drop-in component in the legacy web SDK 5.x, which lists every payment method in a single block.
Payrails Drop-in is our pre-built UI solution for accepting payments on your application. Drop-in shows all payment methods as a list, in the same block.
Tokenizing new cards, adding new payment methods, and managing 3D-Secure or APM redirect flows usually don't require more development work than simply integrating our drop-in into your user journey.
## How it works
From an implementation perspective, a Drop-in integration contains:
* **Server-side**: a single API call that creates the payment session.
* **Client-side**: frontend component, which uses the payment session data to make the payment request and to handle any other actions like redirects or 3D Secure authentication.
* **Webhook server**: receives notifications that inform you about the outcome of each payment.
## Get started
Below is a two-step process for starting drop-in for your client.
### Create the Drop-in component
First, create a Drop-in using the `dropin(configuration)` method of the Payrails client as shown below:
```typescript JavaScript theme={null}
const configuration = {
paymentMethodsConfiguration?: Object,
events?: Object,
styles?: Object,
translations?: Object
};
const dropin = payrailsClient.dropin(configuration);
```
The Drop-in **configuration** is defined as shown below:
| property | type | description |
| :- | :- | :- |
| `paymentMethodsConfiguration` | Object | Payment method configurations that are needed to customize the drop-in behavior. See [Payment methods behavior and configurations](/docs/orchestration/checkout-sdks/web-v5-legacy/drop-in#payment-methods-behavior-and-configurations). |
| `events` | Object | Attach your callables to the events emitted by the Drop-in. See [Event listener](/docs/orchestration/checkout-sdks/web-v5-legacy/drop-in#event-listener). |
| `styles` | Object | Styles that should be applied to the Drop-in. See [Style](/docs/orchestration/checkout-sdks/web-v5-legacy/drop-in#style). |
| `translations` | Object | Translations to localize the Drop-in. See [Translations](/docs/orchestration/checkout-sdks/web-v5-legacy/drop-in#translations). |
### Mount the Drop-in to the DOM
Now, when the `mount(containerId)` method of the Drop-in is called, the Drop-in will be inserted in the specified `div`. For instance, the call below will insert the Drop-in into the div with the id `#dropin-container`.
#### HTML
```html theme={null}
```
#### JavaScript
```javascript theme={null}
dropin.mount("#dropin-container");
```
You can use the `unmount` method to reset the Drop-in to its initial state.
```javascript theme={null}
dropin.unmount();
```
## Payment methods behavior and configurations
```javascript theme={null}
const configuration = {
paymentMethodsConfiguration?: {
cards?: {
showCardHolderName?: boolean;
showStoreInstrumentCheckbox?: boolean;
showStoredInstruments?: boolean;
showExistingCards?: boolean;
alwaysStoreInstrument?: boolean;
layout?: cardFormField[][] // [['CARDHOLDER_NAME'],['CARD_NUMBER'],['EXPIRATION_DATE', 'CVV']],
disablePaymentButton?: boolean;
};
};
};
const dropin = container.dropin(configuration);
```
| property | type | description |
| - | - | - |
| `preselectFirstPaymentOption` | boolean | Automatically preselects the first payment option. Defaults to `false` |
| `cards` | Object | \* *card*\* element configurations: \* `showCardHolderName` to display the cardholder name input \* `showStoreInstrumentCheckbox` to display a checkbox for customers to save their card for later \* `showStoredInstruments` to show existing holder payment instruments \* `alwaysStoreInstrument`to always store instruments \* `layout` to customize cad form layout \* `disablePaymentButton` to set the default state of the payment button |
| `googlePay` | Object | \* *googlePay* element configurations: \* `showStoredInstruments` to show existing holder payment instruments \* `showStoreInstrumentCheckbox` to display a checkbox for customers to save their instrument for later \* `merchantInfo?`: \{ `merchantId: string;` to set merchant id `merchantName?: string;` to set merchant name on payment sheet } |
| `applePay` | Object | \* *applePay*\* element configurations: \* `showStoredInstruments` to show existing holder payment instruments \* `showStoreInstrumentCheckbox` to display a checkbox for customers to save their Apple Pay method for later |
| `payPal` | Object | \* *payPal*\* element configurations: \* `showStoredInstruments` to show existing holder payment instruments \* `showStoreInstrumentCheckbox` to display a checkbox for customers to save their PayPal method for later |
| `lean` | Object | \* lean\_\* element configurations: \* `showStoredInstruments` Instruments are not supported for Lean yet. |
## Event Listener
Attach your callables to the events emitted by the Drop-in:
```javascript theme={null}
const configuration = {
events: {
onAuthorizeSuccess: () => {
console.log("yay!");
},
onAuthorizeFailed: () => {
console.log("nah :(");
},
},
};
const dropin = container.dropin(configuration);
```
| property | type | description |
| :- | :- | :- |
| `onAuthorizeSuccess` | Callable | This event is triggered when the Authorization is successful. |
| `onAuthorizeFailed` | Callable | This event is triggered when the Authorization failed. |
The handler `function() => void` is a callback function you provide, that will be called when the event is fired.
## Style
You can configure the style of the Drop-in and individual elements inside with this configuration object:
```typescript Typescript theme={null}
interface DropInStyles {
// DropIn wrapper specific styles
container?: {
style?: Partial;
className?: string;
};
// Basic styles for each drop-in component wrapper
element?: {
base?: Partial;
active?: Partial;
className?: string;
};
// Lean specific styles
leanButton?: {
button?: {
base?: Partial;
loading?: Partial;,
hover?: Partial;,
},
dialog?: {
themeColor?: string; //'#00B67A',
overlayColor?: string; // 'rgba(0, 0, 0, 0.5)',
},
};
// Google pay specific styles
googlePayButton?: {
buttonColor?: google.payments.api.ButtonColor;
buttonType?: google.payments.api.ButtonType;
buttonSizeMode?: google.payments.api.ButtonSizeMode;
buttonLocale?: string;
storeInstrumentCheckbox?: Partial;
storeInsrtumentCheckboxClassName?: string;
};
// PayPal specific styles
paypalButton?: {
color?: 'gold' | 'blue' | 'silver' | 'white' | 'black';
height?: number;
label?:
| 'paypal'
| 'checkout'
| 'buynow'
| 'pay'
| 'installment'
| 'subscribe'
| 'donate';
shape?: 'rect' | 'pill';
tagline?: boolean;
storeInstrumentCheckbox?: Partial;
storeInsrtumentCheckboxClassName?: string;
};
// Apple pay specific styles
applePayButton?: {
type?:
| 'plain'
| 'buy'
| 'addMoney'
| 'book'
| 'checkout'
| 'continue'
| 'contribute'
| 'donate'
| 'inStore'
| 'order'
| 'reload'
| 'rent'
| 'setUp'
| 'subscribe'
| 'support'
| 'tip'
| 'topUp';
style?: 'white' | 'white-outline' | 'black' | 'automatic';
storeInstrumentCheckbox?: Partial;
storeInsrtumentCheckboxClassName?: string;
};
// Card form specific styles
cardForm?: {
wrapper?: Partial;
base?: Partial;
storeCardCheckbox?: Partial;
storeInstrumentCheckbox?: Partial;
storeInsrtumentCheckboxClassName?: string;
errorTextStyles?: {
base: Partial;
};
inputFields?: {
// Please see the ElementType enum for the available options
[key in ElementType | 'all']?: {
complete?: Partial;
empty?: Partial;
focus?: Partial;
invalid?: Partial;
cardIcon?: Partial;
};
};
labels?: {
// Please see the ElementType enum for the available options
[key in ElementType | 'all']?: Partial;
};
};
cardPaymentButton?: {
base?: Partial;
disabled?: Partial;
loading?: Partial;
hover?: Partial;
};
mercadoPago: {
base?: Partial;
active?: Partial;
className?: string;
};
authSuccess?: Partial;
authFailed?: Partial;
loadingScreen:{
loader: Partial;
overlay:Partial;
}
}
enum ElementType {
CARD_NUMBER = 'CARD_NUMBER',
CARDHOLDER_NAME = 'CARDHOLDER_NAME',
CVV = 'CVV',
EXPIRATION_MONTH = 'EXPIRATION_MONTH',
EXPIRATION_YEAR = 'EXPIRATION_YEAR',
}
```
### Style non sensitive elements
Drop-in config allows you to pass custom classnames for non-sensitive elements. Allowing to easily change the styling of the entire drop-in form to match your branding. You can customize:
* Radio buttons
* Store instrument checkboxes
* Button styles
* Custom font families
* Custom spacing and animations
**Additional information:**
* [CSSStyleDeclaration interface](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration)
* [GooglePay API interface](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/7f801fd9eeed78c5ccddff7694702b04d54842e7/types/googlepay/index.d.ts#L4)
What is not customizable:
* Animations after successful/failed payments
* Animation on Payment button
* If you would like to add some possibilities, please contact the Payrails team.
### Show payment options logo
Payrails dropin also allows you to optionally show payment method logo alongside each payment method name.
You can pass the `showPaymentMethodLogo` as true for the payment methods you would want to show this.
By default this is false.
```
paymentMethodsConfiguration: {
preselectFirstPaymentOption: true,
cards: {
showCardHolderName: true
showExistingCards: true
showPaymentMethodLogo: true,
},
payPal: {
showPaymentMethodLogo: true,
},
googlePay: {
showPaymentMethodLogo: false,
},
applePay: {
showPaymentMethodLogo: false,
},
mercadoPago: { // hpp integration
showPaymentMethodLogo: true,
},
[paymentmethod]: {
showPaymentMethodLogo: true
}
},
}
```
## Translations
For the Drop-in, translations are part of the `dropin(options)` signature, inside the `translations`. here's the complete interface:
```javascript theme={null}
enum ElementType {
CARD_NUMBER = 'CARD_NUMBER',
CARDHOLDER_NAME = 'CARDHOLDER_NAME',
CVV = 'CVV',
EXPIRATION_MONTH = 'EXPIRATION_MONTH',
EXPIRATION_YEAR = 'EXPIRATION_YEAR',
}
interface Translations {
leanButton?: {
label?: string; //The Lean payment button must use the label "Pay by bank with Al Tareq."
},
googlePayButton?: {
labels?: {
label?: string;
storeInstrument?: string;
};
};
cardPaymentButton?: {
label: string;
};
cardForm?: {
placeholders?: {
// please see the ElementType enum for the possible values of key
[key in ElementType]?: string;
};
labels?: {
// please see the ElementType enum for the possible values of key
[key in ElementType | 'saveCreditCard']?: string;
} & {
storeInstrument?: string;
};
};
paymentResult?: {
success?: string;
fail?: string;
pending?: string;
};
paypalButton?: {
labels?: {
saveInstrument?: string;
label?: string;
};
};
mercadoPago: {
label: 'dropin item label',
button: {
label: 'button label'
}
};
errorMessages?:{
'Unknown'?: string;
'UnexpectedProviderResponse'?: string;
'ProviderUnknownError'?: string;
'ProviderConnectionError'?: string;
'Timeout'?: string;
'ProviderTimeout'?: string;
'GenericRejection'?: string;
'DuplicateOperation'?: string;
'OperationNotAllowed'?: string;
'NetworkTransactionReferenceRejected'?: string;
'PayerCanceled'?: string;
'LimitExceeded'?: string;
'InstrumentNotAllowed'?: string;
'InvalidInstrument'?: string;
'InsufficientBalance'?: string;
'BlockedInstrument'?: string;
'ExpiredInstrument'?: string;
'ValidationError'?: string;
'ParamsError'?: string;
'ProviderConfigError'?: string;
'InvalidSignature'?: string;
'InternalServerError'?: string;
'AuthenticationError'?: string;
'PaymentMethodNotSupported'?: string;
'PayerAuthenticationRequired'?: string;
'PayerAuthenticationFailed'?: string;
'FraudRisk';?: string;
}
}
```
## Handle 3DS
We have 2 modes for handling 3ds -
1. **Iframe based flow** - In this flow the user sees the iframe as a popup to validate 3DS. We would recommend this flow to maximise conversion.
2. **Redirection based flow** - In this flow the user is redirected to the new page to validate 3DS. If you have very strong CSP settings which cannot be relaxed to accommodate for 3DS, then we would recommend this flow.
The DropIn integration be default comes with the iframe based flow. In order to switch to redirection based flow, the payrails SDK needs to be initialized with `redirectFor3DS` to true.
```typescript TypeScript theme={null}
Payrails.init(initResult.res, {
redirectFor3DS: true,
});
```
## Authorization failure dialog configuration
Failure message can be hidden automatically after given amount of time.
```typescript theme={null}
Payrails.init(initResult.res, {
configuration: {
authFailMsg?: {
autoHideTTL: number. //time in miliseconds
}
}
});
```
# Elements
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web-v5-legacy/elements
Payrails Elements are payment UI components you can assemble together to build a payment form.
## Get started
Although each Element (cards, Apple Pay, Google Pay, PayPal, etc) has its own signature, they follow a similar implementation.
### Create the Element
```javascript theme={null}
const options = {
/* object with Element configurations */
};
const myElement = payrailsClient.someElement(options);
```
### Mount the Element to your DOM
When the `mount(containerId)` method of the Element is called, the Element will be inserted in the specified `div`. For instance, the call below will insert the Element into the `div` with the id `#element-container`.
#### HTML
```html theme={null}
```
#### JavaScript
```javascript theme={null}
element.mount("#element-container");
```
You can use the `unmount` method to reset the Element to its initial state.
```javascript theme={null}
element.unmount();
```
## Card Form Element
The `cardForm` element is a secure card tokenization form.
```javascript theme={null}
interface OnChange {
isValid?: boolean,
cardNetwork?: string,
bin?: string
}
const element = payrails.cardForm({
showCardHolderName: true,
showStoreInstrumentCheckbox: true,
showSingleExpiryDateField: true,
styles: {
inputFields: {
all: {
base: {
'&:hover': {
border: '1px solid black',
},
},
focus: {
border: '1px solid black',
},
},
},
},
translations: {
placeholders: {
CARDHOLDER_NAME: 'Customized card holder',
CARD_NUMBER: 'Custom card number',
CVV: 'CODE',
EXPIRATION_MONTH: 'MM',
EXPIRATION_YEAR: 'YY',
},
labels: {
saveCreditCard: 'Save card for future use',
},
},
events: {
onChange(e: OnChange) {
console.log('card form change', e);
},
onFocus() {
console.log('user set focus to input field');
}
onReady() {
console.log('triggered when form is rendered and ready')
}
}
});
element.mount('#card-form-container');
```
The Card Form **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| `showCardHolderName` | Boolean | To display the cardholder name input |
| `layout` | cardField\[]\[] | to customize card form layout e.g. `[['CARDHOLDER_NAME'],['CARD_NUMBER'],['EXPIRATION_DATE', 'CVV']]` |
| `showSingleExpiryDateField` | Boolean | to only single expiry dat as `MM/YY` |
| `showStoreInstrumentCheckbox` | Boolean | To display a checkbox for customers to save their cards for later. |
| `styles` | Object | Styles that should be applied to the Element. Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1) . |
| `translations` | Object | Translations to localize the Element. |
| `events` | Object | The below events can be emitted by the Card Form element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary) . \* `onChange` \* `onFocus` \* `onReady` \* `onSaveInstrumentCheckboxChanged` |
### Card form functions
Card form exposes the following functions
| functions | description | params |
| - | - | - |
| `mount` | used to add the card form into the DOM | location: string |
| `unmount` | used to remove the card form from the DOM | |
| `show` | Used to show the card form | |
| `hide` | used to hide the card form. Pass `{reset : true}` if you want to reset the card form values on hide. | `json { reset: true \| false // defaults to false } ` |
| `update` | used to update the styles and translations for the element | `javascript { styles: Object, translations: Object } ` |
## Card List Element
The `cardList` element is a radio button list of the customer's saved instruments.
```javascript theme={null}
const element = payrails.cardList({
styles: {
border: "1px solid #eae8ee",
padding: "10px 16px",
borderRadius: "4px",
color: "#1d1d1d",
},
});
element.mount("#card-list-container");
```
The Card Form **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| `styles` | Object | Styles that should be applied to the Element. Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1) . |
| `events` | Object | The below events can be emitted by the Card List element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary) . \* `onCardChange` |
## Payment Button Element
The `cardButton` element is the card payment authorization button.
The `cardButton`can collect the content of the `cardForm` and `cardList` Elements. So, it would typically be used:
* alone if there are no payment instruments to decide.
* with a `cardForm` if the user will provide a new card.
* with a `cardList` if the user will pick an existing instrument.
* with both `cardForm` and `cardList` if the customer can provide a new card or use an existing instrument.
```javascript theme={null}
const element = payrails.paymentButton({
label: 'Pay',
disabledByDefault?: boolean;
events: {
onAuthorizeSuccess: () => {
console.log('yay!');
},
onAuthorizeFailed: (e) => {
console.log('nah :(', e);
},
onPaymentButtonClicked: (e) => {
console.log('clicked', e);
// If resolved to false authorization will not be triggered
return Promise.resolve(true);
},
onPaymentSessionExpired() {
console.log('session expired');
// dropin has to be initialized with new workflow execution, contact your backend
},
onThreeDSecureChallenge() {
console.log('3ds initiated');
// triggered when 3DS challenge starts
},
onStateChanged(state: 'enabled' | 'disabled') {
console.log(state);
//triggered when button change the state from enabled to disabled
}
},
styles: {
base: {
width: 'auto',
border: '1px solid transparent',
borderRadius: '12px',
backgroundColor: '#0096b2',
borderColor: 'transparent',
color: '#fff',
padding: '8px 24px',
fontSize: '16px',
minHeight: '48px',
},
disabled: {
backgroundColor: '#d8dfe6',
color: '#52667d',
},
hover: {
backgroundColor: '#22a2be',
},
},
});
element.mount('#payment-button-container');
```
The Payment Button **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| `label` | Object | Label for the form field. |
| `disabledByDefault` | boolean | set the default state for the payment button |
| `events` | Object | The below events can be emitted by the payment button element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary) \* `onPaymentButtonClicked` \* `onThreeDSecureChallenge` \* `onPaymentSessionExpired` \* `onAuthorizeSuccess` \* `onAuthorizeFailed` |
| `styles` | Object | Styles that should be applied to the Element. Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1) . |
### `onAuthorizeFailed()` errors
callback receives the object describing the error that happened
```Text TypeScript theme={null}
enum AuthorizationFailureReasons {
VALIDATION_FAILED = 'VALIDATION_FAILED',
AUTHORIZATION_ERROR = 'AUTHORIZATION_ERROR',
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
}
interface AuthorizationError {
code: AuthorizationFauilureReasons
message: string
rawError?: any //if UNKNOWN_ERROR happens, raw error is available on the object
}
```
#### Failure reasons explanation
VALIDATION\_FAILED: `onPaymentButtonClicked` wasn't resolved to true
AUTHORIZATION\_ERROR: authorization failed, no technical reason. Details are in the message (different for 3DS)
AUTHENTICATION\_ERROR: token used in SDK is invalid
UNKNOWN\_ERROR: all other errors
### `onPaymentButtonClicked()` event
Callback receives the object, containing:
```Text TypeScript theme={null}
{
bin: '41111111' // optional, in case of card payment, returns the bin of the card
}
```
### Payment Button functions
Payment Button exposes the following functions
| functions | description | params |
| - | - | - |
| `update` | used to update the styles and translations for the element | `javascript { styles: Object, translations: Object } ` |
## Google Pay Element
The `googlePayButton` element is an authorization button for Google Pay.
```javascript theme={null}
const element = payrails.googlePayButton({
environment?: 'TEST' | 'PRODUCTION';
showStoreInstrumentCheckbox?: boolean,
merchantInfo?: {
merchantId: string;
merchantName?: string;
};
styles?: {
buttonColor: google.payments.api.ButtonColor,
buttonType: google.payments.api.ButtonType,
buttonSizeMode: google.payments.api.ButtonSizeMode,
buttonLocale: string, //@deprecated
locale: string,
storeInstrumentCheckbox: {
color: string,
fontWeight: string,
},
},
translations?: {
labels: {
storeInstrument: string,
},
},
events: {
onAuthorizeSuccess: () => {
console.log('yay!');
},
onAuthorizeFailed: () => {
console.log('nah :(');
},
onPaymentButtonClicked: async() => {
console.log('clicked');
// If resolved to false authorization will not be triggered
return Promise.resolve(true);
},
onPaymentSessionExpired() {
console.log('session expired');
// dropin has to be initialized with new workflow execution, contact your backend
},
}
});
element.mount('#google-pay-button-container');
```
The Google Pay button **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| `events` | Object | The below events can be emitted by the Google Pay element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary) . \* `onPaymentButtonClicked` \* `onThreeDSecureChallenge` \* `onPaymentSessionExpired` \* `onAuthorizeSuccess` \* `onAuthorizeFailed` \* `onGooglePayAvailable` |
| `merchantInfo` | Object | \* `merchantId: string;` merchantId, not needed when in TEST \* `merchantName?: string;` merchant name displayed on payment sheet |
## Google Pay availability
Payrails client provides the method to check if Google Pay is available in the current browser/client session. This is only necessary if one needs to know in advance if the payment method is supported. The `GooglePayButton` element otherwise has this logic encapsulated and will not render if the browser is not supported.c
```typescript theme={null}
const googlePay = isGooglePayAvailable(merchantInfo: google.payments.api.MerchantInfo)
```
| property | type | description |
| - | - | - |
| `merchantInfo` | Object | \* `merchantId: string;` merchantId, not needed when in TEST - `merchantName?: string;` merchant name displayed on payment sheet |
## Apple Pay Element
The `applePayButton` element is an authorization button for Apple Pay.
```javascript theme={null}
const element = payrails.applePayButton({
showStoreInstrumentCheckbox?: boolean,
abortAfterAuthorizeFailed?: boolean;
events: {
onAuthorizeSuccess: () => {
console.log('yay!');
},
onAuthorizeFailed: () => {
console.log('nah :(');
},
onPaymentButtonClicked: async() => {
console.log('clicked');
// If resolved to false authorization will not be triggered
return Promise.resolve(true);
},
onPaymentSessionExpired() {
console.log('session expired');
// dropin has to be initialized with new workflow execution, contact your backend
},
onApplePayAvailable() {
console.log('Apple Pay ready');
// event will fire when Apple Pay Button is initialized
},
styles?: {
type?:
| 'plain'
| 'buy'
| 'addMoney'
| 'book'
| 'checkout'
| 'continue'
| 'contribute'
| 'donate'
| 'inStore'
| 'order'
| 'reload'
| 'rent'
| 'setUp'
| 'subscribe'
| 'support'
| 'tip'
| 'topUp',
style?: 'white' | 'white-outline' | 'black' | 'automatic',
locale: string,
}
}
});
element.mount('#apple-pay-button-container');
```
The Apple Pay button **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| `events` | Object | The below events can be emitted by the Apple Pay element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary) . \* `onPaymentButtonClicked` \* `onThreeDSecureChallenge` \* `onPaymentSessionExpired` \* `onAuthorizeSuccess` \* `onAuthorizeFailed` \* `onApplePayAvailable` |
| abortAfterAuthorizeFailed | Boolean | Hide payment sheet if authorization fails. |
## Apple Pay availability
Payrails client provides the method to check if Apple Pay is available in the current browser/client session. This is only necessary if one needs to know in advance if to render the element. The `ApplePayButton` element otherwise has this logic encapsulated and will not render if the browser is not supported.
```typescript theme={null}
const isAvailable = await payrails.isApplePayAvailable(); // returns boolean
```
## PayPal Element
The `paypalButton` element is an authorization button for PayPal.
```javascript theme={null}
const element = payrails.paypalButton({
showStoreInstrumentCheckbox: boolean; // default false
alwaysStoreInstrument: boolean; // default false
styles: {
color: "'gold' | 'blue' | 'silver' | 'white' | 'black'",
height: 30,
label: "'paypal' | 'checkout' | 'buynow' | 'pay' | 'installment' | 'subscribe' | 'donate'",
shape: "'rect' | 'pill'",
tagline: "true | false",
storeInstrumentCheckbox: Partial,
locale: string,
},
events: {
onAuthorizeSuccess: () => {
console.log('yay!');
},
onAuthorizeFailed: () => {
console.log('nah :(');
},
onPaymentButtonClicked: async() => {
console.log('clicked');
// If resolved to false authorization will not be triggered
return Promise.resolve(true);
},
onPaymentSessionExpired() {
console.log('session expired');
// dropin has to be initialized with new workflow execution, contact your backend
},
onPaypalAvailable() {
// called when paypal is available in the browser
}
},
translations: {
labels: {
saveInstrument: string;
};
};
});
element.mount('#paypal-button-container');
```
The PayPal button **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| `events` | Object | The below events can be emitted by the PayPal element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary) . \* `onPaymentButtonClicked` \* `onThreeDSecureChallenge` \* `onPaymentSessionExpired` \* `onAuthorizeSuccess` \* `onAuthorizeFailed` \* `onPaypalAvailable` |
## PayPal FraudNet integration
Payrails SDK provides an easy way to integrate with PayPal FraudNet's solution.
```typescript theme={null}
/**
| 'home-page'
| 'search-result-page'
| 'category-page'
| 'product-detail-page'
| 'cart-page'
| 'inline-cart-page'
| 'checkout-page';
**/
const page = "checkout-page";
const fraudNet = payrails.fraudNet(page);
fraudNet.mount();
```
## Generic Redirect Element
The `genericRedirectButton` element is an authorization button for Generic Redirect payment methods.
```typescript theme={null}
const element = payrails.genericRedirectButton({
paymentMethod: {
paymentMethodCode: 'mercadoPago' | 'pix' | 'bankAccount' | 'iDeal' | 'revolutPay' | 'yape'
};
openInNewTab: true | false ;
events: {
onAuthorizeSuccess: () => {
console.log('yay!');
},
onAuthorizeFailed: () => {
console.log('nah :(');
},
onPaymentButtonClicked: async() => {
console.log('clicked');
// If resolved to false authorization will not be triggered
return Promise.resolve(true);
},
onPaymentSessionExpired() {
console.log('session expired');
// dropin has to be initialized with new workflow execution, contact your backend
}
},
translations: {
// This value will be mapped to the payment button label
label: "Pay using Payment Method";
},
styles: {
background: "#474847",
fontSize: "16px",
color: "white",
padding: "5px"
},
returnInfo: {
success: "example.com/success",
error: "example.com/error",
pending: "example.com/pending",
cancel:"example.com/cancel"
}
});
element.mount('#genericRedirect-button-container');
```
The Generic Redirect button **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| `events` | Object | The below events can be emitted by the Generic Redirect element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary) . \* `onPaymentButtonClicked` \* `onThreeDSecureChallenge` \* `onPaymentSessionExpired` \* `onAuthorizeSuccess` \* `onAuthorizeFailed` |
| `translations` | Object | \* `label`, the display label will be mapped to the payment button. |
| `styles` | Object | Styling of the payment button. Follow the guidelines for styling [buttons](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button) |
| `paymentMethod` | Object | Payment Method for which the generic redirect button will be created |
| `returnInfo` | Object | Object defining return urls after payment is finalized. `{ success: "example.com/success", error: "example.com/error", pending: "example.com/pending", cancel:"example.com/cancel" }` |
> Note: not all events are applicable for all generic redirect payment methods.
### Style config to match brand guidelines
For the following payment methods, SDK provides look and feel for the button according to the brand guidelines.
#### Revolut Pay
| Property | Type | Default | Description |
| :- | :- | :- | :- |
| theme | `dark` \| `light` \| `lightOutline` | `lightOutline` | Sets the button color scheme. 'dark' renders a dark background, 'light' a white background, 'lightOutline' a white background with a border. |
| width | `string` | `250px` | Any valid CSS width value (e.g. '100%', '320px'). The button preserves its aspect ratio. |
| borderRadius | `string` | - | Any valid CSS border-radius value (e.g. '8px'). Optional |
```typescript theme={null}
payrailsClient.genericRedirectButton(
paymentMethod: { paymentMethodCode: 'revolutPay' },
styles: {
theme: 'dark',
width: '320px',
borderRadius: '8px',
}
)
```
### Generic Redirect functions
Generic Redirect exposes the following functions
| functions | description | params |
| - | - | - |
| `update` | used to update the styles and translations for the element | `javascript { styles: Object, translations: Object } ` |
## Dynamic Element
The `dynamicElement` element is an embedded form element for different payment methods.
```typescript theme={null}
interface OnChange {
isValid?: boolean;
fieldName?: string;
value?: any;
}
const element = payrails.dynamicElement({
paymentMethod: {
paymentMethodCode: "bankAccount", // required
},
styles: {
all: {
base: {
// base field styles
},
focus: {
// focused field styles
},
invalid: {
// invalid field styles
},
},
fields: {
"bankaccount.account.number": {
// Specific bank account number styling
},
},
},
translations: {
fields: {
"bankaccount.account.number": {
label: "Account Number",
placeholder: "Enter your account number",
},
},
},
events: {
onReady() {
console.log("triggered when form is rendered and ready");
},
onValidationChange(e) {
console.log("dynamic element form state changed", e);
},
},
});
element.mount("#dynamic-element-container");
```
The dynamic element **configuration** is defined as shown below:
| property | type | description |
| - | - | - |
| paymentMethod | Object | Configuration for the payment method to collect |
| styles | Object | Styles that should be applied to the Element. Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1). |
| translations | Object | Translations to localize the Element. |
| events | Object | The below events can be emitted by the Dynamic Element. For more details about events, see the [SDK Events Glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary). onReady onValidStateChange |
### Field types
The Dynamic Element automatically generates different field types based on the JSON schema, supported field types are:
1. **Input Fields**
2. **Select Fields**
3. **Tab Fields**
#### Field specific styling configuration
```typescript theme={null}
const config = {
styles: {
all: {
// Styles applied to all field types
input: {
base: {}, // Default input styles
focus: {}, // Focused input styles
invalid: {}, // Invalid input styles
complete: {}, // Completed input styles
empty: {}, // Empty input styles
},
select: {
base: {}, // Default select styles
focus: {}, // Focused select styles
invalid: {}, // Invalid select styles
complete: {}, // Completed select styles
},
tabs: {
container: {
base: {}, // Tab container styles
},
tab: {
base: {}, // Default tab styles
active: {}, // Active tab styles
},
},
errors: {}, // Error message styles
},
},
fields: {
// Field-specific styling (overrides 'all' styles)
"bankAccount.holder.firstName": {
label: {}, // Label styles
input: {
base: {}, // Field-specific input styles
},
errors: {}, // Field-specific error styles
},
},
};
```
### Dynamic element functions
Dynamic element exposes the following functions
| functions | description | params |
| - | - | - |
| mount | used to add the dynamic element into the DOM | `location`: string |
| unmount | used to remove the dynamic element from the DOM | |
| tokenize | used to tokenize the collected payment data | |
## Lean Element
Lean enables users in the UAE to pay via bank transfer using a checkout dialog powered by the Lean SDK. The payment method is available as both a standalone **Element** and within the **Drop-in** integration.
**Important**
The Lean payment button must use the label **"Pay by bank with Al Tareq."**
### How to Use
```typescript theme={null}
const payrailsClient = await Payrails.init({
data: initResponse.data,
version: initResponse.version,
});
const leanButton = payrailsClient.leanButton({
translations: {
label: "", //
},
styles: {
button: {
base: {
backgroundColor: "#00B67A",
borderRadius: "8px",
height: "48px",
},
loading: { opacity: "0.7" },
hover: { backgroundColor: "#009960" },
},
dialog: {
themeColor: "#00B67A",
buttonTextColor: "#FFFFFF",
buttonBorderRadius: "8px",
linkColor: "#007AFF",
overlayColor: "rgba(0, 0, 0, 0.5)",
},
},
events: {
onAuthorizeSuccess: () => console.log("Payment succeeded"),
onAuthorizeFailed: (e) => console.log("Payment failed", e),
},
});
leanButton.mount("#lean-container");
```
### Configuration
The Lean Elements **configuration** is defined as shown below. All properties are optional configure them based on your requirements.
| Property | Type | Description |
| - | - | - |
| `translations.label` | `string` | Button text. Default: `"Pay by bank with Al Tareq"` |
| `styles.button.base` | `Partial` | Default button styles |
| `styles.button.loading` | `Partial` | Styles applied while processing |
| `styles.button.hover` | `Partial` | Styles applied on hover |
| `styles.dialog` | `LeanCustomization` | Lean checkout dialog theming |
| `events` | `PaymentEvents` | Event callbacks |
***
### Dialog Customization
The `styles.dialog` object controls the appearance of the Lean checkout modal.
| Property | Type | Description |
| - | - | - |
| `themeColor` | `string` | Primary color for the dialog UI |
| `buttonTextColor` | `string` | Text color on dialog buttons |
| `buttonBorderRadius` | `string` | Border radius of dialog buttons (CSS value) |
| `linkColor` | `string` | Color of links inside the dialog |
| `overlayColor` | `string` | Overlay backdrop colo |
### Event Callbacks
For event callback details, see [SDK events glossary](/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary).
# Headless Integration using SDK
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web-v5-legacy/headless-integration-using-sdk
The Payrails SDK provides a comprehensive set of API methods and query functions for integrating payment processing capabilities into your application. This documentation covers the headless API methods and configuration query functions available through the main `Payrails` class.
## API Methods
The Payrails SDK provides REST API methods through the `api()` function for making calls to payrails backend directly from the client.
### `api`
| Parameter | Type | Description |
| - | - | - |
| `config` | `ApiConfig & { operation: TOperation }` | Configuration object containing operation details |
**Returns:** `Promise`
### Available Operations
| Operation | Method | Description | Required Parameters |
| :- | :- | :- | :- |
| `deleteInstrument` | DELETE | Deletes a stored payment instrument | `resourceId` |
| `updateInstrument` | PATCH | Updates properties of an existing payment instrument | `resourceId`, `body` |
#### Delete Payment Instrument
```typescript theme={null}
const result = await payrails.api({
operation: "deleteInstrument",
resourceId: "instrument_id_here",
});
```
**Response:** `DeleteInstrumentResponse`
| Field | Type | Description |
| - | - | - |
| `success` | `boolean` | Indicates if the deletion was successful |
#### Update Payment Instrument
```typescript theme={null}
const result = await payrails.api({
operation: "updateInstrument",
resourceId: "instrument_id_here",
body: {
status: "enabled",
default: true,
merchantReference: "your_reference",
networkTransactionReference: "network_ref",
},
});
```
**Request Body Fields:**
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `status` | `'enabled'`\| `'disabled'` | No | Enable or disable the instrument |
| `networkTransactionReference` | `string` | No | Network transaction reference |
| `merchantReference` | `string` | No | Merchant reference identifier |
| `paymentMethod` | `'applepay'` \|`'card'`\|`'googlepay'`\|`'paypal'` | No | Payment Method Type |
| `default` | `boolean` | No | Set as default payment instrument |
**Response Fields:**
| Field | Type | Description |
| - | - | - |
| `id` | `string` | Unique instrument identifier |
| `createdAt` | `string` | Creation timestamp |
| `holderId` | `string` | Holder identifier |
| `paymentMethod` | `PAYMENT_METHOD_CODES` | Payment method type |
| `status` | `PAYMENT_INSTRUMENT_STATUS` | Current status |
| `data` | `INSTRUMENT_OBJECT` | Payment method specific data |
## Query Methods
The query system allows you to retrieve configuration data and SDK state information without making external API calls.
### `query`
| Parameter | Type | Required | Description |
| - | - | - | - |
| `key` | `K extends keyof ConfigurationQueryTypes` | Yes | The configuration key to query |
| `params` | `Record` | No | Optional parameters for specific queries |
**Returns:** `ConfigurationQueryTypes[K]['returnType'] | null`
### Available Query Keys
| Query Key | Parameters | Return Type | Description |
| :- | :- | :- | :- |
| `holderReference` | | `string` | Retrieves the holder reference for the current session |
| `amount` | | `PayrailsAmount` | Gets the current payment amount configuration |
| `executionId` | | `string`\|`undefined` | Retrieves the current workflow execution ID |
| `binLookup` | | `Links`\|`undefined` | Gets the BIN lookup service endpoint |
| `instrumentDelete` | | `Links`\|`undefined` | Gets the instrument deletion endpoint |
| `instrumentUpdate` | | `Links`\|`undefined` | Gets the instrument update endpoint |
| `paymentMethodConfig` | `paymentMethodCode: PAYMENT_METHOD_CODES.CODE \| 'all' \| 'redirect'` | `StorablePaymentCompositionOption`\| `StorablePaymentCompositionOption[]`\|`undefined` | Retrieves configuration for a specific payment method |
| `paymentMethodInstruments` | `paymentMethodCode: string` | `StoredPaymentInstrument
### Query Examples
#### Basic Configuration Queries
```typescript theme={null}
// Get holder reference
const holderRef = payrails.query("holderReference");
// Get current amount
const amount = payrails.query("amount");
// Get execution ID
const executionId = payrails.query("executionId");
```
#### Link Queries
```typescript theme={null}
// Get API endpoints
const binLookupLink = payrails.query("binLookup");
const deleteLink = payrails.query("instrumentDelete");
const updateLink = payrails.query("instrumentUpdate");
```
#### Payment Method Queries
```typescript theme={null}
// Get payment method configuration
const cardConfig = payrails.query("paymentMethodConfig", {
paymentMethodCode: "card",
});
// Get payment method config for all available payment methods
const cardConfig = payrails.query("paymentMethodConfig", {
paymentMethodCode: "all",
});
// Get payment method config for all redirect type payment methods
const cardConfig = payrails.query("paymentMethodConfig", {
paymentMethodCode: "redirect",
});
// Get stored instruments for a payment method
const cardInstruments = payrails.query("paymentMethodInstruments", {
paymentMethodCode: "card",
});
```
## Error Handling
All API methods may throw `PayrailsError` exceptions. It's recommended to wrap API calls in try-catch blocks:
```typescript theme={null}
try {
const result = await payrails.api({
operation: "deleteInstrument",
resourceId: "instrument_id",
});
console.log("Instrument deleted successfully:", result.success);
} catch (error) {
if (error instanceof PayrailsError) {
console.error("Payrails API error:", error.message);
// Handle specific error codes
} else {
console.error("Unexpected error:", error);
}
}
```
Query methods return `null` instead of throwing errors when data is not found:
```typescript theme={null}
const amount = payrails.query("amount");
if (amount === null) {
console.log("Amount not configured");
} else {
console.log("Current amount:", amount);
}
```
## Examples
### Complete Instrument Management Example
```typescript theme={null}
// Initialize Payrails client
const payrails = Payrails.init(initResponse, options);
// Query for stored card instruments
const cardInstruments = payrails.query("paymentMethodInstruments", {
paymentMethodCode: "card",
});
if (cardInstruments && cardInstruments.length > 0) {
const instrument = cardInstruments[0];
// Update the instrument
try {
const updatedInstrument = await payrails.api({
operation: "updateInstrument",
resourceId: instrument.id,
body: {
status: "enabled",
default: true,
},
});
console.log("Instrument updated:", updatedInstrument);
} catch (error) {
console.error("Failed to update instrument:", error);
}
}
```
## Integration Notes
| Aspect | Description |
| - | - |
| **Authentication** | All API calls are automatically authenticated using the SDK's internal token management |
| **Resource IDs** | When working with payment instruments, ensure you have the correct `resourceId` from previous queries or operations |
| **Error Handling** | Always implement proper error handling for API calls, as network issues or invalid requests can cause exceptions |
| **Query Performance** | Query methods are fast local operations that don't make network requests, making them suitable for frequent calls |
| **Parameter Validation** | Some queries require specific parameters (like `paymentMethodCode`). Missing required parameters will result in `null` returns with warning logs |
## Best Practices
| Practice | Recommendation |
| - | - |
| **Cache Query Results** | For frequently accessed configuration data, consider caching query results to improve performance |
| **Handle Null Returns** | Always check for `null` returns from query methods before using the data |
| **Use TypeScript** | Leverage the provided TypeScript types for better development experience and error prevention |
| **Error Recovery** | Implement appropriate error recovery strategies for failed API operations |
| **Logging** | Monitor API usage and errors through your application logging system for better debugging and maintenance |
# SDK Events Glossary
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web-v5-legacy/sdk-events-glossary
This page describes events sent by Payrails SDKs
## Event types
Payrails Web SDK has two categories of events:
| Category | Description | Event Names |
| :- | :- | :- |
| **UI Events** | Events which are triggered on user interaction with an SDK element or when the payment methods gets shown/ removed from the screen. | `onChange`, `onFocus`, `onCardChange`, `onSaveInstrumentCheckboxChanged`, `onPaymentButtonClicked`, `onReady`, `onGooglePayAvailable`, `onApplePayAvailable`, `onPaypalAvailable`,`onAvailable`, `onPaymentOptionSelected` |
| **Payment Events** | Events which are related to the payment authorization lifecycle. | `onAuthorizeSuccess`, `onAuthorizeFailed`, `onAuthorizePending`, `onThreeDSecureChallenge`, `onPaymentSessionExpired` |
| Generic Events | Events which handles the context depending on which action the SDK element has taken. | `onSuccess`, `onFailed`, `onPending`, `onButtonClicked`, `onRequestStart` |
Important Update:
As of @payrails/web-sdk\@5.36.0, Payrails Elements’ payment authorization related events are now marked deprecated. For more information, please checkout this page.
Please refer to these generic events as replacements.
| Old | New |
| :- | :- |
| onAuthorizeSuccess | onSuccess |
| onAuthorizeFailed | onFailed |
| onAuthorizePending | onPending |
| onAuthorizeRequestStart | onRequestStart |
| onPaymentButtonClicked | onButtonClicked |
## Rules for Event Execution
While UI events are async and trigger on user interaction (such as `onChange`, `onFocus`), payment events have some rules and order of execution around them.
`onAuthorizeSuccess` , `onAuthorizeFailed` and `onAuthorizePending` are terminal events. This means each request will result in at least a success or failed state and corresponding event will be fired. Rules for event execution are defined below:
* `onPaymentButtonClicked` (UI Event) - if this returns false, this also calls `onAuthorizeFailed` as terminal event.
* `onThreeDSecureChallenge` - triggered when the user sees the 3D Secure popup.
* `onPaymentSessionExpired` - in case of unauthorized request. Also calls `onAuthorizeFailed` as terminal event.
* `onAuthorizeFailed` - when payment authorization fails. With this event you would also get the reason why the execution failed. This is based on the [operations codes](/docs/resources/payments/operation-results).
* `onAuthorizeSuccess` - when payment authorization is successful
* `onAuthorizePending` - triggered when the execution is in pending state with no further action required from the user. (Please reach out to your account manager on what could be the potential use cases for this event)
## Web SDK Event Glossary
| Name | Type | Description | Function Signature |
| - | - | - | - |
| `onPaymentOptionSelected` | UI | triggered each time the payment option is selected for saved instrument both instrument details and payment method config are returned for payment method only payment method config is returned | `(e: onPaymentMethodSelectedParams) => void` |
| `onSaveInstrumentCheckboxChanged` | UI | triggered each time save instrument checkbox changes state | `({ checked }: { checked: boolean }) => void;` |
| `onPaymentButtonClicked` | UI | triggered each time, user clicks on the payment button. This event is called just before the SDK makes a call to authorize. callback passed to this event should return a Promise which resolves to a boolean If this callback returns false, SDK does not make the authorize call and the user sees a payment not successful screen and also triggers onAuthorizeFailed event. | `(event?: { bin?: string }) => Promise;` |
| `onStateChanged` | UI | triggered when payment button state changes | `(state: 'enabled' \| 'disabled') => void;` |
| `onThreeDSecureChallenge` | Payment | triggered just before the user sees the 3ds popup | `() => void;` |
| `onPaymentSessionExpired` | Payment | triggered when authorization fails due to payment session expiration | `() => void;` |
| onAuthorizeRequestStart | Payment | triggered only for Apple Pay and Google Pay. triggered after the user actually approves the payment on AP and GP modals and before the authorize request is initiated from the SDK. | `() => void;` |
| `onAuthorizeSuccess` | Payment | triggered when payment authorization is successful callback passed to this event can pass a param to get more information regarding the authorization | `(event?: any) => void;` |
| `onAuthorizeFailed` | Payment | generic authorization failure event triggered for all the authorization failure reasons. callback passed to this event can pass a param to get more information regarding the reason of failure | `(event?: { code: 'AUTHORIZATION_ERROR', message: string }) => void` |
| `onChange` | UI | triggered when changes are made to the card form | `(event: OnChange) => void;` |
| `onFocus` | UI | triggered when any input field inside the card form triggers focus event | `() => void;` |
| `onReady` | UI | triggered when the card form is rendered and ready to pay | `() => void;` |
| `onCardChange` | UI | triggered when user selects a different saved instrument in the saved instrument list | `(selectedCard: StoredPaymentInstrument) => void` |
| `onBillingAddressChanged` | UI | triggered when billing address changes | `((e: { isValid: boolean; billingAddress?: IAddress; }) => void)` |
| `onDeliveryAddressChanged` | UI | triggers when shipping address changed for express flow | `(((data?: onDeliveryAddressChangedResponse) => Promise))` |
| onSuccess | Generic | triggers when Payrails Elements successfully done a payment or tokenization | `(action: 'AUTHORIZE' \| 'TOKENIZE', response?: any) => void;` |
| onFailed | Generic | triggers when Payrails Elements failed a payment or tokenization | `(action: 'AUTHORIZE' \| 'TOKENIZE', e?: any) => void;` |
| onPending | Generic | triggers when Payrails Elements payment or tokenization is in pending or review | `(action: 'AUTHORIZE' \| 'TOKENIZE', e?: any) => void;` |
| onButtonClicked | Generic | • triggered each time, user clicks on the element button. • This event is called just before the SDK makes a call to either authorize or tokenize. • callback passed to this event should return a Promise which resolves to a boolean • If this callback returns false, SDK does not make the authorize / tokenize call and the user sees a payment / tokenize not successful screen and also triggers onFailed event. | `(e?: { bin?: string }) => Promise;` |
| onRequestStart | Generic | • only for AP and GP. • triggered after the user actually approves the payment on AP and GP modals and before the authorize / tokenize request is initiated from the SDK. | `() => Promise;` |
## React Native Event Glossary
| Name | Description | Function Signature |
| - | - | - |
| `onAuthorizeSuccess` | Triggered when payment authorization is successful | `() => void;` |
| `onAuthorizeFailure` | Triggered when payment authorization fails | `() => void;` |
| `onAuthorizeError` | Triggered in case of when BE returns 5xx or the client throws. For unsuccessful authorizations onAuthorizeFailure is fired | `(error: any) => void;` |
| `onAuthenticationError` | Triggered when the access token is expired | `() => void;` |
| `onAuthorizeStart` | Triggered when the payment button is clicked. If the resolved value is false, then the payment is canceled. It fires after a user clicks the payment button. It allows a merchant to perform additional checks and block the payment if needed | `() => Promise;` |
| `onRequestProgressChange` | Triggered when authorization request changes state. It can be used to show a loading state | `(isInProgress: boolean) => void;` |
| `onAuthorizeCancel` | Triggered when user closed a popup (for paypal) or a webview (for 3DS) without completing the challenge. This is only available for APM hooks: `useExecuteGooglePayPayment` `useExecuteApplePayPayment` `useExecutePayPalPayment` | `() => void;` |
| `onAuthorizeRequestStart` | triggered after the user actually approves the payment on APM modals (Apple Pay, Google Pay or Paypal) and before the authorize request is initiated from the SDK. | `() => void;` |
| `onSessionExpired` | triggered when the client session is expired. merchants can pass the function which contains their implementation of client init as part of the `onSessionExpired` event. This then can be used to automatically reinitialize the client when the client session is expired instead of merchants having to do this themselves. | `() => Promise` |
# Secure Fields
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web-v5-legacy/secure-fields
Secure Fields provides developers with pre-built form fields to securely collect sensitive data from the client side. These fields are hosted by Payrails and injected into your web page as iFrames. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to collect data securely with Payrails Fields on your web page.
## Get started
Below is a four-step process to start using Secure Fields in your client.
### Create a container
First, create a container for the form fields using the `collectContainer()` method of the Payrails client.
There are 2 types of containers:
* `COMPOSABLE`: you call `mount()` directly on the container, you must define the form's layout, and the number of elements must match the sum of the numbers in the layout.
* `COLLECT`: layout is not necessary. Secure fields are mounted separately.
```javascript theme={null}
const container = payrailsClient.collectContainer({
containerType?: 'COMPOSABLE' || 'COLLECT'; // default composable
layout?: number[]; // required only for composable container type, layout for card form
});
```
### Create a collectible field
```javascript theme={null}
const collectField = {
type: Payrails.ElementType, //Payrails.FieldType enum
inputStyles: {}, //optional styles that should be applied to the form field
labelStyles: {}, //optional styles that will be applied to the label of the collect field
errorTextStyles: {}, //optional styles that will be applied to the errorText of the collect field
label: "string", //optional label for the form field
placeholder: "string", //optional placeholder for the form field
required: false, //optional, indicates whether the field is marked as required. Defaults to 'false'
enableCardIcon: true, //optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER FieldType)
format: String, //optional, format for the field (only applicable currently for EXPIRATION_MONTH and EXPIRATION_YEAR FieldType)
};
const field = payrailsContainer.createCollectElement(collectField);
```
A Payrails **CollectField** is defined as shown below:
| property | type | description |
| - | - | - |
| `type` | Payrails.FieldType (required) | Enum (`CARDHOLDER_NAME`, `CARD_NUMBER`, `EXPIRATION_DATE`, `EXPIRATION_MONTH`, `EXPIRATION_YEAR`, `CVV`). Each type applies the appropriate regex and validations to the form field. |
| `inputStyles` | Object (optional) | Styles that should be applied to the form field |
| `labelStyles` | Object (optional) | Styles that will be applied to the label of the collect field |
| `errorTextStyles` | Object (optional) | Styles that will be applied to the `errorText` of the `collect` field. |
| `label` | String (optional) | Label for the form field |
| `placeholder` | String (optional) | Placeholder for the form field |
| `required` | Boolean | Indicates whether the field is marked as required or not. If not provided, it defaults to `false`. |
| `enableCardIcon` | Boolean | Indicates whether the icon is visible for the `CARD_NUMBER` field defaults to `true` |
| `format` | String | Indicates the format pattern applicable to the date types: \* `EXPIRATION_DATE`: `MM/YY` (default), `MM/YYYY`, `YY/MM`, or `YYYY/MM`. \* `EXPIRATION_YEAR`: `YY` (default), or `YYYY`. Note: if not specified or an invalid value is passed to the `format` then it takes a default value. |
### Mount fields to the DOM
#### Collect container
To specify where the fields will be rendered on your page, create a placeholder `
` fields with unique `id` tags. For instance, the form below has 4 empty divs with unique IDs as placeholders for four Payrails fields.
```html theme={null}
```
Now, when the `mount(domField)` method of the Field is called, the Field will be inserted in the specified div. For instance, the call below will insert the Field into the `div` with the id `#cardNumber`.
```javascript theme={null}
field.mount("#cardNumber");
```
You can use the unmount method to reset any collect field to its initial state.
```javascript theme={null}
field.unmount();
```
#### Composable container
A composable container works similarly to a collect container but is mounted as a whole.
```Text HTML theme={null}
```
```
container.mount('#card-form')
```
### Extract data from secure fields
#### Collect raw data
When the form is ready to be submitted, call the `container.collect()` method on the container object. This will collect the data from all the fields belonging to the container and save it to the vault.
```javascript theme={null}
const data = await container.collect();
console.log(data); // see the id and other details of the entered card
```
#### Tokenize the card and create a payment instrument
Alternatively, you can create a payment instrument.
**Note**: you don't need to use this method in most cases as it might also break for some flows (e.g. when no holder reference is passed during client init). Instead, call `container.collect()` and use raw card data to authorize the payment.
Call the `container.tokenize({storeInstrument: true})` method on the container object. This will collect the data from all the fields belonging to the container and save it to the vault. It will also create a payment instrument and return the payment instrument details.
```javascript theme={null}
try {
const response = await container.tokenize({ storeInstrument: true });
console.log(response); // see the id and other details of your newly tokenized card
} catch (e) {
console.log(e); // solve the case when something went wrong
}
```
## Errors and validation
Payrails Web provides two types of validation for collecting fields.
### Default validations
Every Collect Field has a set of default validations listed below:
| type | validation |
| - | - |
| `CARD_NUMBER` | Card number validation with checksum algorithm (Luhn algorithm). Available card lengths for defined card types are \[12, 13, 14, 15, 16, 17, 18, 19]. A valid 16-digit card number will be in the format `XXXX XXXX XXXX XXXX`. |
| `CARD_HOLDER_NAME` | The name should be 2 or more symbols, and valid characters should match the pattern: `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$`. |
| `CVV` | Card CVV can have 3-4 digits. |
| `EXPIRATION_DATE` | Any date starting from the current month. By default, the valid expiration date should be in the short-year format: MM/YY. |
### UI errors
Helps to display custom error messages on the Payrails Fields through the methods `setError` and `resetError` on the fields.
```javascript theme={null}
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
});
//Set custom error
cardNumber.setError("custom error");
//reset custom error
cardNumber.resetError();
```
`setError(error: string)` method is used to set the error text for the field. When this method is triggered, all the current errors in the field will be overridden with the custom error message passed. This error will be displayed on the field until `resetError()` is triggered on the same field.
`resetError()` method is used to clear the custom error message that is set using `setError`.
## Event Listener
Helps to communicate with Payrails fields by listening to an event:
```javascript theme={null}
field.on(eventName: EventName, handler: function)
```
There are 4 events in `EventName`:
| event | description |
| :- | :- |
| `CHANGE` | A change event is triggered when the Field's value changes. |
| `READY` | A ready event is triggered when the Field is fully rendered. |
| `FOCUS` | A focus event is triggered when the Field gains focus. |
| `BLUR` | A blur event is triggered when the Field loses focus. |
The handler `function(state) => void` is a callback function you provide that will be called when the event is fired with the state object, as shown below.
```javascript theme={null}
state: {
fieldType: ElementType;
isEmpty: boolean;
isFocused: boolean;
isValid: boolean;
value: string;
}
```
Here's a sample code snippet for using listeners:
```javascript theme={null}
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
});
cardNumber.mount("#cardNumberContainer");
// Subscribing to CHANGE event, which gets triggered when field changes.
cardNumber.on(EventName.CHANGE, (state) => {
// Your implementation when Change event occurs.
console.log(state);
});
```
## Style
You can configure the style of the Secure Fields as you wish:
```javascript theme={null}
const field = container.createCollectElement({
inputstyles: {},
labelStyles: {},
errorTextStyles: {},
type: ElementType.CARD_NUMBER,
});
```
The `inputStyles` field accepts a style object that consists of CSS properties that should be applied to the form field in the following states:
| object | description |
| :- | :- |
| `base` | All other variants inherit from these styles. |
| `complete` | Applied when the Field has valid input. |
| `empty` | Applied when the Field has no input. |
| `focus` | Applied when the Field has focus. |
| `invalid` | Applied when the Field has invalid input. |
| `cardIcon` | Applied to the card type icon in `CARD_NUMBER` Field. |
| `copyIcon` | Applied to copy icon in Fields when `enableCopy` option is `true`. |
Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1).
An example of a `inputStyles` object:
```javascript theme={null}
inputStyles:{
base: {
border: "1px solid #eae8ee",
padding: "10px 16px",
borderRadius: "4px",
color: "#1d1d1d",
},
complete: {
color: "#4caf50",
},
empty: {},
focus: {},
invalid: {
color: "#f44336",
},
cardIcon:{
position: "absolute",
left:"8px",
bottom:"calc(50% - 12px)"
},
copyIcon:{
position: "absolute",
right:"8px",
}
}
```
The states that are available for `labelStyles` are `base` and `focus`.
An example of a `labelStyles` object:
```javascript theme={null}
labelStyles: {
base: {
fontSize: "12px",
fontWeight: "bold"
},
focus: {
color: "#1d1d1d"
}
}
```
The state that is available for `errorTextStyles` is only the `base` state, it shows up when there is some error in the `collect` field.
An example of an `errorTextStyles` object:
```javascript theme={null}
errorTextStyles: {
base: {
color: "#f44336";
}
}
```
## Translations
For the **Secure Fields**, translations are part of the container's `createCollectElement(collectField, options)` signature, inside the `collectField` object:
```javascript theme={null}
const field = payrailsContainer.createCollectElement(
{
type: ElementType.CARD_NUMBER, //Payrails.FieldType enum
label: "localized label for the form field",
placeholder: "localized placeholder for the form field",
},
{},
);
```
## End-to-end example
Here's an end-to-end example of card collection with a secure field for a container of `COLLECT` type.
```javascript theme={null}
//Step 1
const container = payrailsClient.collectContainer({
containerType: "COLLECT",
});
//Step 2
const field = container.createCollectElement({
inputstyles: {
base: {
color: "#1d1d1d",
},
cardIcon: {
position: "absolute",
left: "8px",
bottom: "calc(50% - 12px)",
},
},
labelStyles: {
base: {
fontSize: "12px",
fontWeight: "bold",
},
},
errorTextStyles: {
base: {
color: "#f44336",
},
},
placeholder: "Card Number",
label: "card_number",
type: ElementType.CARD_NUMBER,
});
// Step 3
field.mount("#cardNumber"); //assumes there is a div with id="#cardNumber" in the webpage
// Step 4
container.collect();
```
# Using actionRequired event
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/guides/action-required-event
Intercept the actionRequired event to control how your app presents a 3D Secure challenge or a payment-method redirect.
The `actionRequired` event lets your application intercept moments when the SDK is about to navigate the buyer away — either to a 3DS challenge page or to a generic payment-method redirect (voucher, bank handoff, etc.). You can take over presentation (open in a tab, render in your own modal, push a route) instead of letting the SDK mount its popup or call `window.location.assign`.
## When the event fires
The SDK emits `actionRequired` immediately before its default redirect/popup action, in these cases:
| `kind` | Triggered by |
| - | - |
| `'3ds'` | Card or Google Pay authorize returns `authorizePending` with `actionRequired === '3ds'` |
| `'genericRedirect'` | Any payment-method component that uses the new generic-redirect flow |
If no listener is attached — or if attached listeners do **not** call `preventDefault()` — the SDK handles the user flow: it mounts a popup (or redirects, depending on the config).
## Subscribing
```ts theme={null}
import { Payrails, ACTION_REQUIRED_KIND } from "@payrails/web-sdk";
const payrails = await Payrails.init({ ...initOptions });
const unsubscribe = payrails.on("actionRequired", (event) => {
// event: ActionRequiredEvent
// url: string — the URL the SDK would have navigated to
// kind: '3ds' | 'genericRedirect'
// paymentMethodCode: string — e.g. 'card', 'googlePay', 'paypal'
// executionId: string — Payrails workflow execution id
// preventDefault(): void — call to suppress the SDK's default action
});
```
`on()` returns an unsubscribe function. There is also `payrails.off(name, handler)` if you need to remove a specific handler.
**Lifetime note**
`Payrails.init` resets all registered listeners. Subscribe **after** awaiting `Payrails.init`, and re-subscribe if you re-init.
## Three integration patterns
### 1. Observe only — don't change behavior
Subscribe without calling `preventDefault`. Useful for analytics, logging, or showing your own toast before the popup opens.
```ts theme={null}
payrails.on("actionRequired", (event) => {
analytics.track("payrails_action_required", {
kind: event.kind,
paymentMethodCode: event.paymentMethodCode,
executionId: event.executionId,
});
// No preventDefault() — SDK still opens its popup / redirects.
});
```
### 2. Open the action URL in a new tab
For merchants who don't want the buyer to leave the checkout page.
```ts theme={null}
payrails.on("actionRequired", (event) => {
if (event.kind !== ACTION_REQUIRED_KIND.THREE_DS) return;
event.preventDefault(); // suppress the SDK popup
window.open(event.url, "_blank", "noopener,noreferrer");
// You are now responsible for resolving the payment.
// Poll your backend (which polls Payrails) for the final authorize status,
// or wait for a webhook to surface the result to your UI.
});
```
### 3. Render the URL inside your own modal / SPA route
```ts theme={null}
payrails.on("actionRequired", async (event) => {
event.preventDefault();
// Show your in-house modal with an iframe pointing at event.url.
// (You control sizing, branding, close behavior, focus trap, etc.)
await myChallengeModal.open(event.url);
// When your modal detects 3DS completion (postMessage from your own
// return page, or backend polling), close it and reconcile the order.
});
```
Handlers may be `async` — the SDK awaits each one before deciding whether to fall back to its default action. `preventDefault()` only needs to be called synchronously if you want to suppress the default; calling it later in an `await` chain still works as long as it happens before your handler resolves.
## Handler contract & gotchas
* **`preventDefault()` is sticky.** Once called by any handler, the SDK skips its default action for that emission. Other handlers still run.
* **Errors thrown inside handlers are caught.** They are logged via `PayrailsLogger.error` and do **not** prevent other handlers from running, nor do they implicitly call `preventDefault`. If a handler throws, the SDK will still perform its default action unless another handler suppressed it.
* **`executionId` ties back to the Payrails workflow execution** — use this to correlate with backend polling or webhooks.
## UI state when you intercept
When you call `preventDefault()`, the SDK considers its UI obligation discharged. Concretely, in the current release the `CardPaymentButton` loading spinner is **not** automatically reset after interception.
## Unsubscribing
```ts theme={null}
const unsubscribe = payrails.on("actionRequired", handler);
// later
unsubscribe();
// or
payrails.off("actionRequired", handler);
```
Always unsubscribe in your component teardown (React `useEffect` cleanup, Vue `onUnmounted`, etc.) to avoid double-firing if the host component remounts.
## Minimal end-to-end example (React)
```jsx theme={null}
import { useEffect, useRef } from 'react';
import { Payrails, ACTION_REQUIRED_KIND } from '@payrails/web-sdk';
export function Checkout() {
const payrailsRef = useRef(null);
useEffect(() => {
let unsubscribe: (() => void) | undefined;
(async () => {
const payrails = await Payrails.init({ /* ... */ });
payrailsRef.current = payrails;
unsubscribe = payrails.on('actionRequired', (event) => {
if (event.kind !== ACTION_REQUIRED_KIND.THREE_DS) return;
// Take over from the SDK — no popup, no top-level redirect.
event.preventDefault();
// Handle it yourself — here, open the challenge in a new tab.
window.open(event.url, '_blank', 'noopener,noreferrer');
});
// ... mount card form / buttons against `payrails`
})();
return () => {
unsubscribe?.();
};
}, []);
return <>{/* your checkout UI */}>;
}
```
# How to accept alternative payment methods without a redirect
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/guides/how-to-accept-alternative-payment-methods-without-a-redirect
Collect alternative payment method details in a form on your checkout page with the Payrails web SDK, and handle each result.
This guide shows you how to accept alternative payment methods on your own checkout page, without a redirect. These methods collect a few details from the shopper, such as a phone number or an IBAN. You mount a form, pair it with a pay button, and handle the result. It assumes you have already initialized the SDK and hold a `payrails` client instance. If you haven't, start with the [quick start](/docs/orchestration/checkout-sdks/web).
The form renders inside a Payrails-hosted iframe, so the shopper's details are never part of your page's DOM. The fields come from the payment method's configuration, so you don't declare them yourself.
## Supported payment methods
| Payment method | Code | Shopper enters | Currency | Result type |
| - | - | - | - | - |
| MB WAY | `mbWay` | Phone number | EUR | [Approve in app](#approve-in-app-methods) |
| SEPA Direct Debit | `sepaDirectDebit` | Name and IBAN | EUR | [Pending](#pending-methods) |
Pass the code as `paymentMethodCode`. The result type determines which events fire after the shopper clicks the pay button, as described in [Handle the payment result](#4-handle-the-payment-result).
## Before you start
* Use `@payrails/web-sdk` 6.6.0 or later.
* Ask your Payrails account manager to enable the payment method for in-page collection. Until they do, `payrails.apmForm()` throws a `PayrailsError` because the session has no form for that method.
* For SEPA Direct Debit, send the shopper's IP address, user agent and email when you [create the execution](/reference/createexecution), in `meta.clientContext.ipAddress`, `meta.clientContext.userAgent` and `meta.customer.email`. Payrails rejects the payment without them.
## 1. Add containers to your page
```html theme={null}
```
## 2. Mount the form
```js theme={null}
const form = payrails.apmForm({ paymentMethodCode: "mbWay" });
form.on("ready", () => console.log("Form rendered"));
form.on("change", ({ isValid }) => console.log("Form valid:", isValid));
try {
await form.mount("#apm-form");
} catch (error) {
if (error?.context?.reason === "unrenderable-schema") {
showOtherPaymentOptions();
}
}
```
`form.mount()` returns a promise. It rejects when the selector doesn't match an element, or when the SDK can't draw the form. In the second case, the error's `context.reason` is `'unrenderable-schema'`. Use it to offer the shopper another way to pay.
## 3. Mount the pay button
```js theme={null}
const button = payrails.apmButton({
form,
translations: { label: "Pay with MB WAY" },
});
button.mount("#apm-button");
```
The button stays disabled until the form has loaded. A click validates the form first. If a field is invalid, the form shows the error next to the field, the button emits `validate` with `isValid: false`, and the SDK doesn't submit the payment:
```js theme={null}
button.on("validate", ({ isValid }) => {
if (!isValid) scrollToPaymentForm();
});
```
The cancelable `buttonClicked` and `requestStart` instance events run after the form validates, the same as for card payments. A check you already run there also covers these methods.
### Save the instrument for future payments
By default, the button uses the instrument for this payment only. To save it for later payments, for example to charge a SEPA Direct Debit mandate on a subscription, set `alwaysStoreInstrument`:
```js theme={null}
const button = payrails.apmButton({ form, alwaysStoreInstrument: true });
button.mount("#apm-button");
```
The button doesn't show a save checkbox, so set `alwaysStoreInstrument` only when you already have the shopper's consent to save the instrument. The SDK ignores it on a merchant of record workspace.
## 4. Handle the payment result
Outcomes are instance events, so subscribe on the `payrails` client:
```js theme={null}
payrails.on("success", (e) => showConfirmation(e.executionId));
payrails.on("pending", (e) => showOrderReceived(e.executionId));
payrails.on("failed", (e) => showError(e.data?.code));
```
Subscribe to all three for every method. Which one fires depends on the payment method and the provider your workflow routes it to.
One listener fires for every payment method in the session. If your page mounts other payment elements too, filter with `e.paymentMethodCode`.
Which events fire depends on the method's result type in the [supported payment methods](#supported-payment-methods) table:
| Result type | What the shopper sees after clicking | Events |
| - | - | - |
| Approve in app | A wait screen while they approve in another app | `success` or `failed` |
| Pending | Nothing more, the SDK submits the payment | `pending` or `failed` |
### Approve-in-app methods
The shopper approves the payment in another app, such as their MB WAY app. The SDK handles the wait for you: the form switches to a screen with the payment method's logo and instructions, the SDK removes the button, and then waits for the shopper to approve. When they do, the wait screen goes away and `success` fires. Show your own confirmation.
### Pending methods
The payment completes after checkout, so the SDK doesn't have the result yet when the shopper clicks. Treat `pending` as "order placed" and rely on your webhook for the final status.
## 5. Handle a declined or expired approve-in-app payment
If the shopper declines in the app, or doesn't approve in time, the wait screen switches back to the form. The form keeps the shopper's details, and the button reappears. The SDK then fires `sessionExpired`, followed by `failed` with `AUTHORIZATION_ERROR`. Return fresh init options from `sessionExpired` so the shopper's retry starts a new payment:
```js theme={null}
payrails.on("sessionExpired", async () => fetchNewInitResponse());
```
Sometimes the SDK can't confirm the result, for example because the session expired during the wait. The same thing happens, but `failed` carries `AUTHENTICATION_ERROR` or `UNKNOWN_ERROR`. The shopper may still approve in their app afterwards, so check the payment's status before you let them retry, or they could pay twice:
```js theme={null}
payrails.on("failed", async (e) => {
const code = e.data?.code;
if (code === "AUTHENTICATION_ERROR" || code === "UNKNOWN_ERROR") {
await checkPaymentStatus(e.executionId);
}
});
```
Your webhook or an [execution lookup](/reference/getexecution) has the payment's final status.
## 6. Customize the form and button
The form and the button each take their own `appearance`. Form rules cross into the iframe and apply to the `.payrails-*` classes, and rules under `actionScreen` style only the wait screen of an approve-in-app method:
```js theme={null}
const form = payrails.apmForm({
paymentMethodCode: "mbWay",
appearance: {
rules: {
".payrails-input": { padding: "12px", borderRadius: "8px" },
".payrails-field--invalid .payrails-input": { borderColor: "#d33" },
},
actionScreen: {
rules: { ".payrails-text": { textAlign: "center" } },
},
},
translations: {
errors: { required: "This field is required" },
},
});
```
`translations.errors` replaces the form's default error messages. The [appearance reference](/docs/orchestration/checkout-sdks/web/references/appearance-api-reference#apmformappearance) lists every class.
## Offer more than one method
Create a form and button pair per method, each in its own containers. Element ids default to one per payment method, so two methods on the same page don't collide. Pass `id` only if you mount two forms for the same method.
```js theme={null}
const sepaForm = payrails.apmForm({ paymentMethodCode: "sepaDirectDebit" });
await sepaForm.mount("#sepa-form");
payrails.apmButton({ form: sepaForm }).mount("#sepa-button");
```
For every option and event, refer to the [`payrails` reference](/docs/orchestration/checkout-sdks/web/references/payrails-api-reference#apmform-options) and the [events reference](/docs/orchestration/checkout-sdks/web/references/event-api-reference).
# How to accept card payments
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/guides/how-to-accept-card-payments
Mount a PCI-compliant card form with the Payrails web SDK, wire it to a payment button, and handle the authorization result.
This guide shows you how to mount a PCI-compliant card form, wire it to a payment button, and handle the payment result.
The card form and the payment button are two separate elements created from the same `payrails` client. The client wires them together for you: the button stays disabled until the form is valid, and clicking it validates the form, encrypts the card data, and starts the authorization.
## 1. Add containers to your page
```html theme={null}
```
## 2. Mount the card form
```js theme={null}
const cardForm = payrails.cardForm({
showCardHolderName: true,
});
cardForm.on("ready", () => console.log("Card form rendered"));
cardForm.on("change", ({ isValid, cardNetwork }) => {
console.log("Form valid:", isValid, "network:", cardNetwork);
});
cardForm.mount("#card-form");
```
By default the form renders a card number field, separate expiry month and expiry year fields, and a CVV field. The most commonly used `CardFormOptions`:
| Option | Effect |
| - | - |
| `showCardHolderName` | Adds a cardholder name field (off by default). |
| `showSingleExpiryDateField` | Replaces the separate month/year fields with a single `MM/YY` field. |
| `showStoreInstrumentCheckbox` | Renders a "save this card" checkbox; its state is sent as `storeInstrument` with the payment. |
| `translations` | Per-field `placeholders` and `labels`, plus default error messages. |
| `appearance` | CSS rules keyed by the SDK's stable class names — see [customize the appearance](/docs/orchestration/checkout-sdks/web/guides/how-to-customize-the-checkouts-appearance). |
| `layout` | Custom field arrangement as rows of field names, e.g. `[['CARD_NUMBER'], ['EXPIRATION_DATE', 'CVV']]`. |
| `fonts` | Custom font descriptors for the secure fields. |
For cards issued in Argentina, Brazil, Chile, and Mexico, the card form can also collect the cardholder's tax ID. Refer to [How to collect tax IDs for card payments](/docs/orchestration/checkout-sdks/web/guides/how-to-collect-tax-ids-for-card-payments).
For the full option and event list see the [`payrails` reference](/docs/orchestration/checkout-sdks/web/references/payrails-api-reference) and the [events reference](/docs/orchestration/checkout-sdks/web/references/event-api-reference).
## 3. Mount the payment button
```js theme={null}
const paymentButton = payrails.paymentButton({
translations: { label: "Pay now" },
appearance: {
rules: {
".payrails-button": { backgroundColor: "#1a1a1a", color: "#ffffff" },
".payrails-button--disabled": { opacity: "0.5" },
},
},
});
paymentButton.mount("#pay-button");
```
The button starts disabled and enables automatically once the card form is valid. Pass `disabledByDefault: false` if you want it clickable immediately (the form is still validated on click). While the payment is in flight the button shows a loading indicator and carries the `.payrails-button--loading` class.
## 4. Handle the payment result
Payment outcomes are instance events — subscribe on the `payrails` client:
```js theme={null}
payrails.on("success", () => {
// payment authorized — show your confirmation page
});
payrails.on("failed", (e) => {
// e.data: { code?: string, message?: string }
console.error(`Payment failed (${e.data?.code}): ${e.data?.message}`);
});
payrails.on("pending", () => {
// authorization accepted but not final yet — show a pending state
});
```
* `success` — the payment was authorized.
* `failed` — the payment failed. `e.data?.code` is one of the `AuthorizationFailureReasons` values (`VALIDATION_FAILED`, `AUTHORIZATION_ERROR`, `AUTHENTICATION_ERROR`, `USER_CANCELLED`, `UNKNOWN_ERROR`), importable from `@payrails/web-sdk`.
* `pending` — the authorization is still processing and no further shopper action is required.
One listener fires for every payment method in the session; if your page mounts other payment elements too, filter with `e.paymentMethodCode === 'card'`.
3D Secure challenges are handled by the SDK automatically; to observe or take over the challenge, subscribe to `payrails.on('actionRequired', ...)` - see the [events reference](/docs/orchestration/checkout-sdks/web/references/event-api-reference).
## 5. React to form and button state (optional)
Two button-specific events help you build custom UI around the flow:
```js theme={null}
paymentButton.on("stateChanged", ({ state }) => {
// 'enabled' | 'disabled' — mirror the button state elsewhere in your UI
});
paymentButton.on("validate", ({ isValid, error, fieldErrors }) => {
// fires after a click validates the card form
});
```
To gate the payment right before it starts (e.g. run a last check and cancel), use the cancelable instance events:
```js theme={null}
payrails.on("buttonClicked", async (event) => {
if (!(await lastCheckPasses())) event.preventDefault();
});
```
To move focus to the first invalid field yourself (for example from your own "Pay" flow), call `cardForm.focus()`. `cardForm.isValid` exposes the current validity synchronously.
# How to add Apple Pay and Google Pay
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/guides/how-to-add-apple-pay-and-google-pay
Check wallet availability, mount the Apple Pay and Google Pay buttons, and handle payment results and express-checkout address changes.
This guide shows you how to check wallet availability, mount the Apple Pay and Google Pay buttons, and handle payment results and express-checkout address changes.
Both wallets must be enabled for your workflow on the Payrails side; the buttons draw their configuration (supported networks, merchant capabilities, gateway parameters) from the init response.
## 1. Check availability
Only render a wallet button when the shopper's browser and device support it:
```js theme={null}
// Google Pay — optionally override the display name (the merchant ID
// always comes from the backend configuration)
const googlePayReady = await payrails.isGooglePayAvailable("Your Store");
// Apple Pay — Safari / Apple devices only
const applePayReady = await payrails.isApplePayAvailable();
```
Both return a promise resolving to `true` or `false`. Alternatively, construct the button first and read its own capability check — every express button exposes `readonly isAvailable: Promise`:
```js theme={null}
const googlePayButton = payrails.googlePayButton({ ... });
if (await googlePayButton.isAvailable) {
googlePayButton.mount('#google-pay-button');
}
```
The button's `isAvailable` uses the same underlying check and never rejects (resolves to `false` on any check-side failure).
## 2. Mount the Google Pay button
```html theme={null}
```
```js theme={null}
const googlePayButton = payrails.googlePayButton({
merchantName: "Your Store",
styles: {
buttonColor: "black", // default 'black'
buttonType: "long", // default 'pay'
buttonSizeMode: "fill", // default 'fill'
locale: "en",
},
});
// Reveal the container only when Google Pay is usable in this browser/device.
googlePayButton.isAvailable.then((available) => {
if (available) {
document.getElementById("google-pay-button").hidden = false;
googlePayButton.mount("#google-pay-button");
}
});
// Payment outcomes ride on the instance emitter; filter by paymentMethodCode.
payrails.on("success", (e) => {
if (e.paymentMethodCode === "googlePay") {
// payment authorized
}
});
payrails.on("failed", (e) => {
if (e.paymentMethodCode === "googlePay")
console.error("Google Pay failed", e);
});
```
Other options worth knowing:
* `showStoreInstrumentCheckbox` / `defaultStoreInstrumentState` — render a "save for future payments" checkbox under the button.
* `redirectFor3DS` and `returnInfo` — control how a 3D Secure redirect returns to your site.
* The environment (`TEST` / `PRODUCTION`) is resolved from the server-provided SDK configuration passed into `Payrails.init(initResponse)`.
## 3. Mount the Apple Pay button
```html theme={null}
```
```js theme={null}
const applePayButton = payrails.applePayButton({
styles: {
type: "buy", // default 'buy' — also 'plain', 'checkout', 'donate', …
style: "black", // default 'black' — also 'white', 'whiteOutline', 'automatic'
locale: "en",
},
translations: {
labels: {
paymentScreenLabel: "Your Store", // label shown in the Apple Pay sheet
},
},
});
// Reveal the container only when Apple Pay is usable in this browser/device.
applePayButton.isAvailable.then((available) => {
if (available) {
document.getElementById("apple-pay-button").hidden = false;
applePayButton.mount("#apple-pay-button");
}
});
payrails.on("success", (e) => {
if (e.paymentMethodCode === "applePay") {
// payment authorized
}
});
payrails.on("failed", (e) => {
if (e.paymentMethodCode !== "applePay") return;
if (e.data?.code === "USER_CANCELLED") {
// shopper closed the Apple Pay sheet — not an error
return;
}
console.error("Apple Pay failed", e);
});
```
Notes:
* When the shopper cancels the Apple Pay sheet, the `failed` event fires with `data.code === 'USER_CANCELLED'` so you can distinguish cancellation from a real failure.
* `abortAfterAuthorizeFailed: true` closes the sheet on authorization failure instead of showing Apple's inline failure state.
* `showStoreInstrumentCheckbox` works the same way as for Google Pay.
The `success`/`failed`/`pending` events are shared with all other payment elements — one listener covers the whole session; filter by `e.paymentMethodCode` as shown above.
## 4. Validate the delivery address in express checkout (Apple Pay)
When your Apple Pay configuration requires shipping contact fields, the SDK forwards address changes from the payment sheet to the instance-level `deliveryAddressChanged` event. Call `event.preventDefault()` to reject the address (the sheet shows "Delivery to this address is not supported." and asks the shopper to pick another); do nothing to accept it:
```js theme={null}
payrails.on("deliveryAddressChanged", (event) => {
const address = event.deliveryAddress;
// Before authorization Apple only exposes redacted fields:
// city, postalCode, state, and country.code.
if (!isServiceableRegion(address?.postalCode, address?.country?.code)) {
event.preventDefault();
}
});
```
Keep this handler fast — Apple aborts the sheet if it is not answered within roughly 30 seconds, so use it for region/ZIP checks rather than a full cart recompute. The full, unredacted address only becomes available after the shopper authorizes the payment. The same event also fires for PayPal express checkout (with PayPal's payload shape).
# How to collect card data with secure fields
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/guides/how-to-collect-card-data-with-secure-fields-collect-container
Build your own card form field by field with a collect container, validate the secure inputs, and collect an encrypted payload.
This guide shows you how to build your own card form field by field: create a collect container, mount individual secure input elements into your page, validate them, and collect the card data as an encrypted payload - or save it as a reusable payment instrument.
Secure fields are pre-built form elements hosted by Payrails and injected into your web page as iframes. Sensitive card data never touches your front-end application, which reduces your PCI compliance scope.
## 1. Create a collect container
Create a container for the secure fields with the `collectContainer()` method of the Payrails client:
```js theme={null}
const container = payrails.collectContainer({});
```
You choose how the fields end up on the page by how you mount them:
* mount each element individually wherever you want on the page (`element.mount(selector)`) — use this when you control the markup around every field, or
* mount the container once with `container.mount(selector)` to render all created elements into a single wrapper.
The container accepts an optional `containerId` and `fonts` — see [How to customize the checkout's appearance](/docs/orchestration/checkout-sdks/web/guides/how-to-customize-the-checkouts-appearance) for font loading. Styling happens per element via `appearance` (step 2).
## 2. Create collect elements
Create one element per field with `createCollectElement(options)`:
```js theme={null}
import { ElementType } from "@payrails/web-sdk";
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
label: "Card number",
placeholder: "1234 1234 1234 1234",
required: true,
});
```
The options object:
```js theme={null}
const options = {
type: ElementType.CARD_NUMBER, // required, one of the ElementType values
label: "Card number", // optional label rendered above the field
placeholder: "1234 1234 1234 1234", // optional placeholder
required: false, // optional, defaults to false
enableCardIcon: true, // optional, card-brand icon in CARD_NUMBER (default false for standalone collect elements)
format: "MM/YY", // optional, only for EXPIRATION_DATE / EXPIRATION_YEAR
appearance: { rules: {} }, // optional CSS rules applied inside the field's iframe
translations: { error: { default: "Invalid value" } }, // optional custom default error message
};
```
The `type` field takes a Payrails `ElementType`. Each type applies the
appropriate formatting and validations to the field:
* `CARD_NUMBER`
* `CARDHOLDER_NAME`
* `CVV`
* `EXPIRATION_MONTH`
* `EXPIRATION_YEAR`
* `EXPIRATION_DATE`
The `format` values accepted for `EXPIRATION_DATE` are:
* `MM/YY` (default)
* `MM/YYYY`
* `YY/MM`
* `YYYY/MM`
The `format` values accepted for `EXPIRATION_YEAR` are:
* `YY` (default)
* `YYYY`
If `format` is not specified, or an invalid value is passed, the default is used.
For the `appearance` rules syntax, class names, and state modifiers (`.payrails-input--invalid`, …) see [How to customize the checkout's appearance](/docs/orchestration/checkout-sdks/web/guides/how-to-customize-the-checkouts-appearance).
## 3. Mount the elements to the DOM
Create placeholder `
` elements with unique `id` attributes where the fields should render:
```html theme={null}
```
Then mount each element into its placeholder:
```js theme={null}
cardNumber.mount("#cardNumber");
```
Use `unmount()` to remove an element from the DOM:
```js theme={null}
cardNumber.unmount();
```
## 4. React to element events (optional)
Every element exposes the typed `on(eventName, handler)` method shared by all SDK elements; secure fields emit `'change'`, `'focus'`, `'blur'`, and `'ready'` . `on()` returns an unsubscribe function:
```js theme={null}
cardNumber.on("ready", () => {
// the secure iframe has loaded and the field is interactive
});
cardNumber.on("change", (state) => {
// state: { isValid, isEmpty, ... }
// for CARD_NUMBER, state also carries the detected card network
// and the BIN (first digits of the card number)
console.log(state.isValid, state.network, state.bin);
});
```
## 5. Validate and collect the data
When the form is ready to be submitted, you can first validate all fields:
```js theme={null}
const { isValid, error, fieldErrors } = await container.validate();
```
Then call `collect()` on the container. All created elements must be mounted. The card data is encrypted inside the secure iframes and returned as an opaque payload - the raw values are never exposed to your code:
```js theme={null}
const result = await container.collect();
// result: { encryptedData: string, vaultProviderConfigId: string }
```
`collect()` rejects with a validation error when a field is incomplete or invalid.
## 6. Save the card as a payment instrument (optional)
To store the collected card for later payments, call `tokenize()` instead of `collect()`. It collects and encrypts the data, then saves it to Payrails as a payment instrument for the customer (holder) the SDK session was initialized for:
```js theme={null}
const instrument = await container.tokenize({
storeInstrument: true, // defaults to false
futureUsage: "CardOnFile", // 'CardOnFile' (default) | 'Subscription' | 'UnscheduledCardOnFile'
});
console.log("Saved instrument id:", instrument.id);
```
The response contains the new instrument's `id`, `status`, and card metadata such as `data.bin`, `data.network`, and `data.suffix`.
## End-to-end example
```js theme={null}
import { ElementType } from "@payrails/web-sdk";
// Step 1
const container = payrails.collectContainer({});
// Step 2
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
label: "Card number",
placeholder: "1234 1234 1234 1234",
required: true,
appearance: {
rules: {
".payrails-input": { color: "#1d1d1d" },
".payrails-label": { fontSize: "12px", fontWeight: "bold" },
".payrails-error": { color: "#f44336" },
},
},
});
const expiryDate = container.createCollectElement({
type: ElementType.EXPIRATION_DATE,
format: "MM/YY",
});
const cvv = container.createCollectElement({
type: ElementType.CVV,
});
// Step 3 — assumes divs with these ids exist on the page
cardNumber.mount("#cardNumber");
expiryDate.mount("#expiryDate");
cvv.mount("#cvv");
// Step 5
const { encryptedData, vaultProviderConfigId } = await container.collect();
// or Step 6 — save the card instead
const instrument = await container.tokenize({ storeInstrument: true });
```
## Default validations
Every element type has a set of built-in validations:
* `CARD_NUMBER`: card number validation with checksum (Luhn algorithm) and card-scheme-aware length and formatting. A valid 16 digit card number is formatted as `XXXX XXXX XXXX XXXX`.
* `CARDHOLDER_NAME`: name should be 2 or more symbols; valid characters match the pattern `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$`.
* `CVV`: 3–4 digits.
* `EXPIRATION_DATE`: any date starting from the current month, in the configured `format` (default `MM/YY`).
* `EXPIRATION_MONTH`: a valid month (`01`–`12`).
* `EXPIRATION_YEAR`: the current year or later, in the configured `format`(default `YY`).
## UI errors for collect elements
You can display custom error messages on the elements with `setError` and `resetError`.
`setError(error: string)` overrides any current error on the element with the custom message. The message stays visible until `resetError()` is called on the same element.
`resetError()` clears the custom error message set by `setError`.
```js theme={null}
const container = payrails.collectContainer({});
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
});
// set a custom error
cardNumber.setError("custom error");
// reset the custom error
cardNumber.resetError();
```
# How to collect tax IDs for card payments
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/guides/how-to-collect-tax-ids-for-card-payments
Collect the cardholder's tax ID in the web SDK card form when the card's issuer requires one, for Argentina, Brazil, Chile, and Mexico.
Card issuers in some countries require the cardholder's tax ID to authorize a payment. The web SDK collects it for you. When the shopper enters a card from one of these countries, the card form shows a tax ID field. It validates the document and sends it with the payment.
This guide shows you how to enable tax ID collection, customize the field, and track its state. It builds on [How to accept card payments](/docs/orchestration/checkout-sdks/web/guides/how-to-accept-card-payments).
## Requirements
* Web SDK version 6.7.0 or later.
* An integration with the [card form or the drop-in](/docs/orchestration/checkout-sdks/web/guides/how-to-accept-card-payments). Secure Fields integrations don't collect the tax ID.
* Tax ID collection and BIN lookup enabled for your account. Contact your Payrails account manager and list the issuer countries you need.
## How the field works
You don't add the tax ID field to your page. Once Payrails enables tax ID collection for your account, the card form handles it in these steps:
1. The shopper types the card number, and the SDK looks up the card's issuer country.
2. If the issuer country is one of your enabled countries, the card form shows the tax ID field.
3. If the cardholder name field isn't shown already, the card form adds it.
4. The shopper types the tax ID. The field formats it as they type and shows an error on blur if the document isn't valid.
5. The shopper can't click the payment button until the tax ID is valid. On click, the SDK sends the tax ID with the authorization request.
For a card issued in Brazil, the card form shows the cardholder name and the CPF or CNPJ field:
If the shopper changes the card number, the card form hides the field and clears its value. The field shows again if the new card also requires a tax ID.
Cards from other countries don't show the field, and the SDK sends no tax ID for them.
## Supported countries and documents
The supported countries depend on your web SDK version. Version 6.7.0 supports these documents:
| Country | Document | Format |
| - | - | - |
| Argentina | CUIT | `00-00000000-0` |
| Brazil | CPF (individuals) or CNPJ (companies), in the same field | CPF `000.000.000-00`, CNPJ `00.000.000/0000-00` |
| Chile | RUT | `00.000.000-0`, where the last character can be `K` |
| Mexico | RFC | 12 characters for companies, 13 for individuals, no separators |
The shopper can type the document with or without separators, and the field adds them automatically. The SDK validates the format of every document, and the check digit of CPF, CNPJ, CUIT, and RUT.
If your account enables a country that your SDK version doesn't support, the card form doesn't show the field for that country and logs a warning to the browser console. Upgrade the web SDK to collect tax IDs for that country.
## Customize the field text
By default, the field text is in Portuguese for Brazilian cards and in Spanish for cards from Argentina, Chile, and Mexico. To replace it, pass these keys in `translations.labels`:
| Key | Replaces |
| - | - |
| `taxIdTitle` | The field label, for example "CPF/CNPJ do titular" |
| `taxIdSubtitle` | The line that explains why the issuer requires the document |
| `taxIdPlaceholder` | The hint text in the empty input |
| `taxIdError` | The error message for an invalid document |
Each value replaces the default for every country, so write text that fits all the countries you enable.
```js theme={null}
const cardForm = payrails.cardForm({
translations: {
labels: {
taxIdTitle: "Cardholder tax ID",
taxIdSubtitle: "Your card issuer requires this document.",
taxIdError: "Enter a valid tax ID",
},
},
});
```
```js theme={null}
const dropin = payrails.dropin({
translations: {
cardForm: {
labels: {
taxIdTitle: "Cardholder tax ID",
taxIdSubtitle: "Your card issuer requires this document.",
taxIdError: "Enter a valid tax ID",
},
},
},
});
```
## Style the field
Style the field with the `taxId` slot of the card form's appearance. It accepts the same `rules` as the rest of the card form. The field uses the shared class names `.payrails-label`, `.payrails-text`, `.payrails-input` and `.payrails-error`, and the input carries the `--empty`, `--dirty`, `--valid` and `--invalid` state modifiers.
```js theme={null}
const cardForm = payrails.cardForm({
appearance: {
taxId: {
rules: {
".payrails-input": { borderRadius: "8px" },
".payrails-input--invalid": { borderColor: "#d32f2f" },
},
},
},
});
```
```js theme={null}
const dropin = payrails.dropin({
appearance: {
cardForm: {
taxId: {
rules: {
".payrails-input": { borderRadius: "8px" },
".payrails-input--invalid": { borderColor: "#d32f2f" },
},
},
},
},
});
```
For the full rule syntax, refer to [How to customize the checkout's appearance](/docs/orchestration/checkout-sdks/web/guides/how-to-customize-the-checkouts-appearance).
## Read the tax ID
The card form's `change` event includes the tax ID as `taxId`, without separators. `taxId` updates as the shopper types, so check `isValid` before you use it. When the card doesn't require a tax ID, `taxId` is an empty string.
```js theme={null}
cardForm.on("change", ({ isValid, taxId }) => {
// taxId: "52998224725" while the shopper sees "529.982.247-25"
// isValid: false while a required tax ID is missing or invalid
});
```
The drop-in doesn't emit `change`. With the drop-in, subscribe to `taxIdChanged` instead. Its `value` is the tax ID without separators, and `isValid` reports whether the document is valid.
## What the SDK sends
The SDK sends the tax ID without separators in the authorization request, as `taxId` inside the card's `paymentInstrumentData`. When the card doesn't require a tax ID, the request has no `taxId` field.
## Save the tax ID with the instrument
To reuse the tax ID in later payments with the stored card, Payrails must save it with the instrument. This applies to both customer-initiated card-on-file payments and merchant-initiated transactions (MITs).
Newly created workflow configurations already save the tax ID: their template includes the mapping below. Existing workflow configurations may not have it. Make sure the inputs of the [**Create or get instrument**](/docs/orchestration/workflow-studio/actions#create-instrument) action include this mapping:
| From | To |
| - | - |
| `output.authorize.paymentComposition[0].paymentInstrumentData.taxId` | `data.taxId` |
# How to customize the checkout's appearance
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/guides/how-to-customize-the-checkouts-appearance
Style the card form, payment buttons, drop-in, and collect elements with the appearance option, and load custom fonts.
This guide shows you how to style the pre-built card form, payment buttons, drop-in, and individual collect elements with the `appearance` option, how to change their labels and placeholders, and how to load custom fonts. It assumes you have already initialized the SDK and hold a `payrails` client instance - see the [setup guide](/docs/orchestration/checkout-sdks/web) if you have not.
## 1. The base stylesheet
The pre-built components (drop-in, card form, buttons, card list) ship a base stylesheet. It is injected automatically when the SDK loads - no manual import is needed.
Everything below customizes on top of these base styles.
## 2. The `appearance` option
Every element the SDK draws accepts an `appearance` object. Its `rules` field is CSS-like key/value: selectors on the outside, CSS declarations on the inside:
```typescript theme={null}
const paymentButton = payrails.paymentButton({
appearance: {
rules: {
".payrails-button": { backgroundColor: "#1a1a1a", color: "#fff" },
".payrails-button:hover": { backgroundColor: "#333" },
".payrails-button--disabled": { opacity: "0.5" },
},
},
});
```
Selectors target stable class names the SDK guarantees on the DOM:
`.payrails-input`, `.payrails-button`, `.payrails-dropdown`, `.payrails-label`, `.payrails-tile`, `.payrails-container`, `.payrails-row`, `.payrails-cell`, `.payrails-icon`, `.payrails-checkbox`, `.payrails-error`, `.payrails-text`.
State variants use BEM modifiers on those classes:
| State | Class |
| - | - |
| Field with invalid input | `.payrails-input--invalid` |
| Field with valid input | `.payrails-input--valid` |
| Field is empty | `.payrails-input--empty` |
| Field touched since load | `.payrails-input--dirty` |
| Button in loading state | `.payrails-button--loading` |
| Button disabled | `.payrails-button--disabled` |
| Checkbox checked | `.payrails-checkbox--checked` |
| Brand tile selected | `.payrails-tile--selected` |
Any selector a browser supports is valid inside `rules` — pseudo-classes and pseudo-elements (`:hover`, `:focus`, `:focus-visible`, `::placeholder`, `::selection`), media queries (`@media`), and feature queries (`@supports`).
The `--invalid` and `--valid` classes clear while a field is focused, so a field being corrected does not show the error state. For a persistent invalid look on touched fields, key off `--dirty`:
```typescript theme={null}
rules: {
'.payrails-input--dirty:not(:focus).payrails-input--invalid': {
borderColor: '#dc2626',
},
}
```
Rules are scoped to the element instance that declares them — a rule passed to one card form does not leak into another widget. SDK defaults live in the CSS layer `@layer payrails-defaults`; your rules land in
`@layer payrails-appearance`, which always wins over the defaults.
## 3. Style the card form
`payrails.cardForm(options)` accepts a `CardFormAppearance` — root `rules` for the form itself plus one slot per nested widget (installments dropdown, address selector, brand selector):
```typescript theme={null}
const cardForm = payrails.cardForm({
appearance: {
rules: {
".payrails-container": { maxWidth: "480px", gap: "16px" },
".payrails-input": {
border: "1px solid #eae8ee",
borderRadius: "4px",
color: "#1d1d1d",
},
".payrails-input:focus": { borderColor: "#6b7cff" },
".payrails-input--invalid": {
color: "#f44336",
backgroundColor: "#fdeaea",
},
".payrails-label": { fontSize: "12px", fontWeight: "bold" },
".payrails-error": { color: "#f44336" },
},
installments: {
rules: { ".payrails-dropdown": { borderRadius: "8px" } },
},
address: {
rules: { ".payrails-dropdown, .payrails-input": { borderRadius: "8px" } },
},
brandSelector: {
rules: { ".payrails-tile--selected": { borderColor: "#4F46E5" } },
},
},
});
cardForm.mount("#card-form");
```
The card payment button is a sibling of the card form, not a child — pass its appearance to `payrails.paymentButton({ appearance })` directly, or to the `cardPaymentButton` slot of the drop-in's appearance.
## 4. Style the drop-in
The drop-in's appearance is keyed by its building blocks; each slot takes the same shape as the standalone element:
```typescript theme={null}
const dropin = payrails.dropin({
appearance: {
rules: {
".payrails-container": { borderRadius: "12px" },
},
cardForm: {
rules: { ".payrails-input": { border: "1px solid #ddd" } },
},
cardPaymentButton: {
rules: { ".payrails-button": { backgroundColor: "#4F46E5" } },
},
loadingScreen: { rules: {} },
authSuccess: { rules: {} },
authFailed: { rules: {} },
},
});
```
## 5. Provider-drawn wallet buttons keep their own options
Google Pay and PayPal draw their own chrome (a native SDK button, a hosted iframe), so CSS `rules` can't reach them. They take structured `appearance.settings` instead:
```typescript theme={null}
// Google Pay
const googlePayButton = payrails.googlePayButton({
appearance: {
settings: {
buttonColor: "black", // per Google Pay ButtonColor
buttonType: "buy", // per Google Pay ButtonType
buttonSizeMode: "fill",
locale: "de",
},
},
});
// PayPal
const paypalButton = payrails.paypalButton({
appearance: {
settings: {
color: "gold", // 'gold' | 'blue' | 'silver' | 'white' | 'black'
shape: "rect", // 'rect' | 'pill'
label: "paypal",
height: 40,
tagline: false,
locale: "de_DE",
},
},
});
```
## 6. Change labels, placeholders, and error messages
Text on the card form is controlled through `translations`:
```typescript theme={null}
const cardForm = payrails.cardForm({
showStoreInstrumentCheckbox: true,
translations: {
placeholders: {
CARD_NUMBER: "1234 1234 1234 1234",
CVV: "CVC",
},
labels: {
CARD_NUMBER: "Card number",
CVV: "Security code",
storeInstrument: "Save this card for next time",
},
error: {
default: {
CARD_NUMBER: "Please check the card number",
},
},
},
});
```
* `placeholders` and `labels` are keyed by field type (`CARD_NUMBER`, `CARDHOLDER_NAME`, `CVV`, `EXPIRATION_MONTH`, `EXPIRATION_YEAR`, `EXPIRATION_DATE`).
* `labels.storeInstrument` renames the save-card checkbox label.
* `error.default` sets the default validation error message per field type.
The payment button's label is `translations.label` on `payrails.paymentButton(...)`.
## 7. Load custom fonts
The secure fields render inside iframes, so fonts from your page are not automatically available there. Pass font descriptors via `fonts` — either a CSS source or an explicit font-face definition:
```typescript theme={null}
const cardForm = payrails.cardForm({
fonts: [
{
family: "Inter",
src: "url('https://example.com/fonts/inter.woff2')",
weight: 400,
style: "normal",
},
],
appearance: {
rules: {
".payrails-input": { fontFamily: "Inter, sans-serif" },
},
},
});
```
A font descriptor accepts `family`, `src`, `style`, `weight`, `unicodeRange`, `display`, or a `cssSrc` URL. The same `fonts` option is accepted by `payrails.collectContainer(...)`.
## 8. Style individual collect elements
When you build your own form with a collect container (see [How to collect card data with secure fields](/docs/orchestration/checkout-sdks/web/guides/how-to-collect-card-data-with-secure-fields-collect-container)),
each element accepts its own `appearance`; the rules are applied inside that field's secure iframe:
```typescript theme={null}
const cardNumber = container.createCollectElement({
type: ElementType.CARD_NUMBER,
appearance: {
rules: {
".payrails-input": {
border: "1px solid #eae8ee",
padding: "10px 16px",
borderRadius: "4px",
color: "#1d1d1d",
},
".payrails-input--valid": { color: "#4caf50" },
".payrails-input--invalid": { color: "#f44336" },
".payrails-label": { fontSize: "12px", fontWeight: "bold" },
".payrails-error": { color: "#f44336" },
},
},
});
```
Only the root `rules` are forwarded to the iframe — secure fields have no nested widget slots.
## 9. Update appearance and texts after mounting
The card form can be restyled and re-labeled at runtime without remounting:
```typescript theme={null}
cardForm.update({
appearance: {
rules: { ".payrails-input": { borderColor: "#6b7cff" } },
},
translations: {
labels: { CARD_NUMBER: "Kartennummer" },
},
});
```
Updates accumulate: a partial `appearance` merges over the current rules, leaving other selectors and nested widget slots (`installments`, `address`, `brandSelector`) intact. To clear a declaration, pass its key with an empty value; clearing a whole selector requires a remount.
The card payment button supports `paymentButton.update({ translations })` for its label — its appearance cannot be changed after mounting.
# Appearance API reference
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/references/appearance-api-reference
Reference for the web SDK appearance option: option types, rule semantics, and the CSS classes and state modifiers on the DOM.
Reference for the `appearance` option of `@payrails/web-sdk`: the option types,
rule semantics, and the class-name contract — which CSS classes and state
modifiers the SDK guarantees on the DOM.
```js theme={null}
payrails.cardForm({
appearance: {
rules: {
".payrails-input": { border: "1px solid #eae8ee" },
".payrails-input--invalid": { borderColor: "#dc2626" },
"@media (max-width: 480px)": {
".payrails-container": { gap: "8px" },
},
},
},
});
```
## Types
All types are exported from `@payrails/web-sdk`.
### `Appearance`
```ts theme={null}
interface Appearance {
rules?: AppearanceRules;
}
```
The base shape accepted by every element that draws its own UI.
### `AppearanceRules`
```ts theme={null}
interface AppearanceRules {
[selectorOrAtRule: string]: AppearanceDeclarations | AppearanceRules;
}
type AppearanceDeclarations = Record;
```
Keys are CSS selectors or at-rules; values are declaration maps. Under an
at-rule key (`@media …`, `@supports …`) the value is a nested `AppearanceRules`
map.
### `CardFormAppearance`
```ts theme={null}
interface CardFormAppearance extends Appearance {
installments?: Appearance;
address?: Appearance;
brandSelector?: Appearance;
}
```
The card form's nested widgets each take their own `Appearance` under a named
key. The card payment button is a sibling of the card form, not a child — it has
no slot here.
### `DropinAppearance`
```ts theme={null}
interface DropinAppearance extends Appearance {
cardForm?: CardFormAppearance;
cardPaymentButton?: Appearance;
loadingScreen?: Appearance;
authSuccess?: Appearance;
authFailed?: Appearance;
}
```
Root `rules` apply across the drop-in; each building block listed above takes
its own appearance under a matching key. The exported interface also declares
reserved keys (`leanButton`, `orderSummary`, `billingAddressForm`,
`termsAndConditions`, the wallet buttons) and an open index signature — keys not
listed above are accepted by the compiler but currently have no effect. The
provider-drawn wallet buttons (Google Pay, Apple Pay, PayPal) are not stylable
through `appearance` — their chrome is configured via
`paymentMethodsConfiguration..styles`
### `ApmFormAppearance`
```ts theme={null}
interface ApmFormAppearance extends Appearance {
actionScreen?: Appearance;
}
```
Root `rules` style the APM form's fields and every screen the form shows after
the payment is submitted, such as the wait screen of an approve-in-app method.
`actionScreen` `rules` apply only while such a screen is shown, and win over
root `rules` for the same element.
### Where each shape is accepted
| Factory | Option field | Shape |
| - | - | - |
| `payrails.cardForm` | `appearance` | `CardFormAppearance` |
| `payrails.dropin` | `appearance` | `DropinAppearance` |
| `payrails.apmForm` | `appearance` | `ApmFormAppearance` |
| `payrails.paymentButton` | `appearance` | `Appearance` |
| `payrails.cardList` | `appearance` | `Appearance` |
| `payrails.dynamicElement` | `appearance` | `Appearance` |
| `payrails.genericRedirectButton` | `appearance` | `Appearance` |
| `payrails.leanButton` | `appearance` | `Appearance` |
| `container.createCollectElement` | `appearance` | `Appearance` |
## Rule semantics
**Selectors.** Any selector the browser supports is valid: class selectors,
state modifiers, comma lists, descendant combinators, pseudo-classes (`:hover`,
`:focus`, `:focus-visible`, `:disabled`, `:autofill`), pseudo-elements
(`::placeholder`, `::selection`), and nested `@media` / `@supports` at-rules.
**Properties.** Any CSS property is valid. Write property names in camelCase
(`boxShadow`) or kebab-case (`box-shadow`) — camelCase is converted on emission.
Custom properties (`--my-var`) pass through unchanged. Values are strings or
numbers.
**No validation.** The SDK emits your rules verbatim as CSS and does not
currently validate selectors or declarations; anything the browser cannot parse
is silently ignored by the browser, not reported by the SDK.
**Scoping.** Each element's rules apply only within that element's own DOM
subtree. A rule passed to one card form does not affect another card form or any
other widget. Write selectors flat (`.payrails-input`, not a
descendant-of-the-widget path) — the SDK scopes them for you. Known limitation
in 6.0.0-RC: on the host page, scoped rules match descendants of the widget but
not the widget's own root node — for widgets that render a single node (the pay
buttons), host-page rules currently have no effect. Rules applied inside the
secure iframes (card form, collect elements) are not affected.
**Cascade.** SDK default styles live in the CSS layer `payrails-defaults`; your
rules are emitted into the layer `payrails-appearance`, which is declared after
it — your rules always beat the SDK defaults regardless of selector specificity.
Rules in your own external stylesheets are unlayered and beat both. In browsers
without CSS layer support (Safari before 15.4), the SDK's default styles are not
applied; your rules still are.
**Composition.** When a container and one of its children both style the same
node, the child's rules win; the container's rules fill in whatever the child
did not set.
**Secure fields.** Card data inputs render inside Payrails-hosted iframes. The
root `rules` of a card form or collect element are forwarded into the iframe and
applied there in addition to the host page; nested child keys (`installments`,
`address`, `brandSelector`) apply outside the iframe only.
## The class-name contract
The SDK stamps two kinds of class names:
* **Generic classes** (`.payrails-input`, `.payrails-button`, …) mark the UI
primitives and are listed below. They are the public styling surface, stable
within a major version.
* **Widget-specific classes** (for example `.payrails-card-form`) are internal.
They can change in any release and are not part of this contract.
### Generic classes
| Class | Marks |
| - | - |
| `.payrails-container` | The root wrapper of a widget (form, list, screen). |
| `.payrails-row` | A horizontal group (form row, summary line, tile row). |
| `.payrails-cell` | One slot inside a row. |
| `.payrails-field` | A labeled input group (label + input + error). |
| `.payrails-input` | A text input, including the secure card fields. |
| `.payrails-dropdown` | A select input. |
| `.payrails-label` | A text label. |
| `.payrails-text` | Body text (amounts, terms, subtitles). |
| `.payrails-error` | A validation error message. |
| `.payrails-button` | A pay button drawn by the SDK. |
| `.payrails-checkbox` | The save-instrument checkbox. |
| `.payrails-tile` | A selectable tile (card brand, saved card, tab option). |
| `.payrails-icon` | An icon or logo image. |
### State modifiers
State is expressed as BEM modifiers on the generic classes:
| Modifier | Applies when |
| - | - |
| `.payrails-input--invalid` | The input holds invalid input. |
| `.payrails-input--valid` | The input holds valid input. |
| `.payrails-input--empty` | The input is empty. |
| `.payrails-input--dirty` | The input has been edited at least once. |
| `.payrails-field--focused` | A field inside a secure iframe has focus. |
| `.payrails-field--invalid` | The field's input is invalid. |
| `.payrails-dropdown--invalid` | The select holds an invalid selection. |
| `.payrails-button--loading` | A payment is in flight. |
| `.payrails-button--disabled` | The button is disabled. |
| `.payrails-checkbox--checked` | The checkbox is checked. |
| `.payrails-tile--selected` | The tile is selected. |
On secure card fields, `--invalid` and `--valid` are removed while the field has
focus, so a shopper correcting a value does not see the error state; they are
re-evaluated on blur. A selector like `.payrails-input--invalid:focus` therefore
never matches a secure field. For a persistent invalid look on touched fields,
combine with `--dirty`:
```js theme={null}
rules: {
'.payrails-input--dirty:not(:focus).payrails-input--invalid': {
borderColor: '#dc2626',
},
}
```
`--empty` and `--dirty` exist on secure card fields only. Inputs in
schema-driven forms (`dynamicElement`, the billing address form) keep
`--invalid` / `--valid` while focused.
## Per-element contract
The tables below list which generic classes and modifiers appear in each
element's DOM. Native pseudo-classes apply everywhere they are valid.
### Card form — `cardForm({ appearance })`
| Part | Classes | Modifiers |
| - | - | - |
| Form root | `.payrails-container` | — |
| Field row / cell | `.payrails-row` / `.payrails-cell` | — |
| Field group | `.payrails-field` | `--focused`, `--invalid` |
| Card input | `.payrails-input` | `--invalid`, `--valid`, `--empty`, `--dirty` |
| Field label | `.payrails-label` | — |
| Card-brand icon | `.payrails-icon` | — |
| Error text | `.payrails-error` | — |
| Save-instrument row | `.payrails-cell` / `.payrails-checkbox` | `--checked` (on the checkbox) |
Nested widget slots on `CardFormAppearance`:
| Slot | Part | Classes | Modifiers |
| - | - | - | - |
| `installments` | Dropdown | `.payrails-cell`, `.payrails-dropdown` | — |
| `address` | Country / postal fields | `.payrails-cell`, `.payrails-label`, `.payrails-dropdown`, `.payrails-input` | `--invalid` |
| `brandSelector` | Title / subtitle | `.payrails-label` / `.payrails-text` | — |
| | Tile row | `.payrails-row` | — |
| | Brand tile | `.payrails-tile` (logo `.payrails-icon`, name `.payrails-label`) | `--selected` |
### Collect element (secure field) — `createCollectElement({ appearance })`
Rules apply inside the field's iframe.
| Part | Classes | Modifiers |
| - | - | - |
| Field group | `.payrails-field` | `--focused`, `--invalid` |
| Input | `.payrails-input` | `--invalid`, `--valid`, `--empty`, `--dirty` |
| Label | `.payrails-label` | — |
| Icon | `.payrails-icon` | — |
| Error text | `.payrails-error` | — |
### Card payment button — `paymentButton({ appearance })`
| Part | Classes | Modifiers |
| - | - | - |
| Button | `.payrails-button` | `--loading`, `--disabled` |
### Generic redirect button — `genericRedirectButton({ appearance })`
| Part | Classes | Modifiers |
| - | - | - |
| Button | `.payrails-generic-button`, `.payrails-button` | `--loading`, `--disabled` |
For Revolut Pay the button renders Revolut's own artwork configured via
`revolutOptions`; `appearance` does not reach that artwork.
### Lean button — `leanButton({ appearance })`
| Part | Classes | Modifiers |
| - | - | - |
| Button | `.payrails-button` | `--loading` |
The Lean button does not use `--disabled`. The Lean-hosted bank dialog is themed
via `dialogCustomization`, not `appearance`.
### Card list — `cardList({ appearance })`
| Part | Classes | Modifiers |
| - | - | - |
| List root | `.payrails-container` | — |
| Card entry | `.payrails-tile` | — |
| Card label | `.payrails-label` | — |
| Brand icon | `.payrails-icon` | — |
### Dynamic form — `dynamicElement({ appearance })`
Also the shape of the drop-in's billing address form.
| Part | Classes | Modifiers |
| - | - | - |
| Form root | `.payrails-container` | — |
| Field group | `.payrails-field` | `--invalid` |
| Input | `.payrails-input` | `--invalid`, `--valid` |
| Select | `.payrails-dropdown` | `--invalid` |
| Label | `.payrails-label` | — |
| Error text | `.payrails-error` | — |
| Tab row | `.payrails-row` | — |
| Tab option | `.payrails-tile` | `--selected` |
### APM form — `apmForm({ appearance })`
The form's `appearance` styles the form only. The button is a separate element
and takes its own `appearance` (see below), the same way the card form and the
payment button are styled separately.
Renders inside a Payrails-hosted iframe. Your rules cross into the frame and
apply to the classes below.
Payrails lays out each payment method's fields in rows by default. Each row is a
`.payrails-row` of `.payrails-cell` elements, and a rule on either replaces the
default layout, for example to stack a row on narrow screens.
| Part | Classes | Modifiers |
| - | - | - |
| Row | `.payrails-row` | — |
| Cell | `.payrails-cell` | — |
| Field | `.payrails-field` | `--invalid` |
| Label | `.payrails-label` | — |
| Text input | `.payrails-input` | — |
| Select | `.payrails-dropdown` | — |
| Checkbox | `.payrails-checkbox` | `--checked` |
| Static text | `.payrails-text` | — |
| Error text | `.payrails-error` | — |
Media queries in `rules` match the width of the form's frame, not your page.
```ts theme={null}
const form = payrails.apmForm({
paymentMethodCode: "mbWay",
appearance: {
rules: {
".payrails-label": { fontWeight: "600" },
".payrails-input": { padding: "12px", borderRadius: "8px" },
".payrails-field--invalid .payrails-input": { borderColor: "#d33" },
".payrails-error": { color: "#d33" },
"@media (max-width: 480px)": {
".payrails-row": { flexDirection: "column" },
},
},
},
});
```
While an approve-in-app method waits for the shopper (see
[`apmButton`](/docs/orchestration/checkout-sdks/web/references/payrails-api-reference#approve-in-app-methods)), the form shows a
wait screen instead of the fields: the payment method's logo, instructions for
the shopper, and a spinner, which is not stylable. Root `rules` style the wait
screen too. To style it apart from the fields, put the rules under
`actionScreen`; they apply only while the wait screen is shown.
| Part | Classes |
| - | - |
| Screen | `.payrails-container` |
| Logo | `.payrails-icon` |
| Line of text | `.payrails-row` > `.payrails-cell` > `.payrails-text` |
```js theme={null}
payrails.apmForm({
paymentMethodCode: "mbWay",
appearance: {
rules: { ".payrails-text": { color: "#374151" } },
actionScreen: {
rules: { ".payrails-text": { fontSize: "16px" } },
},
},
});
```
### APM button — `apmButton({ appearance })`
Authorizes an alternative payment method. Mount it alongside the form it pays
for, or on its own for a method that collects nothing. It takes its own
`appearance` either way.
| Part | Classes | Modifiers |
| - | - | - |
| Button | `.payrails-button` | `--loading`, `--disabled` |
```ts theme={null}
payrails.apmButton({
form,
appearance: {
rules: {
".payrails-button": { borderRadius: "8px", backgroundColor: "#0a7d4f" },
".payrails-button--disabled": { opacity: "0.5" },
},
},
});
```
### Drop-in — `dropin({ appearance })`
Root `rules` apply across the drop-in's blocks — for example `.payrails-button`
under root `rules` reaches every pay button the drop-in renders, and
`.payrails-container` reaches each block's wrapper. Each slot scopes its rules
to that block:
| Slot | Parts and classes |
| - | - |
| `cardForm` | Full `CardFormAppearance` shape (see card form above). |
| `cardPaymentButton` | `.payrails-button` with `--loading` / `--disabled`. |
| `loadingScreen` | `.payrails-container` with a `.payrails-icon` spinner. |
| `authSuccess` | `.payrails-container` with `.payrails-icon` and `.payrails-label`. |
| `authFailed` | `.payrails-container` with `.payrails-icon` and `.payrails-label`. |
`authSuccess` also styles the payment-pending screen, which reuses the success
screen's markup. The other keys declared on `DropinAppearance` (`leanButton`,
`orderSummary`, `billingAddressForm`, `termsAndConditions`, the wallet buttons)
are reserved and currently have no effect.
# Event API Reference
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/references/event-api-reference
Reference for the web SDK event API: every instance event and element event, the payload each one carries, and when it fires.
The SDK exposes events through the typed `.on(name, handler)` API at two levels:
1. **Instance events** — `payrails.on(name, handler)`. Session-level and
in-flight payment-attempt events (`success`, `failed`, `pending`,
`buttonClicked`, `requestStart`, `actionRequired`, `sessionExpired`,
`deliveryAddressChanged`).
2. **Element events** — `element.on(name, handler)`, where `element` is the
object returned by `payrails.cardForm()`, `payrails.paymentButton()`,
`payrails.googlePayButton()`, and so on. Element events describe one rendered
element (`change`, `focus`, `blur`, `ready`, `saveInstrumentCheckboxChanged`,
`preferredSchemeChanged`, `billingAddressChanged`, `validate`,
`stateChanged`, `paymentOptionSelected`).
`.on()` returns an unsubscribe function. Handlers may be async; the SDK awaits
each handler before proceeding. A handler that throws is logged and isolated
(other handlers still run). `Payrails.init` resets all subscriptions.
```ts theme={null}
const off = payrails.on("success", (event) => {
console.log(event.paymentMethodCode, event.action);
});
// later:
off(); // or payrails.off('success', handler)
```
## Init-time callback (the one exception)
The only bag callback that still exists is `onClientInitialized`, passed inside
`PayrailsClientOptions.events` on `Payrails.init`. It has no `.on()` form
because it fires during construction, before a `.on(...)` listener could
register.
```ts theme={null}
const payrails = await Payrails.init(initResponse, {
events: {
onClientInitialized: (execution) => useExecution(execution),
},
});
```
Fires once when the `Payrails` instance is constructed, and again after a
successful session refresh (see `sessionExpired`). Its argument is the execution
response object (`WorkflowExecutionResponse`, e.g. `execution.id`).
## Instance events — `payrails.on(name, handler)`
Every payment-attempt payload carries `PaymentAttemptContext`:
| Field | Type | Description |
| - | - | - |
| `executionId` | `string` | The workflow execution id of the attempt. |
| `paymentMethodCode` | `string` | The payment method of the attempt. |
| `action` | `'AUTHORIZE' \| 'TOKENIZE'` | Whether it's a payment or an instrument save. |
One listener fires for **any** element's event in the session — filter with
`event.paymentMethodCode` / `event.action` if you need per-method behaviour.
### `success`
Fires when authorization (or tokenization) completes successfully.
Payload: `{ …ctx, data? }`. `data` carries the tokenization result on a
`TOKENIZE` success (notably for Google Pay / Apple Pay) and is absent for flows
that return nothing (e.g. `AUTHORIZE`).
### `failed`
Fires when the attempt fails or is canceled.
Payload: `{ …ctx, data?: { code?, message? } }` where `code` is an
`AuthorizationFailureReasons` value (`VALIDATION_FAILED`, `AUTHORIZATION_ERROR`,
`AUTHENTICATION_ERROR`, `UNKNOWN_ERROR`, `USER_CANCELLED`).
### `pending`
Fires when the payment ends in a pending state (e.g. the result is not final yet
after a redirect or challenge).
Payload: `{ …ctx, data? }`.
### `requestStart` (cancelable)
Fires immediately before the authorize/tokenize request is sent.
Payload: `{ …ctx, preventDefault() }`. Call `preventDefault()` to abort — the
SDK then fires `failed` with `code: 'VALIDATION_FAILED'`.
### `buttonClicked` (cancelable)
Fires when the shopper clicks the payment button, before anything is submitted.
Payload: `{ …ctx, bin?, preventDefault() }`. For card payments `bin` carries the
current BIN. Call `preventDefault()` to abort — the SDK then fires `failed` with
`code: 'VALIDATION_FAILED'`.
`apmButton` runs this gate after its form validates, and `requestStart` after
it, so `preventDefault()` on either blocks an APM attempt the same way it blocks
a card one.
### `sessionExpired`
Fires when the SDK detects the client session has expired, when a payment
attempt fails because the session/vault token is no longer valid, or when an
approve-in-app payment (such as MB WAY) is declined, isn't approved in time or
can't be confirmed, so the shopper's retry starts a new payment.
Payload: `void`. Handlers **may** return a fresh `InitOptions`
(`{ version, data }`); the SDK awaits all handlers, re-inits from the first
non-null result, and then re-runs the init `onClientInitialized` callback.
```ts theme={null}
payrails.on("sessionExpired", async () => fetchNewInitResponse());
```
### `deliveryAddressChanged` (cancelable)
Fires when the shopper changes the shipping address inside an express-checkout
sheet (PayPal, Apple Pay).
Payload: `{ …ctx, deliveryAddress?, preventDefault() }`. Call `preventDefault()`
to reject the address (the Apple Pay sheet shows an error and prompts for
another address; PayPal cancels the change).
* Apple Pay exposes only redacted fields (`city`, `postalCode`, `state`,
`country.code`) before the shopper authorizes; the full address is available
on `success`. Apple Pay requires a response within \~30 seconds — keep this
handler fast.
* On PayPal, the payload shape is
`{ orderID?, paymentID?, paymentToken?, shippingAddress?: { city, countryCode, postalCode, state } }`.
### `actionRequired` (cancelable)
Fires when a payment attempt needs a shopper-facing action and the SDK is about
to perform its default behavior — navigating to a redirect URL or presenting the
3DS challenge (popup or redirect).
Payload (`ActionRequiredEvent`):
| Field | Type | Description |
| - | - | - |
| `url` | `string` | The URL the shopper must be taken to. |
| `kind` | `'genericRedirect' \| '3ds'` | What the action is (`ACTION_REQUIRED_KIND` values). |
| `executionId` | `string` | The workflow execution id of the payment attempt. |
| `paymentMethodCode` | `string` | The payment method of the attempt. |
| `preventDefault()` | `() => void` | Call to take over: the SDK skips its default redirect/popup and you handle `url` yourself. |
```ts theme={null}
payrails.on("actionRequired", (event) => {
if (event.kind === "genericRedirect") {
event.preventDefault();
myRouter.openInModal(event.url);
}
});
```
## Element events — `element.on(name, handler)`
Element events are declared on the object returned by an element factory
(`cardForm`, `paymentButton`, `googlePayButton`, `applePayButton`,
`paypalButton`, `dropin`, `dynamicElement`, `apmForm`, `apmButton`, …). Every
element's `.on()` accepts any element event name, so subscribing to one that an
element never emits compiles but never fires.
### `change`
Fires on every input change.
Payload: `{ isValid, cardNetwork, bin?, billingAddress?, cardSchemes? }`. On a
standalone secure field (collect element) the payload is
`{ isValid, isEmpty, network, bin?, cardSchemes? }` — note `network`, not
`cardNetwork`. On an APM form the payload is `{ isValid }` only.
### `focus` / `blur`
Fires when a card field gains / loses focus. Focusing the card form also clears
a `cardList` selection.
Payload: `void`.
### `ready`
Fires when all secure fields have mounted and the form is interactive. On an APM
form it fires when the form's frame has loaded.
Payload: `void`.
### `saveInstrumentCheckboxChanged`
Fires when the shopper toggles the "save instrument" checkbox.
Payload: `{ checked: boolean }`.
### `preferredSchemeChanged`
Fires when the detected/selected card scheme changes for a co-branded card.
Payload: `{ preferredScheme: string, cardSchemes: CardScheme[] }`.
### `billingAddressChanged`
Fires when the shopper edits the billing address form.
Payload: `{ isValid: boolean, billingAddress? }`.
### `validate`
Fires when a pay button validates its form on click, before the payment request:
the card payment button with the card form, and the APM button with its APM
form.
Payload: `{ isValid, error?, fieldErrors? }`. `fieldErrors` maps fields to their
validation errors. On the APM button, `fieldErrors` names the fields that
failed, and is omitted when the form is valid.
### `stateChanged`
Fires when a payment button switches between enabled and disabled (e.g. as
card-form validity changes).
Payload: `{ state: 'enabled' | 'disabled' }`.
### Wallet availability (not an event)
Wallet availability is exposed as `readonly isAvailable: Promise` on
`googlePayButton`, `applePayButton`, and `paypalButton` — not as an event,
because it is a capability check that settles once. The promise never rejects;
it resolves to `false` on any check-side failure.
### `paymentOptionSelected`
Fires when the shopper selects a payment option in the drop-in.
Payload: `{ paymentMethod?, instrument? }`. `paymentMethod` is the selected
method's configuration; `instrument` is set when a stored instrument was
selected.
## Usage
```ts theme={null}
import { Payrails } from "@payrails/web-sdk";
const payrails = await Payrails.init(initResponse, {
events: { onClientInitialized: (execution) => useExecution(execution) },
});
const cardForm = payrails.cardForm();
cardForm.on("change", (e) => render(e.isValid));
cardForm.on("ready", () => show());
payrails.on("buttonClicked", async (e) => {
if (!(await ok())) e.preventDefault();
});
payrails.on("success", (e) => console.log(e.paymentMethodCode));
payrails.on("failed", (e) => console.log(e.data?.code));
payrails.on("sessionExpired", async () => fetchNewInitResponse());
```
# Payrails API Reference
Source: https://docs.payrails.com/docs/orchestration/checkout-sdks/web/references/payrails-api-reference
Reference for the Payrails class and package exports of the web SDK: initialization, element factories, and payment session data.
## Payrails class
Reference for the `Payrails` class and package exports.
The `Payrails` class is the entry point of `@payrails/web-sdk`. You never
construct it directly — call the static `Payrails.init()` with the SDK
configuration returned by your server, then use the resulting instance to create
UI elements, payment buttons, and to read data from the payment session.
```ts theme={null}
import { Payrails } from "@payrails/web-sdk";
const payrails = await Payrails.init(initResponse);
```
The environment (`TEST` / `PRODUCTION`) and merchant identifier are sourced from
the server-provided SDK configuration inside `initResponse.data`. There is no
client-side override.
### Initialization
#### `Payrails.init(initResponse, options?)` (static)
```ts theme={null}
static init(initResponse: InitOptions, options?: PayrailsClientOptions): Promise
```
Creates the SDK instance from a server-side SDK configuration. `init` is
asynchronous: it loads the SDK bundle and stylesheet from the Payrails CDN
(`assets.payrails.io` — allow it in `script-src` and `style-src` if you set a
Content-Security-Policy) and resolves once the instance is ready. It requires a
DOM — call it in the browser, not during server-side rendering.
| Parameter | Type | Description |
| - | - | - |
| `initResponse` | `InitOptions` | The object returned by the Payrails client-init API: `{ version: string; data: string }`, where `data` is the Base64-encoded SDK configuration. |
| `options` | `PayrailsClientOptions` | Optional client configuration (see below). |
`PayrailsClientOptions`:
| Field | Type | Description |
| - | - | - |
| `events` | `{ onClientInitialized? }` | Init-only callback that fires during construction. `onSessionExpired` and every other event moved to `payrails.on(...)`. |
| `redirectFor3DS` | `boolean` | When `true`, 3DS challenges navigate the page instead of opening a popup. |
| `returnInfo` | `ReturnInfo` | Redirect return URLs: `{ success?, cancel?, error?, pending? }`. |
| `telemetry` | `{ enabled?: boolean; logLevel?: 'debug' \| 'error'; collectMetadata?: boolean }` | SDK telemetry configuration. |
Rejects with a `PayrailsError` (`PAYRAILS_INIT_FAILED`) when the configuration
string is invalid or when the SDK bundle cannot be loaded (blocked CDN, offline,
or a 15-second timeout).
#### `update(updateOptions)`
```ts theme={null}
update(updateOptions: LookupUpdateOptions): void
```
Updates the payment session on the client after initialization.
| Field | Type | Description |
| - | - | - |
| `value`, `currency` | `string` | New amount. Both must be provided together for the amount to change. |
| `meta` | `{ key: 'customer'; value: object }` | Merges execution metadata (customer name, email, phone, country, …). |
| `installmentConfig` | `object \| null` | Card-installment options per country and/or a `defaultInstallment` selection; propagated to a mounted card form or drop-in. |
#### `setState({ instrument })`
```ts theme={null}
setState(state: { instrument }): void
```
Sets the currently selected stored instrument in the SDK's shared state, e.g.
when your own UI (instead of `cardList`) lets the shopper pick a saved card.
`instrument` is a stored instrument object as returned by
`getStoredInstruments()`.
### UI elements
All elements returned by these methods expose `mount(selector: string)` and
`unmount()`.
#### `dropin(options)`
```ts theme={null}
dropin(options: DropinOptions): Dropin
```
Creates the all-in-one drop-in element that renders every available payment
method. `DropinOptions`:
| Field | Description |
| - | - |
| `paymentMethodsConfiguration` | Per-method configuration (`cards`, `payPal`, `googlePay`, `applePay`, `lean`, `mercadoPago`, `revolutPay`, …) plus `preselectFirstPaymentOption` (default `true`) and `showPaymentMethodLogo`. |
| `translations` | Per-component label/text overrides, including `paymentResult` and `errorMessages`. |
| `appearance` | Per-component styling (`DropinAppearance`): root `rules` plus one slot per building block (`cardForm`, `cardPaymentButton`, `orderSummary`, …). Provider-drawn wallet chrome is configured via `paymentMethodsConfiguration..styles`. |
| `fonts` | Custom font descriptors for the secure card fields. |
| `returnInfo` | Redirect return URLs for methods that leave the page. |
| `configuration` | Extra behavior configuration (e.g. `authFailMsg`). |
Subscribe to drop-in events via `payrails.on(...)` (instance-level events like
`success`) and the drop-in's own `.on(...)` (element-level events like
`paymentOptionSelected`).
```ts theme={null}
const dropin = payrails.dropin({});
dropin.mount("#dropin");
payrails.on("success", (event) => {
console.log(event.paymentMethodCode);
});
dropin.on("paymentOptionSelected", (e) => console.log(e.paymentMethod));
```
#### `cardForm(options?)`
```ts theme={null}
cardForm(options?: CardFormOptions): CardForm
```
Creates (or returns the already-created) secure card entry form. One card form
exists per `Payrails` instance; a mounted `paymentButton` is wired to it
automatically. Key `CardFormOptions` fields: `showCardHolderName`,
`showSingleExpiryDateField`, `layout` (rows of `ElementType` field names),
`translations` (placeholders, labels, error texts), `appearance`
(`CardFormAppearance`), `fonts`, `installmentConfig`, and
`enrollInstrumentToNetworkOffers`. Subscribe to events (`change`, `ready`,
`focus`, `blur`, …) via `cardForm.on(...)`.
```ts theme={null}
const cardForm = payrails.cardForm({ showCardHolderName: true });
cardForm.mount("#card-form");
cardForm.on("change", (e) => render(e.isValid));
cardForm.on("ready", () => show());
```
#### `cardList(options?)`
```ts theme={null}
cardList(options?: CardListOptions): CardList
```
Creates (or returns the already-created) list of the shopper's saved cards.
`options.onCardChange(selectedCard)` fires when the shopper selects a card;
selection also enables a mounted `paymentButton`. `options.appearance` styles
the list.
#### `apmForm(options)`
```ts theme={null}
apmForm(options: ApmFormOptions): ApmForm
```
Creates the form for an alternative payment method that needs details from the
shopper, such as a phone number or an IBAN. The fields are drawn inside a
Payrails-hosted iframe from the payment method's configuration, so they are
never part of your page's DOM. Pair it with an [`apmButton`](#apmbutton-options)
to pay.
Options: `paymentMethodCode` (required), `appearance`, `translations`, `id`. The
method throws a `PayrailsError` when the session has no form for that payment
method. The id defaults to `payrails-apm-form-`; pass `id`
only if you mount two forms for the same method.
```ts theme={null}
const form = payrails.apmForm({ paymentMethodCode: "mbWay" });
await form.mount("#mbway-form");
const button = payrails.apmButton({
form,
translations: { label: "Pay with MB WAY" },
});
button.mount("#mbway-button");
form.on("change", (e) => setFormValid(e.isValid));
```
`form.mount()` returns a promise, so await it or attach a `.catch`. It rejects
when the selector isn't found, or when the form can't be drawn, for example
because a field has no label from the configuration or your `translations`. A
form that can't be drawn rejects with a `PayrailsError` whose `context.reason`
is `'unrenderable-schema'`. Branch on that to offer the shopper another way to
pay:
```ts theme={null}
form.mount("#mbway-form").catch((error) => {
if (error?.context?.reason === "unrenderable-schema") {
showOtherPaymentOptions();
}
});
```
`appearance` takes the same `rules` as other elements, plus `actionScreen` for
the screen the form shows while an approve-in-app method waits; see
[`ApmFormAppearance`](/docs/orchestration/checkout-sdks/web/references/appearance-api-reference#apmformappearance).
#### `dynamicElement(options)`
```ts theme={null}
dynamicElement(
options: DynamicElementOptions & Required>
): DynamicElementForm
```
Renders a form directly in the merchant page. To collect payment details, use
[`apmForm`](#apmform-options) instead, which renders in a Payrails-hosted frame.
`options.paymentMethod` is required; the method
throws a `PayrailsError` when the session has no form for that payment
method. Other options: `appearance`, `translations`, `fieldOverrides`. Subscribe to events via `element.on(...)` .
#### `collectContainer(containerOptions)`
```ts theme={null}
collectContainer(containerOptions: CollectContainerOptions): FramesContainer
```
Low-level access to the secure-fields container used by `cardForm`. Lets you
create and mount individual secure fields (`createCollectElement`), validate,
and collect encrypted card data yourself. `CollectContainerOptions`:
`containerId`, `fonts`. Each secure field styles itself via its own `appearance`
option on `createCollectElement`.
### Payment buttons
#### `apmButton(options)`
```ts theme={null}
apmButton(options: ApmButtonOptions): ApmButton
```
Pays with an alternative payment method. Pass either the `form` from
[`apmForm`](#apmform-options), or a `paymentMethodCode` on its own for a method
with nothing to collect. Passing both, or neither, throws. Methods that redirect
the shopper still use [`genericRedirectButton`](#genericredirectbutton-options);
`apmButton` stops at `pending` for them.
With a form, a click validates it first. If a field is invalid, the form shows
the error, the button emits `validate` with `isValid: false`, and nothing is
submitted. Otherwise the button runs the `buttonClicked` and `requestStart`
gates and submits the payment, the same as the card payment button.
Options: `form` or `paymentMethodCode` (exactly one), `translations.label`,
`appearance`, `returnInfo`, `id`, `alwaysStoreInstrument`. The id defaults to
`payrails-apm-button-`. `alwaysStoreInstrument` (default
`false`) saves the instrument for future payments. The button shows no save
checkbox, so set it only when you already have the shopper's consent, for
example for a SEPA mandate. The SDK ignores it on a merchant of record
workspace. See
[Save the instrument for future payments](/docs/orchestration/checkout-sdks/web/guides/how-to-accept-alternative-payment-methods-without-a-redirect#save-the-instrument-for-future-payments).
Subscribe to outcomes via
`payrails.on('success' | 'failed' | 'pending', ...)`, and to `validate` via the
returned button's `.on(...)`. See the
[event reference](/docs/orchestration/checkout-sdks/web/references/event-api-reference).
```ts theme={null}
button.on("validate", (e) => {
if (!e.isValid) scrollToPaymentForm();
});
payrails.on("success", (e) => showConfirmation(e.executionId));
payrails.on("failed", (e) => showError(e.data?.code));
```
#### Approve-in-app methods
Some methods, such as MB WAY, have the shopper approve the payment in another
app. For these the button doesn't stop at `pending`. The form switches to a wait
screen with the payment method's logo and instructions for the shopper, the
button is removed, and the SDK waits for the shopper to approve:
* **Approved:** the wait screen goes away and `success` fires. Show your own
confirmation.
* **Declined, or not approved in time:** the form comes back with what the
shopper typed, the button comes back, `sessionExpired` fires, and then
`failed` with `AUTHORIZATION_ERROR`. Return fresh init options from
`sessionExpired` so the shopper's retry starts a new payment.
* **The SDK couldn't confirm the result,** for example because the session
expired during the wait: the same happens, but `failed` carries
`AUTHENTICATION_ERROR` or `UNKNOWN_ERROR`. The shopper may still approve in
their app, so check the payment's status before letting them retry.
```ts theme={null}
payrails.on("sessionExpired", async () => fetchNewInitResponse());
payrails.on("failed", async (e) => {
const code = e.data?.code;
if (code === "AUTHENTICATION_ERROR" || code === "UNKNOWN_ERROR") {
await checkPaymentStatus(e.executionId);
}
});
```
Your webhook or an execution lookup has the payment's final status.
#### `paymentButton(options)`
```ts theme={null}
paymentButton(options: CardPaymentButtonOptions): CardPaymentButton
```
Creates (or returns the already-created) pay button for card payments. It
submits the card form or the instrument selected via `cardList`/`setState`.
Options: `translations.label`, `appearance`, `redirectFor3DS`, and
`disabledByDefault` (default `true`; the button enables once the card form is
valid or a saved card is selected). Subscribe to payment outcomes via
`payrails.on(...)` and to button-specific events (`stateChanged`, `validate`)
via the returned button's `.on(...)`
#### `googlePayButton(options)`
```ts theme={null}
googlePayButton(options: GooglePayButtonOptions): GooglePayButton
```
Creates a Google Pay button. Options: `merchantName` (overrides the display name
from the backend config; the merchant ID is always taken from the backend),
`redirectFor3DS`, `styles` (`buttonColor`, `buttonType`, `buttonSizeMode`,
`locale`), `returnInfo`, and store-instrument checkbox options. The environment
is inherited from `Payrails.init(...)`. Read `await button.isAvailable` (a
`Promise`) for wallet availability and subscribe to `payrails.on(...)`
for payment outcomes.
#### `isGooglePayAvailable(merchantName?)`
```ts theme={null}
isGooglePayAvailable(merchantName?: string): Promise
```
Resolves `true` when the shopper's browser/device can pay with Google Pay for
the current session configuration. Pass `merchantName` to override the display
name that Google Pay resolves during the availability check.
#### `applePayButton(options)`
```ts theme={null}
applePayButton(options: ApplePayButtonOptions): ApplePayButton
```
Creates an Apple Pay button. Options: `abortAfterAuthorizeFailed`,
`translations`, `styles` (`type`, e.g. `'buy'`/`'checkout'`; `style`, e.g.
`'black'`; `locale`), and store-instrument checkbox options. Read
`await button.isAvailable` (a `Promise`) for wallet availability,
subscribe to `payrails.on('deliveryAddressChanged', …)` for the express-checkout
address sheet, and to `payrails.on(...)` for payment outcomes.
#### `isApplePayAvailable()`
```ts theme={null}
isApplePayAvailable(): Promise
```
Resolves `true` when the browser supports Apple Pay (`ApplePaySession`) and the
Apple Pay SDK loads; `false` otherwise (never rejects).
#### `paypalButton(options?)`
```ts theme={null}
paypalButton(options?: PaypalButtonOptions): PaypalButton
```
Creates a PayPal button. Options: `styles` (`color`, `height`, `label`, `shape`,
`tagline`, `locale`), and store-instrument checkbox options. Read
`await button.isAvailable` (a `Promise`) for wallet availability,
subscribe to `payrails.on('deliveryAddressChanged', …)` for express-checkout
address changes (PayPal payload shape), and to `payrails.on(...)` for payment
outcomes. In PayPal express mode the store-instrument checkbox is forced off.
#### `leanButton(options)`
```ts theme={null}
leanButton(options: LeanButtonOptions): LeanButton
```
Creates a Lean (pay-by-bank) button. Options: `id`, `translations.label`,
`appearance` (the button), `dialogCustomization` (the Lean-hosted dialog's theme
config), and `returnInfo`. Subscribe to payment outcomes via `payrails.on(...)`.
#### `genericRedirectButton(options)`
```ts theme={null}
genericRedirectButton(options: GenericRedirectButtonOptions): GenericRedirectButton
```
Creates a button for any redirect-based payment method. `options.paymentMethod`
(the payment method configuration, required) selects the method; other options:
`translations.label`, `appearance` (for Revolut Pay, pass `revolutOptions` — the
`RevolutPayStyles` shape — instead), `openInNewTab`, and `returnInfo`. Subscribe
to payment outcomes via `payrails.on(...)`; the redirect itself can be
intercepted via the `actionRequired` event (see
[events](/docs/orchestration/checkout-sdks/web/references/event-api-reference#actionrequired-cancelable)).
### Stored instruments and payment methods
Stored instruments are plain objects with `id`, `status`, `paymentMethod`,
`displayName?`, `data?` (e.g. `bin`, `suffix`, `network` for cards, `email` for
PayPal), and `default?`.
#### `getSavedCreditCards()`
Returns the shopper's stored card instruments.
#### `getSavedGooglePayAccounts()`
Returns the shopper's stored Google Pay instruments with status `enabled`.
#### `getSavedApplePayAccounts()`
Returns the shopper's stored Apple Pay instruments.
#### `getSavedPaypalAccounts()`
Returns the shopper's stored PayPal instruments with status `enabled`, or `[]`.
#### `getStoredInstruments()`
Returns stored instruments across all payment methods. Google Pay and PayPal
instruments are included only when their status is `enabled`.
#### `getStoredInstrumentsByPaymentMethod(paymentMethod)`
```ts theme={null}
getStoredInstrumentsByPaymentMethod(paymentMethod: string): object[]
```
Returns the result of `getStoredInstruments()` filtered to one payment method
code (a `PAYMENT_METHOD_CODES` value such as `'card'` or `'payPal'`).
#### `getAvailablePaymentMethods()`
```ts theme={null}
getAvailablePaymentMethods(): string[]
```
Returns the payment method codes available in the current session (values of
`PAYMENT_METHOD_CODES`).
### Data and API
#### `api(config)`
```ts theme={null}
api(config: {
operation: 'deleteInstrument' | 'updateInstrument' | 'payout';
resourceId?: string;
body?: object;
}): Promise