Getting Started
This guide walks you through integrating Odus Elements into a React application — from installation to accepting your first payment.
Prerequisites
Before you begin, make sure you have:
- A publishable key from your Odus Dashboard (safe to expose publicly).
- A backend endpoint that creates payments via the Odus API — see Making Payments.
How It Works
Odus Elements follows a three-step flow:
- Create — Initialize
OdusElementswith your API key, environment, and callbacks. - Mount — Place individual UI elements (card input, email, error display, etc.) into your page using refs or CSS selectors.
- Associate & Authorize — Link a server-side payment with
associatePayment(), then callauthorize()from your submit button.
You choose which elements to mount. Only mounted elements participate in validation and authorization — unmounted elements are automatically skipped. This lets you build anything from a minimal card-only form to a full checkout with address fields and express payment buttons.
Step 1: Install the SDK
npm install @odus/checkout
Step 2: Create and Mount Elements
Initialize OdusElements and mount elements into container refs:
import { OdusElements } from '@odus/checkout/elements';
const checkout = new OdusElements({
environment: 'test',
apiKey: 'pk_test_your_publishable_key',
returnUrl: 'https://your-website.com/checkout/return',
callbacks: {
onPaymentSucceeded: () => {
window.location.href = '/checkout/success';
},
onPaymentFailed: (error) => {
console.error('Payment failed:', error);
},
onActionRequired: (url) => {
window.location.href = url;
},
},
});
// Mount elements into DOM containers
checkout.elements.contact.email.mount(emailRef.current!);
checkout.elements.card.mount(cardRef.current!);
checkout.elements.error.mount(errorRef.current!);
At this point the inputs render on the page, but no payment is linked yet.
Elements ship with built-in styles — no separate CSS import is needed. Each element injects its own scoped styles via Shadow DOM.
Step 3: Associate a Payment and Authorize
Your backend creates a payment via the Odus API and returns a paymentId and checkoutKey. Call associatePayment() to link them, then authorize() when the customer submits.
Learn how to create payments on your backend in the Making Payments guide.
// Link the server-side payment
const { paymentId, checkoutKey } = await fetch('/api/create-payment', {
method: 'POST',
}).then((res) => res.json());
await checkout.associatePayment(paymentId, checkoutKey);
// On submit: validate, then authorize
const { valid, errors } = checkout.validate();
if (valid) {
await checkout.authorize();
}
Complete Example
Here's a full React component tying all three steps together:
import { useEffect, useRef, useState } from 'react';
import { OdusElements } from '@odus/checkout/elements';
export function CheckoutForm() {
const checkoutRef = useRef<OdusElements | null>(null);
const emailRef = useRef<HTMLDivElement>(null);
const cardRef = useRef<HTMLDivElement>(null);
const errorRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isFormValid, setIsFormValid] = useState(false);
useEffect(() => {
const checkout = new OdusElements({
environment: 'test',
apiKey: 'pk_test_your_publishable_key',
returnUrl: 'https://your-website.com/checkout/return',
callbacks: {
onPaymentSucceeded: () => {
window.location.href = '/checkout/success';
},
onPaymentFailed: (errorMessage) => {
setError(`Payment failed: ${errorMessage}`);
},
onActionRequired: (redirectUrl) => {
window.location.href = redirectUrl;
},
},
});
checkout.elements.contact.email.mount(emailRef.current!);
checkout.elements.card.mount(cardRef.current!);
checkout.elements.error.mount(errorRef.current!);
checkout.onChange(() => {
setIsFormValid(checkout.isValid);
});
checkoutRef.current = checkout;
async function setupPayment() {
try {
const response = await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) throw new Error('Failed to create payment');
const { paymentId, checkoutKey } = await response.json();
await checkout.associatePayment(paymentId, checkoutKey);
} catch (err) {
setError('Failed to initialize payment. Please refresh the page.');
} finally {
setIsLoading(false);
}
}
setupPayment();
return () => {
checkoutRef.current?.destroy();
};
}, []);
async function handlePay() {
const checkout = checkoutRef.current;
if (!checkout) return;
const { valid, errors } = checkout.validate();
if (!valid) {
console.log('Validation errors:', errors);
return;
}
await checkout.authorize();
}
return (
<div>
{isLoading && <p>Loading checkout...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
<div ref={emailRef} />
<div ref={cardRef} />
<div ref={errorRef} />
<button onClick={handlePay} disabled={isLoading || !isFormValid}>
Pay
</button>
</div>
);
}
Call associatePayment() as early as possible. This minimizes customer wait time and ensures express payment methods (Apple Pay, PayPal) render promptly.
Updating the Payment
When the customer changes their selected plan or pricing option after the form is already rendered, create a new payment on your backend and call associatePayment() again on the same instance. Do not destroy and recreate the Elements instance — the SDK preserves all user-entered form data and only refreshes the express payment buttons.
const PRICES = [
{ id: 'basic', label: 'Basic — $9/mo', amount: 900 },
{ id: 'pro', label: 'Pro — $29/mo', amount: 2900 },
];
export function CheckoutForm() {
const checkoutRef = useRef<OdusElements | null>(null);
const [selectedPrice, setSelectedPrice] = useState(PRICES[0]);
// Mount once — no dependency on selectedPrice
useEffect(() => {
const checkout = new OdusElements({
/* config */
});
checkout.elements.contact.email.mount(emailRef.current!);
checkout.elements.card.mount(cardRef.current!);
checkoutRef.current = checkout;
return () => {
checkoutRef.current?.destroy();
};
}, []);
// Re-associate when the price changes
useEffect(() => {
const checkout = checkoutRef.current;
if (!checkout) return;
async function updatePayment() {
const response = await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: selectedPrice.amount }),
});
const { paymentId, checkoutKey } = await response.json();
await checkout!.associatePayment(paymentId, checkoutKey);
}
updatePayment();
}, [selectedPrice]);
return (
<div>
{PRICES.map((price) => (
<button key={price.id} onClick={() => setSelectedPrice(price)}>
{price.label}
</button>
))}
<div ref={emailRef} />
<div ref={cardRef} />
<button onClick={handlePay}>Pay</button>
</div>
);
}
Do not destroy and recreate the Elements instance when the price changes. This creates new elements from scratch, which wipes all fields the customer has already filled in.
Next Steps
- Configuration — Customize callbacks, localization, and additional payment methods
- Theming — Style elements using CSS custom properties
- Examples — Integration patterns: minimal, PayPal-only, custom payload, and more
- SDK Reference — Full API reference for all classes and types
- 3DS and Redirect Handling — Handle redirect-based flows like 3DS and PayPal
- Builder — Drag and drop elements, adjust themes, and preview your layout in an interactive playground