Embedded Checkout

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:

FieldTypeDefaultBehaviour
collect.shippingAddressbooleantrue, except with subscriptions (below)When true, the iframe renders the shipping address step and /complete requires shippingAddress.
collect.billingAddressbooleanfalse, with two exceptions (below)When true, the iframe emits a step: 'billing' postMessage and /complete requires billingAddress (or billingSameAsShipping: true).
Defaults match historical behaviour
Omitting 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.shippingAddress resolves to false, for example a subscriptions-only cart), and the application uses a tax provider (tax mode app_based) with an address trigger and the strict fallback 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
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
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
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
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
Collect-flag combinations and the resulting step sequence.

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 as shippingAddress: firstName, lastName, company?, addressLine1, addressLine2?, city, stateProvince, postalCode, countryCode (ISO-3166-1 alpha-2), phone?.
  • billingSameAsShipping — boolean, defaults to false. When true, the server copies the shipping address into the order's billing slot and you can omit billingAddress.
Distinct billing address
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
}
Same as shipping
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.

422 response
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 submitDisabled still composes additively — see Parent Controls.
  • With collect.billingAddress: true, the iframe emits an additional throttle.step.changed event with step: 'billing'.
  • With collect.shippingAddress: false and collect.billingAddress: false, the buyer lands directly on the payment step (this combo replaces the legacy ?mode=payment-only URL flag).

Migration: ?mode=payment-only

Deprecated, still supported for one release
The ?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