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.shippingAddressbooleantrueWhen true, the iframe renders the shipping address step and /complete requires shippingAddress.
collect.billingAddressbooleanfalseWhen true, the iframe emits a step: 'billing' postMessage and /complete requires billingAddress (or billingSameAsShipping: true).
Defaults match historical behaviour
Omitting collect is equivalent to { shippingAddress: true, billingAddress: false }. Existing integrations that don't opt in keep the old shape.

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_production_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_production_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_production_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 and stamped them on metadata.customer_prefill.

curl
curl -X POST https://api.usethrottle.dev/api/v1/checkout/sessions \
  -H "X-API-Key: sk_production_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
    }
  }'

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: name, line1, line2?, city, region, postalCode, country.
  • 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": "card",
  "shippingAddress": {
    "name": "Jane Doe",
    "line1": "1 Market St",
    "city": "San Francisco",
    "region": "CA",
    "postalCode": "94103",
    "country": "US"
  },
  "billingAddress": {
    "name": "Jane Doe",
    "line1": "1 Market St",
    "city": "San Francisco",
    "region": "CA",
    "postalCode": "94103",
    "country": "US"
  },
  "billingSameAsShipping": false
}
Same as shipping
POST /api/v1/checkout-sessions/sess_xxx/complete

{
  "paymentMethod": "card",
  "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