Checkout sessions

A checkout session is a one-time, hosted payment intent. You create one with an amount, a currency, and an allow-list of payment methods; Plugipay returns an object with a HostedURL you redirect the customer to. When they pay — or, for offline methods, when you confirm — the session transitions to completed and emits a webhook. The Go SDK exposes the five checkout-session endpoints behind client.CheckoutSessions; every method takes context.Context first and returns (*plugipay.CheckoutSession, error) or (plugipay.Page[plugipay.CheckoutSession], error). For wire shapes, the full status lifecycle, and per-method validation rules, see API → Checkout sessions.

Field on the Client

client.CheckoutSessions — type *plugipay.CheckoutSessionsResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.

Methods

Create

Signature. func (r *CheckoutSessionsResource) Create(ctx context.Context, in CheckoutSessionCreateInput) (*CheckoutSession, error)

Opens a hosted checkout. Required fields: Amount, Currency, Methods, SuccessURL, CancelURL. LineItems is required by the API but the SDK normalizes a nil slice to [] for you, so passing none is fine. The SDK auto-mints an Idempotency-Key.

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

sess, err := client.CheckoutSessions.Create(ctx, plugipay.CheckoutSessionCreateInput{
    Amount:     199_000_00, // IDR 199,000
    Currency:   plugipay.CurrencyIDR,
    Methods:    []plugipay.CheckoutMethod{
        plugipay.CheckoutMethodQRIS,
        plugipay.CheckoutMethodVA,
    },
    SuccessURL: "https://example.com/thank-you",
    CancelURL:  "https://example.com/cart",
    LineItems: []plugipay.CheckoutSessionLineItem{
        {Name: "Annual Pro plan", Quantity: 1, UnitAmount: 199_000_00},
    },
    Metadata: map[string]string{"order_id": "ord_42"},
})
if err != nil {
    return err
}
// Redirect the buyer to sess.HostedURL.
http.Redirect(w, r, sess.HostedURL, http.StatusSeeOther)

One session, at most one payment. A session resolves to zero or one payments. For recurring billing use Subscriptions; for multi-charge flows, create one session per charge.

Get

Signature. func (r *CheckoutSessionsResource) Get(ctx context.Context, id string) (*CheckoutSession, error)

Fetch a session by id. The current Status tells you where the buyer is in the funnel. Naturally idempotent.

sess, err := client.CheckoutSessions.Get(ctx, "cs_01HX...")
if err != nil {
    return err
}
switch sess.Status {
case "completed":
    // payment captured
case "expired", "canceled":
    // terminal failures
case "open", "pending", "pending_review":
    // still live
}

List

Signature. func (r *CheckoutSessionsResource) List(ctx context.Context, params CheckoutSessionListParams) (Page[CheckoutSession], error)

Cursor-paginated. Filters: Status (string pointer — one of the lifecycle values) and CustomerID (filter to one buyer).

status := "completed"
custID := "cus_01HX..."

page, err := client.CheckoutSessions.List(ctx, plugipay.CheckoutSessionListParams{
    Status:     &status,
    CustomerID: &custID,
})
if err != nil {
    return err
}
for _, s := range page.Data {
    fmt.Println(s.ID, s.Status, s.Amount)
}

Cancel

Signature. func (r *CheckoutSessionsResource) Cancel(ctx context.Context, id string) (*CheckoutSession, error)

Voids an open, pending, or pending_review session. The hosted URL stops accepting payment immediately and serves a cancellation screen. Terminal sessions (completed / expired / canceled) reject the call with 409 conflict. Auto-keyed for idempotency — retrying after a transient failure is safe.

sess, err := client.CheckoutSessions.Cancel(ctx, "cs_01HX...")
if err != nil {
    var pe *plugipay.Error
    if errors.As(err, &pe) && pe.Code == "conflict" {
        // already terminal — treat as already-canceled
        return nil
    }
    return err
}
log.Printf("canceled %s", sess.ID)

Confirm

Signature. func (r *CheckoutSessionsResource) Confirm(ctx context.Context, id string) (*CheckoutSession, error)

Finalizes a session in flows where the buyer pays inline (typically the manual adapter — bank transfer, cash, EDC slip). Most teams never call this; the hosted page calls it on the buyer's behalf. Not auto-keyed — the operation is naturally idempotent server-side, but if you need belt-and-suspenders, drop to client.Do(...) with your own key.

sess, err := client.CheckoutSessions.Confirm(ctx, "cs_01HX...")
if err != nil {
    return err
}
log.Printf("confirmed → %s", sess.Status) // → "completed"

