npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@usethrottle/cart

v3.15.0

Published

Typed REST client for the Throttle Cart API.

Readme

@usethrottle/cart

Typed Node.js REST client for the Throttle Cart API.

Install

npm install @usethrottle/cart

Quickstart

End-to-end: create a cart, build it out, and convert it to an order.

import { CartClient } from '@usethrottle/cart';

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

// 1. Create a cart
const cart = await client.carts.create({
  applicationId: '7f9d4c8a-5b2e-4f16-9a73-2d1e5c8b6f40',
  currency: 'USD',
  netN: 45,
});

// 2. Add a line item
await client.items.add(cart.id, {
  type: 'product',
  name: 'Premium Widget',
  unitPrice: 2999,
  quantity: 2,
});

// 3. Select a shipping method
await client.shipping.select(cart.id, {
  methodId: 'fedex_ground',
  displayName: 'FedEx Ground',
  rateAmount: 599,
});

// 4. Apply a discount code
await client.discounts.apply(cart.id, 'SAVE10');

// 5. Set tax lines
await client.taxLines.set(cart.id, [
  { lineItemId: '...', jurisdictionCode: 'US-NY', taxType: 'sales', rate: 0.04, amount: 400 },
]);

// 6. Checkout → Order
const order = await client.carts.checkout(cart.id, { paymentMethod: 'card' });
console.log(order.id, order.status);

Use applicationId for every public request body. Deprecated store identifiers are rejected by the API.

All monetary values are integers in the smallest currency unit (cents for USD).

Browser carts (backend-less)

CartClient carries a secret sk_ key and must run on your server. For JAMstack / frontend-only storefronts that have no backend, use CartSessionClient instead — it creates and owns a Throttle cart directly from the browser, authorized by a storefront quote token (qt_…, the same token used by StorefrontQuoteClient) plus your application's origin allowlist. Quote tokens minted before qt_ start with pk_ and keep working. A pk_ publishable API key is a different credential and is refused with 401 invalid_quote_token.

Set your allowed origins first (PUT /api/v1/embed-config), then:

import { CartSessionClient } from '@usethrottle/cart';

const cart = new CartSessionClient({
  applicationId: '7f9d4c8a-5b2e-4f16-9a73-2d1e5c8b6f40',
  environmentId: 'a1b2c3d4-...',
  quoteToken: 'qt_...',
});

// Create a new session (store cart.cartSessionId in localStorage),
// or resume one you saved: cart.resume(savedId)
await cart.create({ currency: 'USD' });

await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 2 });
await cart.applyDiscount('SAVE10');
await cart.selectShipping({ methodId: 'fedex_ground', displayName: 'FedEx Ground', rateAmount: 599 });

// Set buyer-editable fields. `customerEmail` is what makes a guest cart
// visible to abandoned-cart recovery. `null` clears a field.
await cart.update({ customerEmail: '[email protected]', notes: 'Leave at the door' });

// Hand off to hosted/embedded checkout and redirect the buyer.
const { checkoutUrl } = await cart.checkout({
  returnUrl: 'https://shop.example.com/thanks',
  cancelUrl: 'https://shop.example.com/cart',
  // Leave the session `active` instead of `converted`, so a buyer who backs
  // out of checkout returns to a live cart instead of an expired one.
  keepOpen: true,
});

create() and get() also return a branding block (CartBranding: logoUrl, primaryColor, merchantName, each nullable) so a storefront rendering its own cart UI doesn't need a separate call to brand it.

To bind to an existing session without a publishable token — e.g. Throttle's own hosted cart page, or a storefront restoring a saved session id — use CartSessionClient.forSession() instead of new CartSessionClient():

const cart = CartSessionClient.forSession(savedCartSessionId);
await cart.addItem({ name: 'Premium Widget', unitPrice: 2999, quantity: 1 });

The opaque cart_… session id is scoped to exactly one cart. See the cart sessions guide.

Hosted cart page

Skip building cart UI entirely and send the buyer to Throttle's own hosted cart page with hostedCartUrl():

const session = await cart.create({
  metadata: {
    returnUrl: 'https://shop.example.com/thanks',
    cancelUrl: 'https://shop.example.com/cart',
    storefrontUrl: 'https://shop.example.com',
  },
});

