PlexySDK DOCS
Web SDK

Drop-in

Integrate the pre-built Plexy checkout UI in your web app

Drop-in

Drop-in is the fastest way to integrate Plexy on the web: a single component renders the full checkout UI for the payment methods you choose.

Quick Start: Drop-in with Sessions Flow

The Sessions flow is the simplest way to accept payments. Your backend creates a session, your client renders Drop-in.

1. Create a session on your backend. Call POST /sessions on the Plexy Checkout API with the amount, country, and return URL. Return id and sessionData to your client. See Backend: API route handlers for a complete Next.js example.

2. Mount Drop-in:

'use client';

import { useEffect, useRef } from 'react';
import {
  PlexyCheckout,
  Dropin,
  Card,
  ApplePay,
  GooglePay,
} from '@plexy/plexy-web';
import '@plexy/plexy-web/styles/plexy.css';
import type {
  CoreConfiguration,
  DropinConfiguration,
  PaymentCompletedData,
  PaymentFailedData,
  PlexyCheckoutError,
  UIElement,
} from '@plexy/plexy-web';

export function Checkout({
  session,
}: {
  session: { id: string; sessionData: string };
}) {
  const dropinRef = useRef<HTMLDivElement>(null);
  const initialized = useRef(false);

  useEffect(() => {
    if (initialized.current || !dropinRef.current) return;
    initialized.current = true;

    const options: CoreConfiguration = {
      clientKey: process.env.NEXT_PUBLIC_CLIENT_KEY!,
      environment: 'test',
      countryCode: 'KZ',
      locale: 'en-US',
      amount: { value: 10000, currency: 'KZT' },
      session,
      onPaymentCompleted(data: PaymentCompletedData, element: UIElement) {
        console.log('completed', data);
      },
      onPaymentFailed(data: PaymentFailedData, element: UIElement) {
        console.log('failed', data);
      },
      onError(error: PlexyCheckoutError) {
        console.error('error', error);
      },
    };

    PlexyCheckout(options).then((checkout) => {
      const config: DropinConfiguration = {
        paymentMethodComponents: [Card, ApplePay, GooglePay],
      };
      new Dropin(checkout, config).mount(dropinRef.current!);
    });
  }, [session]);

  return <div ref={dropinRef} />;
}

The initialized ref is important: React 18's Strict Mode runs effects twice in development, which would mount Drop-in twice without the guard.

Drop-in with Advanced Flow

Use the Advanced flow when you need to control each /payments and /payments/details call from your backend.

'use client';

import { useEffect, useRef } from 'react';
import {
  PlexyCheckout,
  Dropin,
  Card,
  ApplePay,
  GooglePay,
} from '@plexy/plexy-web';
import '@plexy/plexy-web/styles/plexy.css';
import type {
  CoreConfiguration,
  DropinConfiguration,
  SubmitData,
  SubmitActions,
  AdditionalDetailsData,
  AdditionalDetailsActions,
  UIElement,
} from '@plexy/plexy-web';

export function AdvancedCheckout({
  paymentMethodsResponse,
}: {
  paymentMethodsResponse: unknown;
}) {
  const dropinRef = useRef<HTMLDivElement>(null);
  const initialized = useRef(false);

  useEffect(() => {
    if (initialized.current || !dropinRef.current) return;
    initialized.current = true;

    const options: CoreConfiguration = {
      clientKey: process.env.NEXT_PUBLIC_CLIENT_KEY!,
      environment: 'test',
      countryCode: 'KZ',
      locale: 'en-US',
      amount: { value: 10000, currency: 'KZT' },
      paymentMethodsResponse,

      onSubmit: async (
        state: SubmitData,
        component: UIElement,
        actions: SubmitActions,
      ) => {
        try {
          const result = await fetch('/api/payments', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(state.data),
          }).then((r) => r.json());

          if (!result.resultCode) {
            actions.reject();
            return;
          }

          const { resultCode, action, order, donationToken } = result;
          actions.resolve({ resultCode, action, order, donationToken });
        } catch (error) {
          actions.reject();
        }
      },

      onAdditionalDetails: async (
        state: AdditionalDetailsData,
        component: UIElement,
        actions: AdditionalDetailsActions,
      ) => {
        try {
          const result = await fetch('/api/payments/details', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(state.data),
          }).then((r) => r.json());

          if (!result.resultCode) {
            actions.reject();
            return;
          }

          const { resultCode, action, order, donationToken } = result;
          actions.resolve({ resultCode, action, order, donationToken });
        } catch (error) {
          actions.reject();
        }
      },
    };

    PlexyCheckout(options).then((checkout) => {
      const config: DropinConfiguration = {
        paymentMethodComponents: [Card, ApplePay, GooglePay],
      };
      new Dropin(checkout, config).mount(dropinRef.current!);
    });
  }, [paymentMethodsResponse]);

  return <div ref={dropinRef} />;
}

