Skip to content
Last updated

Introduction

This guide explains how to integrate a membership signup flow using the Open API and the Universal Payment Component (UPC). This implementation requires careful orchestration of multiple API calls and payment sessions to handle both recurring and one-time payments.

For general information on working with the Payment API, see Working with Payment API.

Table of Contents

  1. Flow Overview
  2. Step 1: Fetch Membership Offers
  3. Step 2: Get Offer Details
  4. Step 3: Collect Member Information
  5. Step 4: Create Contract Preview
  6. Step 5: Payment Session Logic
  7. Step 6: Mount Universal Payment Component
  8. Step 7: Session Persistence & Page Reload
  9. Step 8: Final Contract Submission
  10. Common Gotchas & Best Practices

Flow Overview

The membership signup flow typically involves these stages:

  1. Fetch offers – Retrieve available membership options from API
  2. Select offer & term – Determine specific contract duration
  3. Collect member info – Gather personal details (name, email, address, DOB)
  4. Generate preview – Calculate pricing with discounts/vouchers
  5. Setup recurring payment – Create MEMBER_ACCOUNT session & mount widget
  6. Setup upfront payment – Create ECOM session & mount widget (if needed)
  7. Submit contract – Finalize membership creation

Critical: Two separate payment sessions are required:

  • Session 1: MEMBER_ACCOUNT scope – for recurring monthly payments (amount: 0)
  • Session 2: ECOM scope – for upfront/setup fees (amount: actual due amount)

Each session must map to the same Finion Pay customer, which is achieved by passing the finionPayCustomerId from Session 1 into Session 2.


Step 1: Fetch Membership Offers

Endpoint: GET /v1/memberships/membership-offers

Purpose: Retrieve all available membership offers that are configured by the studio under Settings / Offer Configuration / Open API.

Example Request

curl -i -X GET \
  https://open-api-demo.open-api.magicline.com/v1/memberships/membership-offers \
  -H 'X-API-KEY: YOUR_API_KEY_HERE'

Response

idinteger(int64)required

Unique identifier for the membership offer.

descriptionstringrequired

Description of the membership offer.

Example: "This offer includes access to all gym facilities and group classes."
namestringrequired

Name of the membership offer.

Example: "Standard Membership"
subDescriptionstring

Sub description for the membership offer.

Example: "Best value for regular gym-goers."
imageUrlstring

Temporary valid download link for rate bundle image. Expires after 2 hours.

Example: "https://example.com"
footnotestring

Contractual or legal comments to be displayed below the offer.

Example: "This offer is valid for new members only. Terms and conditions apply."
preUseTypestring(ContractPreUseType)required

Contract pre-use type information.

Enum ValueDescription
CHARGEABLE

Contract pre-use is chargeable

FREE

Contract pre-use is free

NOT_AVAILABLE

Contract is not available for pre-use

Example: "NOT_AVAILABLE"
limitedOfferingPeriodobject(DatePeriod)required

Represents a date period.

limitedOfferingPeriod.​startDatestring(date)required

Start of the interval

Example: "2025-01-01"
limitedOfferingPeriod.​endDatestring(date)required

End of the interval

Example: "2026-01-01"
rateCodesArray of objects(MembershipOfferRateCode)required

List of rate codes that are part of this membership offer.

rateCodes[].​namestring

The name of the rate code

Example: "Standard Rate"
rateCodes[].​identifierstring

Unique identifier for the rate code.

Example: "RC12345"
termsArray of objects(MembershipOfferTerm)required

List of terms that are part of this membership offer.

terms[].​idinteger(int64)required

Unique identifier of the membership offer term

Example: 1234567890
terms[].​termobject(Term)

Represents a term

terms[].​contractVolumeInformationobject(ContractVolume)

Contains the total amount to be paid during the initial runtime of the contract and the average amounts per month and per the rate's payment frequency (e.g. 1 week)

terms[].​paymentFrequencyobject(MembershipOfferPaymentFrequency)required

Represents the payment frequency

terms[].​paymentFrequency.​idinteger(int64)

The unique identifier of the payment frequency. (null possible for starter package)

Example: 1234567890
terms[].​paymentFrequency.​typestringrequired

Payment frequency type of a contract

Enum ValueDescription
TERM_BASED

Represents that the contract payment frequency is based on terms, with a possibly individual price per term.

NON_RECURRING

Represents that the contract payment frequency is non recurring, meaning only one payment is necessary here.

FREE

Represents that the contract payment frequency is free of charge.

RECURRING

Represents that the contract payment frequency is recurring, meaning that the payment will take place every term.

MONTH_DAY

Represents that the contract payment frequency is based on month days, with a possibly individual price per month day.

Example: "FREE"
terms[].​paymentFrequency.​termobject(Term)

Represents a term

terms[].​paymentFrequency.​priceobject(Money)

Represents a financial data

terms[].​paymentFrequency.​monthDaysToPricesArray of objects(MembershipOfferMonthDayToPrice)

Month day to prices list, used for contract payment frequency type MONTH_DAY

terms[].​paymentFrequency.​termsToPricesArray of objects(MembershipOfferTermToPrice)

Terms to prices list, used for contract payment frequency type TERM_BASED. Note that the price will become active AFTER the respective term has passed

terms[].​paymentFrequency.​recurringboolean

Whether the cycle of payments repeats indefinitely. Only relevant if type is TERM_BASED or MONTH_DAY.

terms[].​paymentFrequency.​ageBasedAdjustmentsArray of objects(MembershipOfferModuleAgeBasedAdjustment)

List of age-based adjustments for the membership offer module.

terms[].​paymentFrequency.​formattedPaymentFrequencystringrequired

The formatted payment frequency

Example: "Every 6 months"
terms[].​extensionTermobject(Term)

Represents a term

Note: The response is an array of MembershipOffer objects.


Step 2: Get Offer Details

Endpoint: GET /v1/memberships/membership-offers/{offerId}

Purpose: Fetch complete offer details including flatFees (setup/registration fees).

Example Request

curl -i -X GET \
  'https://open-api-demo.open-api.magicline.com/v1/memberships/membership-offers/{membershipOfferId}' \
  -H 'X-API-KEY: YOUR_API_KEY_HERE'

Response

idinteger(int64)required

Unique identifier for the membership offer.

descriptionstringrequired

Description of the membership offer.

Example: "This offer includes access to all gym facilities and group classes."
namestringrequired

Name of the membership offer.

Example: "Standard Membership"
subDescriptionstring

Sub description for the membership offer.

Example: "Best value for regular gym-goers."
imageUrlstring

Temporary valid download link for rate bundle image. Expires after 2 hours.

