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 via payouts.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

Plugipay — Payments that don't tax your success