How to Integrate Worldline Payment Gateway in Nextjs 14
It's been a minute, but let's get technical! I just finished integrating Worldline payments, and I thought I could leave some notes here in case any dev wants a quick rundown. I think this is one hell of a payment handler that deserves more attention!
What's Worldline and Why Should You Care?
It's a payment solution that offers "hosted checkout," where Worldline.com handles all the sensitive payment stuff on their secure servers. This means less headache with PCI DSS compliance and better security for your users. Win-win!
Project Setup and Structure
First up, you'll need a Next.js project. Assuming you've got that sorted (if not, just bun create next-app
or check the Next.js docs), here's what our project structure will look like:
๐ฆ nextjs-store
โโโ app/
โ โโโ layout.tsx
โ โโโ page.tsx
โ โโโ shop/
โ โโโ checkout/
โ โ โโโ page.tsx # Checkout form
โ โ โโโ status/
โ โ โโโ page.tsx # Order confirmation page
โโโ features/
โ โโโ payment-gateway/
โ โโโ payment.tsx # Payment form component
โ โโโ client.ts # Payment gateway client
โโโ api/
โโโ create-hosted-checkout/
โโโ route.ts # API route for checkout sessions
Setting Up the Worldline Client
First things first, let's set up our connection to Worldline. Create a new file for your client configuration:
// features/payment-gateway/client.ts
const onlinePaymentsSdk = require("onlinepayments-sdk-nodejs");
export const AznHostedCheckouClient = onlinePaymentsSdk.init({
host: "payment.preprod.anzworldline-solutions.com.au",
apiKeyId: process.env.WORLDLINE_API_KEY_ID,
secretApiKey: process.env.WORLDLINE_SECRET_API_KEY,
integrator: "OnlinePayments",
});
This is our handler that performs a handshake and provides proof of identity to Worldline for you to be able to get the services
Building the Checkout Page
This is where users will review their order and initiate payment.It will logically look like this depending on your implementation, here is snippet of mine:
// app/shop/checkout/page.tsx
export default function CheckoutPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useState(null);
const { items, getCartTotals } = useCartStore();
const { subtotal, shipping, totalDiscount, tax, total } = getCartTotals();
const handleCheckout = async () => {
if (!user) {
toast.error("Please log in to complete your purchase");
return;
}
setIsLoading(true);
try {
// Prepare order data
const orderData = {
items: items.map((item) => ({
productId: item.id,
quantity: item.quantity,
price: item.price,
})),
totals: {
subtotal,
shipping,
totalDiscount,
tax,
total,
},
};
// Create hosted checkout session
const response = await fetch("/api/create-hosted-checkout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(orderData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Checkout creation failed");
}
// Redirect to Worldline's hosted checkout page
if (data.isSuccess && data.body.redirectUrl) {
window.location.replace(data.body.redirectUrl);
}
} catch (error) {
console.error("Checkout error:", error);
toast.error("Payment processing failed. Please try again.");
} finally {
setIsLoading(false);
}
};
return (
<div className="max-w-7xl mx-auto px-4 py-8 mt-24">
<Card>
<CardHeader>
<CardTitle>Order Summary</CardTitle>
</CardHeader>
<CardContent>
{/* Order summary content */}
<Button
onClick={handleCheckout}
disabled={isLoading}
className="min-w-[200px]"
>
{isLoading ? "Processing..." : "Proceed to Payment โ"}
</Button>
</CardContent>
</Card>
</div>
);
}
API Routes
We need two main API endpoints: one to create the checkout session and another to handle the return from Worldline:
// api/create-hosted-checkout/route.ts
export async function POST(request: NextRequest) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { items, totals } = await request.json();
try {
// Create initial order record
const initialOrder = await createOrder({
userId: session.user.id,
status: "Pending",
items: items.map((item) => ({
productId: item.productId,
quantity: item.quantity,
price: item.price,
})),
payment: {
amount: totals.total,
status: "Pending",
},
});
// Set up hosted checkout request
const createHostedCheckoutRequest = {
order: {
amountOfMoney: {
amount: Math.round(totals.total * 100), // Convert to cents!
currencyCode: "AUD",
},
},
cardPaymentMethodSpecificInput: {
authorizationMode: "SALE",
},
hostedCheckoutSpecificInput: {
returnUrl: `${baseUrl}/shop/checkout/status?orderId=${initialOrder.id}`,
},
};
const createHostedCheckoutResponse =
await AznHostedCheckouClient.hostedCheckout.createHostedCheckout(
"test281",
createHostedCheckoutRequest,
null
);
return NextResponse.json({
...createHostedCheckoutResponse,
orderId: initialOrder.id,
});
} catch (error) {
console.error("Error creating payment:", error);
return NextResponse.json(
{ error: "Failed to create payment", details: error.message },
{ status: 500 }
);
}
}
// Handle return from Worldline
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const hostedCheckoutId = searchParams.get("hostedCheckoutId");
const orderId = searchParams.get("orderId");
if (!hostedCheckoutId || !orderId) {
return NextResponse.json(
{ error: "Missing required parameters" },
{ status: 400 }
);
}
try {
const response = await AznHostedCheckouClient.hostedCheckout.getHostedCheckout(
"test281",
hostedCheckoutId,
null
);
const payment = response?.body?.createdPaymentOutput?.payment;
// Update order based on payment status
if (
payment?.status === "CAPTURED" ||
response?.body?.status === "PAYMENT_CREATED"
) {
await updateOrderStatus(orderId, "Paid", payment);
} else {
await updateOrderStatus(orderId, "Canceled");
}
return NextResponse.json({
success: true,
body: response.body,
order: updatedOrder,
});
} catch (error) {
console.error("Error getting hosted checkout status:", error);
return NextResponse.json(
{ error: "Failed to get checkout status" },
{ status: 500 }
);
}
}
Overview
Worldline might not be as widely known as Stripe in the dev community, but it's a robust choice for payment processing. The hosted checkout solution makes security a breeze, and once you get past the initial setup, it's pretty developer-friendly.
Check out Worldline's official documentation for more advanced features and updates.
Subscribe to my newsletter
Read articles from Tinotenda Joe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Tinotenda Joe
Tinotenda Joe
Avid developer