Checkout sessions

A checkout session is a one-time, hosted payment intent. You create one with an amount, a currency, and an allow-list of payment methods; Plugipay returns an object with a hostedUrl you redirect the customer to. When the customer pays — or, for offline methods, when you confirm — the session transitions to completed and emits a webhook.

If you're new to sessions, read Concepts → Checkout session or the Quickstart first. This page assumes Authentication and Conventions — signing, the response envelope, ID prefixes, money handling, and X-Plugipay-On-Behalf-Of aren't repeated here.

Lifecycle

A session moves through one of these states:

Status Meaning Terminal?
open Created and waiting. The hosted URL is live; payment can start at any time. No
pending A payment attempt is in flight at the provider (e.g. customer is on the 3DS step, or a VA bill has been issued and is waiting for transfer). No
pending_review A manual-adapter session (bank transfer, cash, EDC slip) is waiting for the merchant to confirm receipt offline. No
completed Payment captured. A linked Payment record now exists; the session won't change again. Yes
expired TTL elapsed before payment. The hosted URL serves an expiry screen. Yes
canceled Closed via API or because the customer clicked Cancel on the hosted page. Yes

Only open, pending, and pending_review accept state-changing API calls. The three terminal states reject cancel and confirm with 409 conflict.

One session, at most one payment. A session resolves to exactly zero or one payments. For recurring billing use Subscriptions; for multi-charge flows create one session per charge.

Endpoints

Method Path Purpose
POST /v1/checkout-sessions Create a session.
GET /v1/checkout-sessions/:id Retrieve one.
GET /v1/checkout-sessions List with filters + pagination.
POST /v1/checkout-sessions/:id/cancel Close an open / pending / pending_review session.
POST /v1/checkout-sessions/:id/confirm Merchant-confirm a pending_review (manual) session.

Receipt endpoints (/receipt, /receipt.pdf, /receipt.escpos, /receipt.html, /receipt/email, /receipt-link) are documented under Receipts.

Create a session

POST /v1/checkout-sessions

Creates a session and returns it. The Idempotency-Key header is required — duplicate keys return the original session, mismatched bodies on the same key return 409 idempotency_conflict. See Idempotency.

Request body

Field Type Required Notes
amount integer Yes Positive, in the smallest currency unit.
currency string Yes ISO 4217, uppercase. Must be supported by at least one enabled adapter.
methods string[] Yes Non-empty. One or more of qris, va, ewallet, card, retail, paypal.
successUrl string Yes https:// URL. Receives ?session_id=<id> on redirect. http:// is rejected.
cancelUrl string Yes https:// URL the customer lands on after clicking Cancel.
customerId string No A cus_… in the same workspace. Attaches payment + pre-fills email.
lineItems object[] No Display-only breakdown (see below). Defaults to [].
expiresInSec integer No Min 300, max 2592000 (30 days). Default 86400 (24h).
metadata object No Up to 50 string-string pairs. Echoed in webhooks.
templateId string No A tpl_… of kind: 'receipt'. Blank uses workspace default.

Both URLs must be HTTPS. For local development use ngrok or Cloudflare Tunnel — localhost is rejected.

Each entry in lineItems takes name (1–255), quantity (positive integer), unitAmount (non-negative integer), optional description (≤1024 chars) and optional per-line metadata. Lines are display only — the session's amount is authoritative for charging. Plugipay does not sum lines and compare to amount; reconcile on your side before submitting.

Example

curl -X POST https://api.plugipay.com/v1/checkout-sessions \
  -H "Authorization: Plugipay-HMAC-SHA256 keyId=$PLUGIPAY_KEY_ID, scope=*, signature=<sig>" \
  -H "X-Plugipay-Timestamp: $(date +%s)" \
  -H "Idempotency-Key: order-2026-05-12-001" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 250000,
    "currency": "IDR",
    "methods": ["qris", "va", "card"],
    "successUrl": "https://myapp.com/checkout/success",
    "cancelUrl": "https://myapp.com/checkout/cancel",
    "customerId": "cus_01HXxxxxxxxxxxxxxxxxxxxxxx",
    "metadata": { "orderId": "ord_42" }
  }'
