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)
priceIdvsplanId. Historically, Plugipay carried a separatepriceId— 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
- Invoices — the per-period bills auto-issued by subscriptions.
- Plans — the price catalog.
- Portal sessions — let customers self-serve cancel / pause.
- API: Subscriptions — HTTP reference.