Native carts

Build Cart State That Checkout Can Trust

Use @usethrottle/cart when Throttle owns the cart, totals, and checkout handoff for your application.

1

Configure your application

Copy your Application UUID + add at least one allowedOrigins entry from the dashboard. POST /carts and the embed both refuse to render until these are set.

2

Create the cart

Start with applicationId and currency. Keep cart operations on your backend.

3

Add commerce state

Add items, apply discounts, select shipping, and calculate tax before checkout.

4

Lock totals for checkout

Run checkout_final calculation when the buyer commits to shipping and payment.

5

Convert to a draft order

POST /carts/{id}/finalize converts the cart to a draft order. (Legacy alias: /checkout — same handler.) To collect payment, mint a checkout session afterwards (see Embedded Checkout).

0. Configure your application

Following the SDK example verbatim before this step fails with missing_application_id on POST /carts, or the embed renders Embed not authorized. Both checks happen before any SDK error path.

  1. From the dashboard, copy your Application UUID (Settings → Identifiers) into THROTTLE_APPLICATION_ID.
  2. From Embed Config, add your storefront origin (e.g. http://localhost:3000) to allowedOrigins.
  3. From API Keys, create a secret key in the workspace environment you want to use. Non-production keys look like sk_uat_*; production keys look like sk_production_*.
Naming convention
Every field on the wire is camelCase (applicationId, unitPrice, referenceId). Single shape — no snake_case aliases. See Conventions for the full rule.
Sandbox payment provider auto-connects
You don't have to plug in a payment processor to test cart → checkout end-to-end. Every new Application gets a sandbox payment sub-account on signup, so a checkout session minted with an non-production environment key renders the Card Simulator out-of-the-box. See Workspace environments for provider routing details.

Who owns each step

Your backend owns cart mutations and secret-key calls. The browser renders buyer controls and the checkout iframe, but it never receives your sk_ key.

sequenceDiagram
  participant B as Browser
  participant M as Merchant Backend
  participant T as Throttle API
  participant C as Checkout Iframe
  B->>M: add item / apply code / choose shipping
  M->>T: POST /api/v1/carts
  M->>T: POST /api/v1/carts/{id}/items
  M->>T: POST /api/v1/carts/{id}/apply-discount
  M->>T: POST /api/v1/shipping-tax/carts/{id}/calculate
  T-->>M: totals + shipping/tax snapshot
  M->>T: POST /api/v1/checkout/sessions
  M-->>B: sessionId + embed URL
  B->>C: render checkout
  C->>T: POST /api/v1/checkout-sessions/{id}/complete
  T-->>B: throttle.completed
Native cart lifecycle from storefront interaction through checkout completion.

End-to-end cart flow

The cart client wraps the native cart endpoints and throws ThrottleApiError for non-2xx responses. It extends the shared ThrottleError (from @usethrottle/errors, re-exported by every SDK), so one instanceof ThrottleError catch handles errors from all Throttle SDKs. See the error reference.

Install
npm install @usethrottle/cart
Cart quickstart
// PREREQUISITES (Step 0 in /developers/quickstart):
//   1. Sign in to the dashboard. A default Application is auto-created for you.
//      Copy its UUID (App settings -> Identifiers) into THROTTLE_APPLICATION_ID.
//   2. In Embed Config, add your storefront origin (e.g. http://localhost:3000)
//      to allowedOrigins. The PaymentEmbed will render "Embed not authorized" until you do.
//   3. Generate a server-side secret key (for example sk_uat_* or sk_production_*)
//      from API Keys and put it in THROTTLE_API_KEY.
import { CartClient } from '@usethrottle/cart';

const client = new CartClient({
  apiKey: process.env.THROTTLE_API_KEY!,
});

const cart = await client.carts.create({
  applicationId: process.env.THROTTLE_APPLICATION_ID!, // uuid from Step 0.1
  currency: 'USD',
});

await client.items.add(cart.id, {
  type: 'product',
  name: 'Premium Widget',
  unitPrice: 2999, // minor units (cents)
  quantity: 2,
});

await client.discounts.apply(cart.id, 'SAVE10');

// Pass the address inline to set the destination AND get rates in one call
// (it is persisted to the cart) — no separate carts.update needed.
const quote = await client.shippingTax.calculateCart(cart.id, {
  kind: 'cart_estimate',
  shippingAddress: { countryCode: 'US', postalCode: '94107', stateProvince: 'CA' },
});

// /carts/{id}/checkout creates a DRAFT ORDER from the cart. It does NOT
// create a checkout session by itself. To render the payment embed, mint
// a checkout session afterwards (see /developers/embedded-checkout).
const order = await client.carts.checkout(cart.id, {
  paymentMethod: 'card',
});
Spec & playground
The full request/response schemas for every endpoint on this page live in the public OpenAPI 3.1 document at api.usethrottle.dev/openapi.json. Open it in Swagger UI to try requests against a sandbox key, or pipe it into openapi-typescript / openapi-fetch for typed client generation. The pre-generated client lives at @usethrottle/cart.

Shipping and tax

Use cart calculation from your backend when a cart already exists in Throttle. Use storefront quote tokens for read-only estimates before checkout.

Storefront quote
import { StorefrontQuoteClient } from '@usethrottle/cart';

const quotes = new StorefrontQuoteClient({
  applicationId: 'e7efb0a6-892e-46b2-97ab-296bb04c5b29',
  quoteToken: 'pk_publishable_quote_token',
  origin: 'https://shop.example.com',
});

const estimate = await quotes.quote({
  currency: 'USD',
  items: [
    {
      id: 'sku_123',
      quantity: 1,
      subtotalAmount: 12900,
      requiresShipping: true,
      taxCategory: 'standard',
    },
  ],
  addresses: {
    shipping: {
      countryCode: 'US',
      stateProvince: 'CA',
      postalCode: '90001',
    },
  },
});
Quote tokens are publishable
Quote tokens start with pk_ and are scoped by application and allowed origin. They can estimate totals, but they cannot mutate carts or create orders.
Lightweight email capture for recovery
carts.create and carts.update accept an optional customerEmail — a way to attach a buyer’s email to a cart without minting a full customer record (e.g. an “email me my cart” field). When set, an otherwise-anonymous abandoned cart becomes recoverable: the cart.abandoned webhook carries it as customer.email (with id: null). A fully linked customer’s email always takes precedence. Pass null on update to clear it.

The cart is the source of truth for shipping and totals

Selecting a shipping method is a single, atomic call. The response is the full cart with shippingTotal, taxTotal, and total already recomputed — you do not need a calculate-before or a recalculate-after round trip, and you do not need to keep a parallel copy of the selected method or totals in client state.

Atomic shipping select
// Selecting a shipping method is ONE call. The response is the full cart with
// shipping, tax, and total already recomputed — no separate calculate-before or
// recalculate-after, and no need to keep your own copy of the totals.
const cart = await client.shipping.select(cartId, {
  methodId: 'standard',
  displayName: 'Standard (3-5 days)',
  rateAmount: 799,
});

cart.selectedShipping; // { methodId, displayName, rateAmount, currency, ... }
cart.shippingTotal;    // 799
cart.taxTotal;         // recomputed
cart.total;            // recomputed

// GET /carts/{id} returns the same authoritative shape any time you need it:
const fresh = await client.carts.get(cartId);
fresh.selectedShipping; // bind your UI directly to this — it never drifts.
Don't shadow-copy totals
shipping.select and carts.get both return selectedShipping plus all totals and per-jurisdiction taxLines. Totals are recomputed on every cart mutation, so bind your UI directly to the returned cart. Keeping a separate copy of the shipping rate or tax total is the most common source of a stale Order Summary.

Addresses

carts.update accepts shippingAddress and billingAddress in one canonical shape — the same CartAddress type the SDK exports and the shipping/tax calculate endpoint uses. addressLine1, city, and countryCode (ISO-3166-1 alpha-2) are required; the rest (addressLine2, stateProvince, postalCode, firstName, lastName, phone, email) are optional.

Set a shipping address
// One canonical address shape (CartAddress). Required: addressLine1, city,
// countryCode (ISO-3166-1 alpha-2). Validated at write time.
await client.carts.update(cartId, {
  shippingAddress: {
    firstName: 'Ada',
    addressLine1: '1 Market St',
    addressLine2: 'Suite 400',
    city: 'San Francisco',
    stateProvince: 'CA',
    postalCode: '94105',
    countryCode: 'US',
  },
});

// ❌ line1 / state / country / zip are rejected with a did-you-mean hint —
//    they are NOT silently stored.
Non-canonical field names are rejected at write time
Sending line1, state, country, or zip returns a validation_error that names the offending field and the canonical replacement (e.g. line1 → addressLine1). This is deliberate: previously a mismatched address was stored verbatim and only failed much later at checkout with address_required.

Discounts

Native carts support percentage and fixed amount promotion codes. Preview a code in your storefront, then apply it to the cart before checkout so payment receives the discounted total.

Provider-owned carts
External carts are upstream-owned. Apply discounts in the provider first, then pass the provider-owned totals into Throttle checkout.

Native cart endpoints

Create

POST /api/v1/carts

Add item

POST /api/v1/carts/{id}/items

Preview discount

POST /api/v1/discounts/preview

Apply discount

POST /api/v1/carts/{id}/apply-discount

Remove discount

DELETE /api/v1/carts/{id}/discount

Select shipping

POST /api/v1/carts/{id}/shipping

Set tax lines

PUT /api/v1/carts/{id}/tax-lines

Finalize

POST /api/v1/carts/{id}/finalize

Finalize converts the cart to a draft order — not a checkout session
POST /api/v1/carts/{id}/finalize converts the cart to a draft order (status draft). It does not mint a checkout session or render an embed by itself. To collect payment, follow with POST /api/v1/checkout-sessions/embed-token and mount PaymentEmbed against the resulting sessionId. See Embedded Checkout.

The legacy alias POST /api/v1/carts/{id}/checkout still exists and hits the same handler — kept for backward compatibility, but new integrations should call /finalize.