Accept payments

Kasera Pay Checkout

Send the buyer to a payment page we host and keep working.

Create a payment request, send the buyer to checkout_url, and wait for the webhook. That is the whole integration — you write no payment UI at all. The request below sends no checkout object, so the page runs with your account's checkout defaults; send checkout when one payment needs its own steps or asks. checkout_url comes back either way.

curl https://pay.kasera.id/v1/transactions \
  -H "Authorization: Bearer kp_live_..." \
  -H "Idempotency-Key: order-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 150000,
    "description": "Kaos komunitas",
    "external_id": "ORD-1234",
    "return_url": "https://toko.example/selesai"
  }'

# 201 — send the buyer to checkout_url:
# { "id": "payreq_9b2f...", "checkout_url": "https://pay.kasera.id/p/xK3f...", ... }

The steps are yours to declare

checkout.steps is the flow, as an array of step names. The buyer walks it in the order you send, and a step you leave out never appears. Everything under checkout applies to this page only — Direct API has no page to step through and ignores all of it.

StepThe buyerConfigured by
customerFills in the formis_name_required, is_email_required, is_phone_required
payment_methodPicks a payment methodpayment_methods
paymentScans, copies a code, or is sent to their appNothing — always last

Default is ["customer", "payment_method", "payment"]. Drop customer when you already hold the buyer's details; drop payment_method when payment_methods names a single code. Put payment_method first if you would rather the buyer choose how to pay before typing anything — the order is the order they walk.

payment is always last and cannot be dropped — leave it out of the array and we append it. Dropping customer while a method on offer needs a field you have not prefilled is refused 422 customer_required at create, not discovered by the buyer on a dead end.

Three flows out of two fields

checkout.steps and payment_methods together decide how much the buyer does and how much you have already decided for them.

checkout.stepsWho choosesThe buyer sees
["customer", "payment_method", "payment"]The buyerForm → picker → pay
["payment_method", "payment"]You prefill the details, the buyer picks the methodPicker → pay
["payment"]You choose everythingPay — one screen

The last row is worth naming, because it is the one people do not expect a hosted page to do. Your own app shows the payment buttons and decides the method; we render only that method's screen — the VA number, the QR, the redirect, the card form — with its instructions and countdown. You keep the choice, without building four payment screens.

{
  "amount": 150000,
  "external_id": "ORD-1234",

  "payment_methods": ["va_bca"],
  "customer": { "name": "Budi Santoso" },
  "checkout": { "steps": ["payment"] }
}

Drop the payment_method step and payment_methods must name exactly one code. Two codes and no picker has no answer — that is refused 422 at create rather than silently taking the first, because taking the first means shipping a checkout that quietly never offers the second method.

The customer step

The form asks for name, email and phone. Which of the three appear is decided on the create — by the checkout object you send, or, when you send none, by the merchant’s own payment-method settings:

FieldAsk for it withAlso required when
namecheckout.is_name_requiredA Virtual Account is on offer
emailcheckout.is_email_requiredA card is on offer
phonecheckout.is_phone_requiredAn e-wallet is on offer

Two things fill this form, and they compose: what you asked for, and what the methods on offer need. A field you already sent in customer is never asked for again — so prefilling and collecting are not two modes, they are the two ends of the same rule.

{
  "amount": 150000,
  "external_id": "ORD-1234",

  "checkout": {
    "steps": ["customer", "payment_method", "payment"],
    "is_name_required": true,
    "is_email_required": true
  }
}
{
  "amount": 150000,
  "external_id": "ORD-1234",

  "checkout": { "steps": ["payment_method", "payment"] },
  "customer": {
    "name": "Budi Santoso",
    "email": "budi@toko.dev"
  }
}

Whatever the buyer types is returned in customer on the transaction and in the payment.paid webhook — the same field you would have prefilled, so your code reads one place either way.

The payment_method step

By default the page offers every method enabled on your account. Send payment_methods — an array of codes such as ["va_bca", "qris"] — to offer only those, in that order. Name a single code and you can drop this step entirely: the buyer lands straight on the payment. See payment methods for the codes.

The payment step

  • Your business name — the one approved during verification, so the buyer knows who they are paying.
  • The amount, your description, and the lines you sent in order_items.
  • The QR, payment code, or provider redirect for the chosen method, with its instructions and a countdown to expires_at.
  • After payment: a confirmation, and a button back to your return_url with ?id=payreq_...&status=succeeded appended.

Treat the return_url redirect as navigation, not as proof. A buyer can reach that URL without paying. Fulfil the order on the payment.paid webhook or a server-side GET /v1/transactions/:id, never on the redirect alone.

Why this is the default

Every method we add appears here without you deploying anything, and your declared steps grow to fit it — a Virtual Account on offer already puts the name field in the customer step and the banks in the picker, on their own. A checkout integration written today accepts methods that do not exist yet. Choose Direct API instead only when the payment has to happen inside a screen you control.