Plans

A plan is a recurring billing template — "Pro at IDR 299,000/month", "Team at USD 49/month with a 14-day trial". A plan doesn't bill anyone by itself; a subscription references a plan and binds it to a specific customer. One plan can back thousands of subscriptions.

The plan owns the shape of the offer (interval, trial, portal features); the prices nested under it own the amounts, one per currency. To raise a price, you don't mutate the existing one — you add a new price and migrate subscribers when you're ready.

For the portal walk-through see Portal → Plans; for the signing recipe see Authentication.

Plans are templates, not bills. Creating a plan never moves money. The first charge happens when a subscription that references it reaches the end of its trial (or immediately, if there's no trial).

Endpoints

Method Path Purpose
POST /v1/plans Create a plan
GET /v1/plans/:id Retrieve a plan
GET /v1/plans List plans
PATCH /v1/plans/:id Update a plan
POST /v1/plans/:id/archive Archive a plan
POST /v1/plans/:id/prices Add a price
PATCH /v1/prices/:id Activate/deactivate a price

All endpoints require plugipay:plan:read (reads) or plugipay:plan:create / plugipay:plan:write (mutations) on the calling key.

Create a plan

POST /v1/plans

Creates a plan with one or more nested prices. Idempotency-Key is required.

Body parameters:

Field Type Required Notes
name string (1–255 chars) yes Display name. Shown on checkout and the customer portal.
description string (≤1024 chars) no One-liner under the title.
interval day | week | month | year yes The billing period unit.
intervalCount integer ≥ 1 no, default 1 interval=month, intervalCount=3 is quarterly.
trialDays integer ≥ 0 no, default 0 Default trial for new subscriptions. Override per-subscription at signup.
portalFeatures object yes See portal features below.
dunningPolicyId string (dp_...) | null no Smart-retry policy applied on failed renewals.
usageAggregate sum | max | last | null no Required if any price has model=usage.
meteredUnit string (≤64 chars) | null no Display unit ("api_call", "GB").
active boolean no, default true Hide from checkout without archiving.
metadata object no Up to 50 string/string pairs. See Conventions.
prices array of price input yes, ≥1 entry At least one price; typically one per currency.

portalFeatures object — all five fields are required booleans:

{
  "selfServeCancel":   true,
  "selfServePause":    true,
  "selfServeUpgrade":  true,
  "selfServeDowngrade":false,
  "updatePaymentMethod": true
}

These enable the corresponding buttons in the hosted customer portal. Changing them later takes effect immediately for every subscriber on the plan.

Price input — the entries inside prices[]:

Field Type Required Notes
currency ISO 4217 (3 chars) yes IDR, USD, SGD, …
model flat | tiered | volume | usage yes See pricing models.
unitAmount integer ≥ 0 required for flat/usage Smallest unit (cents/rupiah).
tiers array of {upTo, unitAmount, flatAmount} required for tiered/volume upTo is integer or "inf". Must be strictly increasing.
taxMode inclusive | exclusive no, default exclusive Whether unitAmount already includes VAT.
active boolean no, default true Inactive prices are hidden from checkout.
// Node
import { PlugipayClient } from '@plugipay/node';

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

const plan = await plugipay.plans.create({
  name: 'Pro',
  description: 'Everything in Starter, plus team seats and SSO.',
  interval: 'month',
  intervalCount: 1,
  trialDays: 14,
  portalFeatures: {
    selfServeCancel: true, selfServePause: true,
    selfServeUpgrade: true, selfServeDowngrade: true,
    updatePaymentMethod: true,
  },
  prices: [
    { currency: 'IDR', model: 'flat', unitAmount: 299000, taxMode: 'exclusive' },
    { currency: 'USD', model: 'flat', unitAmount: 1900,   taxMode: 'exclusive' },
  ],
  metadata: { product: 'pro' },
});
# Python
from plugipay import Plugipay

plugipay = Plugipay(key_id=os.environ["PLUGIPAY_KEY_ID"], secret=os.environ["PLUGIPAY_SECRET"])

plan = plugipay.plans.create(
    name="Pro",
    interval="month",
    interval_count=1,
    trial_days=14,
    portal_features={
        "selfServeCancel": True, "selfServePause": True,
        "selfServeUpgrade": True, "selfServeDowngrade": True,
        "updatePaymentMethod": True,
    },
    prices=[
        {"currency": "IDR", "model": "flat", "unitAmount": 299000, "taxMode": "exclusive"},
        {"currency": "USD", "model": "flat", "unitAmount":   1900, "taxMode": "exclusive"},
    ],
    metadata={"product": "pro"},
)
// Go
client := plugipay.NewClient(os.Getenv("PLUGIPAY_KEY_ID"), os.Getenv("PLUGIPAY_SECRET"))

plan, err := client.Plans.Create(ctx, &plugipay.PlanCreateParams{
    Name:          "Pro",
    Interval:      "month",
    IntervalCount: 1,
    TrialDays:     14,
    PortalFeatures: plugipay.PortalFeatures{
        SelfServeCancel: true, SelfServePause: true,
        SelfServeUpgrade: true, SelfServeDowngrade: true,
        UpdatePaymentMethod: true,
    },
    Prices: []plugipay.PriceInput{
        {Currency: "IDR", Model: "flat", UnitAmount: 299000, TaxMode: "exclusive"},
        {Currency: "USD", Model: "flat", UnitAmount:   1900, TaxMode: "exclusive"},
    },
    Metadata: map[string]string{"product": "pro"},
})
# curl
plugipay_curl POST '/v1/plans' '{
  "name": "Pro",
  "interval": "month",
  "intervalCount": 1,
  "trialDays": 14,
  "portalFeatures": {
    "selfServeCancel": true, "selfServePause": true,
    "selfServeUpgrade": true, "selfServeDowngrade": true,
    "updatePaymentMethod": true
  },
  "prices": [
    { "currency": "IDR", "model": "flat", "unitAmount": 299000, "taxMode": "exclusive" }
  ]
}'

