Customers
A customer is the persistent record of someone who can pay you — the address book that sits under every charge, subscription, and invoice in your workspace. Guest checkouts work without one; the moment you want a second charge, a stored payment method, or a clean receipt history under a single identity, you reach for a customer. The Go SDK exposes the four customer endpoints behind client.Customers; every method takes context.Context first and returns either (*plugipay.Customer, error) or (plugipay.Page[plugipay.Customer], error). For wire shapes and the full field table, see API → Customers.
Field on the Client
client.Customers — type *plugipay.CustomersResource. The namespace is installed on the *Client returned by NewClient; it shares the same *http.Client, base URL, key, and OnBehalfOf default as the parent. There is no per-namespace state — all methods are safe to call concurrently from many goroutines.
Methods
Create
Signature. func (r *CustomersResource) Create(ctx context.Context, in CustomerCreateInput) (*Customer, error)
Creates a customer in the workspace this key belongs to. Every field on CustomerCreateInput is optional — a zero-value CustomerCreateInput{} is a legal call and returns a customer with only ID and timestamps populated, which you can fill in later via Update. The SDK auto-mints an Idempotency-Key for you, so retries against transient network failures don't create duplicates.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
email := "alice@example.com"
name := "Alice Tan"
extID := "user_19823"
cust, err := client.Customers.Create(ctx, plugipay.CustomerCreateInput{
Email: &email,
Name: &name,
ExternalID: &extID,
Metadata: map[string]string{"segment": "enterprise"},
})
if err != nil {
return err
}
log.Printf("created %s", cust.ID) // "cus_01HXAB7K3M9N2P5QRS8TVWXY3Z"
ExternalIDis unique per workspace. If you re-use one that already exists, the server returns409 conflictand the SDK surfaces*plugipay.Error{Code: "conflict"}. The find-or-create pattern below handles it cleanly.
Get
Signature. func (r *CustomersResource) Get(ctx context.Context, id string) (*Customer, error)
Fetches one customer by id. Returns *plugipay.Error{Status: 404, Code: "not_found"} for both missing-and cross-workspace ids — the server returns 404 (not 403) to avoid leaking existence.
cust, err := client.Customers.Get(ctx, "cus_01HXAB7K3M9N2P5QRS8TVWXY3Z")
if err != nil {
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "not_found" {
return nil // treat as absent
}
return err
}
fmt.Println(cust.Email)
Get is naturally idempotent — safe to retry on any transient failure.
List
Signature. func (r *CustomersResource) List(ctx context.Context, params CustomerListParams) (Page[Customer], error)
Cursor-paginated. Limit is 1–100 (default 25), Cursor is opaque from a previous page, and Email filters by exact match. See Pagination for the cursor model — it's identical across every list method on the SDK.
limit := 50
page, err := client.Customers.List(ctx, plugipay.CustomerListParams{
Limit: &limit,
})
if err != nil {
return err
}
for _, c := range page.Data {
fmt.Println(c.ID, deref(c.Email))
}
if page.HasMore {
next, err := client.Customers.List(ctx, plugipay.CustomerListParams{
Limit: &limit,
Cursor: page.Cursor,
})
_ = next
_ = err
}
Filtering by email:
email := "alice@example.com"
page, err := client.Customers.List(ctx, plugipay.CustomerListParams{Email: &email})
Email is not server-side unique — you may legitimately have two customers with the same address. Treat the result as a list, not a single value.
Update
Signature. func (r *CustomersResource) Update(ctx context.Context, id string, patch CustomerUpdateInput) (*Customer, error)
PATCH semantics — only the fields you pass are touched. To "clear" a nullable field, pass a pointer to the empty string (the API treats "" as null on these columns). Update is not auto-idempotency-keyed (the operation is naturally safe to repeat); if you have a transactional source of truth and want belt-and-suspenders, drop to client.Do(...) with your own key.
newName := "Alice T. Wijaya"
newPhone := "+62811xxxxxxxx"
cust, err := client.Customers.Update(ctx, id, plugipay.CustomerUpdateInput{
Name: &newName,
Phone: &newPhone,
})
The SDK does not surface Metadata on CustomerUpdateInput — the underlying endpoint treats metadata mutations as a separate concern. To touch metadata, use client.Do(...) directly.
Types
The returned shape:
type Customer struct {
ID string `json:"id"` // "cus_..."
AccountID string `json:"accountId"` // workspace
Email *string `json:"email"`
Name *string `json:"name"`
Phone *string `json:"phone"`
ExternalID *string `json:"externalId"`
CreatedAt string `json:"createdAt"` // ISO-8601
UpdatedAt string `json:"updatedAt"`
}
Pointer fields are nullable on the wire. Use a small helper if you find yourself dereferencing them often:
func deref(s *string) string {
if s == nil { return "" }
return *s
}
For per-field validation rules (length limits, email format), see API → Customers.
Common patterns
Find-or-create by external ID
Your CRM is the source of truth; Plugipay's ExternalID is a foreign key into it. The robust "find or create" uses the 409 conflict path:
func ensureCustomer(ctx context.Context, c *plugipay.Client, extID, email string) (*plugipay.Customer, error) {
cust, err := c.Customers.Create(ctx, plugipay.CustomerCreateInput{
ExternalID: &extID,
Email: &email,
})
if err == nil {
return cust, nil
}
var pe *plugipay.Error
if errors.As(err, &pe) && pe.Code == "conflict" {
// Already exists — cache miss on our side. Store the cus_ id
// when we first create the customer; this branch should be rare.
return nil, fmt.Errorf("cache miss for externalId=%s", extID)
}
return nil, err
}
In practice, store the resulting cus_* id on your side at create-time, keyed by your own user id.
Walking the entire customer book
func allCustomers(ctx context.Context, c *plugipay.Client) ([]plugipay.Customer, error) {
var out []plugipay.Customer
limit := 100
var cursor *string
for {
page, err := c.Customers.List(ctx, plugipay.CustomerListParams{
Limit: &limit,
Cursor: cursor,
})
if err != nil {
return out, err
}
out = append(out, page.Data...)
if !page.HasMore {
return out, nil
}
cursor = page.Cursor
}
}
This is the canonical Go cursor loop: copy Page[T].Cursor back into the next call, break when HasMore is false. Wrap in your own retry helper if the dataset is large enough to hit a transient blip.
Cancellation-aware lookup
context.Context is honored on every call:
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
cust, err := client.Customers.Get(ctx, id) // returns timeout if slow
If the parent context is canceled, the SDK aborts the in-flight request and returns *plugipay.Error{Code: "canceled"} (transport status 0).
Errors
The error codes you'll see most often on this namespace:
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Bad email format, name too long, unknown field. |
conflict |
409 | Duplicate ExternalID in this workspace. |
not_found |
404 | Customer id doesn't exist or lives in another workspace. |
insufficient_scope |
403 | Key lacks plugipay:customer:create (or read/update). |
Branch with errors.As:
var pe *plugipay.Error
if errors.As(err, &pe) {
switch pe.Code {
case "conflict": /* dup externalId */
case "not_found": /* missing id */
case "validation_error":
log.Printf("bad input: %s (requestId=%s)", pe.Message, pe.RequestID)
}
}
Full mechanics: Errors. Full code catalog: API → Errors.
Next
- Checkout sessions — charge a customer.
- Subscriptions — recurring billing for a customer.
- Portal sessions — let the customer self-serve.
- API → Customers — HTTP-level reference.