// Node
const session = await plugipay.checkoutSessions.create({
  amount: 250000,
  currency: 'IDR',
  methods: ['qris', 'va', 'card'],
  successUrl: 'https://myapp.com/checkout/success',
  cancelUrl: 'https://myapp.com/checkout/cancel',
  customerId: 'cus_01HXxxxxxxxxxxxxxxxxxxxxxx',
  metadata: { orderId: 'ord_42' },
});
console.log(session.hostedUrl);
# Python
session = plugipay.checkout_sessions.create(
    amount=250000,
    currency="IDR",
    methods=["qris", "va", "card"],
    success_url="https://myapp.com/checkout/success",
    cancel_url="https://myapp.com/checkout/cancel",
    customer_id="cus_01HXxxxxxxxxxxxxxxxxxxxxxx",
    metadata={"orderId": "ord_42"},
)
print(session["hostedUrl"])
// Go
session, err := client.CheckoutSessions.Create(ctx, plugipay.CheckoutSessionInput{
    Amount:     250000,
    Currency:   "IDR",
    Methods:    []string{"qris", "va", "card"},
    SuccessURL: "https://myapp.com/checkout/success",
    CancelURL:  "https://myapp.com/checkout/cancel",
    CustomerID: "cus_01HXxxxxxxxxxxxxxxxxxxxxxx",
    Metadata:   map[string]string{"orderId": "ord_42"},
})

Response

201 Created. The data payload is a full Checkout session object:

{
  "data": {
    "id": "cs_01HYxxxxxxxxxxxxxxxxxxxxxx",
    "arn": "arn:plugipay:acc_01HX...:checkout-session:cs_01HY...",
    "accountId": "acc_01HXxxxxxxxxxxxxxxxxxxxxxx",
    "customerId": "cus_01HXxxxxxxxxxxxxxxxxxxxxxx",
    "mode": "test",
    "status": "open",
    "amount": 250000,
    "currency": "IDR",
    "methods": ["qris", "va", "card"],
    "adapter": null,
    "lineItems": [],
    "successUrl": "https://myapp.com/checkout/success",
    "cancelUrl": "https://myapp.com/checkout/cancel",
    "hostedUrl": "https://plugipay.com/c/cs_01HYxxxxxxxxxxxxxxxxxxxxxx",
    "expiresAt": "2026-05-13T10:42:00.000Z",
    "completedAt": null,
    "paymentId": null,
    "metadata": { "orderId": "ord_42" },
    "createdAt": "2026-05-12T10:42:00.000Z",
    "updatedAt": "2026-05-12T10:42:00.000Z"
  },
  "error": null,
  "meta": { "requestId": "req_01H...", "timestamp": "2026-05-12T10:42:00.123Z" }
}

The hostedUrl is the public payment page. It's safe to share over any channel — see Sharing the URL for patterns.

Errors

HTTP error.code When
400 VALIDATION_ERROR Missing required field, non-positive amount, non-HTTPS URL, empty methods, unknown method enum, malformed metadata. The error.field points at the offender.
404 not_found customerId belongs to a different workspace, or templateId doesn't exist / isn't of kind receipt.
409 idempotency_conflict Same Idempotency-Key reused with a different body.
422 no_adapter_supports_methods None of your enabled adapters can serve any of the requested methods in this currency.

Retrieve a session

GET /v1/checkout-sessions/:id

Fetches a session by ID. Cheap, cacheable on your side for the duration of expiresAt.

curl https://api.plugipay.com/v1/checkout-sessions/cs_01HYxxxxxxxxxxxxxxxxxxxxxx \
  -H "Authorization: Plugipay-HMAC-SHA256 keyId=$PLUGIPAY_KEY_ID, scope=*, signature=<sig>" \
  -H "X-Plugipay-Timestamp: $(date +%s)"
const session = await plugipay.checkoutSessions.get('cs_01HYxxxxxxxxxxxxxxxxxxxxxx');
session = plugipay.checkout_sessions.get("cs_01HYxxxxxxxxxxxxxxxxxxxxxx")
session, err := client.CheckoutSessions.Get(ctx, "cs_01HYxxxxxxxxxxxxxxxxxxxxxx")

Returns 200 OK with the object. 404 not_found if the ID doesn't exist or belongs to a different workspace.

Don't poll this endpoint to learn about completion. Configure a webhook endpoint for checkout_session.completed instead. Polling costs you rate-limit budget and lags real-time by up to your polling interval.

List sessions

GET /v1/checkout-sessions

Returns a cursor-paginated list of sessions, newest first.

Query parameters