Returns 201 Created with the plan object (prices inlined).

Possible errors:

  • 400 VALIDATION_ERRORunitAmount missing for a flat price, tiers not strictly increasing, missing portalFeatures.
  • 400 UNSUPPORTED_CURRENCY — the currency code is unknown or not enabled on your account.
  • 409 IDEMPOTENCY_MISMATCH — same Idempotency-Key, different body.

Retrieve a plan

GET /v1/plans/:id
const plan = await plugipay.plans.get('pln_01HXxxxxxxxxxxxxxxxxxxxxxx');
plugipay_curl GET '/v1/plans/pln_01HXxxxxxxxxxxxxxxxxxxxxxx'

Returns 200 OK with the plan object, or 404 NOT_FOUND.

List plans

GET /v1/plans

Query parameters:

Param Type Default Notes
limit integer 1–100 20
cursor opaque string From meta.cursor of a previous response.
order asc | desc desc By creation time.
active boolean (all) true shows live plans; false shows archived. Omit for both.
// Iterate every live plan.
let cursor;
do {
  const page = await plugipay.plans.list({ active: true, limit: 100, cursor });
  for (const plan of page.data) console.log(plan.id, plan.name);
  cursor = page.cursor;
} while (cursor);
plugipay_curl GET '/v1/plans?active=true&limit=100'

Returns 200 OK with data as an array of plan objects and pagination metadata in meta.page. See Pagination for the cursor recipe.

Update a plan

PATCH /v1/plans/:id

Updates the mutable fields. Idempotency-Key is required.

Field Mutable? Notes
name yes Cosmetic. Existing subscribers see the new name on their next invoice.
description yes Cosmetic.
trialDays yes Applies to new subscriptions only. Existing trials are unaffected.
portalFeatures yes Takes effect immediately for every subscriber.
dunningPolicyId yes Takes effect on the next failed renewal.
active yes Setting false is not the same as archiving — it hides the plan from checkout but doesn't set archivedAt.
metadata yes Send null to clear all keys.
interval, intervalCount, currency, amount no Add a new price and migrate subscribers via subscriptions.update.

Price fields don't mutate — they version. To change the amount on a live plan, add a new price, deactivate the old one, and update each subscription's priceId on your own schedule. This is by design: silently bumping prices on existing subscribers triggers chargebacks. See Portal → Plans → Editing a plan.

const plan = await plugipay.plans.update('pln_01HXxxxxxxxxxxxxxxxxxxxxxx', {
  description: 'Now with priority support.',
  metadata: { product: 'pro', featured: 'true' },
});
plugipay_curl PATCH '/v1/plans/pln_01HXxxxxxxxxxxxxxxxxxxxxxx' '{
  "description": "Now with priority support."
}'

Returns 200 OK with the updated plan object.

Archive a plan

POST /v1/plans/:id/archive

Sets archivedAt to now and active to false. Idempotency-Key is required.

An archived plan:

  • Is hidden from checkout — no new subscriptions can attach to it.
  • Is filtered out of GET /v1/plans by default. Pass ?active=false to see archived plans.
  • Keeps billing every existing subscription that already referenced it.

There is no DELETE /v1/plans/:id. Plans are kept forever for audit and reconciliation. To re-enable a plan after archiving, use PATCH with active: true — that clears archivedAt and brings it back into the default list.

Archive is not cancel. Archiving a plan stops new signups; it does not cancel existing subscriptions. To stop billing the people already on the plan, cancel each subscription (POST /v1/subscriptions/:id/cancel) individually or in batch.

await plugipay.plans.archive('pln_01HXxxxxxxxxxxxxxxxxxxxxxx');
plugipay_curl POST '/v1/plans/pln_01HXxxxxxxxxxxxxxxxxxxxxxx/archive' '{}'

Add a price

POST /v1/plans/:id/prices

Adds a price to a plan — a second currency, a new tier ladder, or the next version of an existing price. Body is a single price input. Idempotency-Key is required.