Example: "https://example.com"
footnotestring

Contractual or legal comments to be displayed below the offer.

Example: "This offer is valid for new members only. Terms and conditions apply."
preUseTypestring(ContractPreUseType)required

Contract pre-use type information.

Enum ValueDescription
CHARGEABLE

Contract pre-use is chargeable

FREE

Contract pre-use is free

NOT_AVAILABLE

Contract is not available for pre-use

Example: "NOT_AVAILABLE"
limitedOfferingPeriodobject(DatePeriod)required

Represents a date period.

limitedOfferingPeriod.​startDatestring(date)required

Start of the interval

Example: "2025-01-01"
limitedOfferingPeriod.​endDatestring(date)required

End of the interval

Example: "2026-01-01"
rateCodesArray of objects(MembershipOfferRateCode)required

List of rate codes that are part of this membership offer.

rateCodes[].​namestring

The name of the rate code

Example: "Standard Rate"
rateCodes[].​identifierstring

Unique identifier for the rate code.

Example: "RC12345"
includedModulesArray of objects(MembershipOfferIncludedModule)required

List of included modules of this membership offer.

includedModules[].​idinteger(int64)required

Unique identifier for the membership offer module.

Example: 1234567890
includedModules[].​namestringrequired

Name of the membership offer module.

Example: "Premium Fitness Package"
includedModules[].​descriptionstringrequired

Description of the membership offer module.

Example: "Includes access to all gym facilities and group classes."
includedModules[].​imageUrlstring

Image url of membership offer module. Expires after 5hours.

Example: "https://some-module.com"
includedModules[].​termobject(MembershipOfferModuleTerm)required

Represents the terms of a membership offer.

includedModules[].​term.​extensionTypestringrequired

Module term extension type

Enum ValueDescription
SUBSEQUENT_RATE_DETAIL

Subsequent rate detail

TERM_EXTENSION

Term extension

NONE

No extension

Example: "TERM_EXTENSION"
includedModules[].​term.​termobject(Term)

Represents a term

includedModules[].​term.​termExtensionobject(Term)

Represents a term

includedModules[].​term.​cancelationPeriodobject(Term)

Represents a term

includedModules[].​term.​extensionCancelationPeriodobject(Term)

Represents a term

includedModules[].​trialPeriodobject(MembershipOfferTrialPeriod)

Represents the trial period of a membership offer.

includedModules[].​consentTextBlockobject(MembershipOfferTextBlock)

Contract text block information.

contractSignaturesRequiredboolean

Indicates if contract signatures are required for this membership offer.

allowedPaymentChoicesArray of stringsuniquerequired

Allowed payment choices for this membership offer.

Items Enum ValueDescription
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

Example: ["CASH"]
maximumNumberOfSelectableModulesinteger(int32)

Maximum number of selectableModules for this membership offer. If 0, no modules can be selected.

contractTextBlocksArray of objects(MembershipOfferTextBlock)required

List of contract text blocks that are part of this membership offer.

contractTextBlocks[].​idinteger(int64)

The unique identifier of the text block

Example: 1234567890
contractTextBlocks[].​titlestring

The title of the text block

Example: "Title of 1. text block"
contractTextBlocks[].​textstring

The text of the text block

Example: "Text of 1. text block"
contractTextBlocks[].​orderinteger(int32)

The order of the text block in the contract

Example: 1
contractTextBlocks[].​hasSignatureboolean

Text block configuration has a signature field. You have the option to show a signature input field and it will be added in the contract document. It is not a required field in magicline. It is up to your implementation if you add this field.

contractTextBlocks[].​showCommunicationPrivacyOptionsboolean

Text block contains information about the communication privacy options. You are advised to show a matrix with communication settings in combination with this text block.

contractTextBlocks[].​attachmentTypestring

Indicates the type of attachment for this block

Enum ValueDescription
CONTRACT_PDF_PREVIEW

Contract PDF preview

NONE

No text block

FILE

File information available in attachedDocument

URL

Url information available in attachedExternalUrlDto

Example: "CONTRACT_PDF_PREVIEW"
contractTextBlocks[].​attachedExternalUrlDtoobject(ExternalUrl)

Represents url information.

contractTextBlocks[].​attachedDocumentobject(AttachedDocument)

Represents document information.

contractTextBlocks[].​rateBundleModuleIdinteger(int64)

The ID of the associated rate bundle module. Only set if this is a module consent text block.

Example: 1234567890
contractTextBlocks[].​confirmationRequiredboolean
selectableModulesArray of objects(MembershipOfferModule)

List of selectable modules for this membership offer. Limited by maximumNumberOfSelectableModules.

termsArray of objects(MembershipOfferTermExtended)

List of terms that are part of this membership offer.

Critical: Understanding flatFees

  • flatFees: Array of one-time fees (registration, setup, admin fees)
  • starterPackage: true: Fee is due at signup (included in upfront payment)
  • Initial payment estimate: Sum of all flatFees with starterPackage: true

Note: The flatFees array provides an estimate of upfront costs. The authoritative source for the exact amount due at signup is the preview API response (dueOnSigningAmount). Always use the preview API before creating payment sessions.


Step 3: Collect Member Information

Gather required personal details from the user:

Required Fields

  • Personal Info:

    • firstName (string) – Required
    • lastName (string) – Required
    • email (string, valid email format) – Required
    • dateOfBirth (string, ISO date format: YYYY-MM-DD) – Required
    • phoneNumberMobile (string, optional but recommended)
  • Address:

    • street (string, including house number) – Required
    • city (string) – Required
    • zipCode (string) – Required
    • countryCode (string, ISO 3166-1 alpha-2, e.g., "DE") – Required
  • Language:

    • language.languageCode (string, ISO 639-1, e.g., "de") – Required
    • language.countryCode (string, ISO 3166-1, e.g., "DE") – Optional
  • Contract Details:

    • startDate (string, ISO date format: YYYY-MM-DD) – Required
    • preuseDate (string, ISO date, optional – allows gym access before contract start)
    • voucherCode (string, optional – for discounts)

Step 4: Create Contract Preview

Endpoint: POST /v1/memberships/signup/preview

Purpose: Calculate exact pricing including discounts, age-based pricing, vouchers, and the authoritative dueOnSigningAmount.

When to Call Preview API

The preview API should be called:

  1. Initially – After selecting a membership term (optional, can be conducted with dummy data)
  2. On voucher application – When voucher code is added or removed
  3. On date of birth change – For age-based pricing adjustments (debounced)
  4. On contract start date change – May affect promotional periods (debounced)
  5. On pre-use date change – May affect initial billing (debounced)
  6. Before payment step – Final pricing confirmation with complete member information

