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=v2The 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. Inspectdata.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.