> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paynext.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration Patterns

> Practical patterns that combine functional and visual customization of the PayNext SDK.

## When to Use Integration Patterns

Use these patterns when you need to:

* **Combine multiple customization approaches**: Integrate functional behavior with visual styling
* **Handle complex payment flows**: Manage subscriptions, one-time payments, and multiple payment methods
* **Optimize for specific markets**: Configure card networks and locales for different regions
* **Implement enterprise-grade experiences**: Build robust, scalable checkout implementations

These examples demonstrate how to effectively combine the PayNext SDK configuration options to create sophisticated checkout form experiences.

## Prerequisites

Before using these advanced patterns, ensure you have:

* **Basic PayNext SDK knowledge**: Familiarity with [Getting Started](/sdk-reference/introduction/getting-started)
* **React/TypeScript experience**: Understanding of React components and TypeScript interfaces
* **PayNext dashboard access**: Ability to configure payment methods and environments
* **Understanding of basic customization**: Knowledge of [Behavior](/sdk-reference/web-sdk/customization/behavior) and [Appearance](/sdk-reference/web-sdk/customization/appearance) customization

<CardGroup cols={2}>
  <Card title="Functional Patterns" icon="sliders" href="/sdk-reference/web-sdk/customization/behavior">
    Payment method configuration, callback handling, and environment management.
  </Card>

  <Card title="Visual Patterns" icon="palette" href="/sdk-reference/web-sdk/customization/appearance">
    Advanced styling approaches using CSS-in-JS and utility classes.
  </Card>
</CardGroup>