Debouncing: For fields that trigger preview updates (DOB, dates), implement debounce to avoid excessive API calls.

Example Request

curl -i -X POST \
  https://open-api-demo.open-api.magicline.com/v1/memberships/signup/preview \
  -H 'Content-Type: application/json' \
  -H 'X-API-KEY: YOUR_API_KEY_HERE' \
  -d '{
    "contract": {
      "contractOfferTermId": 1000,
      "startDate": "2026-02-01",
      "preuseDate": "2026-01-01",
      "notes": "Some notes",
      "thirdPartyId": "1000a",
      "employeeId": 1239812733,
      "referralCode": "A500D",
      "selectedSelectableModuleIds": [
        0
      ],
      "selectedOptionalModuleIds": [
        0
      ],
      "initialPaymentRequestToken": "3JtyH5sakfn2V22vB0napNC2zWMlpFwS9gPQawuk7Jw1F00atOD0BA",
      "contractSignature": {
        "base64SvgSignature": "string"
      },
      "textBlockSignatures": [
        {
          "base64SvgSignature": "string",
          "textBlockId": 0
        }
      ],
      "voucherCode": 123
    },
    "customer": {
      "thirdPartyId": "A1000",
      "firstName": "Peter",
      "secondFirstName": "Thomas",
      "lastName": "Muller",
      "secondLastName": "Meyer",
      "dateOfBirth": "2019-08-24",
      "email": "example@example.com",
      "gender": "MALE",
      "phoneNumberPrivate": "+44123456789",
      "phoneNumberMobile": "+44987654321",
      "street": "Raboisen Street",
      "secondStreet": "Second Street",
      "cityPart": "Tegel",
      "district": "District 12",
      "streetType": "Avenue",
      "streetBlock": "5th block",
      "portal": "Portal 1",
      "stairway": "Right stairway",
      "door": "Door 1",
      "province": "Champagne",
      "additionalAddressInformation": "Additional information",
      "floor": "2nd floor",
      "language": {
        "languageCode": "de",
        "countryCode": "DE"
      },
      "houseNumber": "3-4",
      "buildingName": "Empire State Building",
      "city": "Hamburg",
      "zipCode": "220-99",
      "countryCode": "DE",
      "communicationPreferences": [
        {
          "messageCategory": "CONTRACT",
          "channels": [
            {
              "communicationChannel": "EMAIL",
              "customerOverridable": true,
              "active": true
            }
          ]
        }
      ],
      "documentIdentification": {
        "documentNumber": "CX5432112345DS",
        "documentType": "ID_CARD"
      },
      "paymentRequestToken": "string",
      "additionalInformationFieldAssignments": [
        {
          "additionalInformationFieldId": 1234567890,
          "value": "string"
        }
      ]
    }
  }'

Request Body

contractobject(MembershipSignupContract)required

The contract information used for the membership signup

contract.​contractOfferTermIdinteger(int64)required

Unique ID of the contract offer term

Example: 1000
contract.​startDatestring(date)required

The start date of the contract

Example: "2026-02-01"
contract.​preuseDatestring(date)

The pre use date of the contract. If not provided, it will be evaluated based on contract offer configuration.

Example: "2026-01-01"
contract.​notesstring

The notes related to the contract

Example: "Some notes"
contract.​thirdPartyIdstring

Unique ID of the third party contract in the third party system

Example: "1000a"
contract.​employeeIdinteger(int64)

Unique ID of the employee who created the membership

Example: 1239812733
contract.​referralCodestring

Referral code to link with recruiter

Example: "A500D"
contract.​selectedSelectableModuleIdsArray of integers(int64)

The selected modules from the available selectable modules

contract.​selectedOptionalModuleIdsArray of integers(int64)

The selected modules from the available optional modules

contract.​initialPaymentRequestTokenstring

This token identifies a pre-authorized payment request. It acts as a reference to the payment session initiated by the user. It's not needed for preview endpoints.

Example: "3JtyH5sakfn2V22vB0napNC2zWMlpFwS9gPQawuk7Jw1F00atOD0BA"
contract.​contractSignatureobject(Signature)

Representing customer signature

contract.​textBlockSignaturesArray of objects(TextBlockSignature)

Signatures for text blocks

contract.​voucherCodestring

An optional code for a voucher. Only credit and discount vouchers are supported.

Example: 123
customerobject(MembershipSignupCustomer)required

The customer information used for the membership signup

customer.​thirdPartyIdstring

Unique ID of the third party customer in the third party system

Example: "A1000"
customer.​firstNamestringrequired

First name of the customer

Example: "Peter"
customer.​secondFirstNamestring

Second first name of the customer

Example: "Thomas"
customer.​lastNamestringrequired

Last name of the customer

Example: "Muller"
customer.​secondLastNamestring

Second last name of the customer

Example: "Meyer"
customer.​dateOfBirthstring(date)required

Date of birth of the customer

Example: "2019-08-24"
customer.​emailstringrequired

Email address of the customer

Example: "example@example.com"
customer.​genderstring

Gender of the customer

Enum ValueDescription
MALE

Male gender of the customer

FEMALE

Female gender of the customer

UNISEX

Unisex gender of the customer

Example: "MALE"
customer.​phoneNumberPrivatestring

Private phone number of the customer

Example: "+44123456789"
customer.​phoneNumberMobilestring

Mobile phone number of the customer

Example: "+44987654321"
customer.​streetstringrequired

Street of the customer's address

Example: "Raboisen Street"
customer.​secondStreetstring

Second street line of the customer's address

Example: "Second Street"
customer.​cityPartstring

City part of the customer's address

Example: "Tegel"
customer.​districtstring

District of the customer's address

Example: "District 12"
customer.​streetTypestring

Street type of the customer's address

Example: "Avenue"
customer.​streetBlockstring

Street block of the customer's address

Example: "5th block"
customer.​portalstring

Portal of the customer's address

Example: "Portal 1"
customer.​stairwaystring

Stairway of the customer's address

Example: "Right stairway"
customer.​doorstring

Door of the customer's address

Example: "Door 1"
customer.​provincestring

Province of the customer's address

Example: "Champagne"
customer.​additionalAddressInformationstring

Additional address information of the customer's address

Example: "Additional information"
customer.​floorstring

Floor of the customer's address

Example: "2nd floor"
customer.​languageobject(Language)required

Basic language information

customer.​language.​languageCodestring(ISO 639-1)required

The language code

Example: "de"
customer.​language.​countryCodestring(ISO 3166-1)

The country code

Example: "DE"
customer.​houseNumberstring

House number of the customer's address

Example: "3-4"
customer.​buildingNamestring