location.href = cart.hostedCartUrl(); // https://checkout.usethrottle.dev/cart/cart_...

returnUrl, cancelUrl, and storefrontUrl in metadata are all optional. Omitting storefrontUrl just removes the hosted page's "Continue shopping" button — checkout still works. Omitting returnUrl or cancelUrl falls back to Throttle's own /complete page and this same hosted cart page, respectively.

Resolved carts

A resolved cart's lines are priced by an installed app (a line resolver, such as Product Catalog), never by the browser. Create the session with lineResolver and the environment's currency, then add lines by referenceId. Throttle asks the app for the price, stock and limits on every add, quantity change, sign-in merge and checkout.

import {
  QuantityUnavailableError,
  LineNotPurchasableError,
  LineResolverUnavailableError,
  CartPricesChangedError,
} from '@usethrottle/cart';

await cart.create({
  currency: 'USD', // required with lineResolver; must be the environment's currency
  lineResolver: { extensionId: '0b6a3c1e-8d2f-4a5b-9c7d-1e2f3a4b5c6d' },
  locale: 'en-US', // optional, passed to the app
});

try {
  await cart.addResolvedItem({ referenceId: 'TSHIRT-BLUE-M', quantity: 2 });
} catch (e) {
  if (e instanceof QuantityUnavailableError) showMessage(`Only ${e.maxQuantity ?? 0} available`);
  else if (e instanceof LineNotPurchasableError) showMessage('No longer available');
  else if (e instanceof LineResolverUnavailableError) showMessage('Try again in a moment');
  else throw e;
}

// Re-check every owned line (for example when the buyer comes back to the cart).
const { changed, ...current } = await cart.refresh();

try {
  const { checkoutUrl } = await cart.checkout({ returnUrl, cancelUrl });
  location.href = checkoutUrl;
} catch (e) {
  if (e instanceof CartPricesChangedError) {
    // The new prices are already applied; show them, then check out again.
    if (e.cart) render(e.cart);
  } else throw e;
}

addResolvedItem() sends only referenceId, quantity (default 1) and metadata. The app sets the name, price and image: sending unitPrice, name, imageUrl, taxAmount, discountAmount, a reserved metadata key (lineResolver, requiresShipping) or a metadata key that changes a shipping or tax quote answers 400 price_locked (PriceLockedError.fields names them). updateItem() keeps working for quantity and other metadata; a quantity change is re-checked by the app.

