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 never bills anyone by itself; a subscription references the plan and binds it to a specific customer. One plan can back thousands of subscriptions. The Go SDK exposes the five plan endpoints behind client.Plans; every method takes context.Context first and returns either (*plugipay.Plan, error) or (plugipay.Page[plugipay.Plan], error). For wire shapes and the full field table, see API → Plans.
Field on the Client
client.Plans — type *plugipay.PlansResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.
Methods
Create
Signature. func (r *PlansResource) Create(ctx context.Context, in PlanCreateInput) (*Plan, error)
Creates a plan. Name, Currency, Amount, and Interval are required — the input has no pointers because the API rejects missing values anyway. Amount is in the currency's smallest unit (rupiah for IDR, cents for USD). Interval is one of "day", "week", "month", "year". The SDK auto-mints an Idempotency-Key, so retries don't create duplicates.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
plan, err := client.Plans.Create(ctx, plugipay.PlanCreateInput{
Name: "Pro Monthly",
Currency: plugipay.CurrencyIDR,
Amount: 299_000_00, // IDR 299,000 in smallest units
Interval: "month",
})
if err != nil {
return err
}
log.Printf("plan %s ready", plan.ID) // "plan_01HX..."
Plans are templates, not bills. Creating a plan never moves money. The first charge happens when a subscription that references this plan reaches the end of its trial (or immediately, if there's no trial).
Get
Signature. func (r *PlansResource) Get(ctx context.Context, id string) (*Plan, error)
Fetch one plan by id. Returns *plugipay.Error{Status: 404, Code: "not_found"} if missing or cross-workspace. Naturally idempotent, safe to retry.
plan, err := client.Plans.Get(ctx, "plan_01HXAB7K3M9N2P5QRS8TVWXY3Z")
if err != nil {
return err
}
fmt.Printf("%s — %s %d / %s\n", plan.Name, plan.Currency, plan.Amount, plan.Interval)
List
Signature. func (r *PlansResource) List(ctx context.Context, params PlanListParams) (Page[Plan], error)
Cursor-paginated. Filters: Active (bool pointer — only active or only archived), Order ("asc" or "desc" on createdAt). Standard Limit/Cursor apply. See Pagination.
limit := 100
active := true
page, err := client.Plans.List(ctx, plugipay.PlanListParams{
Limit: &limit,
Active: &active,
})
if err != nil {
return err
}
for _, p := range page.Data {
log.Printf("%s = %s %d/%s", p.ID, p.Currency, p.Amount, p.Interval)
}
Update
Signature. func (r *PlansResource) Update(ctx context.Context, id string, patch PlanUpdateInput) (*Plan, error)
PATCH semantics — only the fields you pass are touched. The mutable fields are Name, Description, Active, and Metadata. To raise a price, do not mutate the plan in place — create a new plan and migrate subscribers when you're ready. Auto-keyed for idempotency.
newName := "Pro Monthly (Q3 wording)"
desc := "Best for teams of 1–10."
plan, err := client.Plans.Update(ctx, id, plugipay.PlanUpdateInput{
Name: &newName,
Description: &desc,
})
Archive
Signature. func (r *PlansResource) Archive(ctx context.Context, id string) (*Plan, error)
Soft-deactivates a plan. Existing subscriptions on this plan keep billing until you cancel or migrate them. New subscriptions that reference an archived plan are rejected with validation_error. Archive is reversible via Update(..., Active: ptrTrue). Auto-keyed for idempotency — retrying is a no-op.
plan, err := client.Plans.Archive(ctx, "plan_01HX...")
if err != nil {
return err
}
if !plan.Active {
log.Println("plan archived; existing subs keep billing")
}
Types
type Plan struct {
ID string `json:"id"` // "plan_..."
AccountID string `json:"accountId"` // workspace
Name string `json:"name"`
Currency CurrencyCode `json:"currency"` // CurrencyIDR | CurrencyUSD
Interval string `json:"interval"` // day | week | month | year
Amount int64 `json:"amount"` // smallest currency unit
Active bool `json:"active"`
CreatedAt string `json:"createdAt"` // ISO-8601
UpdatedAt string `json:"updatedAt"`
}
CurrencyCode is a string type with constants plugipay.CurrencyIDR / plugipay.CurrencyUSD. Use the constants in code — the compiler will catch a typo where "idr" (lowercase) wouldn't.
Field-level details (length limits, Interval validation): API → Plans.
Common patterns
Choose-or-create on startup
A common pattern for an app with a fixed set of tiers is "look up by Name, create if missing" on boot. There's no Name filter on List, so walk the active set:
func findOrCreatePlan(ctx context.Context, c *plugipay.Client, name string, amount int64) (*plugipay.Plan, error) {
active := true
limit := 100
page, err := c.Plans.List(ctx, plugipay.PlanListParams{
Limit: &limit, Active: &active,
})
if err != nil {
return nil, err
}
for _, p := range page.Data {
if p.Name == name {
return &p, nil
}
}
return c.Plans.Create(ctx, plugipay.PlanCreateInput{
Name: name,
Currency: plugipay.CurrencyIDR,
Amount: amount,
Interval: "month",
})
}
For more than 100 plans, drop into the cursor loop — see Customers → Walking the entire customer book.
Migrating from an old plan
To raise a price without grandfathering, archive the old plan + create a new one, then migrate subscribers:
old, _ := c.Plans.Archive(ctx, "plan_old")
neu, _ := c.Plans.Create(ctx, plugipay.PlanCreateInput{
Name: "Pro Monthly", Currency: plugipay.CurrencyIDR,
Amount: 349_000_00, Interval: "month",
})
// then walk subscriptions on `old` and re-create on `neu`.
_ = old; _ = neu
Subscription migration is a separate operation — see Subscriptions.
Struct opts for optional updates
PlanUpdateInput uses pointers for optional fields, so a tiny constructor avoids & noise:
func ptr[T any](v T) *T { return &v }
plan, err := c.Plans.Update(ctx, id, plugipay.PlanUpdateInput{
Name: ptr("Renamed plan"),
Active: ptr(false),
})
The ptr helper is idiomatic across the SDK — keep one in your project's util package.
Context cancellation
ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second)
defer cancel()
plan, err := c.Plans.Get(ctx, id)
If the parent ctx is canceled, the SDK returns *plugipay.Error{Code: "canceled", Status: 0} immediately.
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Bad Interval, Amount <= 0, unknown Currency. |
not_found |
404 | Plan id doesn't exist or is in another workspace. |
conflict |
409 | Trying to archive an already-archived plan. |
insufficient_scope |
403 | Key lacks plugipay:plan:write. |
Standard errors.As recipe:
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "validation_error" {
log.Printf("plan create rejected: %s (requestId=%s)", pe.Message, pe.RequestID)
}
Full mechanics: Errors.
Next
- Subscriptions — bind a plan to a customer.
- Checkout sessions — one-shot charges (no plan needed).
- API → Plans — HTTP-level reference.