Building name

Example: "Empire State Building"
customer.​citystringrequired

City of the customer's address

Example: "Hamburg"
customer.​zipCodestringrequired

Zip code of the customer's address

Example: "220-99"
customer.​countryCodestring(ISO 3166-1)required

Country code of the customer's address

Example: "DE"
customer.​communicationPreferencesArray of objects(CommunicationPreference)

List of communication preferences for the customer

customer.​documentIdentificationobject(DocumentIdentification)

Information from an official document that identifies the customer

customer.​paymentRequestTokenstring

By assigning the paymentRequestToken to the customer, the payment method associated with the token will be used as payment method setting of the customer. Additionally, if the paymentRequestToken is associated with a payment instrument, i.e. a SEPA Mandate, BACS Mandate, Credit Card, or other, the payment instrument will be made available in the member account for future collection via payment runs. By leaving this field empty the payment method of the customer will be set to CASH. For reference check 'Create a user payment session'.

customer.​additionalInformationFieldAssignmentsArray of objects(AdditionalInformationFieldAssignment)

List of additional information field assignments of the customer

Response

basePriceobject(Money)required

Represents a financial data

basePrice.​amountnumberrequired

Amount of the finance data tuple

Example: 20
basePrice.​currencystring(ISO 4217)required

Currency of the finance data tuple

Example: "EUR"
preUseChargeobject(Money)

Represents a financial data

voucherTypestring

Type of a voucher, if a valid voucher code has been provided

Enum ValueDescription
CREDIT

Represents a credit voucher

CHECKIN

Represents a check-in voucher

COMPANY_FEE_SPLIT

Represents a company fee split voucher

CONTRACT

Represents a contract voucher

DISCOUNT

Represents a discount voucher

Example: "DISCOUNT"
voucherRemarksstring

Voucher remarks, if a valid voucher code has been provided

Example: "Yoga class not included"
voucherEffectivePeriodstring

The type of period for which the voucher is applied, if a valid voucher code has been provided

Enum ValueDescription
TIME_BASED

Represents a voucher with a time based effective period

UNLIMITED

Represents a voucher with unlimited effective period

INITIAL_TERM

Represents a voucher effective during the initial term of the membership

Example: "UNLIMITED"
effectivePeriodTimeBasedTermobject(Term)

Represents a term

discountedBasePriceobject(Money)

Represents a financial data

discountTypestring

Type of discount (percentage or absolute), if a valid discount voucher code has been provided

Enum ValueDescription
ABSOLUTE

Represents an absolute discount

PERCENTAGE

Represents a percentage based discount

Example: "PERCENTAGE"
discountValueobject(Money)

Represents a financial data

discountedPreUseChargeobject(Money)

Represents a financial data

creditValueobject(Money)

Represents a financial data

companyFeeSplitTypestring

Type of company fee split (percentage or absolute), if a valid company discount voucher code has been provided

Enum ValueDescription
ABSOLUTE

Represents an absolute discount

PERCENTAGE

Represents a percentage based discount

Example: "ABSOLUTE"
companyFeeSplitValueobject(Money)

Represents a financial data

companyNamestring

Name of the company that will pay the company amount, if a valid company discount voucher code has been provided

Example: "Some company"
companyAmountPreUseChargeobject(Money)

Represents a financial data

companyAmountobject(Money)

Represents a financial data

memberAmountobject(Money)

Represents a financial data

memberAmountPreUseChargeobject(Money)

Represents a financial data

companyAmountWithoutDiscountobject(Money)

Represents a financial data

memberAmountWithoutDiscountobject(Money)

Represents a financial data

voucherBonusPeriodobject(MembershipOfferRateBonusPeriod)

Represents a membership offer rate bonus period.

voucherTextBlocksArray of objects(MembershipOfferTextBlock)

Voucher related text blocks, if a valid voucher code has been provided

flatFeePreviewsArray of objects(FlatFeePreview)

Flat fee previews, including discount if a valid discount voucher code has been provided

voucherSuccessMessagestring

Textual representation of the voucher conditions, if a valid voucher code has been provided

Example: "Voucher code \"123\" successfully applied: 5% discount on the membership fee"
voucherErrorCodestring

An error code, if the voucher code is invalid

ValueDescription
INVALID_CODE

Represents an invalid voucher code

Example: "INVALID_CODE"
ageAdjustedPriceobject(Money)

Represents a financial data

contractVolumeInformationobject(ContractVolume)

Contains the total amount to be paid during the initial runtime of the contract and the average amounts per month and per the rate's payment frequency (e.g. 1 week)

moduleConsentTextBlocksArray of objects(MembershipOfferTextBlock)

Consent texts for modules that are part of the contract

selectedOptionalModulesPreviewsArray of objects(ModuleContractPreview)

Optional module contract preview including discount, company split or credit, if a valid voucher code has been provided

paymentPreviewobject(SignupPaymentPreview)

Represents a preview of the payment schedule for a signup process.

Critical: dueOnSigningAmount

This is the authoritative amount for upfront payment.

  • Location: preview.paymentPreview.dueOnSigningAmount.amount

  • Purpose: Exact amount to charge immediately (setup fees + first payment if applicable)

  • Usage: Use this value when creating the ECOM payment session (Session 2)

  • Configuration in ERP: Go to a membership offer → Offer Options → Set Require Initial Payment for Open API sign-ups

  • Calculation: This amount is calculated based on the rate / term configuration. The amount contains all claims that are due on the contract start date. If a pre-use is configured at a price, this will be included. For more reference, refer to the support documentation.


Step 5: Payment Session Logic

Understanding Payment Scenarios

Based on the preview API response, determine which payment sessions are needed:

function analyzePaymentNeeds(preview) {
  const dueOnSigning = preview.paymentPreview?.dueOnSigningAmount?.amount || 0;
  const totalContractVolume = preview.contractVolumeInformation?.totalContractVolume?.amount || 0;

  let needsRecurring = true;
  let needsUpfront = false;

  // Scenario 1: Full payment upfront (no recurring payments)
  if (dueOnSigning > 0 && dueOnSigning === totalContractVolume) {
    needsRecurring = false;
    needsUpfront = true;
  }
  // Scenario 2: No upfront payment (recurring only)
  else if (dueOnSigning === 0) {
    needsRecurring = true;
    needsUpfront = false;
  }
  // Scenario 3: Both payments needed
  else if (dueOnSigning > 0) {
    needsRecurring = true;
    needsUpfront = true;
  }

  return { needsRecurring, needsUpfront, upfrontAmount: dueOnSigning };
}

Session 1: Recurring Payment (MEMBER_ACCOUNT)

