Collection Flags
Tell Throttle which buyer addresses to collect during the embedded flow. Each session opts into shipping and billing capture independently — useful for digital-only carts, B2B Net 30 invoicing, and storefronts that already collected the address upstream.
The collect block
POST /api/v1/checkout/sessions accepts an optional collect object on the request body:
| Field | Type | Default | Behaviour |
|---|---|---|---|
collect.shippingAddress | boolean | true, except with subscriptions (below) | When true, the iframe renders the shipping address step and /complete requires shippingAddress. |
collect.billingAddress | boolean | false, with two exceptions (below) | When true, the iframe emits a step: 'billing' postMessage and /complete requires billingAddress (or billingSameAsShipping: true). |
collect on a one-off checkout is equivalent to { shippingAddress: true, billingAddress: false }.
Existing integrations that don't opt in keep the old shape.
On a checkout with subscriptions, an omitted collect turns the
shipping step on only when the cart holds a shippable one-time line; subscription lines never
ship. A collect object that leaves shippingAddress out still sets it to true. See Mixed Carts.
An omitted collect.billingAddress defaults to true in two cases, because otherwise the checkout has no
address to tax:
-
the application taxes from the billing address (tax trigger
billing_address); -
the session shows no shipping step (
collect.shippingAddressresolves tofalse, for example a subscriptions-only cart), and the application uses a tax provider (tax modeapp_based) with an address trigger and thestrictfallback policy, which refuses to complete a checkout it could not tax.
In that second case, with the shipping_address trigger, tax is
calculated from the billing address, and the order records metadata.taxTrigger: 'billing_address' so the filed
tax document uses the same address. It also records metadata.taxTriggerSource: 'checkout' to mark that value
as Throttle's: if the buyer retries a declined payment, the order's trigger is worked out
again for the new attempt, and a stamp with that mark is replaced or removed. A metadata.taxTrigger you
set on the cart or the session (the session's wins, as it does on the order) is never
overwritten, and it also replaces a value already on an order that a retry reuses. The quote
and the filed document both follow it, so shipping_address on a session with no shipping step is refused
with tax_address_missing under the strict policy. An explicit billingAddress: false always wins.
Worked combinations
Pick the combo that matches your storefront. Every example below is the request body for POST /api/v1/checkout/sessions.
Shipping only (default — physical goods, no separate billing)
curl -X POST https://api.usethrottle.dev/api/v1/checkout/sessions \
-H "X-API-Key: sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"applicationId": "e7efb0a6-892e-46b2-97ab-296bb04c5b29",
"externalCartId": "cart_abc",
"amount": 2999,
"currency": "USD",
"collect": {
"shippingAddress": true,
"billingAddress": false
}
}' Billing only (digital goods + invoicing)
Common for B2B Net 30 flows where shipping is irrelevant but the AR system needs a billing address.
curl -X POST https://api.usethrottle.dev/api/v1/checkout/sessions \
-H "X-API-Key: sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"applicationId": "e7efb0a6-892e-46b2-97ab-296bb04c5b29",
"externalCartId": "cart_abc",
"amount": 2999,
"currency": "USD",
"collect": {
"shippingAddress": false,
"billingAddress": true
}
}' Both (shipping + separate billing)
The buyer fills shipping, then is asked whether billing matches. If they uncheck "billing same as shipping", a billing form appears.
curl -X POST https://api.usethrottle.dev/api/v1/checkout/sessions \
-H "X-API-Key: sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"applicationId": "e7efb0a6-892e-46b2-97ab-296bb04c5b29",
"externalCartId": "cart_abc",
"amount": 2999,
"currency": "USD",
"collect": {
"shippingAddress": true,
"billingAddress": true
}
}' Neither (replaces ?mode=payment-only)
Use when your storefront has already collected the buyer's addresses. Send them as customer.shippingAddress and customer.billingAddress: they are stored on the session,
returned when you read it, and carried onto the order when the session completes. An address
submitted on /complete always wins over the one sent here.
curl -X POST https://api.usethrottle.dev/api/v1/checkout/sessions \
-H "X-API-Key: sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"applicationId": "e7efb0a6-892e-46b2-97ab-296bb04c5b29",
"externalCartId": "cart_abc",
"amount": 2999,
"currency": "USD",
"collect": {
"shippingAddress": false,
"billingAddress": false
},
"customer": {
"email": "[email protected]",
"shippingAddress": {
"addressLine1": "1 Market St",
"city": "San Francisco",
"stateProvince": "CA",
"postalCode": "94103",
"countryCode": "US"
},
"billingAddress": {
"addressLine1": "500 Howard St",
"city": "San Francisco",
"stateProvince": "CA",
"postalCode": "94105",
"countryCode": "US"
}
}
}' Flow: which steps render
The two boolean flags fan out into four distinct iframe step machines. Use this to confirm the buyer journey before you ship — especially when a connector requires a billing address regardless of shipping.
flowchart LR
Start([Session created]) --> S{collect.shippingAddress?}
S -- true --> AddrShip[address step]
S -- false --> Bcheck1{collect.billingAddress?}
AddrShip --> Bcheck2{collect.billingAddress?}
Bcheck2 -- true --> Bill[billing step]
Bcheck2 -- false --> Pay1[payment step]
Bcheck1 -- true --> BillOnly[billing step]
Bcheck1 -- false --> Pay2[payment step]
Bill --> Pay3[payment step]
BillOnly --> Pay4[payment step]
Pay1 --> Done([throttle.completed])
Pay2 --> Done
Pay3 --> Done
Pay4 --> Done Complete-session billing fields
When collect.billingAddress is true, the iframe POSTs
the billing details on /complete. Two new
request-body fields:
-
billingAddress— same shape asshippingAddress:firstName,lastName,company?,addressLine1,addressLine2?,city,stateProvince,postalCode,countryCode(ISO-3166-1 alpha-2),phone?. -
billingSameAsShipping— boolean, defaults tofalse. Whentrue, the server copies the shipping address into the order's billing slot and you can omitbillingAddress.
POST /api/v1/checkout-sessions/sess_xxx/complete
{
"paymentMethod": "embedded",
"processorTransactionId": "<transaction id from the payment embed>",
"shippingAddress": {
"firstName": "Jane",
"lastName": "Doe",
"addressLine1": "1 Market St",
"city": "San Francisco",
"stateProvince": "CA",
"postalCode": "94103",
"countryCode": "US"
},
"billingAddress": {
"firstName": "Jane",
"lastName": "Doe",
"addressLine1": "1 Market St",
"city": "San Francisco",
"stateProvince": "CA",
"postalCode": "94103",
"countryCode": "US"
},
"billingSameAsShipping": false
} POST /api/v1/checkout-sessions/sess_xxx/complete
{
"paymentMethod": "embedded",
"processorTransactionId": "<transaction id from the payment embed>",
"shippingAddress": { "...": "..." },
"billingSameAsShipping": true
} Validation errors
When a required address is missing, the server returns 422 address_required with a field extra so the iframe can highlight the right
step.
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "address_required",
"message": "Billing address is required for this session.",
"field": "billingAddress"
}
} Iframe behaviour
-
The Pay button auto-disables until every collect-flagged field is complete. Parent-set
submitDisabledstill composes additively — see Parent Controls. -
With
collect.billingAddress: true, the iframe emits an additionalthrottle.step.changedevent withstep: 'billing'. -
With
collect.shippingAddress: falseandcollect.billingAddress: false, the buyer lands directly on the payment step (this combo replaces the legacy?mode=payment-onlyURL flag).
Migration: ?mode=payment-only
?mode=payment-only URL query parameter is still
honoured client-side for one release window to avoid breaking existing storefronts. New
integrators should set
collect: { shippingAddress: false, billingAddress: false }
on session create instead — that path is server-authoritative, carries through to /complete validation, and is visible in webhook
payloads.
Next
- Embedded Checkout — full embed lifecycle and event reference.
- Parent Controls —
parent-side
submitDisabledgate.