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 throughplug.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
PATCHpayload 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.paidevents.