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

Plugipay — Payments that don't tax your success