Four-eyes approvals
A workspace has a four_eyes_threshold in its settings. Any milestone with an amount at or above it needs two approvals from two different people before it can be released. The person who submitted the evidence can never be one of them. Below the threshold one approval is enough.
1. Check the threshold
curl https://api.rekberpay.com/v1/enterprise/settings \
-H "Authorization: Bearer $REKBERPAY_WORKSPACE_KEY"const res = await fetch('https://api.rekberpay.com/v1/enterprise/settings', {
headers: {
Authorization: `Bearer ${process.env.REKBERPAY_WORKSPACE_KEY}`,
},
});
const data = await res.json();import os
import requests
resp = requests.get(
"https://api.rekberpay.com/v1/enterprise/settings",
headers={
"Authorization": f"Bearer {os.environ['REKBERPAY_WORKSPACE_KEY']}",
},
timeout=30,
)
print(resp.status_code, resp.json())<?php
$ch = curl_init('https://api.rekberpay.com/v1/enterprise/settings');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('REKBERPAY_WORKSPACE_KEY'),
],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);200 OK
{
"success": true,
"data": {
"display_name": "Northline Trading",
"legal_name": "PT Northline Trading Indonesia",
"billing_email": "[email protected]",
"kyb_status": "verified",
"risk_tier": "standard",
"is_sandbox": true,
"settings": {
"four_eyes_threshold": 100000000,
"dispute_sla_hours": 24,
"watchlist": [
"Blacklisted Supplier"
],
"account_manager": {
"name": "Maya Patel",
"email": "[email protected]"
}
}
}
}Admins change it with PATCH /v1/enterprise/settings.
2. A milestone above the threshold
This run creates a deal with one milestone of Rp150.000.000, above the Rp100.000.000 threshold:
curl -X POST https://api.rekberpay.com/v1/enterprise/deals \
-H "Authorization: Bearer $REKBERPAY_WORKSPACE_KEY" \
-H "Idempotency-Key: 33333337-3333-4333-8333-333333333333" \
-H "Content-Type: application/json" \
-d '{
"title": "Precision moulds, batch 7",
"counterparty_id": "eeeeeee3-eeee-4eee-8eee-eeeeeeeeeeee",
"milestones": [
{
"title": "Moulds delivered and inspected",
"amount": 150000000,
"required_evidence": "Inspection report"
}
]
}'const res = await fetch('https://api.rekberpay.com/v1/enterprise/deals', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.REKBERPAY_WORKSPACE_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': '33333337-3333-4333-8333-333333333333',
},
body: JSON.stringify({
title: "Precision moulds, batch 7",
counterparty_id: "eeeeeee3-eeee-4eee-8eee-eeeeeeeeeeee",
milestones: [
{
title: "Moulds delivered and inspected",
amount: 150000000,
required_evidence: "Inspection report"
}
]
}),
});
const data = await res.json();import os
import requests
resp = requests.post(
"https://api.rekberpay.com/v1/enterprise/deals",
headers={
"Authorization": f"Bearer {os.environ['REKBERPAY_WORKSPACE_KEY']}",
"Idempotency-Key": "33333337-3333-4333-8333-333333333333",
},
json={
"title": "Precision moulds, batch 7",
"counterparty_id": "eeeeeee3-eeee-4eee-8eee-eeeeeeeeeeee",
"milestones": [
{
"title": "Moulds delivered and inspected",
"amount": 150000000,
"required_evidence": "Inspection report",
},
],
},
timeout=30,
)
print(resp.status_code, resp.json())<?php
$ch = curl_init('https://api.rekberpay.com/v1/enterprise/deals');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('REKBERPAY_WORKSPACE_KEY'),
'Content-Type: application/json',
'Idempotency-Key: 33333337-3333-4333-8333-333333333333',
],
CURLOPT_POSTFIELDS => json_encode([
'title' => 'Precision moulds, batch 7',
'counterparty_id' => 'eeeeeee3-eeee-4eee-8eee-eeeeeeeeeeee',
'milestones' => [
[
'title' => 'Moulds delivered and inspected',
'amount' => 150000000,
'required_evidence' => 'Inspection report',
],
],
]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);After it is activated and funded, an operator submits the evidence. The answer says two approvals are needed:
200 OK
{
"success": true,
"data": {
"milestone_id": "55555557-5555-4555-8555-555555555555",
"status": "evidence_submitted",
"approvals_required": 2,
"deal_status": "funded"
}
}3. The first approval
The approval detail shows the round:
curl https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555 \
-H "Authorization: Bearer $REKBERPAY_WORKSPACE_KEY"const res = await fetch('https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555', {
headers: {
Authorization: `Bearer ${process.env.REKBERPAY_WORKSPACE_KEY}`,
},
});
const data = await res.json();import os
import requests
resp = requests.get(
"https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555",
headers={
"Authorization": f"Bearer {os.environ['REKBERPAY_WORKSPACE_KEY']}",
},
timeout=30,
)
print(resp.status_code, resp.json())<?php
$ch = curl_init('https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('REKBERPAY_WORKSPACE_KEY'),
],
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);200 OK
{
"success": true,
"data": {
"item": {
"milestone_id": "55555557-5555-4555-8555-555555555555",
"deal_id": "44444447-4444-4444-8444-444444444444",
"deal_reference": "DEAL-2026-0011",
"deal_title": "Precision moulds, batch 7",
"milestone_title": "Moulds delivered and inspected",
"status": "evidence_submitted",
"amount": 150000000,
"currency": "IDR",
"counterparty_name": "Harbourline Components Pte Ltd",
"risk_tier": "standard",
"evidence_summary": "Moulds received and measured against drawing rev C.",
"evidence_submitted_at": "2026-10-13T14:33:16.594Z",
"due_at": null,
"approvals_required": 2,
"approvals_done": 0,
"my_decision": null,
"submitted_by": {
"id": "88888884-8888-4888-8888-888888888888",
"name": "Sari Wibowo"
}
},
"proposed": {
"amount": 150000000,
"currency": "IDR",
"destination": {
"label": "Harbourline IDR collection account",
"bank_name": "DBS Bank Indonesia",
"account_number_masked": "3320****43"
},
"deal_progress_pct": 0
},
"how_we_got_here": [
{
"seq": 140557,
"event": "deal.created",
"entity_type": "deal",
"entity_id": "44444447-4444-4444-8444-444444444444",
"actor": {
"id": "33333333-3333-4333-8333-333333333333",
"name": "Dimas Hidayat"
},
"amount": 150000000,
"summary": "Created deal DEAL-2026-0011: Precision moulds, batch 7",
"created_at": "2026-10-13T14:33:16.573Z"
},
{
"seq": 140558,
"event": "deal.activated",
"entity_type": "deal",
"entity_id": "44444447-4444-4444-8444-444444444444",
"actor": {
"id": "33333333-3333-4333-8333-333333333333",
"name": "Dimas Hidayat"
},
"amount": null,
"summary": "Activated deal DEAL-2026-0011",
"created_at": "2026-10-13T14:33:16.582Z"
}
],
"impact": {
"counterparty_paid": 150000000,
"money_moved": 150000000,
"milestones_remaining": 0,
"deal_progress_after_pct": 100
},
"decision": {
"risk_tier": "standard",
"approvals_required": 2,
"approvals": [
{
"id": "77777777-7777-4777-8777-777777777777",
"approver": null,
"status": "pending",
"note": null,
"decided_at": null
},
{
"id": "88888887-8888-4888-8888-888888888888",
"approver": null,
"status": "pending",
"note": null,
"decided_at": null
}
],
"reversibility": "A decision is final once saved. Release is a separate finance step.",
"expires_at": null
},
"evidence": []
}
}Lists are cut to the first 2 items and long text is shortened here.
The first approver decides. The milestone stays in evidence_submitted with approvals_done: 1:
curl -X POST https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555/decide \
-H "Authorization: Bearer $REKBERPAY_WORKSPACE_KEY" \
-H "Idempotency-Key: 99999997-9999-4999-8999-999999999999" \
-H "Content-Type: application/json" \
-d '{
"decision": "approved",
"note": "Measurements within tolerance."
}'const res = await fetch('https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555/decide', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.REKBERPAY_WORKSPACE_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': '99999997-9999-4999-8999-999999999999',
},
body: JSON.stringify({
decision: "approved",
note: "Measurements within tolerance."
}),
});
const data = await res.json();import os
import requests
resp = requests.post(
"https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555/decide",
headers={
"Authorization": f"Bearer {os.environ['REKBERPAY_WORKSPACE_KEY']}",
"Idempotency-Key": "99999997-9999-4999-8999-999999999999",
},
json={
"decision": "approved",
"note": "Measurements within tolerance.",
},
timeout=30,
)
print(resp.status_code, resp.json())<?php
$ch = curl_init('https://api.rekberpay.com/v1/enterprise/approvals/55555557-5555-4555-8555-555555555555/decide');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('REKBERPAY_WORKSPACE_KEY'),
'Content-Type: application/json',
'Idempotency-Key: 99999997-9999-4999-8999-999999999999',
],
CURLOPT_POSTFIELDS => json_encode([
'decision' => 'approved',
'note' => 'Measurements within tolerance.',
]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);200 OK
{
"success": true,
"data": {
"milestone_id": "55555557-5555-4555-8555-555555555555",
"approval_id": "77777777-7777-4777-8777-777777777777",
"decision": "approved",
"milestone_status": "evidence_submitted",
"approvals_required": 2,
"approvals_done": 1
}
}The same person cannot approve twice:
409 Conflict
{
"success": false,
"error": {
"code": "ALREADY_DECIDED",
"message": "You have already decided on this milestone."
}
}And nothing can be released yet:
409 Conflict
{
"success": false,
"error": {
"code": "NOT_APPROVED",
"message": "Only an approved milestone can be released."
}
}4. The second approval
A different finance member approves. The milestone becomes approved:
200 OK
{
"success": true,
"data": {
"milestone_id": "55555557-5555-4555-8555-555555555555",
"approval_id": "88888887-8888-4888-8888-888888888888",
"decision": "approved",
"milestone_status": "approved",
"approvals_required": 2,
"approvals_done": 2
}
}5. Release
curl -X POST https://api.rekberpay.com/v1/enterprise/deals/44444447-4444-4444-8444-444444444444/milestones/55555557-5555-4555-8555-555555555555/release \
-H "Authorization: Bearer $REKBERPAY_WORKSPACE_KEY" \
-H "Idempotency-Key: bbbbbbb7-bbbb-4bbb-8bbb-bbbbbbbbbbbb" \
-H "Content-Type: application/json" \
-d '{
"splits": [
{
"payout_instruction_id": "fffffff3-ffff-4fff-8fff-ffffffffffff",
"amount": 150000000
}
]
}'const res = await fetch('https://api.rekberpay.com/v1/enterprise/deals/44444447-4444-4444-8444-444444444444/milestones/55555557-5555-4555-8555-555555555555/release', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.REKBERPAY_WORKSPACE_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'bbbbbbb7-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
},
body: JSON.stringify({
splits: [
{
payout_instruction_id: "fffffff3-ffff-4fff-8fff-ffffffffffff",
amount: 150000000
}
]
}),
});
const data = await res.json();import os
import requests
resp = requests.post(
"https://api.rekberpay.com/v1/enterprise/deals/44444447-4444-4444-8444-444444444444/milestones/55555557-5555-4555-8555-555555555555/release",
headers={
"Authorization": f"Bearer {os.environ['REKBERPAY_WORKSPACE_KEY']}",
"Idempotency-Key": "bbbbbbb7-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
},
json={
"splits": [
{
"payout_instruction_id": "fffffff3-ffff-4fff-8fff-ffffffffffff",
"amount": 150000000,
},
],
},
timeout=30,
)
print(resp.status_code, resp.json())<?php
$ch = curl_init('https://api.rekberpay.com/v1/enterprise/deals/44444447-4444-4444-8444-444444444444/milestones/55555557-5555-4555-8555-555555555555/release');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('REKBERPAY_WORKSPACE_KEY'),
'Content-Type: application/json',
'Idempotency-Key: bbbbbbb7-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
],
CURLOPT_POSTFIELDS => json_encode([
'splits' => [
[
'payout_instruction_id' => 'fffffff3-ffff-4fff-8fff-ffffffffffff',
'amount' => 150000000,
],
],
]),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);200 OK
{
"success": true,
"data": {
"milestone_id": "55555557-5555-4555-8555-555555555555",
"status": "released",
"deal_status": "completed",
"disbursements": [
{
"id": "ccccccc7-cccc-4ccc-8ccc-cccccccccccc",
"payout_instruction_id": "fffffff3-ffff-4fff-8fff-ffffffffffff",
"amount": 150000000,
"currency": "IDR",
"status": "queued"
}
]
}
}The rules
| Rule | Error |
|---|---|
Only finance, admin and owner can decide | 403 FORBIDDEN |
| The evidence submitter cannot decide | 409 SUBMITTER_CANNOT_APPROVE |
| One decision per person per round | 409 ALREADY_DECIDED |
Release needs the milestone to be approved | 409 NOT_APPROVED |
| A rejection or revision closes the round; resubmitting evidence starts a new one |
A workspace API key decides as the admin who created it, so a key and that admin count as the same person. Give each approver their own login.