Authentication
RekberPay uses HMAC-signed links for partner integrations. No bearer tokens, no OAuth flows, no credentials in the browser. You sign a request on your backend with a shared secret, and the API verifies the signature before accepting it.
Why HMAC?
Traditional API keys have a problem: if they leak, an attacker can impersonate you until you rotate the key. With HMAC signing:
- No credentials in transit — the secret never leaves your server
- Tamper-proof — changing any signed field invalidates the signature
- Time-bound — every link has an
expirestimestamp; old links are rejected - Replay-safe — combined with
ref(your unique order ID), duplicate requests are caught by idempotency
This is the same pattern Stripe uses for webhook signatures and AWS uses for API requests. It's battle-tested at scale.
How it works
1. You sign the request
On your backend, you build a canonical string from the request parameters, HMAC-SHA256 it with your LINK_SIGNING_SECRET, and hex-encode the result.
The canonical string is:
key1=encodeURIComponent(value1)&key2=encodeURIComponent(value2)&...where the keys are sorted alphabetically and drawn from this allowlist:
buyer(optional) — UUID of the buyer in RekberPay, if knowncategory(optional) —goods,service, ordigitaldesc(optional) — longer item descriptionexpires(required) — unix timestamp when the link expiresitem(required) — item or service nameref(required) — your unique order/transaction IDseller(required) — UUID of the seller in RekberPayv(required) — signature version, always1
Example canonical string:
category=service&expires=1748448000&item=Jasa%20Logo%20Design&ref=ORDER_12345&seller=0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d&v=1Then:
const signature = crypto
.createHmac('sha256', LINK_SIGNING_SECRET)
.update(canonical)
.digest('hex');2. You POST the signed payload
Include the signature in the sig field:
{
"seller": "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
"item": "Jasa Logo Design",
"category": "service",
"amount": 250000,
"expires": 1748448000,
"ref": "ORDER_12345",
"v": 1,
"sig": "f3a9c1e8b7d6a5f4c3b2a1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0"
}3. The API verifies
RekberPay recomputes the signature from the received params (excluding sig itself) and compares it in constant time. If they don't match, the request is rejected with 403 INVALID_SIGNATURE.
The API also checks:
expiresis in the futurerefhasn't been used before by this seller (idempotency)sellerexists and is activeamountis within allowed bounds (10,000 – 100,000,000 IDR)
Implementation
Node.js
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');
}
// Usage
const params = {
seller: '0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d',
item: 'Jasa Logo Design',
category: 'service',
expires: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
ref: `ORDER_${Date.now()}`,
v: 1,
};
params.sig = signLink(params);
// POST to /api/escrows/from-link with params + amountPHP
<?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_' . time(),
'v' => 1,
];
$params['sig'] = sign_link($params, $SECRET, $ALLOWED);Python
import hmac
import hashlib
import os
import 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, '')
]
canonical = '&'.join(parts)
return hmac.new(SECRET, canonical.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': f'ORDER_{int(time.time())}',
'v': 1,
}
params['sig'] = sign_link(params)Field reference
Required fields
| Field | Type | Description |
|---|---|---|
seller | UUID | The seller's user ID in RekberPay. Contact support to get this. |
item | string | Item or service name. Max 200 chars. Shown to the buyer on the payment page. |
expires | integer | Unix timestamp (seconds since epoch). Links expire at this time. Max 30 days in the future. |
ref | string | Your unique order/transaction ID. Used for idempotency — same ref = same escrow. Max 100 chars. |
v | integer | Signature version. Always 1. |
Optional fields
| Field | Type | Description |
|---|---|---|
buyer | UUID | Pre-fill the buyer if you already know their RekberPay user ID. If omitted, they can join via QR or email invite. |
category | string | goods, service, or digital. Affects fee calculation and auto-release SLA. Default: goods. |
desc | string | Longer item description. Max 1000 chars. Shown on the escrow detail page. |
Not signed: amount
amount is in the request body but not in the canonical string. This lets you reuse a signed link for the same item with different amounts (e.g., a configurable service tier). The API still validates amount server-side.
Expiry best practices
- Default: 7 days from now
- Max: 30 days
- Min: 5 minutes (to allow for clock skew)
If a link expires before the buyer pays, they'll see a "Link expired" error. You can generate a fresh link with a new expires timestamp and the same ref — the API will return the existing escrow if it's still pending.
Security notes
LINK_SIGNING_SECRET is equivalent to an API key. Anyone with it can create escrows on your behalf. Keep it server-side only. Never commit it to git, never put it in your frontend bundle, never log it.
HMAC prevents tampering, but it doesn't encrypt the payload. Always use HTTPS so the request body (including amount and item) isn't readable in transit.
The API uses crypto.timingSafeEqual to compare signatures. This prevents timing attacks where an attacker measures how long the comparison takes to guess the signature byte by byte. Your verification code should do the same if you're verifying webhooks.
Testing your implementation
Use the Testing guide to verify your signing logic against the sandbox API. The sandbox uses a different LINK_SIGNING_SECRET than production — request both from [email protected].
Next steps
- Quickstart — sign your first link and create an escrow
- Creating escrows — full guide to the escrow lifecycle
- Handling webhooks — verify webhook signatures (same HMAC pattern)