Build Cart State That Checkout Can Trust
Use @usethrottle/cart when Throttle owns the cart, totals, and checkout handoff for your application.
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.
Create the cart
Start with applicationId and currency. Keep cart operations on your backend.
Add commerce state
Add items, apply discounts, select shipping, and calculate tax before checkout.
Lock totals for checkout
Run checkout_final calculation when the buyer commits to shipping and payment.
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.
- From the dashboard, copy your Application UUID (Settings → Identifiers) into
THROTTLE_APPLICATION_ID. - From Embed Config, add your storefront origin (e.g.
http://localhost:3000) toallowedOrigins. - 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 likesk_production_*.
applicationId, unitPrice, referenceId). Single shape — no snake_case aliases. See Conventions for the full rule.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.completedEnd-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.
npm install @usethrottle/cart// 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',
});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.
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',
},
},
});pk_ and are scoped by application and allowed origin. They can estimate totals, but they cannot mutate carts or create orders.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.
// 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.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.
// 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.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.
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
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.