Field Type Notes
limit integer 1–100. Default 20.
cursor string Opaque cursor from a previous response's meta.cursor.
order asc | desc Default desc (newest first).
status string Filter to one of open, pending, pending_review, completed, expired, canceled.
customerId string Restrict to sessions attached to a single customer.
mode live | test Filter by environment. By default, the key's environment is implicit — mode lets a platform-admin key see across both.
createdAfter string ISO 8601 or epoch seconds. Excludes sessions at or before this time.
createdBefore string ISO 8601 or epoch seconds. Excludes sessions at or after this time.

Example

curl "https://api.plugipay.com/v1/checkout-sessions?status=open&limit=50" \
  -H "Authorization: Plugipay-HMAC-SHA256 keyId=$PLUGIPAY_KEY_ID, scope=*, signature=<sig>" \
  -H "X-Plugipay-Timestamp: $(date +%s)"
const page = await plugipay.checkoutSessions.list({ status: 'open', limit: 50 });
for (const s of page.data) console.log(s.id, s.amount);
if (page.hasMore) {
  const next = await plugipay.checkoutSessions.list({ status: 'open', limit: 50, cursor: page.cursor });
}
page = plugipay.checkout_sessions.list(status="open", limit=50)
page, err := client.CheckoutSessions.List(ctx, plugipay.CheckoutSessionListParams{
    Status: "open",
    Limit:  50,
})

Response

{
  "data": [ /* CheckoutSession objects */ ],
  "error": null,
  "meta": {
    "requestId": "req_01H...",
    "timestamp": "2026-05-12T10:42:00.123Z",
    "page": { "limit": 50, "hasMore": true, "nextCursor": "cur_..." }
  }
}

See Pagination for the cursor walk.

Cancel a session

POST /v1/checkout-sessions/:id/cancel

Closes a session immediately. The hosted URL stops accepting payment attempts; any in-flight provider attempt is refused at the next step.

Only open, pending, and pending_review sessions can be canceled. Calling cancel on a terminal session returns 409 conflict. The Idempotency-Key header is required.

curl -X POST https://api.plugipay.com/v1/checkout-sessions/cs_01HYxxxxxxxxxxxxxxxxxxxxxx/cancel \
  -H "Authorization: Plugipay-HMAC-SHA256 keyId=$PLUGIPAY_KEY_ID, scope=*, signature=<sig>" \
  -H "X-Plugipay-Timestamp: $(date +%s)" \
  -H "Idempotency-Key: cancel-cs_01HYxxx" \
  -H "Content-Type: application/json" \
  -d '{}'
const canceled = await plugipay.checkoutSessions.cancel('cs_01HYxxxxxxxxxxxxxxxxxxxxxx');
// canceled.status === 'canceled'
canceled = plugipay.checkout_sessions.cancel("cs_01HYxxxxxxxxxxxxxxxxxxxxxx")
canceled, err := client.CheckoutSessions.Cancel(ctx, "cs_01HYxxxxxxxxxxxxxxxxxxxxxx")

Returns 200 OK with the updated object. Cancel emits no event — if you want a webhook signal, use a short expiresInSec and let the session expire (which fires checkout_session.expired).

Errors

HTTP error.code When
404 not_found Unknown ID for this workspace.
409 conflict Session is already completed, expired, or canceled.

Confirm a manual session

POST /v1/checkout-sessions/:id/confirm

For manual-adapter sessions only (bank transfer, cash, EDC slip), where the customer paid you offline and the system has no automated way to detect receipt. The merchant verifies the deposit, then calls this endpoint; the session flips to completed, a Payment record is created, a receipt is issued, and checkout_session.completed fires through the outbox.

If the session was the payment vehicle for a subscription's first invoice, the invoice is also marked paid and invoice.paid.v1 is emitted in the same transaction. This is the only checkout endpoint that has side effects on invoices.

curl -X POST https://api.plugipay.com/v1/checkout-sessions/cs_01HYxxxxxxxxxxxxxxxxxxxxxx/confirm \
  -H "Authorization: Plugipay-HMAC-SHA256 keyId=$PLUGIPAY_KEY_ID, scope=*, signature=<sig>" \
  -H "X-Plugipay-Timestamp: $(date +%s)" \
  -H "Content-Type: application/json" \
  -d '{}'