Your backend provides paymentMethodsResponse by calling POST /paymentMethods. Inside each callback, forward the state to the matching API route — the route handlers shown in Backend: API route handlers take care of injecting merchantAccount and the API key.

Configuring Drop-in

DropinConfiguration accepts:

import type { DropinConfiguration } from '@plexy/plexy-web';
import { Card, ApplePay, GooglePay } from '@plexy/plexy-web';

const config: DropinConfiguration = {
  // Which payment methods to load, and in what order.
  paymentMethodComponents: [Card, ApplePay, GooglePay],

  // Per-method options. Keyed by payment-method type.
  paymentMethodsConfiguration: {
    card: {
      hasHolderName: true,
      holderNameRequired: true,
      enableStoreDetails: true,
    },
  },
};

Each component class (Card, ApplePay, GooglePay) accepts its own configuration object under the matching key.

React (non-Next.js)

The same pattern works in any React app. Mount on the client, guard against Strict Mode double-invocation, and unmount on teardown:

import { useEffect, useRef } from 'react';
import { PlexyCheckout, Dropin, Card, ApplePay, GooglePay } from '@plexy/plexy-web';
import '@plexy/plexy-web/styles/plexy.css';
import type { CoreConfiguration, UIElement } from '@plexy/plexy-web';

export function Checkout({
  session,
}: {
  session: { id: string; sessionData: string };
}) {
  const ref = useRef<HTMLDivElement>(null);
  const elementRef = useRef<UIElement | null>(null);
  const initialized = useRef(false);

  useEffect(() => {
    if (initialized.current || !ref.current) return;
    initialized.current = true;

    const options: CoreConfiguration = {
      clientKey: import.meta.env.VITE_PLEXY_CLIENT_KEY,
      environment: 'test',
      countryCode: 'KZ',
      locale: 'en-US',
      amount: { value: 10000, currency: 'KZT' },
      session,
    };

    PlexyCheckout(options).then((checkout) => {
      elementRef.current = new Dropin(checkout, {
        paymentMethodComponents: [Card, ApplePay, GooglePay],
      }).mount(ref.current!);
    });

    return () => {
      elementRef.current?.unmount();
      elementRef.current = null;
    };
  }, [session]);

  return <div ref={ref} />;
}

Next.js App Router

Add these environment variables to .env.local:

# Public — safe to expose in the browser
NEXT_PUBLIC_CLIENT_KEY=test_XXXXXXXXXXXXXXXXXXXXXXXX

# Server-only — never expose in the browser
MERCHANT_ACCOUNT=YourMerchantAccount
CHECKOUT_API_KEY=AQEXXXXXXXXXXXXXXXXXXXX
CHECKOUT_API_VERSION=v2

The SDK is browser-only. Always mount it in a component marked 'use client', and create sessions inside a Server Action or Route Handler. Never import '@plexy/plexy-web' from server code.

Backend: API Route Handlers

Proxy every Plexy Checkout API call through your own backend so the API key never reaches the browser. All four handlers follow the same shape: accept the client's body, inject merchantAccount server-side, forward to Plexy with X-Api-Key, return the upstream JSON.

app/api/sessions-setup/route.ts

export async function POST(request: Request) {
  const body = await request.json();
  const fullRequest = {
    ...body,
    merchantAccount: process.env.MERCHANT_ACCOUNT,
  };

  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    'X-Api-Key': process.env.CHECKOUT_API_KEY!,
  };

  const res = await fetch(
    `https://api.plexypay.com/${process.env.CHECKOUT_API_VERSION}/sessions`,
    { method: 'POST', headers, body: JSON.stringify(fullRequest) },
  );

  const response = await res.json();
  return Response.json(response, { status: res.ok ? 200 : res.status });
}