Create this session FIRST to obtain the finionPayCustomerId.

For detailed information on creating payment sessions, see Creating a User Payment Session.

Example Request

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
  }'

Request Body

amountnumberrequired

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.

Example: 19.99
scopestringrequired

Specifies where the created payment instruments will be used, as the available payment methods differ by scope.

Enum ValueDescription
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).

Example: "MEMBER_ACCOUNT"
customerIdinteger(int64)

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 finionPayCustomerId will 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

Example: 1234567890
finionPayCustomerIdstring(uuid)

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 customerId yet.

  • Behavior when omitted: In the absence of a customerId, a new finionPayCustomerId will be automatically created and assigned to the user for the current session.

  • Mutually exclusive with: customerId

Example: "753ea8ec-c2ec-4761-824b-bc46eda3f644"
permittedPaymentChoicesArray of stringsunique

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 ValueDescription
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

Example: ["CASH"]
referenceTextstringrequired

Allows the definition of the reference text shown on the bank statement of the customer.

Example: "Gym Joining Fee 01.07.2025"
requireDirectDebitSignatureboolean

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.

Example: false

For membership recurring payments:

  • Set amount to 0 (MEMBER_ACCOUNT sessions store payment method only)
  • Set scope to "MEMBER_ACCOUNT"
  • Set referenceText to something like "Membership Contract Recurring Payment"
  • Set permittedPaymentChoices to the allowedPaymentChoices from the offer details
  • Do NOT include finionPayCustomerId for a new customer

Response

tokenstringrequired

The token for the user session.

Example: "CllClFmVlSCs3oe0ND0JloLWlNzdb3QseU4507gf1mSVAHqRTwzKWU"
tokenValidUntilstring(date-time)required

The date and time until the token is valid.

Example: "2025-01-07T16:25:09.416924Z"
finionPayCustomerIdstring(uuid)required

Identifies a customer in Finion Pay, i.e. to retreive existing payment instruments.

Example: "753ea8ec-c2ec-4761-824b-bc46eda3f644"

Critical Points

  1. Amount is always 0 – MEMBER_ACCOUNT sessions store payment method only
  2. referenceText is required – This appears on bank statements
  3. No finionPayCustomerId in request – This is the first session for a new customer
  4. For permittedPaymentChoices use allowedPaymentChoices list from offer details endpoint
  5. Store the finionPayCustomerId from response – Required for Session 2
  6. Store the token – Required for widget remounting after page reload

Session 2: Upfront Payment (ECOM)

Create this session SECOND using the finionPayCustomerId from Session 1.

Example Request

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
  }'

Request Body

amountnumberrequired

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.

Example: 19.99
scopestringrequired

Specifies where the created payment instruments will be used, as the available payment methods differ by scope.

Enum ValueDescription
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).

Example: "MEMBER_ACCOUNT"
customerIdinteger(int64)

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 finionPayCustomerId will 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

Example: 1234567890
finionPayCustomerIdstring(uuid)

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 customerId yet.

  • Behavior when omitted: In the absence of a customerId, a new finionPayCustomerId will be automatically created and assigned to the user for the current session.

  • Mutually exclusive with: customerId

Example: "753ea8ec-c2ec-4761-824b-bc46eda3f644"
permittedPaymentChoicesArray of stringsunique

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 ValueDescription
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

Example: ["CASH"]
referenceTextstringrequired

Allows the definition of the reference text shown on the bank statement of the customer.

Example: "Gym Joining Fee 01.07.2025"
requireDirectDebitSignatureboolean

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.

Example: false

For membership upfront payments:

  • Set amount to the dueOnSigningAmount from the preview API response
  • Set scope to "ECOM"
  • Set referenceText to something like "Membership Setup Fee"
  • Omit permittedPaymentChoices to expose all studio-configured payment methods for ECOM scope
  • MUST include finionPayCustomerId from Session 1 response to link both sessions

Response

tokenstringrequired

The token for the user session.

Example: "CllClFmVlSCs3oe0ND0JloLWlNzdb3QseU4507gf1mSVAHqRTwzKWU"
tokenValidUntilstring(date-time)required

The date and time until the token is valid.

Example: "2025-01-07T16:25:09.416924Z"
finionPayCustomerIdstring(uuid)required

Identifies a customer in Finion Pay, i.e. to retreive existing payment instruments.

Example: "753ea8ec-c2ec-4761-824b-bc46eda3f644"

Critical Points

  1. Amount is the actual due amount – Use dueOnSigningAmount from preview
  2. referenceText is required – This appears on bank statements
  3. MUST include finionPayCustomerId – Links both sessions to same customer
  4. Omit permittedPaymentChoices – Recommended to expose all studio-configured payment methods for ECOM scope
  5. Store the token – Required for widget remounting after page reload

Step 6: Mount Universal Payment Component

For detailed widget integration instructions, see Payment Widget Integration Guide.

Use either the preview or stable version URI of the widget

Load Widget Library

<!-- Add to <head> -->
<script src="https://widget.dev.payment.sportalliance.com/widget.js"></script>

Or dynamically:

