Payouts
A payout moves money from your Plugipay balance to your bank account. Plugipay holds the funds in your ledger as collections come in; you request a payout, the funds debit your cash account and credit a payout-in-transit account, and once the bank confirms, the money is yours. This page covers the plugipay.payouts namespace; for the underlying HTTP surface see API: Payouts, and for how payouts fit into the financial model see Concepts → Payout.
Namespace
plugipay.payouts — every method on this namespace:
plugipay.payouts.create(input)
plugipay.payouts.get(id)
plugipay.payouts.list(params?)
plugipay.payouts.cancel(id)
plugipay.payouts.markInTransit(id, reference?) // manual adapter
plugipay.payouts.markPaid(id, reference?) // manual adapter
plugipay.payouts.markFailed(id, failureReason) // manual adapter
plugipay.payouts.balance()
plugipay.payouts.getBankAccount()
plugipay.payouts.updateBankAccount(input)
Methods
payouts.create
Signature. plugipay.payouts.create(input): Promise<Payout>
Requests a payout for the given amount in the given currency. Bank details are optional — if omitted, Plugipay uses the workspace's configured payout bank account (set via updateBankAccount). The SDK auto-attaches an Idempotency-Key.
import { PlugipayClient } from '@forjio/plugipay-node';
const plugipay = new PlugipayClient({
keyId: process.env.PLUGIPAY_KEY_ID!,
secret: process.env.PLUGIPAY_SECRET!,
});
// Payout using the workspace's default bank account:
const payout = await plugipay.payouts.create({
amount: 10_000_000_00, // IDR 10,000,000.00
currency: 'IDR',
note: 'Q4 settlement',
});
// One-off payout to a different bank:
await plugipay.payouts.create({
amount: 5_000_000_00,
currency: 'IDR',
bankCode: 'BCA',
bankName: 'Bank Central Asia',
bankAccountNumber: '1234567890',
bankAccountHolder: 'PT Forjio Teknologi Indonesia',
});
Check
balance()first. The API rejects payouts that exceed the available balance. Always confirm viapayouts.balance()before creating large payouts.
payouts.get
Signature. plugipay.payouts.get(id): Promise<Payout>
const payout = await plugipay.payouts.get('po_01HX...');
console.log(payout.status, payout.completedAt);
payouts.list
Signature. plugipay.payouts.list(params?): Promise<{ data: Payout[]; cursor: string | null; hasMore: boolean }>
Filters: status ('pending' | 'in_transit' | 'paid' | 'failed' | 'cancelled'), limit, cursor.
const { data: pending } = await plugipay.payouts.list({ status: 'pending', limit: 50 });
payouts.cancel
Signature. plugipay.payouts.cancel(id): Promise<Payout>
Cancels a pending payout. Once the payout is in_transit or beyond, you can't cancel — the money is already moving.
await plugipay.payouts.cancel('po_01HX...');
payouts.markInTransit / payouts.markPaid / payouts.markFailed
These are manual-adapter only. For workspaces using automatic disbursement (Xendit, etc.), Plugipay drives the status from provider events; you don't call these. For manual workspaces (you send the wire yourself and report status back to Plugipay), these are the state-machine drivers.
// You initiated the wire transfer. Tell Plugipay it's in flight:
await plugipay.payouts.markInTransit('po_01HX...', 'WIRE-2026-118');
// Bank confirmed the funds landed:
await plugipay.payouts.markPaid('po_01HX...', 'WIRE-2026-118');
// Bank bounced the wire:
await plugipay.payouts.markFailed('po_01HX...', 'Insufficient receiver KYC');
The reference argument is your bank's tracking number; it's stored on the payout for reconciliation.
payouts.balance
Signature. plugipay.payouts.balance(): Promise<AvailableBalance>
Returns the available payout balance — the ledgerBalance minus any locked amounts (in-flight payouts, refund reserves).
const { available, ledgerBalance, locked, currency } = await plugipay.payouts.balance();
console.log(`Available to pay out: ${available} ${currency}`);
console.log(`(of ${ledgerBalance} total, ${locked} locked)`);
payouts.getBankAccount / payouts.updateBankAccount
Manage the workspace's default payout bank account. New payouts without explicit bank fields use this.
const bank = await plugipay.payouts.getBankAccount();
console.log(bank.configured, bank.bankName, bank.bankAccountNumber);
await plugipay.payouts.updateBankAccount({
bankCode: 'BCA',
bankName: 'Bank Central Asia',
bankAccountNumber: '1234567890',
bankAccountHolder: 'PT Forjio Teknologi Indonesia',
});
Types
type PayoutStatus = 'pending' | 'in_transit' | 'paid' | 'failed' | 'cancelled';
type PayoutMethod = 'manual' | 'xendit_disbursement';
interface Payout {
id: string; // 'po_...'
accountId: string;
amount: number; // minor units
currency: string;
status: PayoutStatus;
method: PayoutMethod;
bankCode: string | null;
bankName: string;
bankAccountNumber: string;
bankAccountHolder: string;
note: string | null;
reference: string | null;
failureReason: string | null;
ledgerTransactionId: string | null;
processedAt: string | null;
completedAt: string | null;
createdAt: string;
updatedAt: string;
}
interface AvailableBalance {
ledgerBalance: number;
locked: number;
available: number;
currency: string | null;
}
interface BankAccount {
bankCode: string | null;
bankName: string | null;
bankAccountNumber: string | null;
bankAccountHolder: string | null;
configured: boolean;
}
See API: Payouts for the full state-transition diagram and adapter-specific behavior notes.
Common patterns
Sweep all available balance to bank
The classic "end of month, move everything to the bank" job:
async function sweepBalance() {
const balance = await plugipay.payouts.balance();
if (balance.available <= 0) return null;
return plugipay.payouts.create({
amount: balance.available,
currency: balance.currency ?? 'IDR',
note: `Automated sweep ${new Date().toISOString().slice(0, 10)}`,
});
}
Scheduled weekly payouts
Run this on a cron, gated by a minimum threshold so you don't pay out trivial amounts:
async function weeklyPayout(minAmount: number) {
const balance = await plugipay.payouts.balance();
if (balance.available < minAmount) {
console.log(`Skipping payout — only ${balance.available} available`);
return;
}
await plugipay.payouts.create({
amount: balance.available,
currency: balance.currency ?? 'IDR',
note: 'Weekly auto-payout',
});
}
Reconcile payouts to bank statements
Match payouts to your bank statement by reference. The completedAt timestamp is when Plugipay marked the payout paid; cross-reference it with the deposit timestamp on the bank side.
async function* completedPayouts(sinceIso: string) {
let cursor: string | undefined;
while (true) {
const page = await plugipay.payouts.list({ status: 'paid', limit: 100, cursor });
for (const p of page.data) {
if (p.completedAt && p.completedAt >= sinceIso) yield p;
}
if (!page.hasMore) break;
cursor = page.cursor!;
}
}
Drive the manual-adapter state machine
For ops-managed payouts (you control the wire, Plugipay tracks the books):
async function processManualPayout(payoutId: string) {
// 1. Initiate the wire on your banking side. Get a reference.
const wireRef = await sendWire(/* ... */);
// 2. Tell Plugipay it's in flight.
await plugipay.payouts.markInTransit(payoutId, wireRef);
// 3. Wait for bank confirmation. On success:
await plugipay.payouts.markPaid(payoutId, wireRef);
// (or on failure:)
// await plugipay.payouts.markFailed(payoutId, 'Bank rejected: KYC mismatch');
}
Detect a failed payout and notify
Reconciliation loop — check for failures regularly:
async function alertOnFailedPayouts() {
const { data } = await plugipay.payouts.list({ status: 'failed', limit: 50 });
for (const p of data) {
await sendOpsAlert(`Payout ${p.id} failed: ${p.failureReason}`);
}
}
Errors
| Code | Status | Cause |
|---|---|---|
validation_error |
400 | Bad currency, non-positive amount, malformed bank details. |
insufficient_balance |
422 | amount exceeds available. |
not_found |
404 | Payout ID doesn't exist. |
state_conflict |
409 | Cancelling a payout already in flight, marking-paid a cancelled payout. |
bank_account_unconfigured |
422 | create without explicit bank details, when no default is configured. |
Next
- Ledger — the books that back the balance.
- Adapters — configure automatic disbursement.
- API: Payouts — HTTP reference.