Skip to content

Jasa Pembayaran API ​

Jasa Pembayaran ("payment service") is RekberPay's pay-on-behalf product. A buyer who cannot pay a merchant directly — no international card, no supported wallet, a store that won't ship to Indonesia — asks RekberPay to make the purchase for them. The buyer funds an order; an operator buys the item and hands back whatever proves it (an account credential, a receipt, a license key); the money is released.

Not an escrow

A Jasa Pembayaran order has no counterparty. There is no seller to invite, no chat thread, no dispute, no "mark as shipped". It is a single-party order against RekberPay itself, and it lives in its own table with its own status enum. Do not reuse escrow endpoints or escrow status names here — they will not match.

The money, plainly ​

The buyer pays item_price + service_fee. RekberPay holds it, buys the item, and delivers the result. If the operator does not deliver within the SLA window, the sweeper refunds the buyer in full, automatically — the buyer does not have to ask.

Order lifecycle ​

awaiting_payment ──pay──> pending_review ──confirm──> paid
                                │                       │
                                │                       ▼
                                │                  processing
                                │                       │
                                │              ┌────────┴────────┐
                                │              ▼                 ▼
                                │          delivered         refunded
                                ▼
                            cancelled
StatusLabel (id-ID)Meaning
awaiting_paymentMenunggu PembayaranOrder created, not yet funded
pending_reviewMenunggu KonfirmasiBuyer claims a manual transfer; operator is verifying
paidDalam AntreanFunded and queued. SLA clock is running
processingSedang DikerjakanAn operator is actively buying
deliveredSelesaiResult handed over; money is RekberPay's
refundedDana DikembalikanMoney returned in full
cancelledDibatalkanAbandoned before payment

paid and processing are the only statuses where the SLA clock runs. pending_review, paid, and processing are the statuses where the buyer's money is held with no outcome yet reached.

Endpoints ​

MethodPathAuthIdempotent
GET/api/jasabayar/configsession—
POST/api/jasabayar/quotesession—
POST/api/jasabayarsessionYes
GET/api/jasabayarsession—
GET/api/jasabayar/:idsession—
POST/api/jasabayar/:id/cancelsessionYes

Every route in this group requires authentication and accepts an Idempotency-Key on mutations — the plugin registers both hooks for the whole surface, so the contract in Idempotency applies uniformly here.


GET/api/jasabayar/config

Live pricing and limits. Render your fee preview from this rather than hard-coding numbers — the fee is operator-configurable and changes without a release.

Response ​

200 OK ​

json
{
    "success": true,
    "data": {
        "enabled": true,
        "fee_type": "percentage",
        "fee_value": 5,
        "fee_min_idr": 5000,
        "fee_max_idr": null,
        "sla_hours": 48,
        "min_item_price_idr": 10000,
        "max_item_price_idr": 10000000
    }
}
FieldTypeDescription
enabledbooleanfalse means the feature is off; order creation returns 503
fee_typestringpercentage or fixed
fee_valuenumberPercent (e.g. 5 = 5%) when percentage, IDR when fixed
fee_min_idrintegerFee floor in IDR
fee_max_idrinteger | nullFee cap, or null for uncapped
sla_hoursintegerHours after payment before an undelivered order auto-refunds
min_item_price_idrintegerSmallest accepted item price
max_item_price_idrintegerLargest accepted item price

POST/api/jasabayar/quote

Compute the authoritative fee split for an item price. Use this instead of recomputing the fee client-side.

Request ​

json
{ "item_price": 250000 }
FieldTypeRequiredDescription
item_priceintegerYesIDR, >= 0 for a quote

Response ​

200 OK ​

json
{
    "success": true,
    "data": { "itemPrice": 250000, "serviceFee": 12500, "total": 262500 }
}

camelCase here, snake_case elsewhere

This one response uses itemPrice / serviceFee / total. Order objects use item_price / service_fee / total. That inconsistency is real; code against it rather than normalising and being surprised.


POST/api/jasabayar

Create an order. The server re-quotes from live config and ignores any fee the client supplies, so a stale client price can never underpay.

Request ​

json
{
    "target_url": "https://store.example.com/product/123",
    "target_label": "Adobe CC 1-year license",
    "item_price": 250000,
    "notes": "Deliver the license key to the email on my account."
}
FieldTypeRequiredDescription
target_urlstringYesWhere to buy. http/https only, max 2048 chars
target_labelstringNoWhat is being bought, max 255 chars. Cannot contain < or >
item_priceintegerYesIDR, at least min_item_price_idr (10.000)
notesstringNoInstructions for the operator, max 3000 chars

target_url is validated, not sanitised on output

Only http and https survive validation. javascript: and data: URLs are rejected at creation because this value is rendered as a clickable link in the operator's panel — the allowlist is a stored-XSS control, not formatting.