A resolved cart carries lineResolver: { extensionId } (extensionId is null if the app's installation is gone), and each line carries resolution: null on a line the app does not own, otherwise { status, code, maxQuantity, minQuantity }. status is resolved, refused (the app refused the line; code says why, for example OUT_OF_STOCK) or stale (the last re-check failed; checkout re-checks it). Carts created without lineResolver have neither key.

Shipping on a resolved cart is priced by Throttle, not the browser: pick a method Throttle quotes for the cart with cart.selectShippingMethod({ methodId }), which sends only the method id (the cart needs a shipping address first). A method Throttle does not quote answers 422 shipping_method_unavailable. If you use selectShipping() instead, a rateAmount or currency different from the quote answers 400 price_locked. There is no flat-rate fallback (no selection means shipping is not calculated yet). The checkout hand-off re-checks every line and re-quotes shipping; any change answers 409 cart_prices_changed with the updated cart and, when shipping moved, shipping: { methodId, previousAmount, amount, reason? } (amount is null with reason method_unavailable or quote_unavailable).

From your server (CartClient)

Other apps can integrate the same way with a secret key. Create the cart with lineResolver (and the environment's currency), optionally pricing up to 50 lines in one call with lineItems (all or nothing), then add lines by referenceId:

const cart = await client.carts.create({
  applicationId,
  currency: 'USD',
  lineResolver: { extensionId: '0b6a3c1e-8d2f-4a5b-9c7d-1e2f3a4b5c6d' },
  locale: 'en-GB',
  lineItems: [{ referenceId: 'TSHIRT-BLUE-M', quantity: 2 }],
});
const line = await client.items.addResolvedItem(cart.id, { referenceId: 'MUG-WHITE', quantity: 1 });

A server cart has no refresh: refresh() exists only on a browser cart session (CartSessionClient). From the server, a line is re-checked with the app when you change its quantity with items.update(cartId, itemId, { quantity }). To re-check the whole cart, mint a hosted-cart session for it (POST /api/v1/carts/{id}/sessions, which answers { sessionId, cartUrl, expiresAt }) and call CartSessionClient.forSession(sessionId).refresh(). An owned line's unitPrice, taxAmount, discountAmount and reserved metadata keys answer 400 price_locked for every caller. Both clients throw the same typed errors:

| Error class | Status / code | Typed fields (details) | | --- | --- | --- | | PriceLockedError | 400 price_locked | fields | | QuantityUnavailableError | 409 quantity_unavailable | reason, maxQuantity (or null) | | LineNotPurchasableError | 422 line_not_purchasable | reason on an add; lines at checkout | | LineResolverUnavailableError | 503 line_resolver_unavailable | reason (or null); nothing changed, retry | | LineResolverNotAvailableError | 422 line_resolver_not_available | reason; the app is not installed | | CurrencyNotEnabledError | 422 currency_not_enabled | details.currency, details.enabledCurrencies | | ResolvedLinesRequiredError | 422 resolved_lines_required | the store only accepts resolved lines | | LineLimitReachedError | 422 line_limit_reached | details.maxLines (50 per app) | | CartPricesChangedError | 409 cart_prices_changed | cart, shipping (or null) | | ShippingMethodUnavailableError | 422 shipping_method_unavailable | | | ShippingQuoteUnavailableError | 503 shipping_quote_unavailable | nothing changed, retry | | AmountTooLargeError | 422 amount_too_large | lower the quantity | | CartChangedError | 409 cart_changed | the cart changed during the add; retry |

Every class extends ThrottleApiError (and so ThrottleError), so existing instanceof ThrottleApiError and e.code checks keep working.

Net-N invoice terms

Cart-level invoice terms are optional. Pass netN on cart create or update to set the cart override, or null to clear it. The final invoice term is resolved when the buyer completes checkout with Invoice Terms:

customer.netN ?? cart.netN ?? DEFAULT_NET_N

allowedMethods only controls which payment methods can appear in checkout; it does not change the Net-N day count. For hosted/embed checkout, create a cart with netN and then create a checkout session for that cart.

Shipping and tax

Use the secret-key client on your backend to calculate and persist cart totals with Throttle's native engine:

const estimate = await client.shippingTax.calculateCart(cart.id, {
  kind: 'cart_estimate',
});

const locked = await client.shippingTax.calculateCart(cart.id, {
  kind: 'checkout_final',
  selectedShippingMethodId: estimate.shipping.selectedMethod?.id,
});

External stores can also request read-only storefront quotes with a storefront quote token (qt_…, or a legacy pk_… quote token). Create one from the dashboard Shipping & Tax page or with POST /api/v1/shipping-tax/quote-tokens.

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

const quotes = new StorefrontQuoteClient({
  applicationId: '7f9d4c8a-5b2e-4f16-9a73-2d1e5c8b6f40',
  quoteToken: 'qt_storefront_quote_token',
});

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' },
  },
});

For provider-owned totals, set the store to byo mode on the shipping or tax axis (or both) and push the final snapshot through /api/v1/shipping-tax/external-snapshots. Throttle accepts external pushes when at least one axis is in byo mode:

await client.shippingTax.pushExternalSnapshot({
  applicationId: '7f9d4c8a-5b2e-4f16-9a73-2d1e5c8b6f40',
  cartId: cart.id,
  currency: 'USD',
  totals: {
    subtotal: 12900,
    discountTotal: 0,
    shippingTotal: 600,
    taxTotal: 1215,
    total: 14715,
  },
});

Subscriptions

Put recurring on a line to make it a subscription. A cart can hold any number of one-time lines and up to 10 subscription lines, with the same plan or different ones. The whole cart is paid in one capture, with one shipping fee, and completes as one order linked to every subscription it created.

