Creating Escrows
This guide walks through the full escrow lifecycle from a partner's perspective: signing the link, redirecting the buyer, ship-and-confirm flow, and release.
When to create an escrow
Create an escrow at the moment a buyer commits to a purchase on your platform. The typical trigger is the buyer clicking "Pay" or "Confirm Order" — you sign the link, hit /api/escrows/from-link, and redirect the buyer to the returned escrow_url.
Don't create an escrow earlier than that (e.g., when an item is added to cart). Pending escrows take up a ref slot, and unused escrows clutter your dashboard.
The full lifecycle
PENDING_PAYMENT ──pay──▶ PAID ──send──▶ SENT ──confirm──▶ RECEIVED ──release──▶ RELEASED
│ │ │
│ └──dispute──▶ DISPUTED ──admin resolve──▶ RELEASED / REFUNDED / HOLD
│
└──expires──▶ CANCELLED| State | Who can move it | Webhook event |
|---|---|---|
pending_payment | Buyer (pays) | escrow.created |
paid | Seller (ships) | payment.completed |
sent | Buyer (confirms receipt) | escrow.sent |
received | Buyer (releases) | escrow.received |
released | (terminal) | escrow.released |
disputed | Admin (resolves) | escrow.disputed |
refunded | (terminal) | escrow.refunded |
cancelled | (terminal) | — |
Step 1: Sign the link
See Authentication for the full signing algorithm. The required fields are seller, item, expires, ref, v, and sig.
const params = {
seller: '0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d',
item: 'iPhone 15 Pro 256GB - Garansi Resmi',
category: 'goods',
desc: 'Brand new sealed, garansi resmi iBox 1 tahun',
expires: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
ref: `MARKETPLACE_${orderId}`,
v: 1,
};
params.sig = signLink(params);Step 2: Create the escrow
POST /api/escrows/from-link
Content-Type: application/json
Idempotency-Key: <UUID v4>
{
"seller": "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"item": "iPhone 15 Pro 256GB - Garansi Resmi",
"category": "goods",
"desc": "Brand new sealed, garansi resmi iBox 1 tahun",
"amount": 22500000,
"expires": 1748448000,
"ref": "MARKETPLACE_98765",
"v": 1,
"sig": "f3a9c1..."
}Response:
{
"success": true,
"data": {
"id": "7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c",
"status": "pending_payment",
"escrow_url": "https://rekberpay.com/escrow/7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c/pay",
"expires_at": "2026-05-25T07:00:00.000Z"
}
}Step 3: Redirect the buyer
Send the buyer to data.escrow_url. They'll see:
- Item name, description, and amount in IDR
- Seller name (or your platform name, if configured)
- Payment method picker (QRIS, bank transfer, GoPay, ShopeePay, Crypto)
- Expiry countdown
After they pay, the buyer is redirected back to your return_url (configured in your partner settings).
If you didn't include a buyer UUID when signing, RekberPay matches the buyer by their email when they log in to pay. If they don't have a RekberPay account, they're prompted to create one (takes ~30 seconds with email + phone OTP).
You can also include buyer if you know their RekberPay user ID — this skips the matching step.
Step 4: Listen for payment.completed
When the gateway confirms payment, RekberPay POSTs to your webhook URL:
{
"event": "payment.completed",
"escrow_id": "7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c",
"ref": "MARKETPLACE_98765",
"amount": 22500000,
"status": "paid",
"timestamp": "2026-05-18T09:27:00.000Z",
"signature": "a1b2c3..."
}This is the moment to mark the order as "funds secured" in your system. The seller is also notified by RekberPay (email + in-app).
Step 5: Seller ships, buyer confirms
What happens next depends on category:
Goods (category: "goods")
- Seller marks the item shipped (in their RekberPay dashboard or via your integration). State →
sent. You receiveescrow.sent. - Seller adds tracking number. (Optional but recommended.)
- Buyer receives the item, confirms in their dashboard. State →
received. You receiveescrow.received. - Buyer clicks "Release funds". State →
released. You receiveescrow.released.
Service (category: "service")
- Seller marks the work delivered. State →
sent. - Buyer reviews and either:
- Releases manually →
released - Doesn't act → auto-release after the SLA timer (default 3 days, configurable per-seller)
- Releases manually →
Digital (category: "digital")
- Seller delivers (e.g., emails the file, grants account access).
- Buyer confirms or auto-release fires after 24h.
Auto-release SLA
Each category has a default auto-release timer that releases funds to the seller if the buyer doesn't act:
| Category | Default SLA | Can extend? |
|---|---|---|
goods | 14 days after sent | Yes, up to 30 days |
service | 3 days after sent | Yes, up to 14 days |
digital | 24 hours after sent | Yes, up to 7 days |
The SLA can be overridden per-escrow by including a release_rule field in your signed payload — contact support to enable custom SLAs for your account.
Disputes
At any point between paid and released, either party can open a dispute:
- State →
disputed. You receiveescrow.disputedwith the dispute reason. - Both parties upload evidence in the dispute chat (RekberPay-hosted UI).
- RekberPay admin reviews and resolves with one of:
- Release to seller — you receive
dispute.resolvedwithoutcome: released - Refund to buyer —
outcome: refunded - Hold for further review —
outcome: hold(rare; manual escalation)
- Release to seller — you receive
You don't need to do anything during a dispute except listen for the webhook.
Cancellation
Pending escrows can be cancelled by:
- Buyer (before paying)
- Seller (before paying)
- Auto-cancel when
expiresis reached without payment - Admin (any time, with reason)
You receive no webhook for cancellation by default — request it via escrow.cancelled event subscription if you need it.
Best practices
Make ref predictable from your order ID, e.g., MARKETPLACE_98765. This makes it easy to reconcile escrows with your orders, and the API treats duplicate ref values from the same seller as idempotent — retrying a failed request won't create a duplicate escrow.
Always wait for the payment.completed webhook before considering an order paid. The buyer's browser closing during payment doesn't mean payment failed — the gateway might still confirm asynchronously.
You can have many pending escrows for the same seller at once. There's no limit on concurrent escrows per partner.
Common errors
| Error | Cause | Fix |
|---|---|---|
403 INVALID_SIGNATURE | Signature doesn't match canonical string | Check field order, encoding, secret |
400 LINK_EXPIRED | expires is in the past | Generate a new link |
409 DUPLICATE_REF | ref already used by this seller | Use a new ref, or fetch the existing escrow |
400 INVALID_AMOUNT | amount < 10000 or > 100000000 | Check IDR limits |
404 SELLER_NOT_FOUND | seller UUID isn't valid | Verify with support |
429 RATE_LIMITED | Too many requests in 1 minute | Back off, retry with jitter |
Next steps
- Handling webhooks — verify signatures, handle retries
- Idempotency — make every mutation retry-safe
- Reference: Escrows API — every field documented