Response ​

201 Created ​

Returns the full order object (see Order object).

Errors ​

StatusCodeCause
400INVALID_BODYFailed validation, or target_url is not a valid http/https URL
400AMOUNT_TOO_LARGEitem_price exceeds max_item_price_idr
503FEATURE_DISABLEDJasa Pembayaran is switched off
500DB_ERROROrder was not created; no funds were taken

GET/api/jasabayar

List the authenticated user's orders, newest first.

Query parameters ​

ParamTypeDefaultDescription
statusstring—Filter to one status from the table above
pageinteger1Page number
limitinteger20Page size, max 50

Response ​

200 OK ​

json
{
    "success": true,
    "data": [ /* order objects */ ],
    "pagination": { "page": 1, "limit": 20, "total": 3, "total_pages": 1 }
}

GET/api/jasabayar/:id

Fetch one order, including its event trail and — once delivered — the credentials.

Readable by the order's owner or an admin. Credentials are decrypted for the owner only: an admin fetching the same order gets the order and its events, never the plaintext payload.

Response ​

200 OK ​

The order object, plus:

FieldTypeDescription
credentialsstring | nullPlaintext delivery payload. Owner only, and only once delivered
eventsarrayEvent trail, oldest first, capped at 100

Each event: { "event": "created", "message": null, "actor_role": "buyer", "created_at": "…" }. actor_role is one of buyer, admin, system.

Errors ​

StatusCodeCause
403FORBIDDENNot the owner and not an admin
404NOT_FOUNDNo such order
500DECRYPT_FAILEDStored payload could not be decrypted — contact support with the order ID

first_viewed_at is a receipt

The first time a delivered order's credentials are read, the server stamps first_viewed_at and appends a viewed event. It is the record that the buyer actually received what they paid for, so treat a non-null value as "handed over" in your own UI. The stamp is best-effort and never blocks the response.


POST/api/jasabayar/:id/cancel

Cancel an order. Permitted only while the status is awaiting_payment. Once funded, an order either delivers or refunds — there is no cancel path that could strand the buyer's money.

The guard runs inside a transaction with SELECT … FOR UPDATE and a compare-and-swap on the status, so a payment webhook landing concurrently wins and the cancel becomes a no-op rather than cancelling a paid order.

Response ​

200 OK ​

json
{ "success": true }

Errors ​

StatusCodeCause
403—Not your order
404NOT_FOUNDNo such order
409—Order is no longer awaiting_payment (already funded, delivered, or cancelled)

Order object ​

json
{
    "id": "9f1c6b2a-0f4e-4f0b-9c1e-2a7d5b3e8c40",
    "target_url": "https://store.example.com/product/123",
    "target_label": "Adobe CC 1-year license",
    "notes": "Deliver the license key to the email on my account.",
    "item_price": 250000,
    "service_fee": 12500,
    "total": 262500,
    "status": "paid",
    "sla_deadline_at": "2026-08-06T09:12:44.000Z",
    "paid_at": "2026-08-04T09:12:44.000Z",
    "delivered_at": null,
    "delivery_note": null,
    "has_credentials": false,
    "first_viewed_at": null,
    "refunded_at": null,
    "refund_reason": null,
    "auto_refunded": false,
    "cancelled_at": null,
    "created_at": "2026-08-04T09:05:11.000Z",
    "updated_at": "2026-08-04T09:12:44.000Z"
}
FieldTypeDescription
idstringUUID
target_urlstringNormalised purchase URL
target_labelstring | nullBuyer's description of the item
notesstring | nullInstructions for the operator
item_priceintegerIDR the item costs
service_feeintegerRekberPay's cut, computed server-side
totalintegerWhat the buyer pays: item_price + service_fee
statusstringSee the lifecycle table
sla_deadline_atstring | nullISO-8601. After this, an undelivered order auto-refunds
paid_atstring | nullISO-8601 when funding landed
delivered_atstring | nullISO-8601 when the operator delivered
delivery_notestring | nullFree-text note attached to the delivery
has_credentialsbooleanWhether a credential payload exists. The payload itself is only returned on GET /:id
first_viewed_atstring | nullISO-8601 when the buyer first read the credentials
refunded_atstring | nullISO-8601 when money was returned
refund_reasonstring | nullWhy it was refunded
auto_refundedbooleantrue when the SLA sweeper refunded it rather than an operator
cancelled_atstring | nullISO-8601 when cancelled
created_atstringISO-8601
updated_atstringISO-8601

Paying for an order ​

An order is funded through the same checkout as an escrow — every gateway and payment method is shared. See Payments. The only difference is the target: the payment link points at the order, not an escrow.

A buyer with a RekberPay balance can also pay from it directly, which settles instantly and moves the order straight to paid.

Released under the proprietary RekberPay license. Built for Indonesian merchants.