Refunds

A refund sends money back to a customer for a completed checkout session or a paid invoice. Plugipay handles the provider-side refund call (Xendit, Midtrans, PayPal, etc.), posts the corresponding ledger entries, and emits a webhook when the refund settles. This page covers the plugipay.refunds namespace; for the underlying HTTP surface see API: Refunds, and for how refunds appear in the ledger see Concepts → Ledger.

Namespace

plugipay.refunds — every method on this namespace:

plugipay.refunds.create(input)
plugipay.refunds.get(id)
plugipay.refunds.list(params?)

There are no update / cancel methods. Once submitted, a refund is in the provider's hands; if it fails, you create a new refund (or process the return manually and reconcile via the ledger).

Methods

refunds.create

Signature. plugipay.refunds.create(input): Promise<Refund>

Creates a refund against either a checkout session or an invoice. Pass amount (minor units) to refund partially; omit it to refund the full remaining balance. The SDK auto-attaches an Idempotency-Key, so retries don't double-refund.

import { PlugipayClient } from '@forjio/plugipay-node';

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
});

// Full refund of a checkout session:
const fullRefund = await plugipay.refunds.create({
  sourceType: 'checkout_session',
  sourceId: 'cs_01HX...',
  reason: 'Customer requested cancellation within 24 hours',
});

// Partial refund of an invoice:
const partialRefund = await plugipay.refunds.create({
  sourceType: 'invoice',
  sourceId: 'inv_01HX...',
  amount: 50_000_00,                  // IDR 50,000.00
  reason: 'Shipping issue — goodwill credit',
});

console.log(partialRefund.status); // → 'pending' or 'succeeded' depending on adapter

Refunds are irreversible. Once a refund has reached succeeded, you can't recall the money — the customer must pay again. Partial refunds also can't be "un-partial-ed" upward; create a fresh refund for the additional amount if needed.

refunds.get

Signature. plugipay.refunds.get(id): Promise<Refund>

const refund = await plugipay.refunds.get('rfd_01HX...');
if (refund.status === 'failed') {
  console.error(`Refund failed: ${refund.failureReason}`);
}

refunds.list

Signature. plugipay.refunds.list(params?): Promise<{ data: Refund[]; cursor: string | null; hasMore: boolean }>

Filters: status ('pending' | 'succeeded' | 'failed' | 'canceled'), sourceId (the cs_* or inv_* being refunded), limit, cursor. See Pagination.

// All refunds against one source:
const { data } = await plugipay.refunds.list({ sourceId: 'cs_01HX...' });

// All currently-pending refunds (for ops monitoring):
const pending = await plugipay.refunds.list({ status: 'pending', limit: 100 });

Types

type RefundStatus = 'pending' | 'succeeded' | 'failed' | 'canceled';

interface Refund {
  id: string;                                       // 'rfd_...'
  accountId: string;
  amount: number;                                   // minor units
  currency: 'IDR' | 'USD';
  status: RefundStatus;
  reason: string | null;
  sourceType: 'checkout_session' | 'invoice';
  sourceId: string;
  failureReason: string | null;
  createdAt: string;
  updatedAt: string;
}

For the full state machine (including provider-specific transition rules), see API: Refunds.

Common patterns

Full refund

The simplest case — refund the full remaining balance, no amount argument:

async function refundOrder(checkoutSessionId: string, reason: string) {
  return plugipay.refunds.create({
    sourceType: 'checkout_session',
    sourceId: checkoutSessionId,
    reason,
  });
}

Partial refund

For partial returns (one of three items refunded, shipping refunded but goods kept, etc.):

async function refundLineItem(checkoutSessionId: string, amountMinorUnits: number) {
  return plugipay.refunds.create({
    sourceType: 'checkout_session',
    sourceId: checkoutSessionId,
    amount: amountMinorUnits,
    reason: 'Single line item returned',
  });
}

The remaining balance can be refunded later in subsequent calls until the full amount has been returned.

Wait for a refund to settle

For card / e-wallet refunds, settlement can take hours or days. Poll get or react to plugipay.refund.succeeded.v1 (when emitted by your event subscriptions):

async function waitForRefund(refundId: string, maxAttempts = 20) {
  for (let i = 0; i < maxAttempts; i++) {
    const refund = await plugipay.refunds.get(refundId);
    if (refund.status === 'succeeded' || refund.status === 'failed') {
      return refund;
    }
    await new Promise((r) => setTimeout(r, 30_000));
  }
  throw new Error(`Refund ${refundId} still pending after ${maxAttempts} polls`);
}

For production, prefer webhooks — polling burns API calls and your rate limit.

Reconcile refunds against the ledger

Every refund posts a revenue_refund debit and a cash credit to the ledger. To cross-check refunds against the books:

const ledgerEntries = await plugipay.ledger.list({
  sourceType: 'refund',
  sourceId: 'rfd_01HX...',
});

for (const entry of ledgerEntries.data) {
  console.log(`${entry.code} ${entry.direction} ${entry.amount}`);
}

Handle a failed refund

A failed refund is most often an adapter-level problem (closed card, unsupported region for cross-border refund). The failureReason field tells you why; the recovery is usually to process the return manually and either issue a new refund or create a credit-note invoice:

async function recoverFailedRefund(refundId: string) {
  const refund = await plugipay.refunds.get(refundId);
  if (refund.status === 'failed') {
    await notifyOpsTeam(`Refund ${refundId} failed: ${refund.failureReason}`);
    // Optionally: manually issue a credit via an invoice
  }
}

Errors

Code Status Cause
validation_error 400 Bad sourceType, missing sourceId, negative or zero amount.
not_found 404 Source checkout session or invoice doesn't exist.
state_conflict 409 Trying to refund an unpaid session, voided invoice, or over the available balance.
over_refund 422 amount exceeds the remaining refundable balance on the source.
provider_error 502 The downstream payment provider rejected the refund (status failed on the refund object).

Next

Plugipay — Payments that don't tax your success