Run a marketplace order end to end
This guide runs one order in Sandbox, with a rpe_test_ key: create the buyer and the seller, add the seller's payout bank account, open the order, let the buyer pay, ship, release and pay the seller out. Paste the blocks in order into one terminal. Each block saves the ids it needs from the previous answer, so nothing has to be typed by hand. Nothing here moves real money.
Before you write code
- Call RekberPay only from your server. A workspace key sent from a browser is refused (
403 KEY_IN_BROWSER). - Send a new
Idempotency-Keywith every write, and reuse it when you retry the same request. - Use one key per job, with only the scopes that job needs. See Keep your keys safe.
The waits
Some steps wait on purpose, in the sandbox too:
| Step | Wait | Sandbox shortcut |
|---|---|---|
| A new payout bank account | 24 hours of cooling-off before the first withdrawal | Skip the cooling-off |
| A shipped order nobody confirms | 72 hours with a tracking number, 36 without | Release now |
| Your own confirm for the buyer | 6 hours by default, and the buyer is told | Release now |
That is why the bank account is added right after the seller, before the order.
Sandbox shortcuts
Four calls exist only in Sandbox. Each answers 404 in Live, and each is used in the steps below.
| Shortcut | Call | Key needs | Used in |
|---|---|---|---|
| Settle a KYC | POST /users/:ref/kyc/simulate | kyc | Optional: KYC |
| Pay the checkout | POST https://api.rekberpay.com/v1/platform/checkout/:token/simulate (no key, the token is the credential) | none | Step 5 |
| Release now | POST /deals/:id/milestones/:mid/simulate/advance-time | orders | Step 8 |
| End a bank account's cooling-off | POST /users/:ref/bank-accounts/:id/simulate/skip-cooling-off | withdraw | Step 11 |
What you need
- A sandbox Enterprise workspace with Platform switched on. Step 1 checks it.
- A workspace API key with
read,write,users,orders,balance:readandwithdraw. Addkycto try the KYC step, andorders:confirmonly if you want to try your own confirm in step 9; RekberPay must allow that scope first. curl,jqanduuidgenin your shell.- A webhook endpoint is optional (Receiving webhooks).
export REKBERPAY_WORKSPACE_KEY="rpe_..." # your sandbox key
export API="https://api.rekberpay.com/v1/enterprise/platform"
auth=(-H "Authorization: Bearer $REKBERPAY_WORKSPACE_KEY")
json=(-H "Content-Type: application/json")
idem() { echo "Idempotency-Key: $(uuidgen | tr 'A-Z' 'a-z')"; }1. Check your Platform terms
curl -s "$API/settings" "${auth[@]}" | jq .{
"success": true,
"data": {
"enabled": true,
"frozen": false,
"fee_rate_bps": 200,
"fee_min_idr": 5000,
"default_fee_bearer": "buyer",
"default_split_buyer_pct": 50,
"daily_cap_idr": 50000000,
"confirm_delay_hours": 6,
"allowed_scopes": ["users", "kyc", "orders", "balance:read", "withdraw", "ratings"]
}
}404 NOT_FOUND means Platform is not on for this workspace yet; ask your account manager. 423 PLATFORM_FROZEN means RekberPay paused it.
2. Create the buyer and the seller
curl -s -X POST "$API/users" "${auth[@]}" "${json[@]}" -H "$(idem)" -d '{
"partner_user_ref": "buyer-1001",
"email": "[email protected]",
"full_name": "Sari Wulandari"
}' | jq '.data | {partner_user_ref, link_status}'
curl -s -X POST "$API/users" "${auth[@]}" "${json[@]}" -H "$(idem)" -d '{
"partner_user_ref": "seller-2001",
"email": "[email protected]",
"full_name": "Budi Santoso"
}' | jq '.data | {partner_user_ref, link_status}'Both print "link_status": "active". Sandbox emails must be on a test domain such as example.com. Details, existing accounts and claiming: Create and link users.
Optional: verify the seller
Skip this unless you want to try KYC. The sandbox needs a test NIK ending in 0000 and any small image. The photos below are a 1 pixel test image.
printf 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' | base64 --decode > test.png
curl -s -X POST "$API/users/seller-2001/kyc" "${auth[@]}" -H "$(idem)" \
-F "kyc_id_type=ktp" -F "kyc_full_name_ktp=Budi Santoso" -F "kyc_nik=3171014501900000" \
-F "kyc_whatsapp=081234567890" -F "kyc_province=DKI Jakarta" -F "kyc_city=Jakarta Selatan" \
-F "kyc_district=Kebayoran Baru" -F "kyc_subdistrict=Senayan" -F "kyc_street_address=Jl. Asia Afrika No. 8" \
-F "kyc_bank_name=bca" -F "kyc_account_number=1234567890" \
-F "[email protected]" -F "[email protected]" | jq '.data'
curl -s -X POST "$API/users/seller-2001/kyc/simulate" "${auth[@]}" "${json[@]}" -H "$(idem)" \
-d '{ "result": "approved" }' | jq '.data'The first call prints "kyc_status": "pending", the second settles it. You get platform.user.kyc_updated both times, and the seller's limits no longer apply. Details: KYC through the API.
3. Add the seller's payout bank account now
The holder name must match the seller's name on RekberPay, and the account starts its 24 hour cooling-off now, while the order runs.
BANK_ACCOUNT_ID=$(curl -s -X POST "$API/users/seller-2001/bank-accounts" "${auth[@]}" "${json[@]}" -H "$(idem)" -d '{
"bank_code": "BCA",
"account_number": "1234567890",
"holder_name": "Budi Santoso"
}' | jq -r .data.id)
echo "$BANK_ACCOUNT_ID"A null here means the call was refused; run it without | jq -r .data.id to see why (for example 422 NAME_MISMATCH).
4. Create the order
An order is a deal with one or more milestones. Each milestone is paid, shipped and released on its own. This one has a single milestone.
DEAL_JSON=$(curl -s -X POST "$API/deals" "${auth[@]}" "${json[@]}" -H "$(idem)" -d '{
"buyer_ref": "buyer-1001",
"seller_ref": "seller-2001",
"title": "Fujifilm X-T30 body, used",
"partner_order_ref": "ORD-55012",
"milestones": [
{ "title": "Camera body", "amount": 4500000 }
],
"fee_bearer": "buyer"
}')
DEAL_ID=$(echo "$DEAL_JSON" | jq -r .data.id)
MS_ID=$(echo "$DEAL_JSON" | jq -r '.data.milestones[0].id')
echo "$DEAL_JSON" | jq '.data | {status, platform_fee, milestones: [.milestones[] | {status, funding_amount, payout_amount}]}'{
"status": "awaiting_funding",
"platform_fee": 90000,
"milestones": [
{ "status": "awaiting_payment", "funding_amount": 4590000, "payout_amount": 4500000 }
]
}The fee is 2 percent of the deal (at least Rp 5.000) unless your terms say otherwise. fee_bearer is buyer, seller or split (with fee_split_buyer_pct); leave it out to use your default.
The create checks your daily cap (409 DAILY_CAP_REACHED) and refuses a deal too large for a person without KYC (422 LIMIT_EXCEEDED). Their 24 hour count and lifetime total are checked later: a milestone counts toward them from checkout or payment, so an order nobody pays holds none of their limits. An order takes one place in the 24 hour count however many milestones it has, while the lifetime total adds up each milestone. partner_order_ref is unique in your workspace (409 DUPLICATE_ORDER_REF), so a retry can never open a second order. To run the guide again, use a new partner_order_ref.
5. Let the buyer pay
CHECKOUT_URL=$(curl -s -X POST "$API/deals/$DEAL_ID/milestones/$MS_ID/checkout" "${auth[@]}" -H "$(idem)" | jq -r .data.url)
CHECKOUT_TOKEN=${CHECKOUT_URL##*/checkout/p/}
echo "$CHECKOUT_URL"In live mode you send the buyer to CHECKOUT_URL. In the sandbox the page shows Simulasikan pembayaran instead of real payment methods, or you simulate from the terminal with the token:
curl -s -X POST "https://api.rekberpay.com/v1/platform/checkout/$CHECKOUT_TOKEN/simulate" | jq .{ "success": true, "data": { "milestone_status": "paid", "is_test": true } }You get milestone.paid. Everything about the checkout page, the return page and live payment methods: Checkout for buyers.
6. Check the order
curl -s "$API/deals/$DEAL_ID" "${auth[@]}" | jq '.data | {status, milestone: .milestones[0] | {status, paid_at}}'The deal is funded and the milestone paid. Read the deal like this whenever you are not sure, for example after a missed webhook.
Part of the order called off? Cancel each milestone that is still awaiting_payment with POST /deals/:id/milestones/:mid/cancel. The paid ones carry on, and the deal completes once they are released. Before any payment, POST /deals/:id/cancel cancels the whole deal. A paid milestone is never cancelled: open a dispute.
7. Ship
curl -s -X POST "$API/deals/$DEAL_ID/milestones/$MS_ID/ship" "${auth[@]}" "${json[@]}" -H "$(idem)" \
-d '{ "courier": "JNE", "tracking_number": "JNE0012345678" }' | jq '.data | {status, auto_release_at}'The milestone is shipped with an auto_release_at, and you get milestone.shipped.
8. Release
In live mode the money goes to the seller when the buyer confirms on RekberPay, 6 hours after your own confirm, or at auto_release_at. In the sandbox you do not have to wait: release the shipped milestone now.
curl -s -X POST "$API/deals/$DEAL_ID/milestones/$MS_ID/simulate/advance-time" "${auth[@]}" -H "$(idem)" \
| jq '.data | {status, received_by, released_at, payout_amount}'{ "status": "released", "received_by": "auto", "released_at": "2026-10-21T09:30:00.000Z", "payout_amount": 4500000 }The shortcut (reference) needs the orders scope and works only on a shipped or release_pending sandbox milestone. It releases the money the same way the automatic release or the end of your confirm wait would: you get milestone.auto_released (from shipped) or milestone.released (from release_pending), and the seller's partner balance grows by payout_amount. Any other status, including disputed, answers 409 INVALID_STATE, and a Live call gets 404. The full rules: Shipping, confirm and auto release.
9. Optional: confirm for the buyer
Skip this step if you released in step 8. To try your own confirm, your key needs orders:confirm. Run steps 4 to 7 again with a new partner_order_ref (change ORD-55012 in step 4 to, say, ORD-55013), then:
curl -s -X POST "$API/deals/$DEAL_ID/milestones/$MS_ID/confirm" "${auth[@]}" "${json[@]}" -H "$(idem)" -d '{}' \
| jq '.data | {status, release_at, release_notice_url}'The milestone becomes release_pending until release_at. In the sandbox the answer carries release_notice_url, the page the buyer would get by email; open it to try the dispute button. simulate/advance-time releases a pending confirm at once too.
10. Read the seller's balance
curl -s "$API/users/seller-2001/balance" "${auth[@]}" | jq .{ "success": true, "data": { "partner_balance": 4500000 } }11. Pay the seller out
The bank account from step 3 is still in its cooling-off.
Skip the cooling-off (sandbox)
In the sandbox, end the cooling-off now:
curl -s -X POST "$API/users/seller-2001/bank-accounts/$BANK_ACCOUNT_ID/simulate/skip-cooling-off" "${auth[@]}" -H "$(idem)" \
| jq '.data | {usable, usable_after}'{ "usable": true, "usable_after": "2026-10-21T09:31:00.000Z" }The shortcut (reference) needs the withdraw scope. An account that is already usable comes back as it is, and a Live call gets 404. Then withdraw:
WITHDRAWAL_ID=$(curl -s -X POST "$API/users/seller-2001/withdrawals" "${auth[@]}" "${json[@]}" -H "$(idem)" \
-d "{ \"amount\": 4500000, \"bank_account_id\": \"$BANK_ACCOUNT_ID\" }" | jq -r .data.id)
curl -s "$API/users/seller-2001/withdrawals/$WITHDRAWAL_ID" "${auth[@]}" | jq '.data | {status, amount, fee, net_amount}'A sandbox withdrawal completes at once as test data ("status": "completed"), and you get withdrawal.requested and withdrawal.settled together. The name check, the cooling-off and the second approver on large amounts: Balances and withdrawals.
Going live
When RekberPay approves your business verification, Live unlocks in the same workspace and Platform turns on there with the same terms (unless RekberPay switched Live Platform off for you). Sandbox stays as it is: your test orders, users, balances, rpe_test_ keys and Sandbox webhook endpoints keep working, so you can keep testing. See Going live for the Enterprise side. For Platform mode it means:
- Live has its own keys and endpoints. Make a
rpe_live_key with the Platform scopes you need and get its IP allowlist approved by RekberPay (going live). Add a Live webhook endpoint for live events. - Every read is split by mode. Sandbox users, orders, balances, bank accounts and withdrawals never show in Live, and the reverse. Create your users again in Live, with their real email addresses. You may reuse the same
partner_user_refandpartner_order_refvalues. - Payout bank accounts must be added again, and each new one has its 24 hour cooling-off before the first live payout. There are no shortcuts in Live.
- RekberPay sends the emails itself. The connect, claim, email change and release notice links no longer come back in the answers.
- Checkout charges real payment methods. Simulate answers
404. - Withdrawals go to RekberPay's payout team. A large one waits for a second RekberPay approver (
pending_review). - RekberPay sets your live daily cap. It counts live orders only; Sandbox orders never use it up. Ask your account manager if you need it raised.
Each mode has its own Platform switch and freeze, so RekberPay can pause one mode without the other. Webhook deliveries say which mode an event came from in livemode, and a Live endpoint only receives live events.