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 TrialDays set, no money moves on Create — 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

Plugipay — Payments that don't tax your success