<Note>
  The examples below show configuration fragments. Instantiate with `new PayNextCheckout()` and pass the element ID as the first parameter and the combined config object as the second parameter into `checkout.mount('element-id', { ...config })` as in [Mount the Checkout](/sdk-reference/introduction/getting-started#mount-the-checkout), adding the illustrated properties before calling `mount`.
</Note>

## Quick Start

Here's a minimal example combining functional and visual customization:

```tsx quick-start.tsx theme={"system"}
import { PayNextCheckout, type PayNextConfig, type StylesConfig } from '@paynext/sdk'

const config: PayNextConfig = {
  clientToken: process.env.NEXT_PUBLIC_PAYNEXT_TOKEN!,
  environment: 'sandbox',
  apiVersion: '1.0.0',
  variant: 'compact',
  onCheckoutLoaded: (result) => {
    if (!result.success) {
      console.error('Checkout loading failed:', result.error)
      return
    }
    console.info('Checkout loaded successfully')
  },
  onCheckoutComplete: (result) => {
    console.log('Payment completed:', result.id)
    window.location.href = '/success'
  }
}

const styles: StylesConfig = {
  SubmitButton: {
    className: 'bg-blue-600 text-white font-semibold py-3 rounded-lg hover:bg-blue-700'
  }
}

export async function mountQuickStart(containerId: string) {
  const checkout = new PayNextCheckout()
  await checkout.mount(containerId, {
    ...config,
    styles,
  })
  return checkout
}
```

<Tip>
  This example combines basic functional configuration (environment, callbacks) with simple visual styling. Use this as a foundation for more complex patterns.
</Tip>

<Warning>
  `onCheckoutComplete` runs on the client when the checkout flow finishes — use it for redirects and analytics, not to fulfill orders. It is not guaranteed to run: a closed or backgrounded tab or a lost connection can prevent it, on any payment method (seen on PayPal and Apple Pay). Trigger fulfillment from [payment webhooks](/webhooks/introduction/getting-started), the source of truth for the final payment status (asynchronous methods such as Pix also settle after this callback fires).
</Warning>

***

## Basic Implementation Patterns

<Tabs>
  <Tab title="Transaction Type Handling">
    ```tsx advanced/transactions.tsx theme={"system"}
    import { PayNextCheckout, type PayNextConfig, type PaymentResult } from '@paynext/sdk'

    export async function mountTransactionsExample(containerId: string) {
      const handleComplete = (result: PaymentResult) => {
        if (result.subscription) {
          // Subscription flow
          console.log('Subscription created:', result.subscription.id)
          window.location.href = '/subscription-success'
          return
        }
        // One-time payment flow
        console.log('Payment completed:', result.id)
        window.location.href = '/payment-success'
      }

      const config: PayNextConfig = {
        clientToken: process.env.NEXT_PUBLIC_PAYNEXT_TOKEN!,
        environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
        apiVersion: '1.0.0',
        variant: 'compact',
        onCheckoutLoaded: (result) => {
          if (!result.success) {
            console.error('Checkout loading failed:', result.error)
            return
          }
          console.info('Checkout loaded successfully')
        },
        onCheckoutComplete: handleComplete,
      }

      const checkout = new PayNextCheckout()
      await checkout.mount(containerId, config)
      return checkout
    }
    ```

    <Tip>
      Branch on `result.subscription` to reuse the same checkout for both flows.
    </Tip>
  </Tab>

  <Tab title="Callback Configuration for Analytics">
    ```tsx advanced/callbacks.tsx theme={"system"}
    import { PayNextCheckout, type PayNextConfig, type PaymentResult, type AttemptResult, type LoadedResult, type CheckoutError } from '@paynext/sdk'

    function onCheckoutLoaded(result: LoadedResult) {
      if (!result.success) {
        window.analytics?.track('checkout_unavailable', {
          message: result.error?.status_reason?.message,
        })
        return
      }
      window.analytics?.track('checkout_loaded', { success: true })
    }

    function onCheckoutAttempt({ paymentMethod, cardType }: AttemptResult) {
      // Track payment attempt with method and card brand (if available)
      window.analytics?.track('payment_attempt', {
        method: paymentMethod,
        cardBrand: cardType || null, // Empty string for PayPal/Venmo, card brand for cards/wallets
      })
    }

    function onCheckoutComplete(result: PaymentResult) {
      // Minimal personal information: send only analytical signals you need
      window.analytics?.track('payment_completed', {
        id: result.id,
        amount: result.amount,
        currency: result.currency_code,
        method: result.payment_method?.type,
      })
    }

    function onCheckoutFail(error: CheckoutError) {
      const message =
        error.status_reason?.message ?? 'Payment failed. Please try again or use another method.'

      window.analytics?.track('payment_error', {
        status: error.status,
        message,
      })

      // Display a user-friendly message that matches what the checkout shows
      alert(message)
    }

    const config: PayNextConfig = {
      onCheckoutLoaded,
      onCheckoutAttempt,
      onCheckoutComplete,
      onCheckoutFail,
    }

    export async function mountAnalyticsCheckout(containerId: string) {
      const checkout = new PayNextCheckout()
      await checkout.mount(containerId, config)
      return checkout
    }
    ```

    <Warning>
      Do not send sensitive payment details to analytics tools.
    </Warning>
  </Tab>

  <Tab title="Gate Payments with beforeCheckoutAttempt">
    <Info>
      `beforeCheckoutAttempt` is available in SDK `1.0.17` and later. Upgrade `@paynext/sdk` if your project pins an earlier version.
    </Info>

    ```tsx advanced/before-attempt.tsx theme={"system"}
    import {
      PayNextCheckout,
      type PayNextConfig,
      type AttemptResult,
      type CheckoutError,
    } from '@paynext/sdk'

    async function hasActiveSubscription(): Promise<boolean> {
      const res = await fetch('/api/subscriptions/active', { credentials: 'include' })
      const { active } = await res.json()
      return active
    }

    const config: PayNextConfig = {
      clientToken: process.env.NEXT_PUBLIC_PAYNEXT_TOKEN!,
      environment: 'sandbox',
      apiVersion: '1.0.0',
      // Runs right before each payment attempt. Resolve false to block.
      beforeCheckoutAttempt: async ({ paymentMethod }: AttemptResult) => {
        const alreadyHasSub = await hasActiveSubscription()
        return !alreadyHasSub
      },
      onCheckoutFail: (error: CheckoutError) => {
        // Customers who already have a subscription are routed back to their account
        if (error.status === 'blocked') {
          window.location.href = '/account/subscriptions'
          return
        }
        alert(error.status_reason?.message ?? 'Payment failed.')
      },
    }

    export async function mountGatedCheckout(containerId: string) {
      const checkout = new PayNextCheckout()
      await checkout.mount(containerId, config)
      return checkout
    }
    ```

    <Note>
      Resolve quickly. The submit button stays in a loading state while `beforeCheckoutAttempt` is pending — long checks slow the customer's perceived response time.
    </Note>

    <Warning>
      Treat `beforeCheckoutAttempt` as defense-in-depth, not your only check. Always re-validate on the server before fulfilling the order.
    </Warning>
  </Tab>

  <Tab title="Gate Payments with a Consent Checkbox">
    <Info>
      `paymentsEnabled` and `onCheckoutBlocked` are available in SDK `1.0.19` and later. Upgrade `@paynext/sdk` if your project pins an earlier version.
    </Info>

    Use `paymentsEnabled` to keep the payment buttons visible but non-functional until the customer satisfies a condition you own — for example, ticking a terms or consent checkbox rendered outside the SDK. While disabled, no payment starts and no provider sheet opens; tapping a button fires `onCheckoutBlocked` so you can react. Toggle the state at runtime with `checkout.setPaymentsEnabled(enabled)` — no re-mount required.

    ```tsx advanced/consent-gate.tsx theme={"system"}
    import {
      PayNextCheckout,
      type PayNextConfig,
      type AttemptResult,
    } from '@paynext/sdk'

    export async function mountConsentGatedCheckout(containerId: string) {
      const termsCheckbox = document.querySelector<HTMLInputElement>('#accept-terms')!

      const config: PayNextConfig = {
        clientToken: process.env.NEXT_PUBLIC_PAYNEXT_TOKEN!,
        environment: 'sandbox',
        apiVersion: '1.0.0',
        // Start blocked until the customer accepts your terms.
        paymentsEnabled: termsCheckbox.checked,
        onCheckoutBlocked: ({ paymentMethod }: AttemptResult) => {
          // The user tried to pay before accepting — highlight your checkbox.
          termsCheckbox.closest('label')?.classList.add('needs-attention')
          console.info('Payment blocked, consent not given:', paymentMethod)
        },
      }

      const checkout = new PayNextCheckout()
      await checkout.mount(containerId, config)

      // Push the new state to the SDK whenever the checkbox changes.
      termsCheckbox.addEventListener('change', () => {
        checkout.setPaymentsEnabled(termsCheckbox.checked)
        if (termsCheckbox.checked) {
          termsCheckbox.closest('label')?.classList.remove('needs-attention')
        }
      })

      return checkout
    }
    ```

    <Note>
      The card and Pix forms can still expand while payments are disabled — only the submit is blocked. Button appearance is identical in both states; there is no visual disabled style.
    </Note>

    <Warning>
      `paymentsEnabled` is a client-side gate for UX. Always enforce the underlying condition (consent, eligibility) on your server before fulfilling the order.
    </Warning>
  </Tab>

  <Tab title="Update Config at Runtime">
    <Info>
      `checkout.update(config)` is available in SDK `1.1.0` and later. Upgrade `@paynext/sdk` if your project pins an earlier version.
    </Info>

    Change `locale`, `translate`, `theme`, or `paymentsEnabled` after mount without re-mounting — the checkout updates in place and preserves form state. Fields that initialize the API client (`clientToken`, `environment`, `apiVersion`) can't be updated live: TypeScript blocks them, and a plain-JS call warns and no-ops.

    ```tsx advanced/runtime-update.tsx theme={"system"}
    import { PayNextCheckout, type PayNextConfig } from '@paynext/sdk'

    export async function mountLocalizedCheckout(containerId: string) {
      const config: PayNextConfig = {
        clientToken: process.env.NEXT_PUBLIC_PAYNEXT_TOKEN!,
        environment: 'sandbox',
        apiVersion: '1.0.0',
        locale: 'en',
        theme: 'light',
      }

      const checkout = new PayNextCheckout()
      await checkout.mount(containerId, config)

      // Wire your own language / theme controls — no re-mount, form state kept.
      document.querySelector('#lang-es')?.addEventListener('click', () => {
        checkout.update({ locale: 'es' })
      })
      document.querySelector('#dark-mode')?.addEventListener('change', (event) => {
        const on = (event.target as HTMLInputElement).checked
        checkout.update({ theme: on ? 'dark' : 'light' })
      })

      return checkout
    }
    ```

    <Note>
      `update()` and `setPaymentsEnabled()` share one channel — `checkout.update({ paymentsEnabled })` and `checkout.setPaymentsEnabled(enabled)` are equivalent.
    </Note>
  </Tab>
</Tabs>

***

## Visual Patterns

<Tabs>
  <Tab title="CSS-in-JS Theming">
    ```tsx advanced/theme.tsx theme={"system"}
    import { type StylesConfig } from '@paynext/sdk'

    const theme: StylesConfig = {
      Input: {
        field: {
          styles: {
            backgroundColor: '#0b1020',
            color: '#e6edf3',
            border: '1px solid #2c3654'
          },
          className: 'focus:ring-2 focus:ring-[#2563eb]'
        },
        label: { styles: { color: '#a8b3cf' } },
        error: { styles: { color: '#ef4444' } },
      },
      SubmitButton: {
        styles: {
          background: '#2563eb',
          color: '#fff',
          borderRadius: 10,
          padding: '14px 18px'
        },
        iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
          <path d="M6 12L10 16L18 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
        </svg>`,
        className: 'hover:bg-[#1d4ed8] active:scale-95'
      }
    }

    // When mounting (see Getting Started "Mount the Checkout" example):
    const checkout = new PayNextCheckout()
    await checkout.mount('checkout-container', {
      ...config,
      styles: theme,
    })
    ```

    <Tip>
      Prefer transforms (`scale`, `translate`) for smooth animations.
    </Tip>

    <Note>
      The submit icon automatically hides while the spinner is shown during a payment attempt, then reappears once processing finishes.
    </Note>
  </Tab>

  <Tab title="Utility Class Implementation">
    ```tsx advanced/util-classes.tsx theme={"system"}
    import { type StylesConfig } from '@paynext/sdk'

    const styles: StylesConfig = {
      Input: {
        field: { className: 'bg-slate-50 border-2 border-slate-200 rounded-lg px-4 py-3 focus:border-blue-500' },
        error: { className: 'mt-1 text-sm text-red-600' },
      },
      SubmitButton: { className: 'w-full bg-blue-600 text-white font-semibold py-3 rounded-lg hover:bg-blue-700' },
    }

    // When mounting (see Getting Started "Mount the Checkout" example):
    const checkout = new PayNextCheckout()
    await checkout.mount('checkout-container', {
      ...config,
      styles,
    })
    ```
  </Tab>

  <Tab title="Digital Wallet Button Configuration">
    ```tsx advanced/wallets.tsx theme={"system"}
    import { type StylesConfig } from '@paynext/sdk'

    const wallets: StylesConfig = {
      ApplePayButton: { styles: { type: 'buy', style: 'black', borderRadius: 8, size: 'medium' } },
      GooglePayButton: { styles: { type: 'buy', color: 'default', borderRadius: 6, size: 'medium' } },
      PayPalButton: { styles: { layout: 'horizontal', color: 'gold', shape: 'pill', label: 'checkout', height: 42 } },
    }

    // When mounting (see Getting Started "Mount the Checkout" example):
    const checkout = new PayNextCheckout()
    await checkout.mount('checkout-container', {
      ...config,
      styles: wallets,
    })
    ```

    <Info>
      Follow each processor's brand guidelines. See [Appearance](/sdk-reference/web-sdk/customization/appearance) for full options.
    </Info>
  </Tab>
</Tabs>

***

## Advanced Patterns

### Complete Configuration Example

Comprehensive example combining multiple customization layers:

```tsx advanced/comprehensive.tsx theme={"system"}
import {
  PayNextCheckout,
  type PayNextConfig,
  type StylesConfig,
  type PaymentResult,
  type LoadedResult,
  type CheckoutError,
} from '@paynext/sdk'

