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

Plugipay — Payments that don't tax your success