await client.items.add(cart.id, { name: 'Coffee kit', unitPrice: 4500, quantity: 1 });
await client.items.add(cart.id, {
  name: 'Barista Club',
  unitPrice: 2000,          // per period, per seat
  quantity: 2,              // seats
  recurring: { plan: 'club_monthly', interval: 'monthly', trialDays: 14 },
});
  • interval (weekly, biweekly, monthly, quarterly, yearly), or intervalUnit (hour, day, week, month) + intervalCount: 1 hour to 1 year.
  • totalPayments (2–60) makes an installment plan; trialDays (0–365) a free trial. They cannot be combined (400 invalid_combination).
  • type is forced to subscription, and the line is never charged shipping. It ships only with deliveryMethod: 'shipment', which makes a physical subscription: every renewal order owes a box. requiresShipping: true is rejected with 400 validation_error.
  • A free-trial line is stored with unitPrice: 0 and your price on recurring.amount, and its name gets a "— 14-day free trial" suffix.
  • PATCH may change quantity (seats) and metadata only. Changing unitPrice, taxAmount or discountAmount is 422 recurring_line_immutable. Remove the line and add it again to change its terms.
  • Each line's subscriptionId is set when the checkout completes.

| Status | Code | When | |---|---|---| | 422 | too_many_subscriptions | An 11th subscription line | | 422 | recurring_not_supported_for_cart | The cart is managed by an external cart provider | | 422 | recurring_line_immutable | A PATCH changes a subscription line's price, tax or discount | | 400 | invalid_line_item | A free-trial line carries taxAmount or discountAmount | | 400 | invalid_interval / interval_conflict / invalid_total_payments / invalid_combination / invalid_plan / invalid_trial_days | Terms out of range |

Do not also send a session recurring intent for the same cart. The two are exclusive, and session creation answers 422 recurring_source_conflict.

Events

Every mutation appends an immutable event to the cart's event log. Fetch it with:

const events = await client.events.list(cart.id);
// [{ eventType: 'cart.created', sequence: 1, ... }, ...]

// Poll only new events since the last sequence you saw
const newEvents = await client.events.list(cart.id, { sinceSequence: 5, limit: 50 });

For real-time delivery, subscribe to Throttle's outbound webhooks from your merchant dashboard. Webhooks are signed with HMAC-SHA256 and fire from Throttle's servers to your backend over HTTPS.

Errors

All non-2xx responses throw ThrottleApiError. Inspect code for the machine-readable reason and statusCode for the HTTP status:

import { ThrottleApiError } from '@usethrottle/cart';

try {
  await client.discounts.apply(cart.id, 'EXPIRED');
} catch (e) {
  if (e instanceof ThrottleApiError && e.code === 'discount_invalid') {
    // surface a user-friendly message
  }
  if (e instanceof ThrottleApiError) {
    console.error(e.statusCode, e.code, e.message);
  }
}

ThrottleApiError extends the shared ThrottleError from @usethrottle/errors, re-exported here. If you use more than one Throttle SDK you can catch all of their errors with a single check — instanceof ThrottleApiError keeps working:

import { ThrottleError } from '@usethrottle/cart'; // same class across every SDK

try {
  await cart.items.add(cartId, item);
  await checkout.completeSession(sessionId, payment);
} catch (e) {
  if (e instanceof ThrottleError) {
    console.error(e.statusCode, e.code, e.message);
  }
}

Line item imageUrl

imageUrl accepts an absolute http(s) URL or a relative path like /images/x.png. Relative paths are resolved to an absolute URL against your application's storefront base URL so the image renders on the hosted checkout — set it with PUT /v1/embed-config { "storefrontBaseUrl": "https://yourstore.com" } (the first allowedOrigin is used as a fallback). A relative path with no resolvable base returns 400 image_url_unresolvable.

Client options

| Option | Default | Description | | ----------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | apiKey | — | Required. Your Throttle secret key (sk_…). | | baseUrl | https://api.usethrottle.dev | Optional Throttle API host override for self-hosted/proxy setups. Workspace environment selection comes from the API key. | | timeoutMs | 30000 | Per-request abort timeout in ms. | | fetch | globalThis.fetch | Custom fetch implementation (e.g. node-fetch). |

See also