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 —
localhostis 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.completedinstead. 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
Paymentrecord that acompletedsession 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.