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| Status | Label (id-ID) | Meaning |
|---|---|---|
awaiting_payment | Menunggu Pembayaran | Order created, not yet funded |
pending_review | Menunggu Konfirmasi | Buyer claims a manual transfer; operator is verifying |
paid | Dalam Antrean | Funded and queued. SLA clock is running |
processing | Sedang Dikerjakan | An operator is actively buying |
delivered | Selesai | Result handed over; money is RekberPay's |
refunded | Dana Dikembalikan | Money returned in full |
cancelled | Dibatalkan | Abandoned 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
| Method | Path | Auth | Idempotent |
|---|---|---|---|
GET | /api/jasabayar/config | session | — |
POST | /api/jasabayar/quote | session | — |
POST | /api/jasabayar | session | Yes |
GET | /api/jasabayar | session | — |
GET | /api/jasabayar/:id | session | — |
POST | /api/jasabayar/:id/cancel | session | Yes |
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.
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
{
"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
}
}| Field | Type | Description |
|---|---|---|
enabled | boolean | false means the feature is off; order creation returns 503 |
fee_type | string | percentage or fixed |
fee_value | number | Percent (e.g. 5 = 5%) when percentage, IDR when fixed |
fee_min_idr | integer | Fee floor in IDR |
fee_max_idr | integer | null | Fee cap, or null for uncapped |
sla_hours | integer | Hours after payment before an undelivered order auto-refunds |
min_item_price_idr | integer | Smallest accepted item price |
max_item_price_idr | integer | Largest accepted item price |
Compute the authoritative fee split for an item price. Use this instead of recomputing the fee client-side.
Request
{ "item_price": 250000 }| Field | Type | Required | Description |
|---|---|---|---|
item_price | integer | Yes | IDR, >= 0 for a quote |
Response
200 OK
{
"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.
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
{
"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."
}| Field | Type | Required | Description |
|---|---|---|---|
target_url | string | Yes | Where to buy. http/https only, max 2048 chars |
target_label | string | No | What is being bought, max 255 chars. Cannot contain < or > |
item_price | integer | Yes | IDR, at least min_item_price_idr (10.000) |
notes | string | No | Instructions 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
| Status | Code | Cause |
|---|---|---|
400 | INVALID_BODY | Failed validation, or target_url is not a valid http/https URL |
400 | AMOUNT_TOO_LARGE | item_price exceeds max_item_price_idr |
503 | FEATURE_DISABLED | Jasa Pembayaran is switched off |
500 | DB_ERROR | Order was not created; no funds were taken |
List the authenticated user's orders, newest first.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter to one status from the table above |
page | integer | 1 | Page number |
limit | integer | 20 | Page size, max 50 |
Response
200 OK
{
"success": true,
"data": [ /* order objects */ ],
"pagination": { "page": 1, "limit": 20, "total": 3, "total_pages": 1 }
}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:
| Field | Type | Description |
|---|---|---|
credentials | string | null | Plaintext delivery payload. Owner only, and only once delivered |
events | array | Event 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
| Status | Code | Cause |
|---|---|---|
403 | FORBIDDEN | Not the owner and not an admin |
404 | NOT_FOUND | No such order |
500 | DECRYPT_FAILED | Stored 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.
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
{ "success": true }Errors
| Status | Code | Cause |
|---|---|---|
403 | — | Not your order |
404 | NOT_FOUND | No such order |
409 | — | Order is no longer awaiting_payment (already funded, delivered, or cancelled) |
Order object
{
"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"
}| Field | Type | Description |
|---|---|---|
id | string | UUID |
target_url | string | Normalised purchase URL |
target_label | string | null | Buyer's description of the item |
notes | string | null | Instructions for the operator |
item_price | integer | IDR the item costs |
service_fee | integer | RekberPay's cut, computed server-side |
total | integer | What the buyer pays: item_price + service_fee |
status | string | See the lifecycle table |
sla_deadline_at | string | null | ISO-8601. After this, an undelivered order auto-refunds |
paid_at | string | null | ISO-8601 when funding landed |
delivered_at | string | null | ISO-8601 when the operator delivered |
delivery_note | string | null | Free-text note attached to the delivery |
has_credentials | boolean | Whether a credential payload exists. The payload itself is only returned on GET /:id |
first_viewed_at | string | null | ISO-8601 when the buyer first read the credentials |
refunded_at | string | null | ISO-8601 when money was returned |
refund_reason | string | null | Why it was refunded |
auto_refunded | boolean | true when the SLA sweeper refunded it rather than an operator |
cancelled_at | string | null | ISO-8601 when cancelled |
created_at | string | ISO-8601 |
updated_at | string | ISO-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.