Checkout sessions
A checkout session is the short-lived payment intent behind a hosted payment page. You create one with an amount and a list of accepted methods; Plugipay returns a hostedUrl your customer opens in their browser, picks a method, and pays. When they finish (or abandon, or expire), Plugipay emits a webhook and you fulfil the order. This page covers the plugipay.checkoutSessions namespace; for the HTTP-level field reference see API: Checkout sessions, and for the lifecycle and statuses see Concepts → Checkout session.
Namespace
plugipay.checkoutSessions — every method on this namespace:
plugipay.checkoutSessions.create(input)
plugipay.checkoutSessions.get(id)
plugipay.checkoutSessions.list(params?)
plugipay.checkoutSessions.cancel(id)
plugipay.checkoutSessions.confirm(id)
Methods
checkoutSessions.create
Signature. plugipay.checkoutSessions.create(input): Promise<CheckoutSession>
Creates a session and returns it, including the all-important hostedUrl that you redirect the customer to. The SDK auto-attaches an Idempotency-Key; if your handler is invoked twice for the same order (a flaky retry, double-click on "Pay now"), the second call returns the same session rather than creating a duplicate.
import { PlugipayClient } from '@forjio/plugipay-node';
const plugipay = new PlugipayClient({
keyId: process.env.PLUGIPAY_KEY_ID!,
secret: process.env.PLUGIPAY_SECRET!,
});
const session = await plugipay.checkoutSessions.create({
amount: 150_000_00, // IDR 150,000.00
currency: 'IDR',
methods: ['qris', 'va', 'ewallet'],
successUrl: 'https://yourapp.com/orders/o_123?status=paid',
cancelUrl: 'https://yourapp.com/orders/o_123?status=cancelled',
customerId: 'cus_01HX...',
lineItems: [
{ name: 'Pro plan — annual', quantity: 1, unitAmount: 150_000_00 },
],
expiresInSec: 60 * 60, // 1 hour
metadata: { order_id: 'o_123' },
});
console.log(session.hostedUrl); // → 'https://plugipay.com/c/...'
Always set
metadata.order_id. This is the single most important breadcrumb between your system and Plugipay. Thecheckout_session.completed.v1webhook echoes it back, so your handler can find the right order without trusting the URL.
checkoutSessions.get
Signature. plugipay.checkoutSessions.get(id): Promise<CheckoutSession>
Look up a session by its cs_* ID. The full session object includes status, completedAt, the adapter that handled it, and any metadata you set on create.
const session = await plugipay.checkoutSessions.get('cs_01HX...');
if (session.status === 'completed') {
await fulfilOrder(session.metadata?.order_id);
}
checkoutSessions.list
Signature. plugipay.checkoutSessions.list(params?): Promise<{ data: CheckoutSession[]; cursor: string | null; hasMore: boolean }>
Filters: status, customerId, limit. See Pagination.
const { data } = await plugipay.checkoutSessions.list({
status: 'completed',
customerId: 'cus_01HX...',
limit: 20,
});
checkoutSessions.cancel
Signature. plugipay.checkoutSessions.cancel(id): Promise<CheckoutSession>
Cancels an open session before the customer pays. Useful when the underlying order is cancelled on your side mid-flow. A session that's already pending (customer started paying, payment hasn't confirmed) cannot always be cancelled — the adapter may have committed.
await plugipay.checkoutSessions.cancel('cs_01HX...');
checkoutSessions.confirm
Signature. plugipay.checkoutSessions.confirm(id): Promise<CheckoutSession>
Manually confirms a session. This is only used with manual-adapter sessions (bank transfer where you verify the payment yourself) or for back-office reconciliation. For card / QRIS / VA / e-wallet sessions, the adapter confirms automatically — you don't call this.
// Manual bank-transfer flow:
await plugipay.checkoutSessions.confirm('cs_01HX...');
Types
interface CheckoutSession {
id: string; // 'cs_...'
accountId: string;
customerId: string | null;
amount: number; // minor units
currency: 'IDR' | 'USD';
status: 'open' | 'pending' | 'completed' | 'expired' | 'canceled' | 'pending_review';
methods: CheckoutMethod[]; // ('qris' | 'va' | 'ewallet' | 'card' | 'retail' | 'paypal')[]
adapter: string | null; // which provider handled it
lineItems: unknown;
successUrl: string;
cancelUrl: string;
hostedUrl: string;
expiresAt: string;
completedAt: string | null;
metadata: Record<string, string> | null;
createdAt: string;
updatedAt: string;
}
See API: Checkout sessions for the per-field validation and the full status-transition diagram.
Common patterns
One-shot order checkout
Server-side handler — create the session, return the URL to the browser, redirect:
// Express route
app.post('/orders/:id/pay', async (req, res) => {
const order = await db.orders.get(req.params.id);
const session = await plugipay.checkoutSessions.create({
amount: order.totalCents,
currency: 'IDR',
methods: ['qris', 'va', 'ewallet', 'card'],
successUrl: `https://yourapp.com/orders/${order.id}/thank-you`,
cancelUrl: `https://yourapp.com/orders/${order.id}`,
customerId: order.plugipayCustomerId,
metadata: { order_id: order.id, user_id: order.userId },
});
res.redirect(303, session.hostedUrl);
});
Reconcile a session by your-side order ID
Don't trust the successUrl — webhooks are the source of truth. But during dashboards/debugging, you'll want to look up by your-side ID:
async function findSessionByOrderId(orderId: string) {
const { data } = await plugipay.checkoutSessions.list({ limit: 100 });
return data.find((s) => s.metadata?.order_id === orderId);
}
For high-volume workspaces, store the cs_* ID on your order at create time and look it up directly — that's cheaper than scanning.
Re-mint an expired session
Sessions expire (default 1 hour). If a customer comes back after expiry, create a fresh one rather than trying to extend — the original payment intent is gone.
async function payOrderAgain(orderId: string) {
const order = await db.orders.get(orderId);
// ... same create call as above; old session stays as 'expired' for history.
}
Capture which method the customer used
Once status === 'completed', the adapter field tells you which provider settled it. Useful for fee reconciliation:
const session = await plugipay.checkoutSessions.get(sessionId);
if (session.status === 'completed') {
console.log(`Paid via ${session.adapter}`); // → 'xendit' / 'midtrans' / 'paypal' / 'manual'
}
React to completion via webhooks (not polling)
The recommended pattern is: create the session, send the customer to hostedUrl, then react to the plugipay.checkout_session.completed.v1 webhook to fulfil. See Webhooks for the typed event shape and verification.
import { verifyWebhook } from '@forjio/plugipay-node';
app.post('/webhooks/plugipay', async (req, res) => {
const event = verifyWebhook({
body: req.rawBody,
signature: req.header('Plugipay-Signature')!,
secret: process.env.PLUGIPAY_WEBHOOK_SECRET!,
});
if (event.type === 'plugipay.checkout_session.completed.v1') {
const session = event.data.object;
await fulfilOrder(session.metadata?.order_id);
}
res.sendStatus(200);
});
Errors
| Code | Status | Cause |
|---|---|---|
validation_error |
400 | Bad currency, non-positive amount, unknown method, missing URL. |
not_found |
404 | Session ID doesn't exist or is in another workspace. |
forbidden |
403 | Missing scope, or trying to confirm a non-manual session. |
state_conflict |
409 | Cancelling a session that's already completed / expired / canceled. |
Next
- Refunds — refunding a completed session.
- Receipts — the receipt issued on completion.
- Webhooks — reacting to session events.
- API: Checkout sessions — HTTP reference.