const completed = await plugipay.checkoutSessions.confirm('cs_01HYxxxxxxxxxxxxxxxxxxxxxx');
// completed.status === 'completed', completed.completedAt is set
completed = plugipay.checkout_sessions.confirm("cs_01HYxxxxxxxxxxxxxxxxxxxxxx")
completed, err := client.CheckoutSessions.Confirm(ctx, "cs_01HYxxxxxxxxxxxxxxxxxxxxxx")

Returns 200 OK with the updated object.

Errors

HTTP error.code When
404 not_found Unknown ID for this workspace.
409 conflict Session isn't in pending_review / pending, or its adapter isn't manual.

The checkout session object

Returned by every endpoint on this page and embedded as data.object in every webhook event.

Field Type Notes
id string cs_ + ULID.
arn string arn:plugipay:<accountId>:checkout-session:<id>.
accountId string Owning workspace, acc_….
customerId string | null Attached customer, if any.
mode live | test Inherited from the key.
status string See Lifecycle.
amount integer Smallest currency unit. Immutable.
currency string Uppercase ISO 4217.
methods string[] Allowed methods, as passed in.
adapter string | null Which adapter routed the payment. null until the customer picks a method. Values: xenplatform, xendit, midtrans, paypal, manual.
lineItems object[] Display-only breakdown. [] if none supplied.
successUrl string Set at creation.
cancelUrl string Set at creation.
hostedUrl string https://plugipay.com/c/<id>. Public, safe to share.
expiresAt string ISO 8601 UTC. Immutable.
completedAt string | null When status flipped to completed.
paymentId string | null The linked pay_… once completed.
metadata object | null Echoed in webhooks.
createdAt string ISO 8601 UTC.
updatedAt string Bumps on every state change.

Events

Sessions emit events through the outbox. Subscribe at Webhook endpoints; the data.object payload mirrors the REST shape above.

Event type When
plugipay.checkout_session.created.v1 Reserved — declared in the contract but not fired on every create. Wire for it if you want forward compatibility.
plugipay.checkout_session.completed.v1 Payment captured. Fires from three paths: online provider callback (Xendit / Midtrans), /confirm on a manual session, and subscription-first-charge auto-close.
plugipay.checkout_session.expired.v1 TTL elapsed. Emitted by the expiry sweep cron — expect up to a minute of latency between expiresAt and the event.

There is no canceled event; cancellation is a merchant-initiated state — inspect via GET if you need a record. See Webhooks for envelope shape, signing, and retries.

Common patterns

Attach a customer

Pass customerId and the payment lands in that customer's history, with their email pre-filled on the hosted page. Omit it for anonymous shoppers — the hosted page collects an email at pay-time.

Route events with metadata

Webhooks deliver your metadata verbatim in data.object.metadata, so you can fan out without a follow-up Plugipay lookup:

metadata: { orderId: 'ord_42', tenantId: 'tnt_main' }

Constraints from Conventions → Metadata: up to 50 keys, 40-char keys, 500-char values, strings only.

Success / cancel URLs

The successUrl receives ?session_id=cs_… as a query parameter on redirect:

app.get('/checkout/success', async (req, res) => {
  const session = await plugipay.checkoutSessions.get(req.query.session_id);
  if (session.status !== 'completed') return res.render('pending');
  res.render('success', { paymentId: session.paymentId });
});

Don't fulfill on the redirect — fulfill on the webhook. The redirect is a UX signal, not a delivery confirmation; a determined customer can hit your success URL without paying.

Filter payment methods

methods is a hard allow-list. Pass ["qris"] to make QRIS the only option, or omit a method (e.g. paypal) to hide it even if the adapter supports it. For an Indonesia-only flow:

methods: ['qris', 'va', 'ewallet']  // no card, no paypal

Override the TTL

expiresInSec defaults to 24 hours. Override down for fraud-sensitive flows or up for pay-links you'll send days in advance. Min 300, max 2592000. You can't change the TTL after creation — cancel and recreate.

Customize the receipt with a template

Pass templateId (a tpl_… of kind receipt) to pin which receipt template renders for this session. Omit to use the workspace default. Everything else on the hosted page (brand name, logo, accent color) is workspace-wide — see Customizing the hosted checkout page.

Next

  • Payments — the Payment record that a completed session produces.
  • Refunds — issuing money back; always against a payment, never against a session.
  • Templates — receipt template CRUD.
  • Webhook endpoints — register the URL Plugipay POSTs events to.
  • Idempotency — required for create and cancel.
Plugipay — Payments that don't tax your success