# Working with Payment API ## Introduction 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: 1. **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. 2. **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. ### Key Concepts - **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_ACCOUNT` or `ECOM`) to determine available payment methods. ## Important Usage Notes - Always define **either** `finionPayCustomerId` **or** `customerId` for an existing customer session. When assigning a `paymentRequestToken` to 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 the `finionPayCustomerId` returned by the first. - While the `paymentRequestToken` is unused for posting a usage, **no funds are captured** (if the payment method allows). Authorizing a token with `amount > 0` only 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 `ECOM` and 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 `paymentRequestToken` is automatically cancelled when the related user payment session expires. - A user payment session is automatically invalidated once one `paymentRequestToken` from that session is used — only one token per session can be used. ## Endpoints Using paymentRequestToken The `paymentRequestToken` returned by the UPC can be used in the following scenarios: ### Creating a Payment Instrument in the Customer 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: [POST /v1/memberships/signup](/apis/magicline/openapi/openapi#operation/signupMembership) - Update the payment instrument of an existing customer [POST /v1/customers/{customerId}/account/payment-instrument](/apis/magicline/openapi/openapi/customers-account/updatecustomerpaymentinstrument) - Offering self-service payment method updates *(work in progress)* - Adding a secondary payment method *(planned)* ### Posting a Sellable Entity If the `paymentRequestToken` is authorized with a payment amount, it can be used for purchasing any sellable entity: - Upfront payment in contract creation for a new customer (joining fee or total contract value): [POST /v1/memberships/signup](/apis/magicline/openapi/openapi#operation/signupMembership) - Upfront payment in contract creation for an existing customer (joining fee or total contract value): [POST /v1/memberships/customers/{customerId}/add-membership](/apis/magicline/openapi/openapi#operation/addMembership) - Account balancing for open fees: [POST /v1/customers/{customerId}/account/payment](/apis/magicline/openapi/openapi#operation/bookCustomerPayment) - 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)* - Purchase consumption credit *(planned)* ## Creating a User Payment Session To initiate a payment process or capture a payment instrument, you must first create a user payment session. **Endpoint:** [POST /v1/payments/user-session](/apis/magicline/openapi/openapi#operation/userSession) **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. ### Request Body Parameters ### Example Request ### Response body The `token` returned is the `userSessionToken` required to initialize the UPC in your frontend integration. ## Payment Widget Integration Guide 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](https://widget.dev.payment.sportalliance.com/widget.js) - Preview version - [https://widget.payment.sportalliance.com/widget.js](https://widget.payment.sportalliance.com/widget.js) - Stable version ### Quick Start ```html
``` ### Configuration | Parameter | Type | Description | | --- | --- | --- | | `userSessionToken` | `string` | User session token | | `environment` | `'test' | 'sandbox' | 'live'` | Payment environment | | `region` | `'eu-central-1' | 'us-east-2'` | API region (defaults to `'eu-central-1'`) | | `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 styling - `i18n` - Translation overrides - `featureFlags` - Enable experimental or alternative features - `onSuccess` - Success callback function that receives the payment request token, payment instrument details, and payment instrument token - `onError` - Error callback for external error handling (allows host app to control error display) - `devMode` - Show i18n keys instead of translated text (development only) - `hidePaymentButton` - Hide widget's internal payment buttons for custom button control. **Note:** Redirect-based payment methods (PayPal, iDEAL, TWINT) will use their own built-in buttons even when this is enabled, as they require direct user interaction. - `onPaymentStateChange` - Callback for payment button state changes (processing, canSubmit) - `customerFields` - Pre-fill and configure visibility of customer information (name, email, address) - `baseUrl` - Override the default API base URL (advanced usage) ### Customization The payment widget supports two levels of styling customization: 1. **Widget Styling** - Basic widget appearance (colors, borders, shadows) 2. **Payment Provider Styling** - Advanced styling for Stripe and Adyen payment forms ### Widget Styling Customize the widget's appearance through the styling configuration: ```javascript styling: { primaryColor: '#007bff', secondaryColor: '#6c757d', textColorMain: '#333333', textColorSecondary: '#6c757d', borderColor: '#dee2e6', borderRadius: '4px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', backgroundColor: 'transparent' // or any CSS color value like '#ffffff' } ``` **Available styling options:** | Property | Default | Description | | --- | --- | --- | | `primaryColor` | `#007bff` | Primary brand color for buttons and interactive elements | | `secondaryColor` | `#6c757d` | Secondary color for backgrounds and hover states | | `textColorMain` | `#333333` | Main text color | | `textColorSecondary` | `#6c757d` | Secondary/muted text color | | `borderColor` | `#e6e6e6` | Border color for inputs and cards | | `borderRadius` | `4px` | Border radius for inputs and cards | | `boxShadow` | `0 2px 4px…` | Box shadow for elevated elements | | `backgroundColor` | `#ffffff` | Widget background color. Use `transparent` to inherit from host container | ### Payment Provider Styling For advanced styling of payment forms, you can customize the appearance of Stripe and Adyen payment elements directly through their respective APIs. ##### Stripe Appearance API Stripe Elements can be customized using the [Appearance API](https://docs.stripe.com/elements/appearance-api). Configure the appearance object in your styling options: ```javascript styling: { primaryColor: '#007bff', stripe: { appearance: { theme: 'stripe', variables: { colorPrimary: '#0570de', colorBackground: '#ffffff', colorText: '#30313d', colorDanger: '#df1b41', fontFamily: 'Ideal Sans, system-ui, sans-serif', spacingUnit: '4px', borderRadius: '4px' }, rules: { '.Input': { border: '1px solid #e6e6e6', boxShadow: 'none' }, '.Input:focus': { border: '1px solid #0570de' } } } } } ``` ##### Custom Fonts Load custom fonts for Stripe Elements using the `fonts` option: ```javascript // Using CssFontSource - reference a CSS file with @font-face definitions styling: { stripe: { appearance: { variables: { fontFamily: 'Roboto, sans-serif' } }, fonts: [ { cssSrc: 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&display=swap' } ] } } // Using CustomFontSource - load font files directly styling: { stripe: { appearance: { variables: { fontFamily: 'MyCustomFont, sans-serif' } }, fonts: [ { family: 'MyCustomFont', src: 'url(https://example.com/fonts/MyCustomFont.woff2)', weight: '400' } ] } } ``` See Stripe documentation for [CssFontSource](https://docs.stripe.com/js/appendix/css_font_source_object) and [CustomFontSource](https://docs.stripe.com/js/appendix/custom_font_source_object) options. ##### Adyen CSS Variables Adyen Drop-in can be styled using [CSS variable overrides](https://github.com/Adyen/adyen-web?tab=readme-ov-file#styling). Add custom CSS to your page: ```css :root { --adyen-sdk-color-background-primary: #ffffff; --adyen-sdk-color-background-secondary: #f7f7f8; --adyen-sdk-color-background-always-dark: #00112c; --adyen-sdk-color-label-primary: #00112c; --adyen-sdk-color-label-secondary: #5c687c; --adyen-sdk-color-label-highlight: #0070f5; --adyen-sdk-color-label-critical: #e22d2d; --adyen-sdk-color-outline-primary: #dbdee2; --adyen-sdk-color-outline-primary-active: #00112c; --adyen-sdk-border-radius-m: 8px; } ``` See the [full list of CSS variables](https://github.com/Adyen/adyen-web?tab=readme-ov-file#where-do-we-define-those-css-variables-and-what-is-the-default-value) in the Adyen Web SDK documentation. #### Translations ```javascript i18n: { 'upc.my.payment.instruments': 'My Payment Methods', 'upc.payment.methods.add.new': 'Add New Payment Method' } ``` To hide a UI element entirely, set its key to an empty string: ```javascript i18n: { 'upc.security.notice': '', // hides the security notice 'upc.warning.no.payment.methods': '' // hides the "no payment methods" warning } ``` #### Feature Flags Enable experimental or alternative features: ```javascript featureFlags: { hideMandateCheckbox: false, // Hide mandate acceptance checkbox (default: false) disableStripeLink: false, // Disable Stripe Link autofill feature (default: false) enableMandateDownload: true, // Show mandate download step for BACS and ACH payments (default: true) enableSignatureTypeInput: false, // Show type input tab in signature field (default: false) showPaymentInstrumentList: true // Navigate back to the payment instruments list after success (default: true) } ``` | Flag | Default | Description | | --- | --- | --- | | `hideMandateCheckbox` | `false` | Hides the mandate acceptance checkbox in direct debit forms | | `disableStripeLink` | `false` | Disables Stripe Link, which provides autofill for returning customers | | `enableMandateDownload` | `true` | Shows a mandate download button after ACH or BACS payment instrument creation. When `false`, `onSuccess` is called immediately | | `enableSignatureTypeInput` | `false` | Shows the "Type" tab in signature fields, allowing typed text signatures. When `false`, only the draw pad is shown | | `showPaymentInstrumentList` | `true` | When `true`, navigates back to the payment instruments list after a successful payment. When `false`, only `onSuccess` is called and the widget stays on the current view | #### Development Mode Enable development mode to display i18n keys instead of translated text: ```javascript devMode: true; // Shows i18n keys instead of translations for development ``` #### Success Callback Handle successful payment completion: ```javascript 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 }; ``` ### Integration Examples #### React Integration ```tsx 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, onSuccess: (token, details, instrumentToken) => { onPaymentSuccess(token, details, instrumentToken); } }); } return () => widgetRef.current?.destroy(); }, [userToken, onPaymentSuccess]); return ; }; ``` #### Angular Integration ```typescript import { Component, ElementRef, ViewChild, OnDestroy } from '@angular/core'; @Component({ selector: 'app-payment-widget', template: '' }) 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(); } } ``` #### Vue.js Integration ```vue ``` ### Custom Payment Button Control 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. ```javascript // 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 state ``` #### Basic Usage ```javascript const 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); } }); ``` #### Widget API Methods When you initialize the widget, it returns an instance with the following methods: | Method | Return Type | Description | | --- | --- | --- | | `destroy()` | `void` | Clean up widget resources | | `submitPayment()` | `Promise