Types

type CheckoutSession struct {
    ID          string            `json:"id"`          // "cs_..."
    AccountID   string            `json:"accountId"`
    CustomerID  *string           `json:"customerId"`  // nil for guest
    Amount      int64             `json:"amount"`
    Currency    CurrencyCode      `json:"currency"`
    Status      string            `json:"status"`
    Methods     []CheckoutMethod  `json:"methods"`
    Adapter     *string           `json:"adapter"`     // routed provider
    LineItems   json.RawMessage   `json:"lineItems"`   // opaque to SDK
    SuccessURL  string            `json:"successUrl"`
    CancelURL   string            `json:"cancelUrl"`
    HostedURL   string            `json:"hostedUrl"`   // redirect target
    ExpiresAt   string            `json:"expiresAt"`
    CompletedAt *string           `json:"completedAt"`
    Metadata    map[string]string `json:"metadata"`
    CreatedAt   string            `json:"createdAt"`
    UpdatedAt   string            `json:"updatedAt"`
}

type CheckoutSessionLineItem struct {
    Name       string `json:"name"`
    Quantity   int64  `json:"quantity"`
    UnitAmount int64  `json:"unitAmount"` // smallest currency unit
}

CheckoutMethod is a string type with constants CheckoutMethodQRIS, CheckoutMethodVA, CheckoutMethodEwallet, CheckoutMethodCard, CheckoutMethodRetail, CheckoutMethodPaypal. LineItems on the returned struct is json.RawMessage because the wire shape evolves — decode it into your own slice if you need to inspect it. For the full status lifecycle and adapter routing rules: API → Checkout sessions.

Common patterns

Idempotent cart-to-session

The SDK's auto-keyed idempotency is per-call — a retry of the same Go statement mints a new key. If you want to dedupe by your own cart id (so a double-click on "Pay" never creates two sessions), use the low-level Do:

func createSessionForCart(ctx context.Context, c *plugipay.Client, cartID string, in plugipay.CheckoutSessionCreateInput) (*plugipay.CheckoutSession, error) {
    if in.LineItems == nil {
        in.LineItems = []plugipay.CheckoutSessionLineItem{}
    }
    var out plugipay.CheckoutSession
    err := c.Do(ctx, plugipay.RequestOptions{
        Method:         "POST",
        Path:           "/api/v1/checkout-sessions",
        Body:           in,
        IdempotencyKey: "cart_" + cartID, // your stable key
    }, &out)
    if err != nil {
        return nil, err
    }
    return &out, nil
}

The first call creates the session and stores the key; a retry within 24 hours returns the same session.

Polling for completion

For asynchronous methods (VA, retail) you can poll — though webhooks are recommended:

func pollUntilTerminal(ctx context.Context, c *plugipay.Client, id string) (*plugipay.CheckoutSession, error) {
    backoff := time.Second
    for {
        sess, err := c.CheckoutSessions.Get(ctx, id)
        if err != nil {
            return nil, err
        }
        switch sess.Status {
        case "completed", "expired", "canceled":
            return sess, nil
        }
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(backoff):
        }
        if backoff < 30*time.Second {
            backoff *= 2
        }
    }
}

Pair with Webhooks for production — polling is fine for tests but spends compute.

Struct opts for optional fields

func ptr[T any](v T) *T { return &v }

custID := "cus_01HX..."
expires := 30 * 60 // 30 minutes

sess, err := c.CheckoutSessions.Create(ctx, plugipay.CheckoutSessionCreateInput{
    Amount:       49_99,
    Currency:     plugipay.CurrencyUSD,
    Methods:      []plugipay.CheckoutMethod{plugipay.CheckoutMethodCard},
    SuccessURL:   "https://example.com/ok",
    CancelURL:    "https://example.com/no",
    CustomerID:   &custID,
    ExpiresInSec: &expires,
})

Errors

Code Status Cause
validation_error 400 Amount <= 0, bad URL scheme, unknown method.
not_found 404 Session id doesn't exist or is in another workspace.
conflict 409 Cancel/Confirm on a terminal session.
adapter_not_configured 422 Workspace has no adapter capable of these Methods.
insufficient_scope 403 Key lacks plugipay:checkout:write.
var pe *plugipay.Error
if errors.As(err, &pe) {
    switch pe.Code {
    case "adapter_not_configured":
        // surface "configure Xendit / Midtrans first" to the merchant
    case "validation_error":
        log.Printf("bad input: %s (requestId=%s)", pe.Message, pe.RequestID)
    }
}

Next

Plugipay — Payments that don't tax your success