Expanding & filtering
A few opt-in features keep responses tight by default and let you ask for more when you need it.
Expanding nested objects
By default, related objects are returned as IDs:
{ "id": "sub_…", "customer_id": "cus_…", "plan_code": "pro-monthly-usd"}Use the expand parameter to inline them:
GET /v1/subscriptions/{id}?expand=customer,plan,latest_invoice{ "id": "sub_…", "customer": { "id": "cus_…", "email": "…", … }, "plan": { "code": "pro-monthly-usd", "currency": "USD", … }, "latest_invoice": { "id": "inv_…", "status": "paid", … }}Multiple expansions are comma-separated. Nested expansions use dot
notation: expand=customer.payment_methods returns the customer
inlined, with their payment methods inlined inside that.
Each expansion costs a downstream join; don’t expand things you don’t need. Maximum depth is 3.
Field projection
Limit the response to specific fields with fields:
GET /v1/subscriptions/{id}?fields=id,status,current_period_end{ "id": "sub_…", "status": "active", "current_period_end": "…" }Combine with expand:
GET /v1/subscriptions/{id}?expand=customer&fields=id,status,customer.emailField projection is on the wire only — the server still computes the full object internally. You save bandwidth, not server time.
Filtering lists
List endpoints accept filters as query parameters. The filter name matches the field name on the resource:
GET /v1/subscriptions?status=active,past_dueGET /v1/invoices?customer_id=cus_…&status=openGET /v1/payments?created_at[gte]=2026-05-01T00:00:00Z&amount[gte]=100| Operator | Syntax | Notes |
|---|---|---|
| equals | field=v | Default. |
| in | field=v1,v2,v3 | OR within a single field. |
| not equals | field[ne]=v | |
| not in | field[nin]=v1,v2 | |
| greater than | field[gt]=v | |
| greater-or-equal | field[gte]=v | |
| less than | field[lt]=v | |
| less-or-equal | field[lte]=v | |
| starts with | field[starts]=v | String fields only. |
| contains | field[contains]=v | String fields only. |
| has key | metadata[has]=k | Metadata only. |
| key equals | metadata[k]=v | Metadata only. |
Filters compose with AND. There is no cross-field OR — issue multiple queries and union client-side if you need it.
Filtering on metadata
Every resource carries metadata (a flat map of strings up to 50
entries). Filter on it:
GET /v1/customers?metadata[your_user_id]=u_28471GET /v1/subscriptions?metadata[has]=imported_fromMetadata fields are indexed lazily — the first query against a metadata key may be slow; subsequent queries are fast. If you filter on the same key constantly, it’s already hot.
Date ranges
ISO-8601 with timezone:
?created_at[gte]=2026-05-01T00:00:00Z&created_at[lt]=2026-06-01T00:00:00ZHalf-open ranges ([gte] / [lt]) are easier to compose than
closed ranges; we recommend them.
Combining everything
GET /v1/invoices? status=open,past_due& customer_id=cus_…& total[gte]=100& created_at[gte]=2026-05-01T00:00:00Z& expand=customer& fields=id,status,total,due_at,customer.email& sort=due_at:asc& limit=100The most expensive call you can write probably looks like that. The server handles it; you may want to break it up if your client times out at 30 seconds.
What can’t be expanded or filtered
A few fields aren’t first-class:
- Computed fields (
upcoming_invoiceon a subscription,mrron a customer) cannot be filtered or sorted on. - PDF download URLs cannot be expanded — request them directly from
the resource’s
pdfendpoint. - Audit log entries on a resource — query
/v1/admin/audit-logseparately.
The OpenAPI doc is the authoritative list of expandable / filterable fields per endpoint.