Skip to content

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
StateWho can move itWebhook event
pending_paymentBuyer (pays)escrow.created
paidSeller (ships)payment.completed
sentBuyer (confirms receipt)escrow.sent
receivedBuyer (releases)escrow.received
released(terminal)escrow.released
disputedAdmin (resolves)escrow.disputed
refunded(terminal)escrow.refunded
cancelled(terminal)

See Authentication for the full signing algorithm. The required fields are seller, item, expires, ref, v, and sig.

js
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

bash
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:

json
{
    "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).

Buyer matching

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:

json
{
    "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")

  1. Seller marks the item shipped (in their RekberPay dashboard or via your integration). State → sent. You receive escrow.sent.
  2. Seller adds tracking number. (Optional but recommended.)
  3. Buyer receives the item, confirms in their dashboard. State → received. You receive escrow.received.
  4. Buyer clicks "Release funds". State → released. You receive escrow.released.

Service (category: "service")

  1. Seller marks the work delivered. State → sent.
  2. Buyer reviews and either:
    • Releases manually → released
    • Doesn't act → auto-release after the SLA timer (default 3 days, configurable per-seller)

Digital (category: "digital")

  1. Seller delivers (e.g., emails the file, grants account access).
  2. 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:

CategoryDefault SLACan extend?
goods14 days after sentYes, up to 30 days
service3 days after sentYes, up to 14 days
digital24 hours after sentYes, 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:

  1. State → disputed. You receive escrow.disputed with the dispute reason.
  2. Both parties upload evidence in the dispute chat (RekberPay-hosted UI).
  3. RekberPay admin reviews and resolves with one of:
    • Release to seller — you receive dispute.resolved with outcome: released
    • Refund to buyeroutcome: refunded
    • Hold for further reviewoutcome: hold (rare; manual escalation)

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 expires is 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

Use a deterministic ref

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.

Don't trust client-side state

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.

Concurrency

You can have many pending escrows for the same seller at once. There's no limit on concurrent escrows per partner.

Common errors

ErrorCauseFix
403 INVALID_SIGNATURESignature doesn't match canonical stringCheck field order, encoding, secret
400 LINK_EXPIREDexpires is in the pastGenerate a new link
409 DUPLICATE_REFref already used by this sellerUse a new ref, or fetch the existing escrow
400 INVALID_AMOUNTamount < 10000 or > 100000000Check IDR limits
404 SELLER_NOT_FOUNDseller UUID isn't validVerify with support
429 RATE_LIMITEDToo many requests in 1 minuteBack off, retry with jitter

Next steps

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