Overview

Xendit is a payment gateway that allows you to accept payments from your users especially for Southeast Asia countries. It supports various payment methods such as credit card, bank transfer, and e-wallet. Here's a full checkout flow:

Setup

You need to add more environment variables to your project by following these steps:

  1. Create API Key

    Please create an account and get the API key from Settings > Developers > API Keys. Generate an API key with Money-in write permission.

  2. Add Webhook

    Then, add the webhook URL to your project from Settings > Developers > Webhooks. Add the webhook URL to listen to the invoices event.

    code

    https://example.com/api/webhooks/xendit

  3. Update Environment Variables

    Add the following environment variables to your project:

    .env

    XENDIT_PG_ID="fca6cVSyEpSE" # just a random string as identifier XENDIT_KEY=

Done! You may need to restart the server to apply the changes. And, you can apply for store activation when your landing page is ready.

Client Side

Create a checkout method in the pricing page (app/(landing)/pricing/page.tsx) to hit the order API and redirect the user to the payment page.

page.tsx

'use client' import { Xendit } from '@/lib/payments/xendit' import { hit } from '@/lib/web/hit' import { toast } from 'sonner' export default function Pricing() { // Update this based on your needs const checkout = async (variant: string) => { // Add user authentication check, if needed // if (!user) { // r.push('/auth?r=/pricing') // return // } const resp = await hit('/api/orders', { method: 'POST', body: JSON.stringify({ name: 'John Doe', email: '[email protected]', variant }) }) const order = await resp.json() window.location.href = Xendit.checkout(order) } }

API Endpoint

Then, here's an example of the API route to create an order in the app/api/orders/route.ts:

route.ts

import { Xendit } from '@/lib/payments/xendit' import { db } from '@/lib/server/db' import { parsePayload } from '@/lib/server/payload' import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' export const POST = async (req: NextRequest) => { const body = parsePayload( z.object({ name: z.string(), email: z.string().email(), variant: z.string() }).strict(), await req.json() ) if (body instanceof Error) { return NextResponse.json({ error: body.message }, { status: 400 }) } // You may need to create a user if it is not an authenticated request const user = await db.user.create({ data: { email: body.email, name: body.name } }) // Create an order const data = await db.order.create({ data: { status: 'created', variant: body.variant, amount: 1000, integration: 'xendit', currency: 'IDR', items: [ { name: body.variant, quantity: 1, price: 1000 } ], userId: user.id } }) const order = await new Xendit().invoice(data, { onSuccessUrl: `${process.env.NEXT_PUBLIC_WEB_URL}/dash`, onFailureUrl: `${process.env.NEXT_PUBLIC_WEB_URL}/dash/failed` }) return NextResponse.json(order) }

Note. You may need to add the failed page to display the proper error message to the user in /app/dash/failed/page.tsx.

Webhook Handler

Lastly, you need to create a webhook handler in the app/api/webhooks/xendit/route.ts to listen to the events. Here's an example of how to handle the invoice events:

route.ts

import { db } from '@/lib/db' import { Xendit, XenditWebhook } from '@/lib/payments/xendit' import { NextRequest, NextResponse } from 'next/server' export const POST = async (req: NextRequest) => { const body = await req.json() as XenditWebhook try { // Validate, create invoice, and update order status // return `{ order, user, invoice }` object or throw an error const data = await new Xendit().webhook(body) // Do something with the data, if needed return NextResponse.json({}) } catch (e: any) { return NextResponse.json({ error: e.message || e }, { status: 500 }) } }