> For the complete documentation index, see [llms.txt](https://developers.doku.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.doku.com/wallet-as-a-service/sub-account/sub-account-v2.md).

# Sub Account V2

## Integration Guide

Sub-Account is a powerful infrastructure layer that allows platforms to create sub-accounts, receive money, route, track, hold, and pay out funds across multiple account entities. It combines sub-account management, payment, and payout capabilities into one unified system.

> Availability: ![:flag-id:](https://a.slack-edge.com/production-standard-emoji-assets/16.0/apple-medium/1f1ee-1f1e9@2x.png) Indonesian Business Account only
>
> This documentation covers **Sub-Account V2 API** only. For version differences, see [FAQ](https://docs.doku.com/wallet-as-a-service/sub-account/faq).

> Before you begin, make sure you have an active DOKU account with Sub-Account service activated. [Contact Sales](https://www.doku.com/en-us/contact-sales) if you haven't activated yet.

> **Want to see it work before you build?** The [Sub-Account demo](https://sandbox.doku.com/demo/sub-account) runs the flow below on sandbox without any setup.

***

### Flow Overview

```mermaid
sequenceDiagram
    participant Merchant
    participant DOKU
    participant Customer
    participant Bank

    rect rgb(240, 248, 255)
    Note over Merchant, DOKU: Setup
    Merchant->>DOKU: 1. Get Access Token
    DOKU-->>Merchant: accessToken
    Merchant->>DOKU: 2. Register Sub-Account
    DOKU-->>Merchant: profileId + accountNo + BRI VA
    Merchant->>DOKU: 3. Create Split Rule (optional)
    DOKU-->>Merchant: splitRuleId
    end

    rect rgb(240, 255, 240)
    Note over Merchant, Customer: Money In
    Merchant->>DOKU: 4. Create Payment (Checkout / Direct API)
    DOKU-->>Customer: Payment page / VA number
    Customer->>DOKU: Customer pays
    DOKU-->>Merchant: Payment notification (webhook)
    end

    rect rgb(255, 248, 240)
    Note over Merchant, DOKU: Monitoring
    Merchant->>DOKU: 5. Balance Inquiry
    DOKU-->>Merchant: Account balances
    Merchant->>DOKU: 6. Transaction History
    DOKU-->>Merchant: Transaction list
    Merchant->>DOKU: 7. Transaction Status
    DOKU-->>Merchant: Transaction detail
    end

    rect rgb(255, 240, 245)
    Note over Merchant, Bank: Money Out
    Merchant->>DOKU: 8. Transfer Inquiry
    DOKU-->>Merchant: referenceNo + beneficiary name
    Merchant->>DOKU: 9. Transfer Payment
    DOKU->>Bank: Payout
    DOKU-->>Merchant: Transfer notification (webhook)
    end
```

***

### Step-by-Step

#### 1. Get Access Token

Authenticate with the DOKU API to get a B2B access token. This token is required for all subsequent API calls.

See [Get Token B2B](https://developers.doku.com/accept-payments/direct-api/snap/integration-guide/get-token-api/b2b) for full documentation.

***

#### 2. Register Sub-Account

Create a sub-account for each party in your ecosystem (seller, agent, branch, etc.). Each sub-account gets three account types — `DOKU_MERCHANT_IDR`, `DOKU_MERCHANT_PENDING_IDR` and `DOKU_MERCHANT_POINT` — and an auto-assigned static BRI VA.

```
POST /sub-account/v2.0/register
```

Save the `profileId` from the response — you'll use it in every subsequent call for this sub-account.

***

#### 3. Create Split Rule (optional)

Define how incoming payments should be automatically distributed across sub-accounts. You can create percentage-based or flat-amount rules.

```
POST /sub-account/v2.0/split-rules
```

Save the `splitRuleId` — pass it together with `profileId` in payment requests.

***

#### 4. Create Payment

Accept payments through **Checkout API** or **Direct API**. Include `additionalInfo.account.id` to route the payment to a sub-account, and optionally `additionalInfo.account.split_rule_id` to apply a split rule.

{% tabs %}
{% tab title="Checkout API" %}

```
POST /checkout/v1/payment
```

Returns a payment URL. Customer is redirected to the DOKU hosted payment page where they can choose from all active payment channels.
{% endtab %}

{% tab title="Direct API" %}

```
POST /virtual-accounts/bi-snap-va/v1.1/transfer-va/create-va  (VA)
POST /credit-card/v1/payment-page  (Credit Card)
POST /direct-debit/core/v1/debit/payment-host-to-host  (E-Wallet)
```

Merchant controls the payment UI. Each channel has its own endpoint.
{% endtab %}
{% endtabs %}

After payment, DOKU sends a **webhook notification** to your callback URL. Funds land in `DOKU_MERCHANT_PENDING_IDR` (for Checkout/Direct API) or `DOKU_MERCHANT_IDR` (for BRI VA top-up).

See [Payment Channel Compatibility](https://docs.doku.com/wallet-as-a-service/sub-account#payment-method-compatibility) for which channels support Sub-Account routing.

**How the split runs**

The rule from step 3 is created once; the split itself runs inside DOKU **at settlement**. **There is no API call that triggers it** — you only pass `split_rule_id` on the payment request.

```mermaid
sequenceDiagram
    participant Merchant
    participant DOKU
    participant Customer
    participant Seller as Seller Sub-Account
    participant Platform as Platform Sub-Account

    rect rgb(240, 248, 255)
    Note over Merchant, DOKU: Setup — once per rule
    Merchant->>DOKU: 1. Create Split Rule (transactionType + rules[])
    DOKU-->>Merchant: splitRuleId
    end

    rect rgb(240, 255, 240)
    Note over Merchant, Customer: Money In
    Merchant->>DOKU: 2. Create Payment (Checkout / Direct API)
    Note right of Merchant: additionalInfo.account.id<br/>+ split_rule_id
    DOKU-->>Customer: Payment page / VA number
    Customer->>DOKU: 3. Customer pays Rp 500,000
    DOKU->>Seller: Credited to Pending IDR — held, not yet split
    DOKU-->>Merchant: Payment notification (webhook)
    end

    rect rgb(255, 248, 240)
    Note over DOKU, Platform: Settlement — the split runs here
    DOKU->>DOKU: 4. Deduct PG fee (Rp 5,000)
    Note right of DOKU: net = Rp 495,000<br/>split is calculated on net, not gross
    DOKU->>Seller: 5a. 95% → Rp 470,250 to Merchant IDR
    DOKU->>Platform: 5b. 5% → Rp 24,750 to Merchant IDR
    Note over Seller, Platform: Now withdrawable via Transfer Payment
    end
```

Four things to build against:

* **The split runs at settlement, not at payment.** The payment first lands in `DOKU_MERCHANT_PENDING_IDR`, held and unsplit. Only at settlement is the fee deducted, the rule applied, and each destination credited.
* **Destination balances do not reflect the split until settlement completes.** Do not poll a destination balance immediately after payment and expect its share — there is a real gap, and it can span more than a day.
* **The split is calculated on the net amount, after PG fees** — not the gross customer payment. In the example above a Rp 5,000 fee is deducted before the 95/5 rule applies, so the destinations receive 470,250 and 24,750, not 475,000 and 25,000.
* **Two silent failures.** An invalid `split_rule_id` returns **no error** — the payment lands in the sub-account unsplit. An invalid `additionalInfo.account.id` also returns no error, and the transaction must be resolved manually by the ops team. Validate both before sending, since neither surfaces at request time.

In [Transaction History](https://developers.doku.com/wallet-as-a-service/sub-account/sub-account-v2) the settlement produces several rows sharing one reference — the `PAYMENT`, a `SETTLEMENT_FEE`, and one `SPLIT_TRANSACTION` per destination. Group by reference to reassemble a single payment, and expect the fee row to explain why the split amounts do not sum to the gross.

***

#### 5. Balance Inquiry

Check the current balance of any sub-account at any time.

```
POST /sub-account/v2.0/balance-inquiries
```

Returns available and reserved balances for each account: `DOKU_MERCHANT_IDR`, `DOKU_MERCHANT_PENDING_IDR` and `DOKU_MERCHANT_POINT`.

Querying your **merchant profile** additionally returns `DOKU_SYSTEM_POINT` — the funding source for point top-ups, which carries a **negative** balance equal to the points outstanding across your sub-accounts. That is normal, not an error. See Point Flow.

***

#### 6. Transaction History

Retrieve a paginated list of all transactions for a sub-account within a date range.

```
POST /sub-account/v2.0/transaction-history-list
```

Each record includes mutation type (CREDIT/DEBIT), transaction type, amount, channel, status, and timestamp.

Two things to build against:

* **`status` can be `VOID`**, alongside `SUCCESS`, `PENDING` and `FAILED`. Voided rows stay in history as an audit trail but are **excluded from balances** — so `balance = sum of rows where status != VOID`. Summing every row overstates it.
* **`mutationType` is relative to the account you query.** The same transaction is a DEBIT on one side and a CREDIT on the other; both legs share a `referenceNo`.

***

#### 7. Transaction Status

Check the real-time status of any individual transaction.

```
POST /sub-account/v2.0/transactions-status
```

Returns the latest transaction status (`00` = Success, `03` = Pending, `04` = Refunded, `05` = Canceled, `06` = Failed) and refund history if applicable. `05` is returned for a voided point top-up.

***

#### 8. Transfer Inquiry

Before sending money out, validate the destination account.

```
POST /sub-account/v2.0/transfer-inquiry
```

Returns a `referenceNo` and confirmed beneficiary name. You must use this `referenceNo` in the next step.

> **Important:** The `partnerReferenceNo` must be the same in both the inquiry and payment requests.

***

#### 9. Transfer Payment

Execute the payout using the `referenceNo` from Transfer Inquiry.

```
POST /sub-account/v2.0/transfer-payment
```

Supports four transfer types:

| Type               | Description                                                                            |
| ------------------ | -------------------------------------------------------------------------------------- |
| `BANK_ACCOUNT`     | Payout to 100+ Indonesian banks (via BI\_FAST or ONLINE)                               |
| `DOKU_SUB_ACCOUNT` | Internal transfer between sub-accounts. Set `currency: POINT` for a point P2P transfer |
| `DOKU_WALLET`      | Transfer to DOKU e-wallet                                                              |
| `DOKU_NON_FIAT`    | **Point top-up** — issues points from `DOKU_SYSTEM_POINT`. See Point Flow              |

`beneficiaryAccountName` is **required**. Use the name returned by Transfer Inquiry.

After transfer, DOKU sends a **webhook notification** with the transaction status. The timestamp field on the notification is `trxDateTime`, not `transactionDate`.

***

### Debit & Debit Cancel

Deduct funds from a sub-account per transaction, and reverse them fully or partially. Use this when a sub-account holder buys goods, pays for a service, or when your platform needs to deduct programmatically.

```
POST /sub-account/v2.0/debit
POST /sub-account/v2.0/debit/cancel
```

#### Debit Flow

```mermaid
sequenceDiagram
    participant Platform as Your Platform
    participant DOKU
    participant Sub as Sub-Account
    participant L1 as Level 1 Profile

    rect rgb(240, 248, 255)
    Note over Platform, DOKU: Debit — deduct per transaction
    Platform->>DOKU: 1. POST /sub-account/v2.0/debit
    Note right of Platform: fromAccount + transactionType<br/>+ amount (IDR or POINT)
    DOKU->>Sub: DEBIT Rp 150,000
    DOKU->>L1: CREDIT Rp 150,000
    DOKU-->>Platform: Response 2005400 Success<br/>referenceNo + toAccount (the credited account)
    DOKU-->>Platform: Debit notification (webhook)
    end

    rect rgb(255, 248, 240)
    Note over Platform, L1: Debit Cancel — full or partial
    Platform->>DOKU: 2. POST /sub-account/v2.0/debit/cancel
    Note right of Platform: originalPartnerReferenceNo<br/>+ refundAmount ≤ original
    DOKU->>L1: DEBIT Rp 50,000
    DOKU->>Sub: CREDIT Rp 50,000
    DOKU-->>Platform: Response 2005700 Success<br/>refundNo
    DOKU-->>Platform: Debit cancel notification (webhook)
    end
```

Four things to build against:

* **You never send `toAccount`.** DOKU fills it in and returns it — the credit side of the double entry. For an IDR debit it is the Level 1 profile's `DOKU_MERCHANT_IDR`; for a point purchase it is `DOKU_SYSTEM_POINT`, so the points are retired rather than moved to another sub-account.
* **A cancel moves money the other way** — `refundAmount` is debited from the Level 1 profile and credited back to the sub-account.
* **Cancels can be partial, and repeated**, up to the original amount. A debit of 150,000 can be cancelled as 50,000 now and 100,000 later.
* **A cancel writes new ledger rows.** The original `PURCHASE` and the `REFUND_PURCHASE` both stay `SUCCESS` in transaction history — nothing is rewritten. This is the opposite of Void Topup, which writes no rows and flips the original to `VOID`.

Both steps send a webhook notification. Point debits and cancels use exactly the same endpoints — only `amount.currency` and the account change; see Point Flow.

***

### Point Flow (Unified Ledger)

Points are a **non-cash ledger** — no money moves through DOKU. Use them for loyalty points, credits, rewards, or cash-in-hand tracking, held in each sub-account's `DOKU_MERCHANT_POINT` account.

> **Points require the Unified Ledger service**, activated separately from Collect and Route / Deposit System / Fund Oversight. Your merchant profile also needs a `DOKU_SYSTEM_POINT` account — the funding source every point top-up draws from. Check for it with a balance inquiry before your first top-up; without it, point balances cannot be created.

#### Point Flow Overview

```mermaid
sequenceDiagram
    participant Merchant
    participant DOKU
    participant SystemPoint as DOKU_SYSTEM_POINT
    participant Agent1 as Sub-Account A
    participant Agent2 as Sub-Account B

    rect rgb(240, 248, 255)
    Note over Merchant, DOKU: Issue points
    Merchant->>DOKU: 1. Transfer Inquiry + Payment (type: DOKU_NON_FIAT)
    DOKU->>SystemPoint: DEBIT — goes negative
    DOKU->>Agent1: CREDIT points
    DOKU-->>Merchant: Notification (transactionType: TOPUP)
    end

    rect rgb(240, 255, 240)
    Note over Agent1, Agent2: Move and spend points
    Merchant->>DOKU: 2. Transfer (type: DOKU_SUB_ACCOUNT, currency: POINT)
    DOKU->>Agent1: DEBIT
    DOKU->>Agent2: CREDIT
    Merchant->>DOKU: 3. Debit (currency: POINT)
    DOKU->>Agent1: DEBIT
    DOKU->>SystemPoint: CREDIT — points retired
    end

    rect rgb(255, 248, 240)
    Note over Merchant, Agent1: Reverse
    Merchant->>DOKU: 4a. Debit Cancel — reverses a purchase
    Merchant->>DOKU: 4b. Void Topup — reverses a top-up
    DOKU->>Agent1: Points removed
    DOKU-->>Merchant: Notification (status: 05 Canceled)
    end
```

#### Point Operations

| Operation                       | API                                                 | Key fields                                                                                                              |
| ------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Top-up** (issue points)       | `POST /transfer-inquiry` → `POST /transfer-payment` | `type: DOKU_NON_FIAT`, `amount.currency: POINT`. `fromAccount` is ignored — DOKU always routes from `DOKU_SYSTEM_POINT` |
| **P2P** (move points)           | `POST /transfer-inquiry` → `POST /transfer-payment` | `type: DOKU_SUB_ACCOUNT`, `amount.currency: POINT`. System Point is not involved                                        |
| **Purchase** (spend points)     | `POST /sub-account/v2.0/debit`                      | `amount.currency: POINT`, `fromAccount` = a point account. `toAccount` is filled in as System Point                     |
| **Refund** (reverse a purchase) | `POST /sub-account/v2.0/debit/cancel`               | `refundAmount.currency: POINT`. Partial refunds allowed                                                                 |
| **Void** (reverse a top-up)     | `POST /sub-account/v2.0/topup/void`                 | `transactionType: VOID_TOPUP`, `voidAmount` must equal the original exactly                                             |

`transactionType` does **not** change between cash and points — the account number and currency determine which wallet moves.

#### Void a Top-Up

```
POST /sub-account/v2.0/topup/void
```

```json
{
  "partnerReferenceNo": "VOID-20260904-001",
  "originalPartnerReferenceNo": "TOPUP-20260904-001",
  "transactionType": "VOID_TOPUP",
  "voidAmount": { "value": "1000.00", "currency": "POINT" }
}
```

Three behaviours differ from Debit Cancel, and catch people out:

* **Full amount only.** `voidAmount.value` must equal the original top-up exactly. A partial value returns `403` `4034415`.
* **Not idempotent.** Voiding twice returns `403` `4034415` `Transaction is already voided`.
* **No new ledger rows.** The original `TOPUP` rows change status from `SUCCESS` to `VOID` on both the destination account and System Point. They stay in history as an audit trail.

Success returns `latestTransactionStatus: "05"` (Canceled) — a status no other endpoint returns.

> **Voided rows are excluded from balances.** They remain visible in transaction history with `status: "VOID"`, so summing every history row overstates the balance by the voided amount. Reconcile with `balance = sum of rows where status != VOID`.

Use **Void Topup** to reverse a top-up and **Debit Cancel** to reverse a purchase — they are not interchangeable.

***

#### Additional Operations

| Operation        | API                                   | Description                       |
| ---------------- | ------------------------------------- | --------------------------------- |
| **Debit**        | `POST /sub-account/v2.0/debit`        | Deduct funds from a sub-account   |
| **Debit Cancel** | `POST /sub-account/v2.0/debit/cancel` | Reverse a debit (full or partial) |
| **Void Topup**   | `POST /sub-account/v2.0/topup/void`   | Reverse a point top-up in full    |

***

### Notifications (Webhooks)

DOKU posts notifications to a callback URL registered with DOKU at merchant setup. It is not a field on any API request.

**One URL can serve every event** — register, top-up, transfer, debit, debit cancel and void all arrive at one endpoint, and you branch on the payload. Or register two URLs and split by concern:

| URL                 | Receives                                                            |
| ------------------- | ------------------------------------------------------------------- |
| **Register URL**    | Sub-account creation results                                        |
| **Transaction URL** | Everything else — top-up, transfer, debit, debit cancel, void topup |

#### URL requirements

| Requirement                           |                                      |
| ------------------------------------- | ------------------------------------ |
| Specific path or pattern              | **Not required** — any path works    |
| Path must match DOKU's endpoint names | **No**                               |
| HTTPS                                 | Yes                                  |
| **Maximum length**                    | **64 characters**                    |
| Response                              | `200` with the acknowledgement below |

**There is no required URL pattern.** The host and path are entirely yours, and DOKU posts to exactly the URL you register. All of these are valid:

```
https://api.yourcompany.com/notify
https://api.yourcompany.com/doku
https://yourcompany.com/v3/callbacks/sac
https://staging.yourcompany.com/anything
```

#### Required response

Every notification endpoint must return `200` with:

```json
{
  "responseCode": "2000000",
  "responseMessage": "Successful"
}
```
