Product Catalog

Line resolvers

For extension builders. The Product Catalog is a line resolver: the catalog carts a storefront creates are resolved carts, and Throttle calls the catalog to price their lines. This page describes the catalog's resolver, POST /throttle/line-resolver, as an example of the contract. Throttle's Line resolvers page is the full contract for building your own.

When Throttle calls it

Throttle never takes a price from the browser for a line the catalog owns. It calls the resolver with an intent:

intentWhenLines sent
addA line is added to the cart.That line.
updateA line’s quantity changes.That line.
refreshA shopper signs in with the cart, or the cart is refreshed.Every catalog line.
checkoutThe shopper starts checkout.Every catalog line. With “Reserve stock at checkout” on, the catalog also holds their stock for 15 minutes.

Throttle waits at most 3 seconds and does not retry an add or update (the shopper is waiting); it retries a checkout once.

The request

POST /throttle/line-resolver
{
  "type": "cart.lines.resolve",
  "id": "4b0f3a52-8f6e-4c4f-9d0a-3c1b2e7d9a10",
  "installationId": "62e32355-72ed-4bf6-b738-fcd554a3e851",
  "applicationId": "7f9d4c8a-5b2e-4f16-9a73-2d1e5c8b6f40",
  "environmentId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "cartId": "0f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
  "intent": "checkout",
  "currency": "USD",
  "customerId": "c3d4e5f6-0a1b-4c2d-8e3f-4a5b6c7d8e9f",
  "customerEmailVerified": true,
  "locale": "en-US",
  "lines": [
    {
      "lineId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
      "referenceId": "BRK-PAD-01",
      "quantity": 2
    },
    { "lineId": null, "referenceId": "BRK-FLUID-1L", "quantity": 5 }
  ]
}
  • type is always cart.lines.resolve, never a webhook event type; id is Throttle's request id.
  • installationId, applicationId and environmentId only say which installation to look up. The signature decides: the catalog checks that the signed body names the same application and environment as that installation.
  • customerId is the cart's customer, or null; group prices apply only when customerEmailVerified is true. locale picks the language of the line names.
  • 1 to 50 lines, each { lineId, referenceId, quantity }. referenceId is the variant SKU, matched exactly; the body is at most 64 KB.

The signature

Throttle signs every call with the installation's webhook signing secret, in the header X-Throttle-Resolver-Signature: t=<unix seconds>,v1=<hex>. The signed string is throttle.line-resolver.v1.<t>.<raw body>, HMAC-SHA256. The purpose label throttle.line-resolver.v1 keeps resolver and webhook signatures apart, though they share a secret: neither can be replayed as the other.

  • The catalog verifies the raw body before it parses it as JSON, with a constant-time compare.
  • t must be within 5 minutes; during a secret rotation the header carries one v1 per secret, and any match passes.
  • Every verification failure is the same 401, whatever the reason.

The answer

200, no-store, with one line per requested line, in order. A priced line carries ok: true and the name, unit price in minor units, currency, image, quantity limits and shipping flag; a refused line carries ok: false and a code. The answer has one other field, internal to Throttle's stock handling.

The answer’s lines
{
  "lines": [
    {
      "referenceId": "BRK-PAD-01",
      "ok": true,
      "name": "Ceramic brake pads, front",
      "unitPrice": 4999,
      "currency": "USD",
      "imageUrl": "https://cdn.example.com/brk-pad-01.jpg",
      "minQuantity": null,
      "maxQuantity": 10,
      "requiresShipping": true,
      "metadata": {
        "catalogInstallationId": "62e32355-72ed-4bf6-b738-fcd554a3e851",
        "catalogPriceMinor": 4999,
        "sku": "BRK-PAD-01",
        "variantId": "1042"
      }
    },
    {
      "referenceId": "BRK-FLUID-1L",
      "ok": false,
      "code": "QUANTITY_UNAVAILABLE",
      "maxQuantity": 3
    }
  ]
}
Refusal codeMeaning
NOT_FOUNDNo public variant with that SKU in this installation (another store’s SKU too).
NOT_PURCHASABLEA draft, archived or unpriced variant.
OUT_OF_STOCKA tracked variant with none on hand.
QUANTITY_UNAVAILABLENot enough available; maxQuantity is the most that can be ordered now, and can be 0.
BELOW_MINUnder the variant’s minimum order quantity.
ABOVE_MAXOver the variant’s maximum order quantity.
NO_PRICE_IN_CURRENCYNo price in the cart’s currency.
PRICE_SOURCE_MISMATCHThe source that set the price does not hold the cart’s currency.

The price always comes from the catalog, never from the request. Stock quantities are never answered: maxQuantity on a priced line is the order limit, and on a refused line only what a refused quantity needs.

The reserved metadata namespace

A priced line's metadata is stored on the Throttle line, and a browser can never write it. The catalog fills it with catalogInstallationId, catalogPriceMinor, sku and variantId. It is at most 2 KB of JSON; an answer over that is never sent.

Metadata is a hint, not proof
The catalog never trusts line metadata alone on an order: a line counts as a catalog line only when its SKU is one of the catalog's variants and its unit price is a price the catalog would have answered.

Refusals and failing closed

AnswerWhen
401 AUTHENTICATION_FAILEDA missing or bad signature, an unknown installation, or a body that names another store.
400 INVALID_REQUESTA signed body that is not a valid request.
409 INSTALLATION_NOT_ACTIVEThe installation is not active.
429 RATE_LIMITEDMore than 6,000 calls a minute for one installation. Retry-After: 60.
503 STOREFRONT_UNAVAILABLEThe catalog cannot answer right now, or its resolver is switched off.

Throttle fails closed. When the resolver times out, refuses the call or answers anything malformed (a missing line, an invalid line), the cart change is refused with 503 line_resolver_unavailable and nothing on the cart changes. Throttle never falls back to a browser price or a stale one. The catalog does the same on its side: if it cannot read the catalog, or would build an answer Throttle would refuse, it answers 503 rather than guess.

Storefronts never call this route; they see its results as cart errors.