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. The checkout_session.completed.v1 webhook 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

Plugipay — Payments that don't tax your success