function loadWidgetLibrary() {
  return new Promise((resolve, reject) => {
    if (window.paymentWidget) {
      resolve();
      return;
    }

    const script = document.createElement('script');
    script.src = 'https://widget.dev.payment.sportalliance.com/widget.js';
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

Mount Recurring Payment Widget

// Ensure DOM element exists
const containerEl = document.getElementById('recurring-payment-container');
if (!containerEl) {
  throw new Error('Container element not found');
}

// Clear any existing content
containerEl.innerHTML = '';

// Initialize widget
const recurringWidget = window.paymentWidget.init({
  userSessionToken: recurringSession.token,
  container: containerEl, // Can be element or element ID
  countryCode: 'DE',
  locale: 'en-US',
  environment: 'test', // or 'sandbox', 'live'

  onSuccess: (paymentRequestToken, paymentInstrumentDetails) => {
    console.log('Recurring payment setup successful');
    console.log('Payment Request Token:', paymentRequestToken);
    console.log('Payment Method:', paymentInstrumentDetails);

    // Store token for final submission
    recurringPaymentToken = paymentRequestToken;

    // Proceed to upfront payment (if needed)
    if (needsUpfront) {
      mountUpfrontWidget();
    }
  },

  onError: (error) => {
    console.error('Recurring payment error:', error);
    // Handle error appropriately
  }
});

Mount Upfront Payment Widget

const containerEl = document.getElementById('upfront-payment-container');
if (!containerEl) {
  throw new Error('Container element not found');
}

containerEl.innerHTML = '';

const upfrontWidget = window.paymentWidget.init({
  userSessionToken: upfrontSession.token,
  container: containerEl,
  countryCode: 'DE',
  locale: 'en-US',
  environment: 'test',

  onSuccess: (paymentRequestToken, paymentInstrumentDetails) => {
    console.log('Upfront payment successful');
    console.log('Payment Request Token:', paymentRequestToken);
    console.log('Transaction:', paymentInstrumentDetails);

    // Store token for final submission
    upfrontPaymentToken = paymentRequestToken;

    // Mark upfront payment as complete
    upfrontPaymentComplete = true;
  },

  onError: (error) => {
    console.error('Upfront payment error:', error);
    // Handle error appropriately
  }
});

Widget Cleanup

Always destroy widgets before unmounting components:

// Before component unmounts or page navigation
function cleanup() {
  if (recurringWidget && typeof recurringWidget.destroy === 'function') {
    recurringWidget.destroy();
  }

  if (upfrontWidget && typeof upfrontWidget.destroy === 'function') {
    upfrontWidget.destroy();
  }
}

// In React/Svelte/Vue lifecycle
onDestroy(() => {
  cleanup();
});

Step 7: Session Persistence & Page Reload

The Problem

Payment widgets render iframes with external payment forms (credit card inputs, bank redirects, etc.). If the user:

  • Refreshes the page
  • Navigates away and back
  • Browser session times out

...the widget state is lost, forcing the user to re-enter payment details.

The Solution: Session Token Persistence

Store session tokens and remount widgets on page load. For more details, see Handling Redirects.

Implementation

1. Store Session Data

// After creating payment sessions, store tokens
const sessionData = {
  // Session tokens for remounting widgets
  recurringSessionToken: recurringSession.token,
  upfrontSessionToken: upfrontSession.token,

  // Payment tokens (if already completed)
  recurringPaymentToken: recurringPaymentToken || null,
  upfrontPaymentToken: upfrontPaymentToken || null,

  // FinionPay customer mapping
  finionPayCustomerId: recurringSession.finionPayCustomerId,

  // Other state
  preview: preview,
  personalInfo: personalInfo,
  selectedOfferId: offer.id,
  selectedTermId: term.id,

  // Timestamp for TTL
  timestamp: Date.now()
};

// Store in sessionStorage (cleared on tab close)
sessionStorage.setItem('contractFlowState', JSON.stringify(sessionData));

2. Restore Session on Page Load

function restoreSession() {
  const storedData = sessionStorage.getItem('contractFlowState');
  if (!storedData) return null;

  const data = JSON.parse(storedData);

  // Check TTL (e.g., 1 hour)
  const ONE_HOUR = 60 * 60 * 1000;
  if (Date.now() - data.timestamp > ONE_HOUR) {
    sessionStorage.removeItem('contractFlowState');
    return null;
  }

  return data;
}

3. Remount Widgets

async function remountWidgets() {
  const session = restoreSession();
  if (!session) return;

  // Remount recurring widget if session exists
  if (session.recurringSessionToken) {
    const containerEl = document.getElementById('recurring-payment-container');
    if (!containerEl) return;

    containerEl.innerHTML = '';

    recurringWidget = window.paymentWidget.init({
      userSessionToken: session.recurringSessionToken,
      container: containerEl,
      countryCode: 'DE',
      locale: 'en-US',
      environment: 'test',
      onSuccess: (paymentRequestToken, paymentInstrumentDetails) => {
        recurringPaymentToken = paymentRequestToken;
        updateSessionStorage({ recurringPaymentToken });
      },
      onError: (error) => {
        console.error('Remount error:', error);
      }
    });

    // If payment was already completed, restore token
    if (session.recurringPaymentToken) {
      recurringPaymentToken = session.recurringPaymentToken;
      recurringPaymentComplete = true;
    }
  }

  // Remount upfront widget if session exists
  if (session.upfrontSessionToken) {
    // Similar logic...
  }
}

// Call on component mount
onMount(async () => {
  await loadWidgetLibrary();
  await remountWidgets();
});

4. Update Session Storage

function updateSessionStorage(updates) {
  const storedData = sessionStorage.getItem('contractFlowState');
  if (!storedData) return;

  const data = JSON.parse(storedData);
  const updatedData = { ...data, ...updates, timestamp: Date.now() };

  sessionStorage.setItem('contractFlowState', JSON.stringify(updatedData));
}

Session Token Expiry

  • Token validity: Check tokenValidUntil from API response
  • Typical duration: 15-30 minutes
  • Handle expiry: If token expired, create new session and remount widget
function isTokenValid(tokenValidUntil) {
  const expiryTime = new Date(tokenValidUntil).getTime();
  return Date.now() < expiryTime;
}

// Before remounting
if (!isTokenValid(session.recurringSession.tokenValidUntil)) {
  // Create new session
  const newSession = await createPaymentSession({ /* ... */ });
  updateSessionStorage({ recurringSessionToken: newSession.token });
}

Step 8: Final Contract Submission

Endpoint: POST /v1/memberships/signup

Example Request

curl -i -X POST \
  https://open-api-demo.open-api.magicline.com/v1/memberships/signup \
  -H 'Content-Type: application/json' \
  -H 'X-API-KEY: YOUR_API_KEY_HERE' \
  -d '{
    "contract": {
      "contractOfferTermId": 1000,
      "startDate": "2026-02-01",
      "preuseDate": "2026-01-01",
      "notes": "Some notes",
      "thirdPartyId": "1000a",
      "employeeId": 1239812733,
      "referralCode": "A500D",
      "selectedSelectableModuleIds": [
        0
      ],
      "selectedOptionalModuleIds": [
        0
      ],
      "initialPaymentRequestToken": "3JtyH5sakfn2V22vB0napNC2zWMlpFwS9gPQawuk7Jw1F00atOD0BA",
      "contractSignature": {
        "base64SvgSignature": "string"
      },
      "textBlockSignatures": [
        {
          "base64SvgSignature": "string",
          "textBlockId": 0
        }
      ],
      "voucherCode": 123
    },
    "customer": {
      "thirdPartyId": "A1000",
      "firstName": "Peter",
      "secondFirstName": "Thomas",
      "lastName": "Muller",
      "secondLastName": "Meyer",
      "dateOfBirth": "2019-08-24",
      "email": "example@example.com",
      "gender": "MALE",
      "phoneNumberPrivate": "+44123456789",
      "phoneNumberMobile": "+44987654321",
      "street": "Raboisen Street",
      "secondStreet": "Second Street",
      "cityPart": "Tegel",
      "district": "District 12",
      "streetType": "Avenue",
      "streetBlock": "5th block",
      "portal": "Portal 1",
      "stairway": "Right stairway",
      "door": "Door 1",
      "province": "Champagne",
      "additionalAddressInformation": "Additional information",
      "floor": "2nd floor",
      "language": {
        "languageCode": "de",
        "countryCode": "DE"
      },
      "houseNumber": "3-4",
      "buildingName": "Empire State Building",
      "city": "Hamburg",
      "zipCode": "220-99",
      "countryCode": "DE",
      "communicationPreferences": [
        {
          "messageCategory": "CONTRACT",
          "channels": [
            {
              "communicationChannel": "EMAIL",
              "customerOverridable": true,
              "active": true
            }
          ]
        }
      ],
      "documentIdentification": {
        "documentNumber": "CX5432112345DS",
        "documentType": "ID_CARD"
      },
      "paymentRequestToken": "string",
      "additionalInformationFieldAssignments": [
        {
          "additionalInformationFieldId": 1234567890,
          "value": "string"
        }
      ]
    }
  }'

Request Body

contractobject(MembershipSignupContract)required

The contract information used for the membership signup

contract.​contractOfferTermIdinteger(int64)required

Unique ID of the contract offer term

Example: 1000
contract.​startDatestring(date)required

The start date of the contract

Example: "2026-02-01"
contract.​preuseDatestring(date)

The pre use date of the contract. If not provided, it will be evaluated based on contract offer configuration.

Example: "2026-01-01"
contract.​notesstring

The notes related to the contract

Example: "Some notes"
contract.​thirdPartyIdstring

Unique ID of the third party contract in the third party system

Example: "1000a"
contract.​employeeIdinteger(int64)

Unique ID of the employee who created the membership

Example: 1239812733
contract.​referralCodestring

Referral code to link with recruiter

Example: "A500D"
contract.​selectedSelectableModuleIdsArray of integers(int64)

The selected modules from the available selectable modules

contract.​selectedOptionalModuleIdsArray of integers(int64)

The selected modules from the available optional modules

contract.​initialPaymentRequestTokenstring

This token identifies a pre-authorized payment request. It acts as a reference to the payment session initiated by the user. It's not needed for preview endpoints.

Example: "3JtyH5sakfn2V22vB0napNC2zWMlpFwS9gPQawuk7Jw1F00atOD0BA"
contract.​contractSignatureobject(Signature)

Representing customer signature

contract.​textBlockSignaturesArray of objects(TextBlockSignature)

Signatures for text blocks

contract.​voucherCodestring

An optional code for a voucher. Only credit and discount vouchers are supported.

Example: 123
customerobject(MembershipSignupCustomer)required

The customer information used for the membership signup

customer.​thirdPartyIdstring

Unique ID of the third party customer in the third party system

Example: "A1000"
customer.​firstNamestringrequired

First name of the customer

Example: "Peter"
customer.​secondFirstNamestring

Second first name of the customer

Example: "Thomas"
customer.​lastNamestringrequired

Last name of the customer

Example: "Muller"
customer.​secondLastNamestring

Second last name of the customer

Example: "Meyer"
customer.​dateOfBirthstring(date)required

Date of birth of the customer

Example: "2019-08-24"
customer.​emailstringrequired

Email address of the customer

Example: "example@example.com"
customer.​genderstring

Gender of the customer

Enum ValueDescription
MALE

Male gender of the customer

FEMALE

Female gender of the customer

UNISEX

Unisex gender of the customer

Example: "MALE"
customer.​phoneNumberPrivatestring

Private phone number of the customer

Example: "+44123456789"
customer.​phoneNumberMobilestring

Mobile phone number of the customer

Example: "+44987654321"
customer.​streetstringrequired

Street of the customer's address

Example: "Raboisen Street"
customer.​secondStreetstring

Second street line of the customer's address

Example: "Second Street"
customer.​cityPartstring

City part of the customer's address

Example: "Tegel"
customer.​districtstring

District of the customer's address

Example: "District 12"
customer.​streetTypestring

Street type of the customer's address

Example: "Avenue"
customer.​streetBlockstring

Street block of the customer's address

Example: "5th block"
customer.​portalstring

Portal of the customer's address

Example: "Portal 1"
customer.​stairwaystring

Stairway of the customer's address

Example: "Right stairway"
customer.​doorstring

Door of the customer's address

Example: "Door 1"
customer.​provincestring

Province of the customer's address

Example: "Champagne"
customer.​additionalAddressInformationstring

Additional address information of the customer's address

Example: "Additional information"
customer.​floorstring

Floor of the customer's address

Example: "2nd floor"
customer.​languageobject(Language)required

Basic language information

customer.​language.​languageCodestring(ISO 639-1)required

The language code

Example: "de"
customer.​language.​countryCodestring(ISO 3166-1)

The country code

Example: "DE"
customer.​houseNumberstring

House number of the customer's address

Example: "3-4"
customer.​buildingNamestring

Building name

Example: "Empire State Building"
customer.​citystringrequired

City of the customer's address

Example: "Hamburg"
customer.​zipCodestringrequired

Zip code of the customer's address

Example: "220-99"
customer.​countryCodestring(ISO 3166-1)required

Country code of the customer's address

Example: "DE"
customer.​communicationPreferencesArray of objects(CommunicationPreference)

List of communication preferences for the customer

customer.​documentIdentificationobject(DocumentIdentification)

Information from an official document that identifies the customer

customer.​paymentRequestTokenstring

By assigning the paymentRequestToken to the customer, the payment method associated with the token will be used as payment method setting of the customer. Additionally, if the paymentRequestToken is associated with a payment instrument, i.e. a SEPA Mandate, BACS Mandate, Credit Card, or other, the payment instrument will be made available in the member account for future collection via payment runs. By leaving this field empty the payment method of the customer will be set to CASH. For reference check 'Create a user payment session'.

customer.​additionalInformationFieldAssignmentsArray of objects(AdditionalInformationFieldAssignment)

List of additional information field assignments of the customer

Critical: Two Payment Tokens

The API expects two separate payment tokens in specific locations:

  1. customer.paymentRequestToken: Recurring payment token (MEMBER_ACCOUNT)

    • Used for monthly membership fees
    • From Session 1 onSuccess callback
  2. contract.initialPaymentRequestToken: Upfront payment token (ECOM)

    • Used for immediate payment (setup fees)
    • From Session 2 onSuccess callback

Never use the same token for both fields. Each widget session produces a unique token.

Response

customerIdinteger(int64)required

Unique ID of the customer

Example: 1000

Note: The API returns the customerId of the newly created membership. You can use this ID to fetch additional customer details if needed via other endpoints.

Error Handling

try {
  const result = await createMembership(signupRequest);

  // Success - clear session storage
  sessionStorage.removeItem('contractFlowState');

  // Handle successful signup
  console.log('Membership created successfully:', result.customerId);

  // Navigate to next step or confirmation
  return { success: true, customerId: result.customerId };

} catch (error) {
  console.error('Membership signup failed:', error);

  if (error.status === 400) {
    // Validation error - check error.validationErrors for field-specific issues
    return { success: false, type: 'validation', errors: error.validationErrors };
  } else if (error.status === 409) {
    // Duplicate customer - customer with this email may already exist
    return { success: false, type: 'duplicate_customer', message: error.message };
  } else {
    // Generic error
    return { success: false, type: 'generic', message: 'Failed to create membership' };
  }
}

Common Gotchas & Best Practices

1. Payment Amount Format

Always use decimal format, NOT cents.

// ❌ WRONG
const request = { amount: Math.round(49.99 * 100) }; // 4999 - will charge €4999!

// ✅ CORRECT
const request = { amount: 49.99 }; // Decimal format

2. FinionPay Customer ID Mapping

// ❌ WRONG - Creating both sessions without customer ID
const session1 = await createSession({ scope: 'MEMBER_ACCOUNT' });
const session2 = await createSession({ scope: 'ECOM' }); // Missing finionPayCustomerId

// ✅ CORRECT - Linking sessions
const session1 = await createSession({ scope: 'MEMBER_ACCOUNT' });
const customerId = session1.finionPayCustomerId;
const session2 = await createSession({
  scope: 'ECOM',
  finionPayCustomerId: customerId
});

3. Preview API - Authoritative Source

// ❌ WRONG - Using offer flatFees for payment amount
const upfrontAmount = offer.terms[0].flatFees.reduce((sum, fee) =>
  sum + fee.paymentFrequency.price.amount, 0
);

// ✅ CORRECT - Using preview API
const preview = await createContractPreview(/* ... */);
const upfrontAmount = preview.paymentPreview.dueOnSigningAmount.amount;

4. Widget Container Management

// ❌ WRONG - Not clearing container before remount
const widget = window.paymentWidget.init({ container: containerEl });

// ✅ CORRECT - Clear container first
containerEl.innerHTML = '';
const widget = window.paymentWidget.init({ container: containerEl });

5. Token Storage Security

// ❌ WRONG - Storing in localStorage (persists across sessions)
localStorage.setItem('paymentTokens', JSON.stringify(tokens));

// ✅ CORRECT - Use sessionStorage (cleared on tab close)
sessionStorage.setItem('contractFlowState', JSON.stringify(state));

// ✅ BETTER - Clear after successful submission
sessionStorage.removeItem('contractFlowState');

6. Debouncing Preview Updates

// ❌ WRONG - Calling preview on every keystroke
inputField.addEventListener('input', () => {
  fetchPreview(); // Excessive API calls
});

// ✅ CORRECT - Debounce API calls
let debounceTimer;
inputField.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => fetchPreview(), 500);
});

