Quickstart
Get from zero to a paid escrow in five minutes. We'll sign a partner link in your backend, create the escrow, redirect your buyer to pay, and verify the webhook on the way back.
- A
LINK_SIGNING_SECRETfrom RekberPay (request via [email protected]) - A seller account ID (a UUID — your platform's seller user as it exists in RekberPay)
- A backend that can compute HMAC-SHA256 (every modern language can)
- A public webhook URL (use ngrok for local development)
1. Sign the link
The signature covers every parameter in the request. If anyone changes the amount, the item, or the seller after you sign it, the API rejects the call.
The canonical string is built by:
- Take the allowlisted fields:
buyer,category,desc,expires,item,ref,seller,v. Drop any that areundefinedor empty. - Sort the remaining keys alphabetically.
- Format as
key=encodeURIComponent(value)pairs joined by&.
Then HMAC-SHA256 the result with your LINK_SIGNING_SECRET and hex-encode it.
import crypto from 'node:crypto';
const SECRET = process.env.LINK_SIGNING_SECRET;
const ALLOWED = ['buyer', 'category', 'desc', 'expires', 'item', 'ref', 'seller', 'v'];
function signLink(params) {
const canonical = ALLOWED
.filter((k) => params[k] !== undefined && params[k] !== '')
.map((k) => `${k}=${encodeURIComponent(String(params[k]))}`)
.join('&');
return crypto.createHmac('sha256', SECRET).update(canonical).digest('hex');
}
const params = {
seller: '0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d',
item: 'Jasa Logo Design',
category: 'service',
expires: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7 days
ref: 'ORDER_12345',
v: 1,
};
params.sig = signLink(params);<?php
$SECRET = getenv('LINK_SIGNING_SECRET');
$ALLOWED = ['buyer', 'category', 'desc', 'expires', 'item', 'ref', 'seller', 'v'];
function sign_link(array $params, string $secret, array $allowed): string {
$parts = [];
foreach ($allowed as $k) {
if (isset($params[$k]) && $params[$k] !== '') {
$parts[] = $k . '=' . rawurlencode((string) $params[$k]);
}
}
return hash_hmac('sha256', implode('&', $parts), $secret);
}
$params = [
'seller' => '0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d',
'item' => 'Jasa Logo Design',
'category' => 'service',
'expires' => time() + 7 * 24 * 60 * 60,
'ref' => 'ORDER_12345',
'v' => 1,
];
$params['sig'] = sign_link($params, $SECRET, $ALLOWED);import hmac, hashlib, os, time
from urllib.parse import quote
SECRET = os.environ['LINK_SIGNING_SECRET'].encode()
ALLOWED = ['buyer', 'category', 'desc', 'expires', 'item', 'ref', 'seller', 'v']
def sign_link(params: dict) -> str:
parts = [
f"{k}={quote(str(params[k]), safe='')}"
for k in ALLOWED
if params.get(k) not in (None, '')
]
return hmac.new(SECRET, '&'.join(parts).encode(), hashlib.sha256).hexdigest()
params = {
'seller': '0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d',
'item': 'Jasa Logo Design',
'category': 'service',
'expires': int(time.time()) + 7 * 24 * 60 * 60,
'ref': 'ORDER_12345',
'v': 1,
}
params['sig'] = sign_link(params)amount is in the request body, not the signed canonical string. This keeps links shareable as URLs (one signed link can be reused for the same item with the same partner reference). The API still validates amount against the seller's escrow draft on the server side.
Never put LINK_SIGNING_SECRET in your frontend bundle. It's the equivalent of an API key — anyone with it can mint escrows on your behalf. Sign on your backend, return the signed payload to the browser, then redirect.
2. Create the escrow
POST the signed payload to /api/escrows/from-link. Always include Idempotency-Key so retries don't create duplicates.
curl -X POST https://api.rekberpay.com/api/escrows/from-link \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"seller": "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"item": "Jasa Logo Design",
"category": "service",
"amount": 250000,
"expires": 1748448000,
"ref": "ORDER_12345",
"v": 1,
"sig": "f3a9c1..."
}'import { randomUUID } from 'node:crypto';
const res = await fetch('https://api.rekberpay.com/api/escrows/from-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify(params),
});
const { data } = await res.json();
console.log('Redirect buyer to:', data.escrow_url);A successful response looks like this:
{
"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"
}
}3. Redirect the buyer
Send the buyer to data.escrow_url. They'll see your branded checkout (logo, seller name, item description, total) and pick a payment method: QRIS, bank transfer, GoPay, ShopeePay, Crypto.
You don't have to host anything — the page is rendered by RekberPay and returns to the URL you provided in your partner config when payment completes.
4. Listen for the webhook
The moment the gateway confirms the payment, RekberPay POSTs a webhook to your configured URL.
{
"event": "payment.completed",
"escrow_id": "7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c",
"ref": "ORDER_12345",
"amount": 250000,
"status": "paid",
"timestamp": "2026-05-18T09:27:00.000Z",
"signature": "a1b2c3..."
}Verify the signature before trusting the payload. The signature is HMAC-SHA256(event + escrow_id + amount + status + timestamp, WEBHOOK_SECRET).
import crypto from 'node:crypto';
function verifyWebhook(body, secret) {
const canonical = `${body.event}${body.escrow_id}${body.amount}${body.status}${body.timestamp}`;
const expected = crypto.createHmac('sha256', secret).update(canonical).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(body.signature, 'hex'),
Buffer.from(expected, 'hex'),
);
}Return 200 OK quickly (under 5 seconds). Process the work async — we'll retry up to 3 times on non-2xx responses with exponential backoff.
You're done
That's the whole integration. The buyer paid, the funds are in escrow, and your system has been notified.
Next, depending on your flow:
- Goods marketplace — read Creating escrows to understand the seller-ships → buyer-confirms flow that releases funds.
- Service marketplace — funds release on buyer confirmation by default, or after a configurable SLA timer (auto-release).
- Disputes — see Handling webhooks for the events fired when a buyer or seller opens a dispute.
Move from sandbox to production by swapping LINK_SIGNING_SECRET and the base URL. Everything else stays identical.