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&currency=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't pending.
  • 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.createdpending 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:

  1. List payouts for the period: GET /v1/payouts?createdSince=...&createdUntil=...&status=paid.
  2. Match each payout to a bank deposit by amount plus the reference field (your bank's wire memo contains it).
  3. 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.
  4. Drill into individual entries via the ledger row's sourceType (payment, refund, fee) and sourceId.

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.
Plugipay — Payments that don't tax your success