7. Payment Choice Restrictions

// ❌ WRONG - Using same payment choices for both sessions
const paymentChoices = ['SEPA', 'CREDIT_CARD', 'CASH'];
createSession({ scope: 'MEMBER_ACCOUNT', permittedPaymentChoices: paymentChoices });
createSession({ scope: 'ECOM', permittedPaymentChoices: paymentChoices });

// ✅ CORRECT - Use offer choices for MEMBER_ACCOUNT, omit for ECOM
const recurringChoices = offer.allowedPaymentChoices || ['SEPA', 'BACS', 'CREDIT_CARD', 'CASH', 'BANK_TRANSFER'];

await createSession({
  scope: 'MEMBER_ACCOUNT',
  permittedPaymentChoices: recurringChoices
});

// For ECOM, omit permittedPaymentChoices to expose all studio-configured payment methods
await createSession({
  scope: 'ECOM',
  // No permittedPaymentChoices - allows all payment methods configured for ECOM in studio
});

8. Widget Lifecycle Management

// ❌ WRONG - Not destroying widget before navigation
navigate('/next-page'); // Widget iframe still active in background

// ✅ CORRECT - Always cleanup
function cleanup() {
  if (widget && typeof widget.destroy === 'function') {
    widget.destroy();
  }
}

// Before navigation
cleanup();
navigate('/next-page');

