Enterprise quickstart
This guide walks one milestone through the whole enterprise flow: create a deal, activate it, fund it in the sandbox, submit evidence, collect two approvals, release to two bank accounts, follow the event feed and export the audit log. Every request was sent to a real API and every response below is the real answer. IDs are replaced with sample UUIDs and the names belong to the demo workspace.
You need a sandbox workspace. New workspaces start in the sandbox, and the sandbox is the only place where POST .../fund works. In a live workspace RekberPay confirms funding when the money arrives.
Before you start
Who calls what
The enterprise API acts as a signed-in user, and that user's workspace role decides what is allowed. The flow below needs three roles. Four-eyes control means the person who submits evidence can never approve it, and two different people must approve a large milestone.
| Step | Role needed | Key used below |
|---|---|---|
| Create and activate the deal, add documents, submit evidence | operator | $OPERATOR_KEY |
| Fund, approve, release | finance | $FINANCE_KEY |
| Second approval | admin or owner (any second approver with the finance role or higher) | $ADMIN_KEY |
Choose how to authenticate
There are three ways to call the enterprise API:
| Method | Header | Workspace |
|---|---|---|
| Workspace API key | Authorization: Bearer rpe_... | Fixed by the key. No X-Enterprise-Account needed |
| Personal API key | Authorization: Bearer <api-key> | X-Enterprise-Account, or the workspace you joined first |
| Session cookie | The cookie from POST /v1/auth/login, plus X-CSRF-Token on writes | X-Enterprise-Account, or the workspace you joined first |
A workspace API key is the usual choice for a server integration. An admin or owner creates it on the Developers page of the dashboard (or with POST /v1/enterprise/api-keys). It starts with rpe_, is shown once, and only works under /v1/enterprise:
curl https://api.rekberpay.com/v1/enterprise/deals \
-H "Authorization: Bearer rpe_..."A read key acts as a viewer. A read + write key acts with the role of the admin who created it, so one key is one person for four-eyes purposes: it cannot both submit evidence and approve it, or give both approvals. A key cannot change members, settings or webhooks and cannot call /me, invites, notifications or /api-keys. See Workspace API keys.
This guide uses personal API keys, one per role, because the flow needs three different people.
Authenticate with an API key
Each person creates their own key from their account with POST /v1/auth/me/api-key/rotate (see Authentication and API keys), then sends it on every call:
Authorization: Bearer <api-key>
X-Enterprise-Account: 11111111-1111-4111-8111-111111111111A key acts as the user who created it, so create a dedicated user for each integration and give it the smallest role that works. In a browser the same calls work with the session cookie plus an X-CSRF-Token header on writes. Bearer calls do not need the CSRF header.
X-Enterprise-Account is the workspace id. If you leave it out, the API uses the workspace you joined first. GET /v1/enterprise/me shows the id.
Setup
export BASE=https://api.rekberpay.com
export WORKSPACE_ID=11111111-1111-4111-8111-111111111111
export OPERATOR_KEY=... # operator user's key
export FINANCE_KEY=... # finance user's key
export ADMIN_KEY=... # admin user's key1. Check the workspace
curl "$BASE/v1/enterprise/me" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"{
"success": true,
"data": {
"account": {
"id": "ccccccc4-cccc-4ccc-8ccc-cccccccccccc",
"display_name": "Northline Trading",
"legal_name": "PT Northline Trading Indonesia",
"country_code": "ID",
"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]"
}
}
},
"membership": {
"id": "ddddddd4-dddd-4ddd-8ddd-dddddddddddd",
"role": "finance",
"joined_at": "2026-09-29T04:01:15.108Z"
},
"memberships": [
{
"account_id": "ccccccc4-cccc-4ccc-8ccc-cccccccccccc",
"display_name": "Northline Trading",
"role": "finance"
}
],
"invites": []
}
}is_sandbox: true means funding works. four_eyes_threshold is the amount at which a milestone needs two approvals. Here it is IDR 100,000,000, so the IDR 150,000,000 milestone below needs two.
2. Pick the counterparty and its payout instructions
A deal needs a verified counterparty, and a release can only pay a verified payout instruction that belongs to that counterparty. Look them up:
curl "$BASE/v1/enterprise/counterparties?q=Harbourline&kyb_status=verified" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"{
"success": true,
"data": [
{
"id": "eeeeeee6-eeee-4eee-8eee-eeeeeeeeeeee",
"name": "Harbourline Components Pte Ltd",
"country_code": "SG",
"contact_email": "[email protected]",
"contact_name": "Daniel Tan",
"relationship": "seller",
"kyb_status": "verified",
"created_at": "2026-09-29T10:01:15.108Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1
}
}Use the id as counterparty_id in the next steps. To add a new counterparty, POST /v1/enterprise/counterparties as an operator. Its KYB status starts at not_started and is set by RekberPay, not by you.
curl "$BASE/v1/enterprise/payout-instructions?counterparty_id=22222221-2222-4222-8222-222222222222&status=verified" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"{
"success": true,
"data": [
{
"id": "ddddddd6-dddd-4ddd-8ddd-dddddddddddd",
"label": "Harbourline IDR collection account",
"counterparty": {
"id": "eeeeeee6-eeee-4eee-8eee-eeeeeeeeeeee",
"name": "Harbourline Components Pte Ltd"
},
"currency": "IDR",
"bank_name": "DBS Bank Indonesia",
"account_number_masked": "3320****43",
"account_holder": "Harbourline Components Pte Ltd",
"country_code": "ID",
"status": "verified",
"signed": true,
"verified_at": "2026-09-29T20:31:15.108Z",
"created_at": "2026-09-29T16:31:15.108Z",
"updated_at": "2026-10-13T03:31:15.210Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1
}
}Account numbers are never returned in full. A new instruction (POST /payout-instructions, finance role) starts as pending_review and cannot receive money until RekberPay operations verifies it. This walkthrough uses two verified instructions on the same counterparty, because step 8 splits the payout across both. If you only have one, send a single split.
3. Note where the event feed is
Read the newest event once, and keep its next_after as your cursor. Everything you do from here shows up after it.
curl "$BASE/v1/enterprise/events?order=desc&limit=1" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"{
"success": true,
"data": {
"items": [
{
"seq": 135668,
"id": "00000007-0000-4000-8000-000000000000",
"event": "milestone.approved",
"entity_type": "milestone",
"entity_id": "aaaaaaa7-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"deal_id": "bbbbbbb7-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"deal_reference": "DEAL-2026-0003",
"actor": {
"id": "77777772-7777-4777-8777-777777777777",
"name": "Maya Santoso"
},
"actor_label": null,
"amount": 150000000,
"summary": "Approved DEAL-2026-0003 / Goods in transit for release",
"created_at": "2026-10-13T03:22:12.962Z",
"livemode": false
}
],
"next_before": 135668,
"next_after": 135668
}
}4. Create the deal
Send an Idempotency-Key (a UUID v4) so a retry cannot create a second deal. A deal holds 1 to 20 milestones and starts as a draft.
curl -X POST "$BASE/v1/enterprise/deals" \
-H "Authorization: Bearer $OPERATOR_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Idempotency-Key: 66666665-6666-4666-8666-666666666666" \
-H "Content-Type: application/json" \
-d '{
"title": "Electronic control boards, lot 8",
"external_ref": "PO-2026-0188",
"counterparty_id": "22222221-2222-4222-8222-222222222222",
"currency": "IDR",
"cross_border": true,
"milestones": [
{ "title": "PO signed and registered", "amount": 50000000,
"required_evidence": "Countersigned PO", "due_at": "2026-10-20T10:00:00+07:00" },
{ "title": "Goods in transit", "amount": 150000000,
"required_evidence": "Bill of lading and export declaration" },
{ "title": "Final delivery", "amount": 60000000 }
]
}'{
"success": true,
"data": {
"deal": {
"id": "88888885-8888-4888-8888-888888888888",
"reference": "DEAL-2026-0009",
"title": "Electronic control boards, lot 8",
"description": null,
"external_ref": "PO-2026-0188",
"status": "draft",
"currency": "IDR",
"total_amount": 260000000,
"funded_amount": 0,
"released_amount": 0,
"cross_border": true,
"owner": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"created_at": "2026-10-13T03:23:04.417Z",
"updated_at": "2026-10-13T03:23:04.417Z",
"counterparty": {
"id": "00000005-0000-4000-8000-000000000000",
"name": "Harbourline Components Pte Ltd",
"country_code": "SG",
"kyb_status": "verified",
"relationship": "seller"
}
},
"milestones": [
{
"id": "aaaaaaa5-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"sequence_no": 1,
"title": "PO signed and registered",
"required_evidence": "Countersigned PO",
"amount": 50000000,
"status": "draft",
"due_at": "2026-10-20T03:00:00.000Z",
"funded_at": null,
"funding_method": null,
"evidence_summary": null,
"evidence_submitted_at": null,
"approved_at": null,
"released_at": null,
"disputed_at": null,
"dispute_reason": null,
"approvals_required": 1,
"approvals": [],
"documents": [],
"disbursements": []
},
{
"id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"sequence_no": 2,
"title": "Goods in transit",
"required_evidence": "Bill of lading and export declaration",
"amount": 150000000,
"status": "draft",
"due_at": null,
"funded_at": null,
"funding_method": null,
"evidence_summary": null,
"evidence_submitted_at": null,
"approved_at": null,
"released_at": null,
"disputed_at": null,
"dispute_reason": null,
"approvals_required": 2,
"approvals": [],
"documents": [],
"disbursements": []
},
{
"id": "ccccccc5-cccc-4ccc-8ccc-cccccccccccc",
"sequence_no": 3,
"title": "Final delivery",
"required_evidence": null,
"amount": 60000000,
"status": "draft",
"due_at": null,
"funded_at": null,
"funding_method": null,
"evidence_summary": null,
"evidence_submitted_at": null,
"approved_at": null,
"released_at": null,
"disputed_at": null,
"dispute_reason": null,
"approvals_required": 1,
"approvals": [],
"documents": [],
"disbursements": []
}
],
"events": [
{
"seq": 135673,
"event": "deal.created",
"entity_type": "deal",
"entity_id": "88888885-8888-4888-8888-888888888888",
"actor": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"amount": 260000000,
"summary": "Created deal DEAL-2026-0009: Electronic control boards, lot 8",
"created_at": "2026-10-13T03:23:04.418Z"
}
],
"outcome": {
"time_to_release_minutes": null,
"approvals_by_people": 0,
"revisions": 0
}
}
}The deal's total_amount, funded_amount, released_amount and status are computed from the milestones. Note approvals_required: 2 for the IDR 150,000,000 milestone, 1 for the other two.
5. Activate it
curl -X POST "$BASE/v1/enterprise/deals/33333331-3333-4333-8333-333333333333/activate" \
-H "Authorization: Bearer $OPERATOR_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Idempotency-Key: ccccccc7-cccc-4ccc-8ccc-cccccccccccc" \
-H "Content-Type: application/json" \
-d '{}'The answer is the full deal again, now awaiting_funding. The first response is cut to the deal summary here:
{
"success": true,
"data": {
"deal": {
"id": "88888885-8888-4888-8888-888888888888",
"reference": "DEAL-2026-0009",
"title": "Electronic control boards, lot 8",
"description": null,
"external_ref": "PO-2026-0188",
"status": "awaiting_funding",
"currency": "IDR",
"total_amount": 260000000,
"funded_amount": 0,
"released_amount": 0,
"cross_border": true,
"owner": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"created_at": "2026-10-13T03:23:04.417Z",
"updated_at": "2026-10-13T03:23:04.509Z",
"counterparty": {
"id": "00000005-0000-4000-8000-000000000000",
"name": "Harbourline Components Pte Ltd",
"country_code": "SG",
"kyb_status": "verified",
"relationship": "seller"
}
}
}
}6. Fund the milestone (sandbox)
curl -X POST "$BASE/v1/enterprise/deals/33333331-3333-4333-8333-333333333333/milestones/55555551-5555-4555-8555-555555555555/fund" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Idempotency-Key: ddddddd7-dddd-4ddd-8ddd-dddddddddddd" \
-H "Content-Type: application/json" \
-d '{ "method": "sandbox", "reference": "TEST-001" }'{
"success": true,
"data": {
"milestone": {
"id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"deal_id": "88888885-8888-4888-8888-888888888888",
"status": "funded",
"amount": 150000000,
"funding_method": "sandbox"
},
"deal_status": "funded"
}
}7. Submit evidence
Register the file first. Documents are metadata: the file itself lives at an https URL.
curl -X POST "$BASE/v1/enterprise/documents" \
-H "Authorization: Bearer $OPERATOR_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"title": "Bill of lading HLCU-5521",
"document_type": "shipping",
"file_url": "https://files.northline.example/bl/HLCU-5521.pdf",
"checksum_sha256": "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
"deal_id": "33333331-3333-4333-8333-333333333333",
"milestone_id": "55555551-5555-4555-8555-555555555555"
}'{
"success": true,
"data": {
"id": "eeeeeee7-eeee-4eee-8eee-eeeeeeeeeeee",
"deal_id": "88888885-8888-4888-8888-888888888888",
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"document_type": "shipping",
"title": "Bill of lading HLCU-5521",
"file_url": "https://files.northline.example/bl/HLCU-5521.pdf",
"checksum_sha256": "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
"visibility": "account",
"uploaded_by": "99999995-9999-4999-8999-999999999999"
}
}Then submit it with a short summary:
curl -X POST "$BASE/v1/enterprise/deals/33333331-3333-4333-8333-333333333333/milestones/55555551-5555-4555-8555-555555555555/submit-evidence" \
-H "Authorization: Bearer $OPERATOR_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Idempotency-Key: ddddddd5-dddd-4ddd-8ddd-dddddddddddd" \
-H "Content-Type: application/json" \
-d '{
"evidence_summary": "Bill of lading HLCU-5521 and the export declaration for all 600 boards.",
"document_ids": ["99999991-9999-4999-8999-999999999999"]
}'{
"success": true,
"data": {
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"status": "evidence_submitted",
"approvals_required": 2,
"deal_status": "funded"
}
}The milestone is now evidence_submitted and waiting for two approvals. deal_status is still funded because nothing has been released yet.
8. Decide: two approvers
A finance member can read everything needed to decide in one call:
curl "$BASE/v1/enterprise/approvals/55555551-5555-4555-8555-555555555555" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"The answer has item, proposed (amount and the masked destination), how_we_got_here, impact, decision and evidence. It is cut to the first two how_we_got_here events here:
{
"success": true,
"data": {
"item": {
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"deal_id": "88888885-8888-4888-8888-888888888888",
"deal_reference": "DEAL-2026-0009",
"deal_title": "Electronic control boards, lot 8",
"milestone_title": "Goods in transit",
"status": "evidence_submitted",
"amount": 150000000,
"currency": "IDR",
"counterparty_name": "Harbourline Components Pte Ltd",
"risk_tier": "standard",
"evidence_summary": "Bill of lading HLCU-5521 and the export declaration for all 600 boards.",
"evidence_submitted_at": "2026-10-13T03:23:04.614Z",
"due_at": null,
"approvals_required": 2,
"approvals_done": 0,
"my_decision": null,
"submitted_by": {
"id": "99999995-9999-4999-8999-999999999999",
"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": 135673,
"event": "deal.created",
"entity_type": "deal",
"entity_id": "88888885-8888-4888-8888-888888888888",
"actor": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"amount": 260000000,
"summary": "Created deal DEAL-2026-0009: Electronic control boards, lot 8",
"created_at": "2026-10-13T03:23:04.418Z"
},
{
"seq": 135674,
"event": "deal.activated",
"entity_type": "deal",
"entity_id": "88888885-8888-4888-8888-888888888888",
"actor": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"amount": null,
"summary": "Activated deal DEAL-2026-0009",
"created_at": "2026-10-13T03:23:04.510Z"
}
],
"impact": {
"counterparty_paid": 150000000,
"money_moved": 150000000,
"milestones_remaining": 2,
"deal_progress_after_pct": 58
},
"decision": {
"risk_tier": "standard",
"approvals_required": 2,
"approvals": [
{
"id": "55555556-5555-4555-8555-555555555555",
"approver": null,
"status": "pending",
"note": null,
"decided_at": null
},
{
"id": "66666666-6666-4666-8666-666666666666",
"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": [
{
"id": "eeeeeee7-eeee-4eee-8eee-eeeeeeeeeeee",
"title": "Bill of lading HLCU-5521",
"document_type": "shipping",
"file_url": "https://files.northline.example/bl/HLCU-5521.pdf",
"checksum_sha256": "2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881",
"visibility": "account",
"uploaded_by": "99999995-9999-4999-8999-999999999999",
"created_at": "2026-10-13T03:23:04.563Z"
}
]
}
}Now the first approver decides:
curl -X POST "$BASE/v1/enterprise/approvals/55555551-5555-4555-8555-555555555555/decide" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Idempotency-Key: 44444446-4444-4444-8444-444444444444" \
-H "Content-Type: application/json" \
-d '{ "decision": "approved" }'{
"success": true,
"data": {
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"approval_id": "55555556-5555-4555-8555-555555555555",
"decision": "approved",
"milestone_status": "evidence_submitted",
"approvals_required": 2,
"approvals_done": 1
}
}One approval is not enough: the milestone stays evidence_submitted and approvals_done is 1 of 2. A second, different person decides:
curl -X POST "$BASE/v1/enterprise/approvals/55555551-5555-4555-8555-555555555555/decide" \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Idempotency-Key: fffffff7-ffff-4fff-8fff-ffffffffffff" \
-H "Content-Type: application/json" \
-d '{ "decision": "approved" }'{
"success": true,
"data": {
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"approval_id": "66666666-6666-4666-8666-666666666666",
"decision": "approved",
"milestone_status": "approved",
"approvals_required": 2,
"approvals_done": 2
}
}The milestone is approved. A decision is final once saved. The person who submitted the evidence cannot decide (409 SUBMITTER_CANNOT_APPROVE for finance and above, 403 FORBIDDEN for an operator), and the same person cannot decide twice (409 ALREADY_DECIDED).
9. Release with splits
Release pays the milestone to up to five verified payout instructions. The amounts must add up to the milestone amount, exactly.
curl -X POST "$BASE/v1/enterprise/deals/33333331-3333-4333-8333-333333333333/milestones/55555551-5555-4555-8555-555555555555/release" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID" \
-H "Idempotency-Key: 77777776-7777-4777-8777-777777777777" \
-H "Content-Type: application/json" \
-d '{
"splits": [
{ "payout_instruction_id": "77777771-7777-4777-8777-777777777777", "amount": 100000000 },
{ "payout_instruction_id": "88888881-8888-4888-8888-888888888888", "amount": 50000000 }
]
}'{
"success": true,
"data": {
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"status": "released",
"deal_status": "in_progress",
"disbursements": [
{
"id": "00000006-0000-4000-8000-000000000000",
"payout_instruction_id": "aaaaaaa6-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"amount": 100000000,
"currency": "IDR",
"status": "queued"
},
{
"id": "bbbbbbb6-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"payout_instruction_id": "ccccccc6-cccc-4ccc-8ccc-cccccccccccc",
"amount": 50000000,
"currency": "IDR",
"status": "queued"
}
]
}
}Release is final. Each split is a queued disbursement that RekberPay settles. Splits that do not add up are refused and nothing is released:
{
"success": false,
"error": {
"code": "SPLITS_MISMATCH",
"message": "Splits must add up to the milestone amount."
}
}10. Poll the event feed
Pass your cursor as after. deal_id narrows the feed to this deal.
curl "$BASE/v1/enterprise/events?after=<cursor from step 3>&deal_id=33333331-3333-4333-8333-333333333333&limit=100" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"The page below held 12 events, from deal.created to the last disbursement.queued. It is cut to the first 2 and the last event here, and next_after is the highest seq of the full page. Store it and send it as after on the next poll. When a page comes back with limit items, poll again straight away.
{
"success": true,
"data": {
"items": [
{
"seq": 135673,
"id": "88888887-8888-4888-8888-888888888888",
"event": "deal.created",
"entity_type": "deal",
"entity_id": "88888885-8888-4888-8888-888888888888",
"deal_id": "88888885-8888-4888-8888-888888888888",
"deal_reference": "DEAL-2026-0009",
"actor": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"actor_label": null,
"amount": 260000000,
"summary": "Created deal DEAL-2026-0009: Electronic control boards, lot 8",
"created_at": "2026-10-13T03:23:04.418Z",
"livemode": false
},
{
"seq": 135674,
"id": "99999997-9999-4999-8999-999999999999",
"event": "deal.activated",
"entity_type": "deal",
"entity_id": "88888885-8888-4888-8888-888888888888",
"deal_id": "88888885-8888-4888-8888-888888888888",
"deal_reference": "DEAL-2026-0009",
"actor": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"actor_label": null,
"amount": null,
"summary": "Activated deal DEAL-2026-0009",
"created_at": "2026-10-13T03:23:04.510Z",
"livemode": false
}
],
"next_after": 135684
}
}RekberPay does not push enterprise events to your server. Poll every 15 to 60 seconds. Each event has a seq that only goes up, so you never miss one and can safely re-read from the last seq you processed.
11. Check the result
curl "$BASE/v1/enterprise/deals/33333331-3333-4333-8333-333333333333" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"The deal is in_progress (one of three milestones released), the released milestone shows both disbursements, and outcome summarises the run. Cut to the deal and outcome:
{
"success": true,
"data": {
"deal": {
"id": "88888885-8888-4888-8888-888888888888",
"reference": "DEAL-2026-0009",
"title": "Electronic control boards, lot 8",
"description": null,
"external_ref": "PO-2026-0188",
"status": "in_progress",
"currency": "IDR",
"total_amount": 260000000,
"funded_amount": 150000000,
"released_amount": 150000000,
"cross_border": true,
"owner": {
"id": "99999995-9999-4999-8999-999999999999",
"name": "Sari Wibowo"
},
"created_at": "2026-10-13T03:23:04.417Z",
"updated_at": "2026-10-13T03:23:04.739Z",
"counterparty": {
"id": "00000005-0000-4000-8000-000000000000",
"name": "Harbourline Components Pte Ltd",
"country_code": "SG",
"kyb_status": "verified",
"relationship": "seller"
}
},
"outcome": {
"time_to_release_minutes": 0,
"approvals_by_people": 2,
"revisions": 0
}
}
}Payouts you queued, newest first:
curl "$BASE/v1/enterprise/payouts?status=queued&limit=2" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"{
"success": true,
"data": {
"items": [
{
"id": "bbbbbbb6-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"deal_id": "88888885-8888-4888-8888-888888888888",
"deal_reference": "DEAL-2026-0009",
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"milestone_title": "Goods in transit",
"payout_instruction_id": "ccccccc6-cccc-4ccc-8ccc-cccccccccccc",
"instruction_label": "Harbourline freight sub-account",
"bank_name": "DBS Bank Indonesia",
"account_number_masked": "3320****99",
"amount": 50000000,
"currency": "IDR",
"status": "queued",
"failure_reason": null,
"settled_at": null,
"created_at": "2026-10-13T03:23:04.739Z",
"updated_at": "2026-10-13T03:23:04.739Z"
},
{
"id": "00000006-0000-4000-8000-000000000000",
"deal_id": "88888885-8888-4888-8888-888888888888",
"deal_reference": "DEAL-2026-0009",
"milestone_id": "bbbbbbb5-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"milestone_title": "Goods in transit",
"payout_instruction_id": "aaaaaaa6-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"instruction_label": "Harbourline IDR collection account",
"bank_name": "DBS Bank Indonesia",
"account_number_masked": "3320****43",
"amount": 100000000,
"currency": "IDR",
"status": "queued",
"failure_reason": null,
"settled_at": null,
"created_at": "2026-10-13T03:23:04.739Z",
"updated_at": "2026-10-13T03:23:04.739Z"
}
],
"summary": {
"today_count": 4,
"today_amount": 253000000,
"avg_settlement_minutes": 32,
"queued_amount": 178000000
}
},
"pagination": {
"page": 1,
"limit": 2,
"total": 3,
"total_pages": 2
}
}And the books balance:
curl "$BASE/v1/enterprise/reconciliation" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"{
"success": true,
"data": {
"deals": [
{
"id": "88888885-8888-4888-8888-888888888888",
"reference": "DEAL-2026-0009",
"stored_funded": 150000000,
"computed_funded": 150000000,
"stored_released": 150000000,
"computed_released": 150000000,
"balanced": true
}
],
"balanced_all": true,
"truncated": false,
"checked_at": "2026-10-13T03:23:04.848Z"
}
}12. Export the audit log
The CSV export needs the finance role or above. Dates are YYYY-MM-DD and the default window is the last 90 days. The ip column only appears for admins and owners; a finance caller gets the same file without it.
curl -o audit.csv \
"$BASE/v1/enterprise/audit/export.csv?from=2026-10-13&to=2026-10-13" \
-H "Authorization: Bearer $FINANCE_KEY" \
-H "X-Enterprise-Account: $WORKSPACE_ID"The response is text/csv with a Content-Disposition filename. These are the header and the three rows for the release:
seq,created_at,event,actor,deal,amount,summary,previous_state,new_state
135682,2026-10-13T03:23:04.739Z,milestone.released,Maya Santoso,DEAL-2026-0009,150000000.00,Released DEAL-2026-0009 / Goods in transit in 2 payouts,"{""status"":""approved""}","{""status"":""released"",""splits"":[{""disbursement_id"":""00000001-0000-4000-8000-000000000000"",""payout_instruction_id"":""77777771-7777-4777-8777-777777777777"",""amount"":100000000},{""disbursement_id"":""aaaaaaa1-aaaa-4aaa-8aaa-aaaaaaaaaaaa"",""payout_instruction_id"":""88888881-8888-4888-8888-888888888888"",""amount"":50000000}]}"
135683,2026-10-13T03:23:04.740Z,disbursement.queued,Maya Santoso,DEAL-2026-0009,100000000.00,Payout queued for DEAL-2026-0009 / Goods in transit,,"{""status"":""queued"",""payout_instruction_id"":""77777771-7777-4777-8777-777777777777""}"
135684,2026-10-13T03:23:04.740Z,disbursement.queued,Maya Santoso,DEAL-2026-0009,50000000.00,Payout queued for DEAL-2026-0009 / Goods in transit,,"{""status"":""queued"",""payout_instruction_id"":""88888881-8888-4888-8888-888888888888""}"One export holds at most 10,000 rows. When there were more, the response carries X-Export-Truncated: true; export again with a narrower date range.
The whole flow in one script
The same steps as a script. It reads the three keys and the workspace id from the environment, and reads the base URL from REKBERPAY_BASE_URL (default https://api.rekberpay.com). Run it only against a sandbox workspace: it creates a real deal each time.
import { randomUUID } from 'node:crypto';
const BASE = process.env.REKBERPAY_BASE_URL ?? 'https://api.rekberpay.com';
const WORKSPACE_ID = process.env.WORKSPACE_ID;
const KEYS = {
operator: process.env.OPERATOR_KEY,
finance: process.env.FINANCE_KEY,
admin: process.env.ADMIN_KEY,
};
async function api(role, method, path, body, { idempotent = false } = {}) {
const headers = {
Authorization: `Bearer ${KEYS[role]}`,
'X-Enterprise-Account': WORKSPACE_ID,
};
if (method !== 'GET') headers['Content-Type'] = 'application/json';
if (idempotent) headers['Idempotency-Key'] = randomUUID();
const res = await fetch(`${BASE}/v1/enterprise${path}`, {
method,
headers,
body: method === 'GET' ? undefined : JSON.stringify(body ?? {}),
});
const json = await res.json();
if (!res.ok) throw new Error(`${method} ${path} failed: ${res.status} ${json.error?.code}`);
return json.data;
}
// 1. Find the counterparty and two verified payout instructions for it.
const [counterparty] = await api('finance', 'GET', '/counterparties?q=Harbourline&kyb_status=verified');
const instructions = await api(
'finance',
'GET',
`/payout-instructions?counterparty_id=${counterparty.id}&status=verified`,
);
// 2. Remember where the event feed is now, so we only read what happens next.
const { next_after: cursor } = await api('finance', 'GET', '/events?order=desc&limit=1');
// 3. Create and activate a deal (operator).
const created = await api(
'operator',
'POST',
'/deals',
{
title: 'Electronic control boards, lot 8',
external_ref: 'PO-2026-0188',
counterparty_id: counterparty.id,
currency: 'IDR',
cross_border: true,
milestones: [
{ title: 'PO signed and registered', amount: 50000000, required_evidence: 'Countersigned PO' },
{ title: 'Goods in transit', amount: 150000000, required_evidence: 'Bill of lading and export declaration' },
{ title: 'Final delivery', amount: 60000000 },
],
},
{ idempotent: true },
);
const dealId = created.deal.id;
const milestoneId = created.milestones[1].id;
await api('operator', 'POST', `/deals/${dealId}/activate`, {}, { idempotent: true });
// 4. Fund the milestone (finance, sandbox workspaces only).
await api(
'finance',
'POST',
`/deals/${dealId}/milestones/${milestoneId}/fund`,
{ method: 'sandbox', reference: 'TEST-001' },
{ idempotent: true },
);
// 5. Register the evidence file and submit it (operator).
const doc = await api('operator', 'POST', '/documents', {
title: 'Bill of lading HLCU-5521',
document_type: 'shipping',
file_url: 'https://files.northline.example/bl/HLCU-5521.pdf',
deal_id: dealId,
milestone_id: milestoneId,
});
await api(
'operator',
'POST',
`/deals/${dealId}/milestones/${milestoneId}/submit-evidence`,
{
evidence_summary: 'Bill of lading HLCU-5521 and the export declaration for all 600 boards.',
document_ids: [doc.id],
},
{ idempotent: true },
);
// 6. Two different approvers decide (the amount is at or above the four-eyes threshold).
for (const role of ['finance', 'admin']) {
const decision = await api(
role,
'POST',
`/approvals/${milestoneId}/decide`,
{ decision: 'approved' },
{ idempotent: true },
);
console.log(role, 'approved:', decision.approvals_done, 'of', decision.approvals_required);
}
// 7. Release to two payout instructions. The splits must add up to the milestone amount.
const released = await api(
'finance',
'POST',
`/deals/${dealId}/milestones/${milestoneId}/release`,
{
splits: [
{ payout_instruction_id: instructions[0].id, amount: 100000000 },
{ payout_instruction_id: instructions[1].id, amount: 50000000 },
],
},
{ idempotent: true },
);
console.log('released:', released.status, released.disbursements.length, 'payouts');
// 8. Read the feed from the cursor.
const feed = await api('finance', 'GET', `/events?after=${cursor}&deal_id=${dealId}&limit=100`);
for (const event of feed.items) console.log(event.seq, event.event);
console.log('next cursor:', feed.next_after);import os
import uuid
import requests
BASE = os.environ.get("REKBERPAY_BASE_URL", "https://api.rekberpay.com")
WORKSPACE_ID = os.environ["WORKSPACE_ID"]
KEYS = {
"operator": os.environ["OPERATOR_KEY"],
"finance": os.environ["FINANCE_KEY"],
"admin": os.environ["ADMIN_KEY"],
}
def api(role, method, path, body=None, idempotent=False):
headers = {
"Authorization": f"Bearer {KEYS[role]}",
"X-Enterprise-Account": WORKSPACE_ID,
}
if idempotent:
headers["Idempotency-Key"] = str(uuid.uuid4())
res = requests.request(
method,
f"{BASE}/v1/enterprise{path}",
headers=headers,
json=None if method == "GET" else (body or {}),
timeout=30,
)
payload = res.json()
if not res.ok:
raise RuntimeError(f"{method} {path} failed: {res.status_code} {payload['error']['code']}")
return payload["data"]
# 1. Find the counterparty and two verified payout instructions for it.
counterparty = api("finance", "GET", "/counterparties?q=Harbourline&kyb_status=verified")[0]
instructions = api(
"finance", "GET", f"/payout-instructions?counterparty_id={counterparty['id']}&status=verified"
)
# 2. Remember where the event feed is now, so we only read what happens next.
cursor = api("finance", "GET", "/events?order=desc&limit=1")["next_after"]
# 3. Create and activate a deal (operator).
created = api(
"operator",
"POST",
"/deals",
{
"title": "Electronic control boards, lot 8",
"external_ref": "PO-2026-0188",
"counterparty_id": counterparty["id"],
"currency": "IDR",
"cross_border": True,
"milestones": [
{"title": "PO signed and registered", "amount": 50000000, "required_evidence": "Countersigned PO"},
{"title": "Goods in transit", "amount": 150000000, "required_evidence": "Bill of lading and export declaration"},
{"title": "Final delivery", "amount": 60000000},
],
},
idempotent=True,
)
deal_id = created["deal"]["id"]
milestone_id = created["milestones"][1]["id"]
api("operator", "POST", f"/deals/{deal_id}/activate", idempotent=True)
# 4. Fund the milestone (finance, sandbox workspaces only).
api(
"finance",
"POST",
f"/deals/{deal_id}/milestones/{milestone_id}/fund",
{"method": "sandbox", "reference": "TEST-001"},
idempotent=True,
)
# 5. Register the evidence file and submit it (operator).
doc = api(
"operator",
"POST",
"/documents",
{
"title": "Bill of lading HLCU-5521",
"document_type": "shipping",
"file_url": "https://files.northline.example/bl/HLCU-5521.pdf",
"deal_id": deal_id,
"milestone_id": milestone_id,
},
)
api(
"operator",
"POST",
f"/deals/{deal_id}/milestones/{milestone_id}/submit-evidence",
{
"evidence_summary": "Bill of lading HLCU-5521 and the export declaration for all 600 boards.",
"document_ids": [doc["id"]],
},
idempotent=True,
)
# 6. Two different approvers decide (the amount is at or above the four-eyes threshold).
for role in ("finance", "admin"):
decision = api(role, "POST", f"/approvals/{milestone_id}/decide", {"decision": "approved"}, idempotent=True)
print(role, "approved:", decision["approvals_done"], "of", decision["approvals_required"])
# 7. Release to two payout instructions. The splits must add up to the milestone amount.
released = api(
"finance",
"POST",
f"/deals/{deal_id}/milestones/{milestone_id}/release",
{
"splits": [
{"payout_instruction_id": instructions[0]["id"], "amount": 100000000},
{"payout_instruction_id": instructions[1]["id"], "amount": 50000000},
]
},
idempotent=True,
)
print("released:", released["status"], len(released["disbursements"]), "payouts")
# 8. Read the feed from the cursor.
feed = api("finance", "GET", f"/events?after={cursor}&deal_id={deal_id}&limit=100")
for event in feed["items"]:
print(event["seq"], event["event"])
print("next cursor:", feed["next_after"])<?php
$base = getenv('REKBERPAY_BASE_URL') ?: 'https://api.rekberpay.com';
$workspaceId = getenv('WORKSPACE_ID');
$keys = [
'operator' => getenv('OPERATOR_KEY'),
'finance' => getenv('FINANCE_KEY'),
'admin' => getenv('ADMIN_KEY'),
];
function uuid_v4(): string {
$b = random_bytes(16);
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
function api(string $role, string $method, string $path, ?array $body = null, bool $idempotent = false): array {
global $base, $workspaceId, $keys;
$headers = [
'Authorization: Bearer ' . $keys[$role],
'X-Enterprise-Account: ' . $workspaceId,
];
if ($method !== 'GET') {
$headers[] = 'Content-Type: application/json';
}
if ($idempotent) {
$headers[] = 'Idempotency-Key: ' . uuid_v4();
}
$ch = curl_init($base . '/v1/enterprise' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
if ($method !== 'GET') {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body ?? new stdClass()));
}
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$payload = json_decode($raw, true);
if ($status >= 400) {
throw new RuntimeException("$method $path failed: $status " . ($payload['error']['code'] ?? ''));
}
return $payload['data'];
}
// 1. Find the counterparty and two verified payout instructions for it.
$counterparty = api('finance', 'GET', '/counterparties?q=Harbourline&kyb_status=verified')[0];
$instructions = api('finance', 'GET', '/payout-instructions?counterparty_id=' . $counterparty['id'] . '&status=verified');
// 2. Remember where the event feed is now, so we only read what happens next.
$cursor = api('finance', 'GET', '/events?order=desc&limit=1')['next_after'];
// 3. Create and activate a deal (operator).
$created = api('operator', 'POST', '/deals', [
'title' => 'Electronic control boards, lot 8',
'external_ref' => 'PO-2026-0188',
'counterparty_id' => $counterparty['id'],
'currency' => 'IDR',
'cross_border' => true,
'milestones' => [
['title' => 'PO signed and registered', 'amount' => 50000000, 'required_evidence' => 'Countersigned PO'],
['title' => 'Goods in transit', 'amount' => 150000000, 'required_evidence' => 'Bill of lading and export declaration'],
['title' => 'Final delivery', 'amount' => 60000000],
],
], true);
$dealId = $created['deal']['id'];
$milestoneId = $created['milestones'][1]['id'];
api('operator', 'POST', "/deals/$dealId/activate", null, true);
// 4. Fund the milestone (finance, sandbox workspaces only).
api('finance', 'POST', "/deals/$dealId/milestones/$milestoneId/fund", [
'method' => 'sandbox',
'reference' => 'TEST-001',
], true);
// 5. Register the evidence file and submit it (operator).
$doc = api('operator', 'POST', '/documents', [
'title' => 'Bill of lading HLCU-5521',
'document_type' => 'shipping',
'file_url' => 'https://files.northline.example/bl/HLCU-5521.pdf',
'deal_id' => $dealId,
'milestone_id' => $milestoneId,
]);
api('operator', 'POST', "/deals/$dealId/milestones/$milestoneId/submit-evidence", [
'evidence_summary' => 'Bill of lading HLCU-5521 and the export declaration for all 600 boards.',
'document_ids' => [$doc['id']],
], true);
// 6. Two different approvers decide (the amount is at or above the four-eyes threshold).
foreach (['finance', 'admin'] as $role) {
$decision = api($role, 'POST', "/approvals/$milestoneId/decide", ['decision' => 'approved'], true);
echo "$role approved: {$decision['approvals_done']} of {$decision['approvals_required']}\n";
}
// 7. Release to two payout instructions. The splits must add up to the milestone amount.
$released = api('finance', 'POST', "/deals/$dealId/milestones/$milestoneId/release", [
'splits' => [
['payout_instruction_id' => $instructions[0]['id'], 'amount' => 100000000],
['payout_instruction_id' => $instructions[1]['id'], 'amount' => 50000000],
],
], true);
echo "released: {$released['status']} " . count($released['disbursements']) . " payouts\n";
// 8. Read the feed from the cursor.
$feed = api('finance', 'GET', "/events?after=$cursor&deal_id=$dealId&limit=100");
foreach ($feed['items'] as $event) {
echo $event['seq'] . ' ' . $event['event'] . "\n";
}
echo 'next cursor: ' . $feed['next_after'] . "\n";The first two payout instructions it finds for Harbourline must be verified. A successful run prints something like this (sequence numbers differ in your workspace):
finance approved: 1 of 2
admin approved: 2 of 2
released: released 2 payouts
135698 deal.created
135699 deal.activated
135700 milestone.funded
135701 document.added
135702 milestone.evidence_submitted
135703 approval.requested
135704 approval.decided
135705 approval.decided
135706 milestone.approved
135707 milestone.released
135708 disbursement.queued
135709 disbursement.queued
next cursor: 135709When something goes wrong
Every error uses the same envelope. These are the ones you are most likely to hit in this flow:
| Status | Code | What to do |
|---|---|---|
400 | INVALID_BODY | A field is missing or out of range. error.details.fieldErrors names it |
400 | SPLITS_MISMATCH | Make the split amounts add up to the milestone amount |
403 | FORBIDDEN | The key's user has too low a role for this step |
403 | NO_WORKSPACE | The key's user is not an active member of the workspace in X-Enterprise-Account |
403 | LIVE_FUNDING_IS_PLATFORM_CONFIRMED | The workspace is live, so do not call fund |
409 | ALREADY_DECIDED | You already decided on this milestone |
409 | SUBMITTER_CANNOT_APPROVE | Ask someone else to decide |
409 | NOT_APPROVED | Release only works on an approved milestone |
422 | IDEMPOTENCY_KEY_CONFLICT | You reused a key with a different body. Use a new key |
429 | RATE_LIMITED | Wait Retry-After seconds and retry |
The full list is in the Errors reference and Errors.