Subscriptions

A subscription binds a customer to a plan (and a specific price on that plan) and tells Plugipay to bill on the plan's interval until cancelled or paused. The Python SDK wraps the six subscription endpoints behind plugipay.subscriptions. Updates beyond cancel/pause/resume drop down to plug.request(...) — see the HTTP reference at API → Subscriptions.

Namespace

plug.subscriptions      # _Subscriptions

Methods

create

plug.subscriptions.create(
    *,
    customer_id: str,
    plan_id: str,
    price_id: str,
    trial_days: int | None = None,
    payment_token_id: str | None = None,
    collection_method: str | None = None,
    metadata: dict[str, str] | None = None,
    initial_discount: int | None = None,
) -> Subscription

Creates a subscription. customer_id, plan_id, and price_id are required — a plan can carry multiple prices (e.g. monthly + yearly + promo) and the subscription locks to one. collection_method is "charge_automatically" (the default if omitted) or "send_invoice" to bill via emailed invoices. initial_discount is integer minor units off the first cycle. The SDK auto-generates an idempotency key.

sub = plug.subscriptions.create(
    customer_id="cus_01HX...",
    plan_id="pln_01HX...",
    price_id="prc_01HX...",
    trial_days=14,
    metadata={"campaign": "spring_launch"},
)
print(sub["id"])             # "sub_01HX..."
print(sub["status"])         # "trialing" while trial_days > 0

get

plug.subscriptions.get(subscription_id: str) -> Subscription

Polls current state. Status moves through trialing → active → past_due → cancelled (or paused).

sub = plug.subscriptions.get("sub_01HX...")
if sub["status"] == "past_due":
    notify_billing_team(sub["customerId"])

list

plug.subscriptions.list(
    *,
    limit: int | None = None,
    status: str | None = None,
    customer_id: str | None = None,
    plan_id: str | None = None,
) -> PageResult[Subscription]

Lists subscriptions. Filter by status to track dunning queues, by customer_id for "what's this user paying for?", by plan_id for plan migration jobs.

# All active subscribers on the Pro plan
cursor = None
active_pro = []
while True:
    page = plug.subscriptions.list(
        limit=100,
        status="active",
        plan_id="pln_pro",
    )
    active_pro.extend(page.data)
    if not page.has_more:
        break
    cursor = page.cursor

cancel

plug.subscriptions.cancel(
    subscription_id: str,
    *,
    at: str = "period_end",
) -> Subscription

at="period_end" (default) lets the current cycle finish, then cancels — buyer keeps service through the period they've paid for. at="now" cancels immediately and may prorate (see plan settings). SDK sends auto idempotency key.

plug.subscriptions.cancel("sub_01HX...")               # at period end
plug.subscriptions.cancel("sub_01HX...", at="now")     # immediate

pause

plug.subscriptions.pause(
    subscription_id: str,
    *,
    resume_at: str | None = None,
) -> Subscription

Suspends billing without losing the subscription record. resume_at is an ISO 8601 UTC string — if omitted, the subscription stays paused until you call resume. The body sent is {"resumeAt": resume_at} only when resume_at is truthy; otherwise an empty body.

plug.subscriptions.pause("sub_01HX...", resume_at="2026-07-01T00:00:00Z")

resume

plug.subscriptions.resume(subscription_id: str) -> Subscription

Lifts a pause. Billing picks up on the next scheduled cycle. SDK sends auto idempotency key.

plug.subscriptions.resume("sub_01HX...")

No SDK-exposed update. Plan-swaps, price-changes, discount edits, and quantity tweaks all go through plug.request("PATCH", "/api/v1/subscriptions/<id>", body={...}). The HTTP API documents the field set at API → Subscriptions.

Types

from plugipay import Subscription, PageResult

Subscription is a Resource subclass. Likely-read fields:

  • sub["id"]sub_ + ULID.
  • sub["status"]"trialing" | "active" | "past_due" | "paused" | "cancelled".
  • sub["customerId"], sub["planId"], sub["priceId"].
  • sub["currentPeriodStart"], sub["currentPeriodEnd"] — ISO 8601 UTC.
  • sub["cancelAt"] — non-null once cancelled at period end.
  • sub.get("metadata") — your pass-through bag.

Full reference at API → Subscriptions → The subscription object.

Common patterns

Trial-to-paid transition handler. Catch plugipay.subscription.trial_will_end.v1 (fires 72h before trial end) to upsell:

event = verify_webhook(raw_body, sig_header, secret)
if event.type == "plugipay.subscription.trial_will_end.v1":
    sub = event.object
    send_upsell_email(sub["customerId"], sub["currentPeriodEnd"])

Pause-or-cancel decision tree. When a buyer asks to "stop":

def stop_billing(plug, sub_id: str, *, refundable_window_days: int = 14):
    sub = plug.subscriptions.get(sub_id)
    if sub["status"] == "trialing":
        plug.subscriptions.cancel(sub_id, at="now")
    elif sub.get("metadata", {}).get("can_pause") == "true":
        plug.subscriptions.pause(sub_id)
    else:
        plug.subscriptions.cancel(sub_id, at="period_end")

Dunning sweep. Past-due subscriptions need a manual touch:

page = plug.subscriptions.list(limit=100, status="past_due")
for sub in page.data:
    customer = plug.customers.get(sub["customerId"])
    send_dunning_email(customer.get("email"), sub["id"])

Idempotent recreate after cancel(at="now"). Cancellation is final. To "uncancel" before the period ends you can swap to pause-then-resume; otherwise create a new subscription:

sub = plug.subscriptions.get(old_id)
if sub["status"] == "cancelled":
    new_sub = plug.subscriptions.create(
        customer_id=sub["customerId"],
        plan_id=sub["planId"],
        price_id=sub["priceId"],
        metadata={"resumed_from": sub["id"]},
    )

Errors

err.status err.code Cause
400 validation_error Missing customer_id / plan_id / price_id, bad at value.
400 price_plan_mismatch price_id doesn't belong to plan_id.
404 not_found Subscription, customer, plan, or price missing.
409 invalid_state_transition pause on a cancelled sub, resume on an active sub, cancel on an already-cancelled sub.
409 idempotency_key_reused Same Idempotency-Key against a different body.
422 payment_method_required collection_method="charge_automatically" but no payment_token_id and no default token on the customer.

All raise PlugipayError. See Errors.

Next

  • API → Subscriptions — every field plus the PATCH payload for plan/price changes.
  • Plans — what the subscription bills against.
  • Invoices — what collection_method="send_invoice" emits.
  • Webhooks — react to subscription.created, subscription.updated, invoice.paid events.
Plugipay — Payments that don't tax your success