This documentation describes how to integrate with a unified and secure payment system for initiating payments or tokenizing payment instruments. The approach follows a two-step flow:
Obtain a concludable payment request token – This token represents either an authorized payment (ready for capture) or an authorized payment instrument for future use. To obtain it, the frontend mounts the Universal Payment Component (UPC) using a short-lived user payment session token securely obtained from the backend.
Post a usage with the obtained payment request token – The token can be used to create a payment instrument in the customer account, attach it to a contract, or post a usage (e.g., credit top-up, ticket purchase). If the usage involves an amount greater than zero, capture is deferred until the related good or service is booked, minimizing refunds. If unused within its validity period, the authorization is cancelled or refunded.
- Universal Payment Component (UPC) – Embeddable JavaScript component handling the authorization of a payment request token.
- Universal Payment Gateway (UPG) – Public API the UPC communicates with, authorized via a short-lived session token.
- Payment Instrument – A user-specific object used to attempt a payment.
- Payment Method – The type of payment instrument (e.g., credit card, direct debit, PayPal).
- Scope – Defines the payment context (
MEMBER_ACCOUNTorECOM) to determine available payment methods.
- Always define either
finionPayCustomerIdorcustomerIdfor an existing customer session. When assigning apaymentRequestTokento a usage, the system checks that the token belongs to the correct customer. If this check fails, the operation will not succeed. - Example: Selling a contract online and collecting a payment method for the upfront fee requires two
paymentRequestTokens— one for the payment instrument and one for the actual upfront payment. This means creating two separate user payment sessions. If the flow is for a new customer, create the second session using thefinionPayCustomerIdreturned by the first. - While the
paymentRequestTokenis unused for posting a usage, no funds are captured (if the payment method allows). Authorizing a token withamount > 0only authorizes the payment; capture happens when posting the usage. This prevents unnecessary collections or refunds in case of process errors or user cancellation. - Saving payment methods: If the scope is
ECOMand the payment method supports saving, the user can choose to store the method for future use. - Authorizing saved payment methods: Stored payment methods are already authorized, so they are not re-authorized when selected via the component. The payment result is returned upon posting the usage.
- Any unused
paymentRequestTokenis automatically cancelled when the related user payment session expires. - A user payment session is automatically invalidated once one
paymentRequestTokenfrom that session is used — only one token per session can be used.
The paymentRequestToken returned by the UPC can be used in the following scenarios:
- Create a payment instrument and link it to the customer so it can be used in future payment runs (e.g., membership fees).
- Applies to:
- Creating a new customer and contract (work in progress)
- Adding a contract to an existing customer (work in progress)
- Offering self-service payment method updates (work in progress)
- Adding a secondary payment method (planned)
If the paymentRequestToken is authorized with a payment amount, it can be used for purchasing any sellable entity:
- Upfront payment in contract creation (joining fee or total contract value) (work in progress)
- Account balancing for open fees (work in progress)
- Purchasing a day ticket (work in progress)
- Purchasing a value voucher (planned)
- Purchasing a contract voucher (planned)
- Purchasing a course contingent (planned)
- Purchasing an appointment (e.g., personal training contingent) (planned)
To initiate a payment process or capture a payment instrument, you must first create a user payment session.
Endpoint: POST /v1/payments/user-session
Required Scope: PAYMENT_WRITE
Description: This request generates a short-lived token used by the UPC to authenticate payment flows. It can be for immediate transactions or for storing payment instruments for future recurring payments.
Specifies the payment amount for the initiated transaction. Should equal 0 to capture a payment instrument for future recurring payments. The currency is defined by the studio.
Specifies where the created payment instruments will be used, as the available payment methods differ by scope.
| Enum Value | Description |
|---|---|
| MEMBER_ACCOUNT | Use when initiating a payment user session to collect a payment instrument intended for future payment runs (e.g., BACS, credit card). |
| ECOM | Use when the user is making a purchase or when the payment instrument will be used for future user-initiated payments (e.g., saving a credit card for later purchases). |
This field represents the unique identifier for an existing customer within ERP. Providing this ID ensures the payment session is linked to the correct customer record. It is a mutually exclusive field with finionPayCustomerId, meaning you can only provide one or the other.
Conditions for use: This ID is required for payment sessions involving existing customers.
Behavior when omitted: If this field is left empty, a new customer will be treated as a “potential customer” and a
finionPayCustomerIdwill be automatically generated and returned in the response. If omitted, it will not be possible to use it for existing customers.Mutually exclusive with:
finionPayCustomerId
This field is the identifier for a customer within the Finion Pay payment service, typically used for customers who are not yet registered in ERP. Use this ID to track repeat payment sessions for a potential customer.
Conditions for use: This ID should only be provided for subsequent payment sessions for a customer who has been previously identified by Finion Pay but doesn’t have an ERP
customerIdyet.Behavior when omitted: In the absence of a
customerId, a newfinionPayCustomerIdwill be automatically created and assigned to the user for the current session.Mutually exclusive with:
customerId
List of permitted payment choices, i.e. obtained by the contract offer. Acts as a filter for the available payment methods defined by the scope
| Items Enum Value | Description |
|---|---|
| BACS | Payment by BACS direct debit |
| IDEAL | Payment by ideal |
| PAYPAL | Payment by paypal |
| TWINT | Payment by Twint |
| SEPA | Payment by SEPA direct debit |
| BANK_TRANSFER | Payment by bank transfer |
| CH_DD | Payment by CH_DD direct debit |
| CASH | Payment by cash |
| BANCONTACT | Payment by bancontact |
| CREDIT_CARD | Payment by credit card |
Allows the definition of the reference text shown on the bank statement of the customer.
When set to true the direct debit form will show a signature field to the user that is required to proceed. This applies to the payment methods SEPA, CH_DD and LSV.
- Demo tenant
https://open-api-demo.open-api.magicline.com/v1/payments/user-session
- curl
- JavaScript
- Node.js
- Python
- Java
- C#
- PHP
- Go
- Ruby
- R
- Payload
curl -i -X POST \
https://open-api-demo.open-api.magicline.com/v1/payments/user-session \
-H 'Content-Type: application/json' \
-H 'X-API-KEY: YOUR_API_KEY_HERE' \
-d '{
"amount": 19.99,
"scope": "MEMBER_ACCOUNT",
"customerId": 1234567890,
"finionPayCustomerId": "753ea8ec-c2ec-4761-824b-bc46eda3f644",
"permittedPaymentChoices": [
"CASH"
],
"referenceText": "Gym Joining Fee 01.07.2025",
"requireDirectDebitSignature": false
}'The token for the user session.
The date and time until the token is valid.
Identifies a customer in Finion Pay, i.e. to retreive existing payment instruments.
The token returned is the userSessionToken required to initialize the UPC in your frontend integration.
An embeddable payment interface that can be integrated into any web application.
The following URIs are available:
- https://widget.dev.payment.sportalliance.com/widget.js - Preview version
- https://widget.payment.sportalliance.com/widget.js - Stable version
<script src="INSERT_WIDGET_URI_HERE"></script>
<div id="payment-widget"></div>
<script>
const widget = window.paymentWidget.init({
userSessionToken: 'your-session-token',
environment: 'live',
countryCode: 'US',
locale: 'en',
container: 'payment-widget'
});
// Clean up when done
widget.destroy();
</script>| Parameter | Type | Description |
|---|---|---|
userSessionToken | string | User session token |
environment | 'test' | 'sandbox' | 'live' | Payment environment |
countryCode | string | ISO country code (e.g., 'US') |
locale | string | Locale (e.g., 'en') |
container | string | HTMLElement | Element ID or element reference |
Optional:
styling- Custom theme colors and stylingi18n- Translation overridesfeatureFlags- Enable experimental or alternative featuresonSuccess- Success callback function that receives the payment request token, payment instrument details, and payment instrument tokendevMode- Show i18n keys instead of translated text (development only)hidePaymentButton- Hide widget's internal payment buttons for custom button controlonPaymentStateChange- Callback for payment button state changes (processing, canSubmit)customerData- Pre-fill payment forms with customer information (name, email, address)
Styling:
styling: {
primaryColor: '#007bff',
textColorMain: '#333333',
borderRadius: '4px'
}Translations:
i18n: {
'upc.my.payment.instruments': 'My Payment Methods',
'upc.payment.methods.add.new': 'Add New Payment Method'
}Feature Flags:
featureFlags: {
useRubiksUI: true; // Enable Rubiks Styleguide components (default: false)
}The useRubiksUI flag switches the payment forms to use the Rubiks design system instead of the default Tailwind-based UI. This provides a more modern and consistent look aligned with the Rubiks component library.
Development Mode:
devMode: true; // Shows i18n keys instead of translations for developmentSuccess Callback:
onSuccess: (
paymentRequestToken,
paymentInstrumentDetails,
paymentInstrumentToken
) => {
// paymentRequestToken: string - The payment request token
// paymentInstrumentDetails: object - Payment instrument details (card info, bank details, etc.)
// paymentInstrumentToken: string - The payment instrument token for future use
};import React, { useEffect, useRef } from 'react';
export const PaymentWidget = ({ userToken, onPaymentSuccess }) => {
const containerRef = useRef(null);
const widgetRef = useRef(null);
useEffect(() => {
if (containerRef.current && window.paymentWidget) {
widgetRef.current = window.paymentWidget.init({
userSessionToken: userToken,
environment: 'live',
countryCode: 'US',
locale: 'en',
container: containerRef.current,
featureFlags: {
useRubiksUI: true
},
onSuccess: (token, details, instrumentToken) => {
onPaymentSuccess(token, details, instrumentToken);
}
});
}
return () => widgetRef.current?.destroy();
}, [userToken, onPaymentSuccess]);
return <div ref={containerRef} />;
};import { Component, ElementRef, ViewChild, OnDestroy } from '@angular/core';
@Component({
selector: 'app-payment-widget',
template: '<div #paymentContainer></div>'
})
export class PaymentWidgetComponent implements OnDestroy {
@ViewChild('paymentContainer', { static: true }) containerRef!: ElementRef;
private widget: any;
ngAfterViewInit() {
const sessionToken =
sessionStorage.getItem('paymentSessionToken') ||
this.getUserToken();
sessionStorage.setItem('paymentSessionToken', sessionToken);
this.widget = window.paymentWidget.init({
userSessionToken: sessionToken,
environment: 'live',
countryCode: 'US',
locale: 'en',
container: this.containerRef.nativeElement,
onSuccess: (
token,
paymentInstrumentDetails,
paymentInstrumentToken
) => {
sessionStorage.removeItem('paymentSessionToken');
this.handlePaymentSuccess(
token,
paymentInstrumentDetails,
paymentInstrumentToken
);
}
});
}
ngOnDestroy() {
this.widget?.destroy();
}
}<template>
<div ref="paymentContainer"></div>
</template>
<script>
export default {
name: 'PaymentWidget',
props: ['userToken'],
mounted() {
const sessionToken =
sessionStorage.getItem('paymentSessionToken') || this.userToken;
sessionStorage.setItem('paymentSessionToken', sessionToken);
this.widget = window.paymentWidget.init({
userSessionToken: sessionToken,
environment: 'live',
countryCode: 'US',
locale: 'en',
container: this.$refs.paymentContainer,
onSuccess: (
token,
paymentInstrumentDetails,
paymentInstrumentToken
) => {
sessionStorage.removeItem('paymentSessionToken');
this.$emit('paymentSuccess', {
token,
paymentInstrumentDetails,
paymentInstrumentToken
});
}
});
},
beforeUnmount() {
this.widget?.destroy();
}
};
</script>The widget supports hiding its internal payment buttons, allowing host applications to use custom buttons while maintaining full control over styling and placement.
Multiple Widget Instances: The widget fully supports mounting multiple instances on the same page. Each instance maintains its own isolated state and API reference.
// Example: Multiple widget instances on the same page
const widget1 = window.paymentWidget.init({
userSessionToken: 'token-1',
container: 'payment-widget-1'
// ... other config
});
const widget2 = window.paymentWidget.init({
userSessionToken: 'token-2',
container: 'payment-widget-2'
// ... other config
});
// Each widget operates independently
await widget1.submitPayment(); // Only affects widget1
const state2 = widget2.getPaymentState(); // Only returns widget2 stateconst widget = window.paymentWidget.init({
userSessionToken: 'user-session-token',
environment: 'live',
countryCode: 'US',
locale: 'en',
container: 'payment-widget',
hidePaymentButton: true,
onPaymentStateChange: (state) => {
// Update custom button based on payment state
const button = document.getElementById('custom-pay-button');
button.disabled = !state.canSubmit;
button.textContent = state.isProcessing ? 'Processing...' : 'Pay Now';
}
});
// Custom button handler
document
.getElementById('custom-pay-button')
.addEventListener('click', async () => {
try {
await widget.submitPayment();
} catch (error) {
console.error('Payment failed:', error);
}
});When you initialize the widget, it returns an instance with the following methods:
interface PaymentWidgetInstance {
destroy(): void; // Clean up widget
submitPayment(): Promise<void>; // Trigger payment submission
getPaymentState(): PaymentButtonState; // Get current payment state
updateCustomerData(data: CustomerData): void; // Update customer data dynamically
}
interface PaymentButtonState {
isProcessing: boolean; // Payment is being processed
canSubmit: boolean; // Payment can be submitted (see Payment Method Behavior below)
}
interface CustomerData {
name?: string;
email?: string;
address?: CustomerAddress;
}
interface CustomerAddress {
line1?: string;
line2?: string;
city?: string;
postalCode?: string;
state?: string;
country?: string;
}The canSubmit state varies by payment method when using custom buttons:
Provider-based payments (Stripe/Adyen - Credit Card, PayPal, etc.):
canSubmitbecomestrueimmediately when the payment provider loads- The provider's internal validation will handle form completeness during submission
Form-based payments (SEPA, CH_DD, LSV Direct Debit):
canSubmitstarts asfalse- Becomes
trueonly when required checkboxes are checked - Ensures mandate acceptance before allowing submission
// Example: Handling different payment methods
onPaymentStateChange: (state) => {
// For SEPA: Button disabled until checkbox checked
// For Stripe/Adyen: Button enabled when provider loads
customButton.disabled = !state.canSubmit || state.isProcessing;
};import React, { useState, useEffect } from 'react';
export const CustomPaymentButton = ({ userToken }) => {
const [widget, setWidget] = useState(null);
const [buttonState, setButtonState] = useState({
disabled: true,
text: 'Pay Now'
});
useEffect(() => {
const widgetInstance = window.paymentWidget.init({
userSessionToken: userToken,
environment: 'live',
countryCode: 'US',
locale: 'en',
container: 'payment-widget',
hidePaymentButton: true,
onPaymentStateChange: (state) => {
setButtonState({
disabled: !state.canSubmit,
text: state.isProcessing ? 'Processing...' : 'Pay Now'
});
}
});
setWidget(widgetInstance);
return () => widgetInstance?.destroy();
}, [userToken]);
const handlePayment = async () => {
try {
await widget?.submitPayment();
} catch (error) {
console.error('Payment failed:', error);
}
};
return (
<div>
<div id="payment-widget" />
<button
onClick={handlePayment}
disabled={buttonState.disabled}
className="custom-pay-button"
>
{buttonState.text}
</button>
</div>
);
};import { Component, ElementRef, ViewChild, OnDestroy } from '@angular/core';
@Component({
selector: 'app-payment-with-custom-button',
template: `
<div #paymentContainer></div>
<button
(click)="handlePayment()"
[disabled]="!canSubmit"
class="custom-pay-button"
>
{{ buttonText }}
</button>
`
})
export class PaymentWithCustomButtonComponent implements OnDestroy {
@ViewChild('paymentContainer', { static: true }) containerRef!: ElementRef;
private widget: any;
canSubmit = false;
buttonText = 'Pay Now';
ngAfterViewInit() {
this.widget = window.paymentWidget.init({
userSessionToken: this.getUserToken(),
environment: 'live',
countryCode: 'US',
locale: 'en',
container: this.containerRef.nativeElement,
hidePaymentButton: true,
onPaymentStateChange: (state) => {
this.canSubmit = state.canSubmit;
this.buttonText = state.isProcessing
? 'Processing...'
: 'Pay Now';
}
});
}
async handlePayment() {
try {
await this.widget?.submitPayment();
} catch (error) {
console.error('Payment failed:', error);
}
}
ngOnDestroy() {
this.widget?.destroy();
}
}The widget supports pre-filling payment forms with customer information to improve the user experience. Customer data can be provided during initialization or updated dynamically at any time.
Provide customer data when initializing the widget:
const widget = window.paymentWidget.init({
userSessionToken: 'user-session-token',
environment: 'live',
countryCode: 'US',
locale: 'en',
container: 'payment-widget',
customerData: {
name: 'John Doe',
email: 'john.doe@example.com',
address: {
line1: '123 Main Street',
line2: 'Apt 4B',
city: 'New York',
postalCode: '10001',
state: 'NY',
country: 'US'
}
}
});Update customer data after the widget is initialized:
// Update customer data dynamically
widget.updateCustomerData({
name: 'Jane Smith',
email: 'jane.smith@example.com',
address: {
line1: '456 Oak Avenue',
city: 'Los Angeles',
postalCode: '90001',
state: 'CA',
country: 'US'
}
});import React, { useState, useEffect, useRef } from 'react';
export const PaymentWithCustomerData = () => {
const widgetRef = useRef(null);
const [customerData, setCustomerData] = useState({
name: '',
email: '',
address: {
line1: '',
city: '',
postalCode: '',
country: 'US'
}
});
useEffect(() => {
widgetRef.current = window.paymentWidget.init({
userSessionToken: 'user-session-token',
environment: 'live',
countryCode: 'US',
locale: 'en',
container: 'payment-widget',
customerData: customerData
});
return () => widgetRef.current?.destroy();
}, []);
// Update customer data dynamically
useEffect(() => {
if (widgetRef.current && customerData.name) {
widgetRef.current.updateCustomerData(customerData);
}
}, [customerData]);
const handleFormChange = (field, value) => {
setCustomerData(prev => ({
...prev,
[field]: value
}));
};
return (
<div>
<h3>Customer Information</h3>
<input
type="text"
placeholder="Name"
value={customerData.name}
onChange={(e) => handleFormChange('name', e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={customerData.email}
onChange={(e) => handleFormChange('email', e.target.value)}
/>
<div id="payment-widget" />
</div>
);
};import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-payment-with-customer-data',
template: `
<form [formGroup]="customerForm">
<input formControlName="name" placeholder="Name" />
<input formControlName="email" placeholder="Email" />
<input formControlName="line1" placeholder="Address Line 1" />
<input formControlName="city" placeholder="City" />
<input formControlName="postalCode" placeholder="Postal Code" />
</form>
<div #paymentContainer></div>
`
})
export class PaymentWithCustomerDataComponent implements OnInit, OnDestroy {
@ViewChild('paymentContainer', { static: true }) containerRef!: ElementRef;
private widget: any;
customerForm: FormGroup;
constructor(private fb: FormBuilder) {
this.customerForm = this.fb.group({
name: [''],
email: [''],
line1: [''],
city: [''],
postalCode: [''],
country: ['US']
});
}
ngOnInit() {
this.widget = window.paymentWidget.init({
userSessionToken: this.getUserToken(),
environment: 'live',
countryCode: 'US',
locale: 'en',
container: this.containerRef.nativeElement,
customerData: this.customerForm.value
});
// Update widget when form changes
this.customerForm.valueChanges.subscribe(data => {
this.widget.updateCustomerData({
name: data.name,
email: data.email,
address: {
line1: data.line1,
city: data.city,
postalCode: data.postalCode,
country: data.country
}
});
});
}
ngOnDestroy() {
this.widget?.destroy();
}
}Customer data prefilling is supported by the following payment providers:
- Stripe: Pre-fills name, email, and full billing address in payment forms
- Adyen: Pre-fills cardholder name, email, and address fields for card payments
Note: All customer data fields are optional. The widget will pre-fill only the fields that are provided.
For 3D Secure authentication, users may be redirected to their bank. The widget automatically detects and resumes payment processing after redirect.
Store session token to ensure continuity:
function initializeWidget() {
// Get token from storage or current session
const userSessionToken =
sessionStorage.getItem('paymentSessionToken') || getCurrentUserToken();
// Store for redirect continuity
if (!sessionStorage.getItem('paymentSessionToken')) {
sessionStorage.setItem('paymentSessionToken', userSessionToken);
}
const widget = window.paymentWidget.init({
userSessionToken: userSessionToken,
environment: 'live',
countryCode: 'US',
locale: 'en',
container: 'payment-widget',
onSuccess: (
token,
paymentInstrumentDetails,
paymentInstrumentToken
) => {
sessionStorage.removeItem('paymentSessionToken');
handlePaymentSuccess(
token,
paymentInstrumentDetails,
paymentInstrumentToken
);
}
});
}
// Initialize widget on page load
initializeWidget();Common validation errors:
- Container element not found
- Missing required parameters
- Invalid environment value
try {
const widget = window.paymentWidget.init(config);
} catch (error) {
console.error('Widget initialization failed:', error.message);
}interface PaymentWidget {
init(config: PaymentConfig): PaymentWidgetInstance;
}
interface PaymentWidgetInstance {
destroy(): void;
submitPayment(): Promise<void>;
getPaymentState(): PaymentButtonState;
updateCustomerData(data: CustomerData): void;
}
interface PaymentButtonState {
isProcessing: boolean;
canSubmit: boolean;
}
interface PaymentConfig {
userSessionToken: string;
environment: 'test' | 'sandbox' | 'live';
countryCode: string;
locale: string;
container: string | HTMLElement;
styling?: {
primaryColor?: string;
textColorMain?: string;
borderRadius?: string;
// ... other style options
};
i18n?: Record<string, string>;
featureFlags?: PaymentFeatureFlags;
hidePaymentButton?: boolean;
customerData?: CustomerData;
onSuccess?: (
paymentRequestToken: string,
paymentInstrumentDetails?: PaymentInstrumentDetails,
paymentInstrumentToken?: string
) => void;
onPaymentStateChange?: (state: PaymentButtonState) => void;
devMode?: boolean;
}
interface PaymentFeatureFlags {
useRubiksUI?: boolean; // Enable Rubiks Styleguide components
}
interface CustomerData {
name?: string;
email?: string;
address?: CustomerAddress;
}
interface CustomerAddress {
line1?: string;
line2?: string;
city?: string;
postalCode?: string;
state?: string;
country?: string;
}
interface BankAccountDetails {
accountHolder: string;
bankName: string;
bic: string;
iban: string;
signature?: string;
}
interface PaymentInstrumentDetails {
creditCard?: {
brand?: string;
cardHolder: string;
cardNumber: string;
expiry: string;
issuerCountry?: string;
};
sepa?: {
bankAccountDetails: BankAccountDetails;
};
bacs?: {
accountHolder: string;
bankAccountNumber: string;
bankLocationId: string;
directDebitPdfFormUrl: string;
mandateId: string;
shopperEmail: string;
};
chDD?: {
bankAccountDetails: BankAccountDetails;
};
lsvDD?: {
bankAccountDetails: BankAccountDetails;
};
ideal?: {
issuer: string;
};
banContactCard?: {
cardHolder: string;
cardNumber: string;
expiry: string;
};
paypal?: {};
twint?: {};
cash?: {};
bankTransfer?: {};
}