const price = await plugipay.plans.addPrice('pln_01HXxxxxxxxxxxxxxxxxxxxxxx', {
  currency: 'USD',
  model: 'flat',
  unitAmount: 2400, // raising USD price from $19 -> $24
  taxMode: 'exclusive',
});
plugipay_curl POST '/v1/plans/pln_01HXxxxxxxxxxxxxxxxxxxxxxx/prices' '{
  "currency": "USD", "model": "flat", "unitAmount": 2400, "taxMode": "exclusive"
}'

Returns 201 Created with the price object. New subscriptions can opt into it by passing the new priceId to subscriptions.create.

Activate or deactivate a price

PATCH /v1/prices/:id

Only active is mutable on an existing price. Use this to retire an old price after you've added a new one: deactivated prices are rejected by subscriptions.create, but existing subscriptions referencing them keep billing.

plugipay_curl PATCH '/v1/prices/pr_01HXxxxxxxxxxxxxxxxxxxxxxx' '{ "active": false }'

Sending any other field returns 400 VALIDATION_ERROR ("only active is mutable on price"). To change the amount, add a new price instead.

The plan object

{
  "id": "pln_01HXxxxxxxxxxxxxxxxxxxxxxx",
  "arn": "arn:plugipay:acc_01HX...:plan/pln_01HX...",
  "accountId": "acc_01HXxxxxxxxxxxxxxxxxxxxxxx",
  "name": "Pro",
  "description": "Everything in Starter, plus team seats and SSO.",
  "interval": "month",
  "intervalCount": 1,
  "trialDays": 14,
  "prices": [
    {
      "id": "pr_01HYxxxxxxxxxxxxxxxxxxxxxx",
      "planId": "pln_01HXxxxxxxxxxxxxxxxxxxxxxx",
      "currency": "IDR",
      "model": "flat",
      "unitAmount": 299000,
      "tiers": null,
      "taxMode": "exclusive",
      "active": true,
      "createdAt": "2026-05-12T10:42:00.123Z"
    }
  ],
  "usageAggregate": null,
  "meteredUnit": null,
  "portalFeatures": {
    "selfServeCancel": true, "selfServePause": true,
    "selfServeUpgrade": true, "selfServeDowngrade": true,
    "updatePaymentMethod": true
  },
  "dunningPolicyId": null,
  "active": true,
  "archivedAt": null,
  "metadata": { "product": "pro" },
  "createdAt": "2026-05-12T10:42:00.123Z",
  "updatedAt": "2026-05-12T10:42:00.123Z"
}
Field Type Notes
id string pln_ + ULID.
arn string Fully-qualified resource name, useful for cross-service references.
accountId string The workspace this plan belongs to.
name string Display name.
description string | null One-liner.
interval enum day | week | month | year.
intervalCount integer Combined with interval: e.g. month × 3 = quarterly.
trialDays integer Default trial length applied to new subscriptions.
prices array One price per currency/model. At least one is always present.
usageAggregate enum | null For usage-priced plans: how to roll up reported usage in a period (sum / max / last).
meteredUnit string | null The display unit ("api_call", "GB").
portalFeatures object See portal features.
dunningPolicyId string | null The smart-retry policy applied on failed renewals.
active boolean false hides from checkout without archiving.
archivedAt timestamp | null Set when the plan is archived.
metadata object | null Your own key/value pairs.
createdAt, updatedAt timestamp UTC ISO 8601.

Price objects

A price is the amount-and-currency leg of a plan. Subscriptions reference a price (not just a plan), so changing the price on a subscription is how you migrate one subscriber between price versions.

Field Type Notes
id string pr_ + ULID.
planId string Back-reference to the parent plan.
currency ISO 4217 One currency per price.
model enum See pricing models.
unitAmount integer | null Smallest unit. null for tiered/volume.
tiers array | null [{ upTo: 100, unitAmount: 500, flatAmount: 0 }, { upTo: "inf", unitAmount: 400, flatAmount: 0 }] — for tiered/volume.
taxMode inclusive | exclusive Whether unitAmount already includes VAT.
active boolean Inactive prices are rejected by subscriptions.create; existing subscribers are unaffected.
createdAt timestamp When this price was added to the plan.

Pricing models

Model When to use
flat Same amount every billing period. The default.
tiered Graduated: first 100 at IDR 5,000/unit, next 1,000 at IDR 4,000/unit, ….
volume Single-tier: the customer's total usage falls into one tier and the entire amount is priced at that tier's unitAmount.
usage Metered. subscriptions.reportUsage records consumption; the period's usageAggregate (sum/max/last) is multiplied by unitAmount at renewal.

Events

The plan resource emits these webhook events:

Event When
plan.created POST /v1/plans succeeded.
plan.updated A mutable field changed, a price was added, or a price's active flag flipped.
plan.archived POST /v1/plans/:id/archive succeeded.

The payload's data is the full plan object (with nested prices) at the moment the event fired. There is no separate price.* event family — price-level changes ride on plan.updated. To replay or audit, query Events (retained 30 days).

Next

  • Subscriptions — the resource that binds a plan to a customer and moves money.
  • Invoices — the per-period billing record generated from a plan + subscription.
  • Portal → Plans — the same concepts from the dashboard's point of view.
  • Pagination, Idempotency, Errors — the shared conventions every resource follows.
Plugipay — Payments that don't tax your success