export async function mountComprehensiveCheckout(containerId: string) {
  const handleLoaded = (result: LoadedResult) => {
    if (!result.success) {
      console.error('Checkout loading failed:', result.error)
      return
    }

    console.info('Checkout loaded successfully')
  }

  const handleComplete = (result: PaymentResult) => {
    // Log payment completion for analytics
    console.log('Payment completed:', result.id)
    
    // Handle subscription vs one-time payment
    if (result.subscription) {
      window.location.href = '/subscription-success'
    } else {
      window.location.href = '/payment-success'
    }
  }

  const handleError = (error: CheckoutError) => {
    const message = error.status_reason?.message ?? 'Payment failed. Please try again.'

    console.error('Payment error:', error.status, message)
    
    // PayNext automatically displays user-friendly error messages
    // Only add custom logic if needed for analytics or special handling
    alert(message)
  }

  const config: PayNextConfig = {
    clientToken: process.env.NEXT_PUBLIC_PAYNEXT_TOKEN!,
    environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
    apiVersion: '1.0.0',
    variant: 'compact',
    locale: 'en',
    onCheckoutLoaded: handleLoaded,
    onCheckoutComplete: handleComplete,
    onCheckoutFail: handleError,
  }

  const styles: StylesConfig = {
    Input: {
      field: {
        styles: {
          backgroundColor: '#ffffff',
          border: '2px solid #e5e7eb',
          borderRadius: '8px',
          padding: '12px 16px',
          fontSize: '16px',
          fontFamily: 'system-ui, sans-serif',
          transition: 'border-color 0.2s ease'
        },
        className: 'focus:border-blue-500 focus:outline-none'
      },
      label: {
        styles: {
          color: '#374151',
          fontSize: '14px',
          fontWeight: '600',
          marginBottom: '6px',
          display: 'block'
        }
      },
      error: {
        styles: {
          color: '#ef4444',
          fontSize: '13px',
          marginTop: '4px'
        },
        className: 'flex items-center gap-1'
      },
      container: {
        styles: { marginBottom: '16px' },
        focus: 'ring-2 ring-blue-500 ring-opacity-20'
      }
    },
    SubmitButton: {
      styles: {
        backgroundColor: '#3b82f6',
        color: 'white',
        border: 'none',
        borderRadius: '8px',
        padding: '16px 24px',
        fontSize: '16px',
        fontWeight: '600',
        width: '100%',
        cursor: 'pointer',
        transition: 'all 0.2s ease'
      },
      className: 'hover:bg-blue-700 active:scale-98 disabled:opacity-50'
    },
    ApplePayButton: {
      styles: {
        type: 'buy',
        style: 'black',
        borderRadius: 8,
        size: 'medium'
      }
    },
    PayPalButton: {
      styles: {
        layout: 'horizontal',
        color: 'gold',
        shape: 'rect',
        label: 'checkout',
        height: 48
      }
    },
    GooglePayButton: {
      styles: {
        type: 'buy',
        color: 'default',
        borderRadius: 8,
        size: 'medium'
      }
    }
  }

  const checkout = new PayNextCheckout()
  await checkout.mount(containerId, {
    ...config,
    styles,
  })
  return checkout
}
```

***

## Best Practices

### Configuration Management

Keep configuration objects immutable to prevent unnecessary re-renders:

* Define configuration objects outside component scope when possible
* Use useMemo for dynamic configurations
* Separate styling from functional configuration
* Test configurations across different environments

### Error Handling

Implement comprehensive error handling:

* Provide user-friendly error messages
* Log detailed errors for debugging
* Handle different error types appropriately
* Test error scenarios thoroughly

### Performance Considerations

Optimize checkout performance:

* Minimize runtime style calculations
* Use CSS classes over CSS-in-JS for static styles
* Cache configuration objects
* Test on various devices and connection speeds

### Testing Approach

Validate integration patterns:

* Test all payment methods in the sandbox
* Verify styling across browsers and devices
* Test error scenarios and edge cases
* Validate callback integrations

***

## Common Pitfalls

Avoid these integration mistakes:

* **Configuration conflicts** between different customization layers
* **Missing error handling** for payment failures
* **Performance issues** from unstable configuration objects
* **Inconsistent styling** across different states
* **Browser compatibility** issues with advanced features

<Info>
  Start with minimal customization and add complexity incrementally. Test each customization layer thoroughly before combining multiple patterns.
</Info>
