Currency

Subscriptions

A subscription is the customer×plan relationship that drives recurring billing. Once active, Plugipay auto-issues an invoice each cycle, attempts payment via the customer's configured method, and emits webhooks when status changes (trial → active → past_due → canceled). This page covers the plugipay.subscriptions namespace; for the underlying HTTP surface see API: Subscriptions, and for the state machine see Concepts → Subscription.

Namespace

plugipay.subscriptions — every method on this namespace:

plugipay.subscriptions.create(input)
plugipay.subscriptions.get(id)
plugipay.subscriptions.list(params?)
plugipay.subscriptions.cancel(id, at?)
plugipay.subscriptions.pause(id, resumeAt?)
plugipay.subscriptions.resume(id)

Methods

subscriptions.create

Signature. plugipay.subscriptions.create(input): Promise<Subscription>

Creates a subscription, attaching a planId to a customerId. Optional trialDays defers the first charge; collectionMethod defaults to 'charge_automatically' (Plugipay charges the saved payment method) but can be 'send_invoice' if you want emailed invoices instead. 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!,
});

const sub = await plugipay.subscriptions.create({
  customerId: 'cus_01HX...',
  planId: 'plan_pro_monthly',
  priceId: 'price_pro_monthly_idr',
  trialDays: 14,
  paymentTokenId: 'pt_01HX...',
  collectionMethod: 'charge_automatically',
  metadata: { internal_signup_id: 's_42' },
});

console.log(sub.status); // → 'trialing' (or 'active' if trialDays omitted)

priceId vs planId. Historically, Plugipay carried a separate priceId — the per-currency variant of a plan. In current accounts the plan and price IDs match for single-currency plans, but the SDK still expects both for forward compatibility with multi-currency plans.

subscriptions.get

Signature. plugipay.subscriptions.get(id): Promise<Subscription>

const sub = await plugipay.subscriptions.get('sub_01HX...');
console.log(sub.status, sub.currentPeriodEnd);

subscriptions.list

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

Filters: status, customerId, planId, limit. Pagination via cursor — see Pagination.

// All past-due subs in the workspace (for dunning).
const { data } = await plugipay.subscriptions.list({ status: 'past_due', limit: 100 });

subscriptions.cancel

Signature. plugipay.subscriptions.cancel(id, at?: 'now' | 'period_end'): Promise<Subscription>

Cancels a subscription. Default is 'period_end' — the customer keeps service until the current period closes, then status flips to 'canceled' and no further invoices are auto-issued. Pass 'now' to terminate immediately.

// Customer-friendly cancellation: keep service through current period.
await plugipay.subscriptions.cancel('sub_01HX...');

// Immediate hard stop (e.g. fraud, chargeback):
await plugipay.subscriptions.cancel('sub_01HX...', 'now');

When the subscription is set to cancel at period end, cancelAtPeriodEnd flips to true but status stays 'active' until the period actually closes.

subscriptions.pause

Signature. plugipay.subscriptions.pause(id, resumeAt?: string): Promise<Subscription>

Pauses billing. While paused, no invoices are issued and the period doesn't advance. Optionally pass resumeAt (ISO-8601) to auto-resume on a schedule.

// Pause indefinitely until manually resumed:
await plugipay.subscriptions.pause('sub_01HX...');

// Pause for 30 days:
const in30 = new Date(Date.now() + 30 * 86_400_000).toISOString();
await plugipay.subscriptions.pause('sub_01HX...', in30);

subscriptions.resume

Signature. plugipay.subscriptions.resume(id): Promise<Subscription>

Resume a paused subscription. The current period continues from where it was paused (Plugipay tracks elapsed-vs-paused time on the subscription record).

await plugipay.subscriptions.resume('sub_01HX...');

Types

interface Subscription {
  id: string;                  // 'sub_...'
  accountId: string;
  customerId: string;
  planId: string;
  status: 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused' | 'incomplete';
  currentPeriodStart: string;
  currentPeriodEnd: string;
  cancelAtPeriodEnd: boolean;
  trialEndsAt: string | null;
  createdAt: string;
  updatedAt: string;
}

See API: Subscriptions for the full field reference including server-set fields, the initialDiscount calculation, and the full state-transition diagram.

Common patterns

Handle past_due with dunning

When a renewal payment fails, the subscription transitions to past_due and Plugipay emits plugipay.invoice.payment_failed.v1. Your dunning loop is to retry the payment a few times, email the customer, and eventually cancel:

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

app.post('/webhooks/plugipay', async (req, res) => {
  const event = verifyWebhook({
    body: req.rawBody,
    signature: req.header('Plugipay-Signature')!,
    secret: process.env.PLUGIPAY_WEBHOOK_SECRET!,
  });

  if (event.type === 'plugipay.invoice.payment_failed.v1') {
    const invoice = event.data.object;
    const sub = await plugipay.subscriptions.get(invoice.customerId);
    if (sub.status === 'past_due') {
      await sendDunningEmail(sub.customerId);
    }
  }
  res.sendStatus(200);
});

After N attempts, escalate to cancellation:

async function escalatePastDue(subId: string, attempts: number) {
  if (attempts >= 3) {
    await plugipay.subscriptions.cancel(subId, 'now');
  }
}

Pause-then-resume (snooze)

For a "skip next month" feature:

async function skipNextMonth(subId: string) {
  const sub = await plugipay.subscriptions.get(subId);
  const resumeAt = new Date(sub.currentPeriodEnd);
  resumeAt.setMonth(resumeAt.getMonth() + 1);
  await plugipay.subscriptions.pause(subId, resumeAt.toISOString());
}

Migrate to a new plan (with proration)

Plan migrations are a cancel-and-create pair, with initialDiscount on the new sub to credit unused time from the old one:

async function migrateToPlan(subId: string, newPlanId: string, newPriceId: string) {
  const old = await plugipay.subscriptions.get(subId);
  const remainingMs = Date.parse(old.currentPeriodEnd) - Date.now();
  const periodMs = Date.parse(old.currentPeriodEnd) - Date.parse(old.currentPeriodStart);
  const oldPlan = await plugipay.plans.get(old.planId);
  const creditAmount = Math.floor(oldPlan.amount * (remainingMs / periodMs));

  await plugipay.subscriptions.cancel(subId, 'now');
  return plugipay.subscriptions.create({
    customerId: old.customerId,
    planId: newPlanId,
    priceId: newPriceId,
    initialDiscount: creditAmount,
  });
}

Cancel at period end (graceful churn)

The most common cancel UX: customer clicks "cancel", you call cancel(id) (default 'period_end'), and they keep service until their next renewal date.

async function userCanceledSubscription(subId: string) {
  const sub = await plugipay.subscriptions.cancel(subId);
  return { effectiveAt: sub.currentPeriodEnd };
}

Until the period actually ends, subscriptions.resume() is not the right way to reverse it — that's for paused subs. To un-cancel, the API call is via the Portal or a direct client.request PATCH; the SDK doesn't currently expose a dedicated method.

Errors

Code Status Cause
validation_error 400 Missing customer/plan/price, bad trialDays.
not_found 404 Subscription, customer, or plan ID doesn't exist.
state_conflict 409 Pausing a canceled sub, cancelling a canceled sub, resuming a non-paused one.
payment_token_invalid 402 paymentTokenId doesn't belong to this customer or has been revoked.

See Errors for the SDK error model and API errors for the full catalog.

Next

Currency
What you pay Plugipay in. Rupiah settles by QRIS or bank transfer; US dollars go through PayPal.
Plugipay — Payments that don't tax your success