Payouts
A payout is a transfer from Plugipay's settlement balance to your bank account. Cleared payments (minus fees and refunds settled in the same window) accumulate into a per-currency balance; when a payout fires, that balance is debited and a wire goes out to the destination set at Settings → Payment methods.
Most payouts happen automatically on the schedule your provider sets — daily, weekly, or monthly per currency. The API exists so you can list them, retrieve a specific one for reconciliation, and (when allowed) trigger a manual release ahead of the next cycle.
Read the portal page first. Portal → Payouts covers settlement timing, cadence, and failure modes. This page is the wire-level reference.
This page assumes Authentication and Conventions. Examples are signed the same way and return the same envelope.
Lifecycle
A payout moves through five possible statuses:
| Status | Meaning |
|---|---|
pending |
Created. Queued in our system, not yet released to the bank rail. Counts as in-flight against available balance. |
in_transit |
We've handed the disbursement to the provider or bank rail. The funds have left Plugipay's settlement account but haven't landed yet. |
paid |
The bank confirmed receipt. The ledger transaction has been posted under code payout and completedAt is set. |
failed |
The bank rejected the transfer (wrong account, name mismatch, daily limit, etc.). Funds return to your settlement balance and become eligible again on the next cycle. |
cancelled |
A pending payout that was cancelled before it left. Only pending payouts can be cancelled — once in_transit or later, the bank rail owns it. |
Allowed transitions: pending → in_transit → paid (happy path), pending → cancelled, and pending|in_transit → failed. Anything else returns 409 invalid_transition.
Endpoints
Base path: /v1/payouts. All endpoints require the plugipay:payout:read scope (read) or plugipay:payout:write scope (mutating).
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/payouts |
List payouts (paginated, filterable) |
GET |
/v1/payouts/{id} |
Retrieve one payout |
POST |
/v1/payouts |
Trigger a manual payout (requires available balance + bank account) |
POST |
/v1/payouts/{id}/cancel |
Cancel a pending payout |
GET |
/v1/payouts/balance |
The current ledger balance, in-flight (locked) total, and available balance |
GET |
/v1/payouts/bank-account |
The destination bank account on file |
PATCH |
/v1/payouts/bank-account |
Update the destination bank account |
Three more endpoints (/{id}/mark-in-transit, /{id}/mark-paid, /{id}/mark-failed) exist for platform-admin reconciliation workflows. Merchant keys lack the scope for them; see Admin operations.
Retrieve a payout
GET /v1/payouts/{id}
Path parameters:
| Field | Type | Notes |
|---|---|---|
id |
string | The payout ID, e.g. po_01HXxxxxxxxxxxxxxxxxxxxxxx. |
Sample request
curl https://api.plugipay.com/v1/payouts/po_01HX7P3K5Q9V2W8N4M6T1B3F5G \
-H "Authorization: Plugipay-HMAC-SHA256 keyId=pk_test_xxx, scope=*, signature=..." \
-H "X-Plugipay-Timestamp: 1715526783"
// Node
const payout = await plugipay.payouts.retrieve('po_01HX7P3K5Q9V2W8N4M6T1B3F5G');
# Python
payout = plugipay.payouts.retrieve("po_01HX7P3K5Q9V2W8N4M6T1B3F5G")
// Go
payout, err := client.Payouts.Retrieve(ctx, "po_01HX7P3K5Q9V2W8N4M6T1B3F5G")
Sample response (200 OK)
{
"data": {
"id": "po_01HX7P3K5Q9V2W8N4M6T1B3F5G",
"amount": 4250000,
"currency": "IDR",
"status": "paid",
"method": "xendit_disbursement",
"bankCode": "BCA",
"bankName": "Bank Central Asia",
"bankAccountNumber": "1234567890",
"bankAccountHolder": "PT Contoh Indonesia",
"note": null,
"reference": "disb-9f3a2b1c",
"failureReason": null,
"ledgerTransactionId": "payout:po_01HX7P3K5Q9V2W8N4M6T1B3F5G",
"processedAt": "2026-05-12T03:15:42.000Z",
"completedAt": "2026-05-12T07:48:11.000Z",
"createdAt": "2026-05-12T02:00:00.000Z",
"updatedAt": "2026-05-12T07:48:11.000Z"
},
"error": null,
"meta": { "requestId": "req_01H...", "timestamp": "2026-05-13T09:00:00.000Z" }
}
Errors
404 not_found— ID doesn't exist or isn't owned by your workspace.
List payouts
GET /v1/payouts
Query parameters:
| Field | Type | Notes |
|---|---|---|
limit |
int | 1–100, default 20. |
cursor |
string | Opaque cursor from meta.page.nextCursor of the previous page. |
status |
string | Filter by status: pending, in_transit, paid, failed, cancelled. |
currency |
string | ISO 4217 code. Useful if you settle in multiple currencies. |
createdSince |
ISO 8601 or epoch | Only payouts created at or after this time. |
createdUntil |
ISO 8601 or epoch | Only payouts created before this time (exclusive). |
Results are sorted by createdAt descending. See Pagination for cursor mechanics.
Sample request
curl "https://api.plugipay.com/v1/payouts?status=paid¤cy=IDR&limit=50&createdSince=2026-05-01T00:00:00Z" \
-H "Authorization: Plugipay-HMAC-SHA256 keyId=pk_test_xxx, scope=*, signature=..." \
-H "X-Plugipay-Timestamp: 1715526783"
// Node
const page = await plugipay.payouts.list({
status: 'paid',
currency: 'IDR',
createdSince: '2026-05-01T00:00:00Z',
limit: 50,
});
# Python
page = plugipay.payouts.list(
status="paid",
currency="IDR",
created_since="2026-05-01T00:00:00Z",
limit=50,
)
// Go
page, err := client.Payouts.List(ctx, &plugipay.PayoutListParams{
Status: plugipay.String("paid"),
Currency: plugipay.String("IDR"),
CreatedSince: plugipay.String("2026-05-01T00:00:00Z"),
Limit: plugipay.Int(50),
})
Sample response (200 OK)
{
"data": [
{ "id": "po_01HX...", "amount": 4250000, "currency": "IDR", "status": "paid", "...": "..." },
{ "id": "po_01HY...", "amount": 1875000, "currency": "IDR", "status": "in_transit", "...": "..." }
],
"error": null,
"meta": {
"requestId": "req_01H...",
"timestamp": "2026-05-13T09:00:00.000Z",
"page": { "limit": 50, "hasMore": true, "nextCursor": "po_01HX..." }
}
}
Trigger a manual payout
POST /v1/payouts
Releases the requested amount from your available balance ahead of the next scheduled cycle. Subject to provider rules — some BYO providers don't allow manual payouts.
Idempotency-Key is required. Manual payouts move real money, so we reject the request without it. Use a UUID or a meaningful business key (e.g.
payout-2026-05-13-monthly). See Idempotency.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
amount |
int | yes | Smallest currency unit. Must be ≤ available balance. |
currency |
string | yes | ISO 4217 code. Must match an available-balance currency. |
bankCode |
string | no | Override the workspace default. |
bankName |
string | no | Override the workspace default. |
bankAccountNumber |
string | no | Override the workspace default. |
bankAccountHolder |
string | no | Override the workspace default. |
note |
string | no | Free-form, max 500 chars. Appears on the payout detail page. |
If bank-account fields are omitted, the workspace default from PATCH /v1/payouts/bank-account is used. If neither is set, you get 400 bank_account_missing.
Sample request
curl https://api.plugipay.com/v1/payouts \
-X POST \
-H "Authorization: Plugipay-HMAC-SHA256 keyId=pk_test_xxx, scope=*, signature=..." \
-H "X-Plugipay-Timestamp: 1715526783" \
-H "Idempotency-Key: payout-2026-05-13-monthly" \
-H "Content-Type: application/json" \
-d '{"amount":4250000,"currency":"IDR","note":"May 2026 sweep"}'
// Node
const payout = await plugipay.payouts.create(
{ amount: 4_250_000, currency: 'IDR', note: 'May 2026 sweep' },
{ idempotencyKey: 'payout-2026-05-13-monthly' },
);
# Python
payout = plugipay.payouts.create(
amount=4_250_000,
currency="IDR",
note="May 2026 sweep",
idempotency_key="payout-2026-05-13-monthly",
)
// Go
payout, err := client.Payouts.Create(ctx, &plugipay.PayoutCreateParams{
Amount: 4250000,
Currency: "IDR",
Note: plugipay.String("May 2026 sweep"),
IdempotencyKey: plugipay.String("payout-2026-05-13-monthly"),
})
Errors
400 invalid_amount— amount is zero, negative, or not an integer.400 bank_account_missing— no destination on file and none provided.409 insufficient_balance— the error message includes running balance and in-flight total so you can reconcile (Requested 5000000 exceeds available balance 4250000 (running 5100000 − in-flight 850000)).
Cancel a pending payout
POST /v1/payouts/{id}/cancel
Only pending payouts can be cancelled; anything in_transit or later is in the bank rail's hands. Cancelling releases the locked balance immediately. Requires an Idempotency-Key header.
Errors
409 invalid_transition— the payout isn'tpending.404 not_found— wrong ID or wrong workspace.
Get available balance
GET /v1/payouts/balance
Returns the breakdown the manual-payout endpoint uses to validate amount.
{
"data": {
"ledgerBalance": 5100000,
"locked": 850000,
"available": 4250000,
"currency": "IDR"
},
"error": null,
"meta": { "...": "..." }
}
available = ledgerBalance − locked, where locked is the sum of all pending + in_transit payouts. This is the maximum you can request via POST /v1/payouts.
The payout object
| Field | Type | Notes |
|---|---|---|
id |
string | po_<ULID>. |
amount |
int | Smallest currency unit. The net disbursed amount (fees already deducted at settlement time). |
currency |
string | ISO 4217 code. |
status |
string | See Lifecycle. |
method |
string | manual (API/portal-triggered) or xendit_disbursement (automated rail). More methods will be added as providers come online. |
bankCode |
string | Provider-specific bank code (BCA, BNI, etc.). Nullable when the rail doesn't require one. |
bankName |
string | Human-readable bank name. |
bankAccountNumber |
string | Destination account number. We return it unmasked over the authenticated API; the portal masks it. |
bankAccountHolder |
string | Account-holder name. Indonesian banks reject on mismatch — keep this exactly as the bank prints it. |
note |
string | null | Optional free-form note set on creation. |
reference |
string | null | Upstream provider's disbursement ID (e.g. Xendit disb-...). This is what your bank's wire memo references. Use it to reconcile against the bank statement. |
failureReason |
string | null | Bank or provider failure message. Set only when status = failed. |
ledgerTransactionId |
string | null | The ID of the corresponding ledger entry under code payout. Set when status reaches paid. |
processedAt |
timestamp | null | When the payout left Plugipay (transitioned to in_transit). |
completedAt |
timestamp | null | When the payout reached paid or failed. |
createdAt |
timestamp | When the payout was queued. |
updatedAt |
timestamp | Last state change. |
The response omits accountId — the API key implicitly scopes every request to one workspace. Platform admins target a specific merchant via X-Plugipay-On-Behalf-Of; see Authentication.
Events
Subscribe via Webhook endpoints. Three events fire per payout in the happy path; failures swap the third.
| Event | When | Payload |
|---|---|---|
payout.initiated |
A pending payout transitions to in_transit. The funds have left Plugipay. |
Full payout object |
payout.paid |
The bank confirmed receipt. completedAt and ledgerTransactionId are set. |
Full payout object |
payout.failed |
The bank rejected the transfer. failureReason is populated; funds return to settlement balance. |
Full payout object |
We don't emit payout.created — pending is an internal queue state, so the first webhook signal is payout.initiated. To react at queue time, poll GET /v1/payouts?status=pending.
Filtering: date range and currency
The two most common reconciliation queries:
GET /v1/payouts?createdSince=2026-05-01T00:00:00Z&createdUntil=2026-06-01T00:00:00Z
GET /v1/payouts?currency=USD&status=paid
createdSince / createdUntil accept ISO 8601 strings or Unix epoch seconds (see Conventions → Timestamps); both bounds are optional and independent.
For period-end reconciliation, filter by createdAt rather than processedAt — a payout queued on the 31st but disbursed on the 1st belongs to the month it was earned.
Reconciliation: joining payouts to ledger entries
Each paid payout posts exactly one ledger entry under code payout, with sourceType = "payout" and sourceId = <payout id>. The payout's ledgerTransactionId is that entry's ID. Typical reconciliation flow:
- List payouts for the period:
GET /v1/payouts?createdSince=...&createdUntil=...&status=paid. - Match each payout to a bank deposit by amount plus the
referencefield (your bank's wire memo contains it). - To audit what went into a payout — the payments and refunds that filled the balance it drained — pull the ledger entries between this payout's entry and the previous
payout-coded one. Everything in between is the contents of the later transfer. - Drill into individual entries via the ledger row's
sourceType(payment,refund,fee) andsourceId.
The same data is available as CSV at Portal → Reports → Payouts.
The ledger is the source of truth, not the payout list. Payouts tell you about money movement to your bank; the ledger tells you the financial events that produced that balance. For tax and audit purposes, run reconciliation off the ledger and use payouts as the “cash-out” column.
Next
- Ledger — the entries each payout settles.
- Webhooks — the signature recipe for verifying
payout.*events. - Portal → Payouts — schedule cadence, settlement timing, failure recovery.
- Reports — CSV exports of payouts and fees.