Paycose Docs
Setup

Expand

expand[] objects on retrieve and list responses.

Status: shipped. Affects every retrieve/list route in scope.

What it does

Pass ?expand[]=customer&expand[]=invoice on a wallet retrieve or list request and the response carries the hydrated customer / invoice object as a sibling of the flat customer_id / invoice_id fields. No expand → response is identical to today.

The flat ID stays present on every response. The sibling object appears only when the corresponding expand[] is in the query string. Nothing is removed; the addition is opt-in per request.

Default vs expanded shape

Examples use $WALLET for the API base — {base_url}/api/v1/{app}/platform/wallet (see Quick guide).

# Default — IDs only, byte-identical to pre-expand response
curl -u $KEY: "$WALLET/charges/chg_123"

{
  "id": "chg_123",
  "customer_id": "cus_abc",
  "invoice_id": "inv_xyz",
  ...
}

# With expand — sibling objects appear
curl -u $KEY: "$WALLET/charges/chg_123?expand[]=customer&expand[]=invoice"

{
  "id": "chg_123",
  "customer_id": "cus_abc",
  "customer": { "id": "cus_abc", "object": "customer", "name": "...", ... },
  "invoice_id": "inv_xyz",
  "invoice":  { "id": "inv_xyz", "object": "invoice", "total": ..., ... },
  ...
}

Grammar

  • Repeat the parameter for each path: ?expand[]=customer&expand[]=invoice.
  • Dot-notation walks nested resources: ?expand[]=invoice.customer hydrates the charge's invoice and that invoice's customer in one request.
  • Maximum depth is 4 segments. Going deeper returns 400 with expand path "..." exceeds max depth 4.
  • Path segments must match [a-z][a-z0-9_]*. Anything else returns 400.
  • Duplicates and overlapping paths are deduped: expand[]=customer&expand[]=customer.address&expand[]=customer is equivalent to expand[]=customer.address. The orchestrator walks each unique sub-tree once per page.

What can be expanded

The registry is centralized at backend/wallet/app/bootstrap/expand_registry.go. Every entry is one line — adding a new expandable field on a new (or existing) route is a one-line change there plus the response-struct interface methods on the target domain.

ResourceFields
balancecustomer
payment_methodcustomer
payoutcustomer
receiptcustomer
chargecustomer, invoice, payment_method, balance
invoicecustomer, quote, subscription
subscriptioncustomer, payment_method
transferfrom_customer, from_balance, to_customer, to_balance
refundcharge, transfer
credit_notecustomer, invoice, subscription
quotecustomer, invoice
balance_transactionbalance, transfer

Requesting a field that isn't registered for the resource returns 400.

Performance

Each (field, page) is one batch DB round-trip with the deduped parent IDs. A page of 20 charges with ?expand[]=customer&expand[]=invoice is exactly two extra queries — never N+1.

Failure mode

If a resolver fails on a single path, the sibling field is omitted (omitempty keeps it absent), the orchestrator logs the failure, and the rest of the response renders as if expand wasn't requested for that field. Other expands on the same request are unaffected. The HTTP response is still 200.

Out of scope (today)

  • Field-level partial selection (Stripe doesn't have it either; expand always returns the full target object).
  • Expand on POST / PUT bodies — only retrieve and list routes apply.
  • Expand on webhook payloads — webhooks have their own response shaping in app/common/http/webhook.publisher.go.

How to add a new expandable field

  1. On the parent response struct: add a pointer field (Customer *customer.CustomerResponse \json:"customer,omitempty"`) and the two ExpandableResponse methods (GetExpandableIDs, SetExpanded). If the type doesn't already implement Resource()`, add that too.
  2. Make sure the target domain has FindManyByIDs(ctx, ids, appCountryID) on its repository — copy the shape used by customer, charge, invoice, etc.
  3. One line in BuildExpandRegistry: r.Register("<resource>", "<field>", <targetResolver>(repos.<TargetRepo>)).

That's the whole thing. No handler change, no pagination change, no test change beyond a smoke test on the new field.

On this page