Subscriptions
A subscription binds a customer to a plan and bills it on the plan's cadence. Each billing cycle Plugipay generates an invoice and (depending on collectionMethod) charges a stored payment method or sends the invoice for manual collection. The Go SDK exposes the six subscription methods behind client.Subscriptions. For wire shapes, lifecycle states, and proration rules see API → Subscriptions.
Field on the Client
client.Subscriptions — type *plugipay.SubscriptionsResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.
Methods
Create
Signature. func (r *SubscriptionsResource) Create(ctx context.Context, in SubscriptionCreateInput) (*Subscription, error)
Creates a subscription. Required: CustomerID, PlanID, PriceID. Optional: TrialDays, PaymentTokenID (for charge_automatically), CollectionMethod ("charge_automatically" or "send_invoice", default per plan), InitialDiscount, and Metadata. Auto-keyed for idempotency.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
trial := 14
collection := "charge_automatically"
tokenID := "ptok_01HX..."
sub, err := client.Subscriptions.Create(ctx, plugipay.SubscriptionCreateInput{
CustomerID: "cus_01HX...",
PlanID: "plan_01HX...",
PriceID: "price_01HX...",
TrialDays: &trial,
PaymentTokenID: &tokenID,
CollectionMethod: &collection,
Metadata: map[string]string{"team": "ops"},
})
if err != nil {
return err
}
log.Printf("sub %s status=%s", sub.ID, sub.Status)
Trial vs. immediate charge. With
TrialDaysset, no money moves onCreate— the first invoice posts when the trial ends. Without it, the first invoice posts immediately and charges (or queues for manual collection) the moment the subscription is created.
Get
Signature. func (r *SubscriptionsResource) Get(ctx context.Context, id string) (*Subscription, error)
Fetch one subscription. Naturally idempotent. Returns *plugipay.Error{Status: 404, Code: "not_found"} if missing or cross-workspace.
sub, err := client.Subscriptions.Get(ctx, "sub_01HX...")
if err != nil {
return err
}
fmt.Println(sub.Status, sub.CurrentPeriodEnd)
List
Signature. func (r *SubscriptionsResource) List(ctx context.Context, params SubscriptionListParams) (Page[Subscription], error)
Cursor-paginated. Filters: Status (one of "trialing", "active", "past_due", "paused", "canceled"), CustomerID, PlanID.
status := "active"
limit := 50
page, err := client.Subscriptions.List(ctx, plugipay.SubscriptionListParams{
Status: &status,
Limit: &limit,
})
for _, s := range page.Data {
log.Printf("%s plan=%s cust=%s", s.ID, s.PlanID, s.CustomerID)
}
Cancel
Signature. func (r *SubscriptionsResource) Cancel(ctx context.Context, id string, at string) (*Subscription, error)
Cancels a subscription. Pass "now" for immediate cancellation (issues a final pro-rated invoice if the plan opts into it) or "period_end" to let the current period run out (default). The SDK normalizes an empty at string to "period_end". Auto-keyed for idempotency — canceling twice is a no-op on the server.
// Cancel at end of period (most common):
sub, err := client.Subscriptions.Cancel(ctx, "sub_01HX...", "period_end")
// Cancel right now:
sub, err := client.Subscriptions.Cancel(ctx, "sub_01HX...", "now")
Cancel("...", "period_end") flips CancelAtPeriodEnd to true; the subscription keeps billing until CurrentPeriodEnd, then transitions to canceled.
Pause
Signature. func (r *SubscriptionsResource) Pause(ctx context.Context, id string, resumeAt *string) (*Subscription, error)
Pauses billing. Pass resumeAt (an ISO-8601 timestamp pointer) to schedule auto-resume, or nil for an indefinite pause that you'll lift with Resume. While paused, no invoices are generated. Auto-keyed for idempotency.
// Indefinite pause:
sub, err := client.Subscriptions.Pause(ctx, "sub_01HX...", nil)
// Pause until a specific date:
resume := "2026-08-01T00:00:00Z"
sub, err := client.Subscriptions.Pause(ctx, "sub_01HX...", &resume)
Resume
Signature. func (r *SubscriptionsResource) Resume(ctx context.Context, id string) (*Subscription, error)
Resumes a paused subscription. The next billing cycle begins on time.Now() (not the original anchor). Auto-keyed for idempotency.
sub, err := client.Subscriptions.Resume(ctx, "sub_01HX...")
Types
type Subscription struct {
ID string `json:"id"` // "sub_..."
AccountID string `json:"accountId"`
CustomerID string `json:"customerId"`
PlanID string `json:"planId"`
Status string `json:"status"` // trialing | active | past_due | paused | canceled
CurrentPeriodStart string `json:"currentPeriodStart"`
CurrentPeriodEnd string `json:"currentPeriodEnd"`
CancelAtPeriodEnd bool `json:"cancelAtPeriodEnd"`
TrialEndsAt *string `json:"trialEndsAt"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
Field-level details: API → Subscriptions.
Common patterns
Listing a customer's active subscriptions
Customers may have multiple active subscriptions (different plans, different brands). The right primary key for a "what is this customer paying for?" view is (CustomerID, Status="active"):
func customerSubs(ctx context.Context, c *plugipay.Client, custID string) ([]plugipay.Subscription, error) {
status := "active"
limit := 100
var out []plugipay.Subscription
var cursor *string
for {
page, err := c.Subscriptions.List(ctx, plugipay.SubscriptionListParams{
CustomerID: &custID,
Status: &status,
Limit: &limit,
// Note: CustomerListParams doesn't have Cursor; use the low-level Do
// for cursored walks longer than 100 here.
})
if err != nil {
return out, err
}
out = append(out, page.Data...)
if !page.HasMore {
return out, nil
}
cursor = page.Cursor
_ = cursor // pass via Do(...) — params struct doesn't expose it
}
}
For deep pagination, drop into plugipay.DoList[plugipay.Subscription] directly — see Pagination.
Soft-cancel with a save offer
A typical retention flow: cancel at period end, expose CancelAtPeriodEnd in the UI so the customer can still reverse:
sub, err := c.Subscriptions.Cancel(ctx, subID, "period_end")
if err != nil { return err }
// In your UI: "You'll keep access until {sub.CurrentPeriodEnd}.
// [Reactivate] would call Update via the API to flip CancelAtPeriodEnd=false."
Reactivation is via the API's PATCH /subscriptions/:id { "cancelAtPeriodEnd": false } — the SDK doesn't expose a typed Update for subscriptions, so use client.Do(...):
err := c.Do(ctx, plugipay.RequestOptions{
Method: "PATCH",
Path: "/api/v1/subscriptions/" + subID,
Body: map[string]any{"cancelAtPeriodEnd": false},
}, nil)
Errors-aware retry on Create
Create is auto-keyed for idempotency, so a network-layer retry is safe:
var sub *plugipay.Subscription
for attempt := 0; attempt < 3; attempt++ {
sub, err = c.Subscriptions.Create(ctx, in)
if err == nil {
break
}
var pe *plugipay.Error
if !errors.As(err, &pe) || (pe.Status != 0 && pe.Status < 500 && pe.Status != 429) {
return err // non-retryable
}
time.Sleep(time.Duration(1<<attempt) * 200 * time.Millisecond)
}
Context cancellation
ctx, cancel := context.WithTimeout(parentCtx, 4*time.Second)
defer cancel()
sub, err := c.Subscriptions.Pause(ctx, id, nil)
The SDK aborts the in-flight call and returns *plugipay.Error{Code: "canceled"} if ctx is canceled.
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Missing PriceID, bad at value, unknown collection method. |
not_found |
404 | Subscription / customer / plan id doesn't exist. |
conflict |
409 | Resume a non-paused sub, cancel a canceled sub. |
payment_method_required |
422 | charge_automatically with no PaymentTokenID. |
insufficient_scope |
403 | Key lacks plugipay:subscription:write. |
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "payment_method_required" {
// surface "add a payment method" to the customer
}
Full mechanics: Errors.
Next
- Invoices — per-cycle billing documents.
- Portal sessions — let the customer cancel/pause themselves.
- API → Subscriptions — HTTP-level reference.