app/api/payments/route.ts

export async function POST(request: Request) {
  const body = await request.json();
  const fullRequest = {
    ...body,
    merchantAccount: process.env.MERCHANT_ACCOUNT,
  };

  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    'X-Api-Key': process.env.CHECKOUT_API_KEY!,
  };

  const res = await fetch(
    `https://api.plexypay.com/${process.env.CHECKOUT_API_VERSION}/payments`,
    { method: 'POST', headers, body: JSON.stringify(fullRequest) },
  );

  const response = await res.json();
  return Response.json(response, { status: res.ok ? 200 : res.status });
}

app/api/payments/details/route.ts

export async function POST(request: Request) {
  const body = await request.json();
  const fullRequest = {
    ...body,
    merchantAccount: process.env.MERCHANT_ACCOUNT,
  };

  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    'X-Api-Key': process.env.CHECKOUT_API_KEY!,
  };

  const res = await fetch(
    `https://api.plexypay.com/${process.env.CHECKOUT_API_VERSION}/payments/details`,
    { method: 'POST', headers, body: JSON.stringify(fullRequest) },
  );

  const response = await res.json();
  return Response.json(response, { status: res.ok ? 200 : res.status });
}

app/api/payment-methods/route.ts

export async function POST(request: Request) {
  const body = await request.json();
  const fullRequest = {
    ...body,
    merchantAccount: process.env.MERCHANT_ACCOUNT,
  };

  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    'X-Api-Key': process.env.CHECKOUT_API_KEY!,
  };

  const res = await fetch(
    `https://api.plexypay.com/${process.env.CHECKOUT_API_VERSION}/paymentMethods`,
    { method: 'POST', headers, body: JSON.stringify(fullRequest) },
  );

  const response = await res.json();
  return Response.json(response, { status: res.ok ? 200 : res.status });
}

Handling the Payment Result

Three callbacks on CoreConfiguration cover every outcome:

  • onPaymentCompleted(data, element) — the payment reached a terminal state. Inspect data.resultCode.
  • onPaymentFailed(data, element) — payment failed (refused, cancelled, expired, etc.).
  • onError(error) — an SDK or network error, not a payment outcome.

Common resultCode values: Authorised, Refused, Pending, Cancelled, Error, Received, ChallengeShopper, RedirectShopper.

onPaymentCompleted(data, element) {
  if (data.resultCode === 'Authorised') {
    // Success UX. Always verify on your server via a webhook or GET /sessions/{id}.
  } else {
    // Refused, Cancelled, etc.
  }
},

Never treat the client-side resultCode as a source of truth for "payment completed". Confirm on the server via Plexy webhooks or a GET /sessions/{id} poll before fulfilling the order.

Redirect Flow

For 3DS challenges and payment methods that navigate away from your site, Plexy returns to the returnUrl you pass to POST /sessions or POST /payments. That URL should point at a page in your app that finalises the checkout.

Render a /redirect page on the client, read sessionId and redirectResult from the query string, and submit the details back to Plexy:

'use client';

import { useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { PlexyCheckout } from '@plexy/plexy-web';

export default function RedirectPage() {
  const params = useSearchParams();

  useEffect(() => {
    const sessionId = params.get('sessionId');
    const redirectResult = params.get('redirectResult');
    if (!sessionId || !redirectResult) return;

    (async () => {
      const checkout = await PlexyCheckout({
        clientKey: process.env.NEXT_PUBLIC_CLIENT_KEY!,
        environment: 'test',
        session: { id: sessionId, sessionData: '' },
        onPaymentCompleted(data) {
          // Show final state to the shopper.
        },
        onPaymentFailed(data) {
          // Show failure state.
        },
      });

      await checkout.submitDetails({ details: { redirectResult } });
    })();
  }, [params]);

  return <p>Finalising your payment…</p>;
}

Set the matching returnUrl in your /sessions or /payments request to https://yoursite.com/redirect.

On this page