// In framework lifecycle hooks
onBeforeUnmount(() => cleanup());

9. Error Recovery

// ❌ WRONG - No recovery from failed session creation
const session = await createSession({ /* ... */ });
mountWidget(session.token);

// ✅ CORRECT - Handle errors and allow retry
try {
  const session = await createSession({ /* ... */ });
  mountWidget(session.token);
} catch (error) {
  console.error('Failed to create payment session:', error);
  // Store error state and provide retry mechanism
  return { success: false, error: error.message, canRetry: true };
}

10. Preview Before Payment

// ❌ WRONG - Creating payment session without final preview
collectPersonalInfo();
createPaymentSessions(); // Amount might be wrong

// ✅ CORRECT - Always fetch final preview first
collectPersonalInfo();
const preview = await fetchPreview(); // Get final amount
const upfrontAmount = preview.paymentPreview.dueOnSigningAmount.amount;
createPaymentSessions({ upfrontAmount });

11. Required Fields

// ❌ WRONG - Missing referenceText
const sessionRequest = {
  amount: 0,
  scope: "MEMBER_ACCOUNT"
};

// ✅ CORRECT - Include all required fields
const sessionRequest = {
  amount: 0,
  scope: "MEMBER_ACCOUNT",
  referenceText: "Membership Contract Recurring Payment" // Required!
};

Summary

This integration requires careful orchestration of multiple API calls and payment sessions. Key points:

  1. Preview API is authoritative – Always use dueOnSigningAmount for upfront payment
  2. Two separate sessions – MEMBER_ACCOUNT (recurring) and ECOM (upfront)
  3. Customer ID mapping – Pass finionPayCustomerId from Session 1 to Session 2
  4. Session persistence – Store tokens for widget remounting after page reload
  5. Two payment tokens – Store both tokens and submit in correct fields:
    • customer.paymentRequestToken for recurring payments
    • contract.initialPaymentRequestToken for upfront payments
  6. Amount format – Always use decimal format (not cents)
  7. Required fields – Include referenceText in all payment session requests
  8. Widget cleanup – Always destroy widgets before unmounting
  9. Response structure – API returns only { customerId: number }

Following this guide ensures a robust membership signup flow that handles edge cases, maintains state across page reloads, and correctly processes both recurring and upfront payments.


Relevant Endpoints

For payment-related endpoints and concepts, see Working with Payment API.