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.
| Step | The buyer | Configured by |
|---|---|---|
customer | Fills in the form | is_name_required, is_email_required, is_phone_required |
payment_method | Picks a payment method | payment_methods |
payment | Scans, copies a code, or is sent to their app | Nothing — 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.steps | Who chooses | The buyer sees |
|---|---|---|
["customer", "payment_method", "payment"] | The buyer | Form → picker → pay |
["payment_method", "payment"] | You prefill the details, the buyer picks the method | Picker → pay |
["payment"] | You choose everything | Pay — 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:
| Field | Ask for it with | Also required when |
|---|---|---|
name | checkout.is_name_required | A Virtual Account is on offer |
email | checkout.is_email_required | A card is on offer |
phone | checkout.is_phone_required | An 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 inorder_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_urlwith?id=payreq_...&status=succeededappended.
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.