> For the complete documentation index, see [llms.txt](https://help.dollarlabs.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.dollarlabs.io/dollarlabs-b2b-custom-pricing/developer-tools/ui-sdk-for-storefront.md).

# UI SDK for Storefront

Read customer prices, line totals, ordering rules and the next volume break for a custom storefront.

Enable the **Collection Pricing** app embed to use these JavaScript helpers. They read pricing for the current buyer; they do not change the cart or apply discounts themselves.

Everything lives on one object, `window.Dollarlabs`:

| Member                                            | Returns                        | Use it for                                                                                              |
| ------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `Dollarlabs.pricing.get(variantId, quantity = 1)` | `Promise<PricingAnswer>`       | Unit price, line total, ordering rules and the next volume break for one variant, from a single lookup. |
| `Dollarlabs.pricing.getMany(items)`               | `Promise<PricingAnswer[]>`     | The same answer for a whole grid or list, loaded in one request.                                        |
| `Dollarlabs.pricing.columns(variantId)`           | `Promise<PricingStringResult>` | Raw pricing strings and rule metadata for every applicable column.                                      |
| `Dollarlabs.buyer`                                | object                         | Who the page is priced for: login, tags, company, currency and country.                                 |
| `Dollarlabs.version`                              | number                         | The SDK version. Currently `1`.                                                                         |

The pricing functions return **Promises**. Always use `await`, including when the variant is already on the page. A variant not loaded yet is fetched through the Storefront API.

Numeric IDs and Shopify variant GIDs are accepted. Money is always in **cents**, and every amount comes with a formatted string that follows your store's money format.

## Wait for the SDK

Use the readiness flag and event rather than relying on `DOMContentLoaded`:

```js
function whenPricingReady(callback) {
  if (window.DollarlabsPricingReady) callback();
  else document.addEventListener('dollarlabs:pricing:ready', callback, { once: true });
}

whenPricingReady(async () => {
  const answer = await window.Dollarlabs.pricing.get('45132559613996', 1);
  const target = document.querySelector('[data-my-price]');
  if (target && answer.status !== 'unavailable' && answer.price.unitFormatted !== null) {
    target.textContent = answer.price.unitFormatted;
  }
});
```

Replace the example ID with your selected variant ID.

Keep the embed enabled even when **Rewrite theme product cards automatically** is off. Mark custom-rendered cards with `data-custom-price` so automatic rewriting leaves them alone. See [Collection page](/dollarlabs-b2b-custom-pricing/show-final-prices-on/collection-page.md#leave-selected-cards-alone).

## `Dollarlabs.pricing.get(variantId, quantity = 1)`

One call returns every part of the price for one variant at one quantity. All parts are read from the same lookup, so the unit price, line total and ordering rules always come from the same price-list column.

```js
const answer = await window.Dollarlabs.pricing.get(variantId, quantity);

if (answer.status === 'unavailable' || answer.line.totalFormatted === null) {
  // Keep the current display and offer a retry.
} else if (!answer.ordering.valid) {
  // Ask the buyer to choose a valid quantity, e.g. answer.ordering.next.
} else {
  unitElement.textContent = answer.price.unitFormatted;
  totalElement.textContent = answer.line.totalFormatted;
  if (answer.nextTier) nudgeElement.textContent = answer.nextTier.message;
}
```

Example: rolls of tape at $6 each, sold in cases of six for $24 (entered per case), with extra rolls at $5. The buyer asks for eight rolls:

```json
{
  "status": "ready",
  "column": "wholesale",
  "quantity": 8,
  "price": {
    "unit": 400,
    "unitFormatted": "$4.00",
    "retail": 600,
    "retailFormatted": "$6.00",
    "hasDiscount": true,
    "savingsPercent": 33.3
  },
  "line": {
    "total": 3400,
    "totalFormatted": "$34.00",
    "averageUnit": 425,
    "averageUnitFormatted": "$4.25",
    "caseQuantity": 6,
    "looseQuantity": 2,
    "caseUnit": 400,
    "caseUnitFormatted": "$4.00",
    "looseUnit": 500,
    "looseUnitFormatted": "$5.00"
  },
  "ordering": {
    "caseSize": 6,
    "minimum": 0,
    "floor": 1,
    "step": 1,
    "valid": true,
    "previous": 7,
    "next": 9,
    "looseAllowed": true,
    "looseMode": "split",
    "priceBasis": "case"
  },
  "nextTier": null
}
```

### Top-level fields

| Field      | Type                              | Meaning                                                                                                 |
| ---------- | --------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `status`   | `ready \| no_rule \| unavailable` | Lookup outcome. See [Handle missing pricing](#handle-missing-pricing).                                  |
| `column`   | string or null                    | Price-list column the buyer is charged by; null when none applies. The Default column is `__default__`. |
| `quantity` | number                            | Requested units, rounded down. Zero, negative or non-numeric input becomes `0`.                         |
| `price`    | object                            | Unit price at this quantity's tier.                                                                     |
| `line`     | object                            | What the whole line costs, with case and loose-unit breakdown.                                          |
| `ordering` | object                            | Minimums, case steps and the nearest allowed quantities.                                                |
| `nextTier` | object or null                    | The next volume break worth reaching; null when there is none.                                          |

### `price`

| Field             | Type           | Meaning                                                                                  |
| ----------------- | -------------- | ---------------------------------------------------------------------------------------- |
| `unit`            | number         | Displayed unit price at this quantity's tier, in cents.                                  |
| `unitFormatted`   | string or null | Formatted unit price; null when the catalog price is unknown.                            |
| `retail`          | number         | Catalog unit price in cents.                                                             |
| `retailFormatted` | string or null | Formatted catalog price.                                                                 |
| `hasDiscount`     | boolean        | Whether the unit price is below the catalog price.                                       |
| `savingsPercent`  | number         | Saving against the catalog price, rounded to one decimal place. Zero without a discount. |

`ready` does not guarantee `hasDiscount: true`: the current quantity may not reach a discounted tier.

### `line`

| Field                  | Type           | Meaning                                                                                                                                                                               |
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `total`                | number         | Line total in cents.                                                                                                                                                                  |
| `totalFormatted`       | string or null | Formatted line total; null when the catalog price is unknown.                                                                                                                         |
| `averageUnit`          | number         | `total ÷ quantity`, rounded to whole cents. This is what a cart line shows; it differs from `price.unit` once a case is broken. Do not multiply it to rebuild the total; use `total`. |
| `averageUnitFormatted` | string or null | Formatted average unit price.                                                                                                                                                         |
| `caseQuantity`         | number         | Units in the case-priced portion, **not number of cases**. Without a rule, this equals the requested quantity.                                                                        |
| `looseQuantity`        | number         | Units priced at the loose rate. In all-loose mode, an incomplete order can put every unit here.                                                                                       |
| `caseUnit`             | number         | Price per unit in the case-priced portion, in cents.                                                                                                                                  |
| `caseUnitFormatted`    | string or null | Formatted case-tier unit price.                                                                                                                                                       |
| `looseUnit`            | number or null | Loose price per unit in cents; null when there is no loose-priced portion, including a complete-case order.                                                                           |
| `looseUnitFormatted`   | string or null | Formatted loose-unit price.                                                                                                                                                           |

A six-roll case costs $24 here, but `caseUnit` is **400 cents ($4 per roll)**. `caseQuantity: 6` means one case of six.

A case-tier unit price is not a line total. For example:

| Pricing rule                                              | Quantity   | Correct total           |
| --------------------------------------------------------- | ---------- | ----------------------- |
| Six rolls per $24 case; extras $5 each                    | 8 rolls    | $24 + 2 × $5 = **$34**  |
| Twelve pencils per $24 case; incomplete orders $2.50 each | 13 pencils | 13 × $2.50 = **$32.50** |

The first rule uses **split** pricing. The second uses **all-loose** pricing. Multiplying `price.unit` by every unit gives the wrong result for both examples; use `line.total`.

### `ordering`

| Field          | Type           | Meaning                                                                                                                              |
| -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `caseSize`     | number         | Units per case; one without a case rule.                                                                                             |
| `minimum`      | number         | Configured minimum; zero when none is configured.                                                                                    |
| `floor`        | number         | First orderable quantity.                                                                                                            |
| `step`         | number         | Gap between allowed quantities: one when loose units are allowed, otherwise case size.                                               |
| `valid`        | boolean        | Whether the requested quantity is a positive quantity on the allowed ladder. This is not an inventory or checkout-eligibility check. |
| `previous`     | number or null | Previous allowed quantity; null at or below the floor.                                                                               |
| `next`         | number         | Next allowed quantity; the floor if the requested quantity is below it.                                                              |
| `looseAllowed` | boolean        | Whether extra units can be ordered outside complete cases.                                                                           |
| `looseMode`    | `split \| all` | Price extras separately, or price the whole incomplete order at the loose rate.                                                      |
| `priceBasis`   | `unit \| case` | Basis in which the rule's prices were entered.                                                                                       |

Valid quantities follow `floor + n × step`. For example, with cases of six, no loose units and a minimum of twelve, eighteen is valid with `previous: 12` and `next: 24`; thirteen is not valid.

Quantity zero always returns `valid: false`, a zero `line.total` and `nextTier: null`. An invalid quantity above zero can still return a calculated amount; do not present that as an allowed order.

```js
const { status, ordering } = await window.Dollarlabs.pricing.get(variantId, quantity);
if (status === 'ready') {
  quantityInput.min = ordering.floor;
  quantityInput.step = ordering.step;
  minusButton.disabled = ordering.previous === null;
}
```

### `nextTier`

| Field            | Type   | Meaning                                                                                                                                                                                                                                         |
| ---------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quantity`       | number | Quantity to set to reach the break. Always an orderable quantity.                                                                                                                                                                               |
| `itemsNeeded`    | number | Units to add to the requested quantity.                                                                                                                                                                                                         |
| `unit`           | number | Unit price at that break, in cents.                                                                                                                                                                                                             |
| `unitFormatted`  | string | Formatted unit price at that break.                                                                                                                                                                                                             |
| `savingsPercent` | number | Saving against the current tier's unit price, rounded to one decimal place.                                                                                                                                                                     |
| `message`        | string | English nudge. Its ending follows the rule's discount method: `$8.00 per unit` for a specific price, `20% off` for a percentage, `$2.00 off per unit` for a fixed amount. Build your own localized wording from the numeric fields when needed. |

Example: $10 per unit, with an $8 tier from ten units. At five units:

```json
{
  "quantity": 10,
  "itemsNeeded": 5,
  "unit": 800,
  "unitFormatted": "$8.00",
  "savingsPercent": 20,
  "message": "Add 5 more for volume pricing: $8.00 per unit"
}
```

The nudge follows the buyer's ordering rules and the way checkout prices tiers:

* It only suggests a quantity the buyer can actually order: at or above the minimum and on the case step.
* When one case jumps past several tiers, it quotes the tier the buyer actually lands on.
* It skips tiers that cost the same or more than the current price. A column whose prices rise with quantity returns `null`.

To preview the line total at the suggested quantity, call `pricing.get` again with `nextTier.quantity`.

### Tiers count complete cases

When a column has a case size, the volume tier is chosen on the units in **complete cases**, the same way checkout does. Loose units on top do not count toward the tier.

Example: cases of twelve with loose units allowed, and a cheaper tier from thirteen units. An order of fourteen is one case plus two loose units, so the tier is judged on twelve and the thirteen-unit rate does not apply. The nudge from fourteen points to `quantity: 24`, the first quantity that reaches the tier. An order below one full case is judged on its own quantity.

## `Dollarlabs.pricing.getMany(items)`

Use this for a grid, quick order list or comparison table. It loads every unknown variant in one Storefront API request (up to 250 per request) instead of one request per row, then answers each item exactly as `pricing.get` would.

Each item is either a variant ID, which uses quantity one, or an object with `variantId` and an optional `quantity`:

```js
const answers = await window.Dollarlabs.pricing.getMany([
  { variantId: '45132559613996', quantity: 8 },
  { variantId: 'gid://shopify/ProductVariant/45132559614028', quantity: 24 },
  '45132559614060',
]);

answers.forEach((answer, i) => {
  rows[i].querySelector('[data-price]').textContent =
    answer.line.totalFormatted ?? rows[i].dataset.fallbackPrice;
});
```

Answers come back in the same order as the items. Variants loaded by `getMany` are cached for the page, so a later `pricing.get` for one of them does not make another request.

A failed batch returns `status: "unavailable"` for the affected items. Calling `getMany` again retries them.

## `Dollarlabs.pricing.columns(variantId)`

Returns the raw pricing strings for every column that applies to the buyer, plus decoded rule metadata. Use it when you need to show the full tier ladder yourself. For calculated amounts, use `pricing.get`.

Example: the six-roll $24 case with $5 loose rolls:

```json
{
  "found": true,
  "status": "ready",
  "pricings": [
    {
      "tagName": "wholesale",
      "pricingString": "1:24;c:6;b:c;l:5",
      "isCustomerTag": true,
      "isDefault": false,
      "caseSize": 6,
      "minimum": 0,
      "priceBasis": "case",
      "looseAllowed": true,
      "looseMode": "split",
      "loosePrice": 5
    }
  ],
  "matchedTag": "wholesale",
  "customerTags": [
    "wholesale"
  ]
}
```

| Field          | Type                              | Meaning                                                       |
| -------------- | --------------------------------- | ------------------------------------------------------------- |
| `found`        | boolean                           | At least one applicable, non-empty pricing string was found.  |
| `status`       | `ready \| no_rule \| unavailable` | Lookup outcome.                                               |
| `pricings`     | array                             | Applicable pricing-column entries; can contain more than one. |
| `matchedTag`   | string or null                    | Column selected by the resolver; null when none is selected.  |
| `customerTags` | string array                      | Current buyer's tags, also returned when no rule is found.    |

Each entry in `pricings` contains:

| Field           | Type           | Meaning                                                                                                                                 |
| --------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `tagName`       | string         | Pricing column name; Default may be represented as `__default__`.                                                                       |
| `pricingString` | string         | Encoded tiers and buying rules. See [CSV grammar](/dollarlabs-b2b-custom-pricing/price-list/csv-import-and-export.md).                  |
| `isCustomerTag` | boolean        | Whether the buyer carries this tag.                                                                                                     |
| `isDefault`     | boolean        | Whether this is the fallback Default column.                                                                                            |
| `caseSize`      | number         | Units per case.                                                                                                                         |
| `minimum`       | number         | Configured minimum; zero when unset.                                                                                                    |
| `priceBasis`    | `unit \| case` | Basis of the stored pricing values.                                                                                                     |
| `looseAllowed`  | boolean        | Whether a positive loose value exists.                                                                                                  |
| `looseMode`     | `split \| all` | Handling of incomplete case orders.                                                                                                     |
| `loosePrice`    | number or null | First positive loose value in the stored tiers, or null. This is **raw rule data**, not cents or necessarily a resolved currency price. |

`loosePrice` follows the rule's discount method: it can mean a specific price, a percentage off or a fixed amount off. With different loose values at different tiers, this field alone does not tell you the value for a requested quantity. Use `pricing.get` for calculated amounts.

## `Dollarlabs.buyer`

A plain property, not a Promise. It reports who the page was priced for:

```json
{
  "loggedIn": true,
  "tags": ["wholesale"],
  "company": "Paper & Press Supply",
  "currency": "USD",
  "country": "CA"
}
```

| Field      | Type           | Meaning                                                             |
| ---------- | -------------- | ------------------------------------------------------------------- |
| `loggedIn` | boolean        | Whether a customer is signed in.                                    |
| `tags`     | string array   | The customer's tags; empty for a guest.                             |
| `company`  | string or null | The customer's current B2B company name, when Shopify provides one. |
| `currency` | string or null | Active presentment currency, e.g. `USD`.                            |
| `country`  | string or null | Buyer country used for catalog prices.                              |

Use it to choose what to show, for example a "Sign in for trade pricing" prompt for guests. Do not use it to decide which column applies; `column` on each pricing answer already tells you.

## Handle missing pricing

Every pricing answer includes `status`:

| Status        | Meaning                                  | What to do                                            |
| ------------- | ---------------------------------------- | ----------------------------------------------------- |
| `ready`       | An applicable pricing rule was resolved. | Render the result; check `ordering.valid` separately. |
| `no_rule`     | No applicable rule was found.            | Show the returned catalog price if available.         |
| `unavailable` | Pricing could not be loaded.             | Keep the existing price or show a retry state.        |

A value that is not a variant ID, such as an empty string or a product handle, returns `no_rule` straight away. It is never fetched, so it never reports `unavailable`.

A formatted price can be `null` when the variant's catalog price is unknown. Do not show a numeric zero as “free” without checking the formatted value. Calling the function again retries a failed lookup.

### Catalog price known, no app rule

For a $10 item at quantity two, `pricing.get` returns:

```json
{
  "status": "no_rule",
  "column": null,
  "quantity": 2,
  "price": { "unit": 1000, "unitFormatted": "$10.00", "retail": 1000, "retailFormatted": "$10.00", "hasDiscount": false, "savingsPercent": 0 },
  "line": { "total": 2000, "totalFormatted": "$20.00", "averageUnit": 1000, "averageUnitFormatted": "$10.00", "caseQuantity": 2, "looseQuantity": 0, "caseUnit": 1000, "caseUnitFormatted": "$10.00", "looseUnit": null, "looseUnitFormatted": null },
  "ordering": { "caseSize": 1, "minimum": 0, "floor": 1, "step": 1, "valid": true, "previous": 1, "next": 3, "looseAllowed": true, "looseMode": "split", "priceBasis": "unit" },
  "nextTier": null
}
```

The `ordering` values are unrestricted defaults. `pricing.columns` returns `found: false`, `status: "no_rule"`, `pricings: []` and `matchedTag: null`; `customerTags` still contains the buyer's tags.

### Lookup unavailable, catalog price unknown

`pricing.get` returns `status: "unavailable"` and `column: null`. All formatted fields are null, numeric prices and totals are zero placeholders, `line.looseUnit` is null and `nextTier` is null. The `ordering` block holds the same unrestricted defaults as above, so even `valid: true` is a fallback value, not confirmation that ordering is allowed.

`pricing.columns` returns `found: false`, `status: "unavailable"`, `pricings: []` and `matchedTag: null`; buyer tags are retained.

Keep the last confirmed display or offer retry. Unknown prices can also be null in a `no_rule` answer, so check the formatted value as well as status.

All examples use a USD storefront money format; the actual formatted text follows your store's format and currency context.

## Previews, not checkout

Use these answers for previews. After a cart update, display **Shopify's returned line totals and cart total**. The SDK does not replace checkout validation or account for every other discount, bundle, subscription or cart adjustment.

## Currency, variants and quick views

Catalog fetches use Shopify's buyer-country context. Keep the store's normal Shopify currency context available and test each market you support. Prefer the formatted fields over hard-coded currency symbols.

Call `pricing.get` again when the selected variant or quantity changes. In quick views, scope your price target to that dialog. If requests overlap, ignore an answer for an older selection so it cannot overwrite the new price.

## Earlier function names

Themes built before `window.Dollarlabs` keep working. These global functions are still available. They read the same cached pricing as `window.Dollarlabs`, so the numbers agree at every quantity.

| Earlier function                                           | Returns                            | New equivalent                         |
| ---------------------------------------------------------- | ---------------------------------- | -------------------------------------- |
| `DollarlabsGetBestPrice(variantId, quantity = 1)`          | `Promise<BestPriceResult>`         | `pricing.get`, `price` block           |
| `DollarlabsGetNextTierWarning(variantId, currentQuantity)` | `Promise<NextTierWarning \| null>` | `pricing.get`, `nextTier` block        |
| `DollarlabsGetPricingString(variantId)`                    | `Promise<PricingStringResult>`     | `pricing.columns` — identical response |

They all return **Promises**; always use `await`. They wait for the same `dollarlabs:pricing:ready` event. The tier, case-step and nudge rules described above apply to them too.

Differences to watch when you move a theme across:

* Each earlier function answers one part of the price, so a display that needs a price and a nudge makes two lookups. `pricing.get` returns all of them from one lookup.
* Field names differ. For example, `priceInCents` is now `price.unit` and `nextTierQuantity` is now `nextTier.quantity`.
* `DollarlabsGetNextTierWarning` returns prices in **currency units** (`9` means $9) and has no `status` field. The new `nextTier` block uses **cents**, like every other amount.
* Pass a finite, positive integer for quantity. These functions do not normalize it the way `pricing.get` does.

New work should use `window.Dollarlabs`.

### `DollarlabsGetBestPrice(variantId, quantity = 1)`

```js
const result = await window.DollarlabsGetBestPrice(variantId, quantity);
if (result.status !== 'unavailable' && result.formattedPrice !== null) {
  priceElement.textContent = result.formattedPrice;
}
```

Returns `Promise<BestPriceResult>`. Example: a $10 catalog item priced at $8 for this buyer:

```json
{
  "hasDiscount": true,
  "status": "ready",
  "formattedPrice": "$8.00",
  "priceInCents": 800,
  "originalPrice": 1000,
  "savingsPercentage": 20
}
```

| Field               | Type                              | Meaning                                                                                  | New field              |
| ------------------- | --------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------- |
| `hasDiscount`       | boolean                           | Whether the displayed price is below the catalog price.                                  | `price.hasDiscount`    |
| `status`            | `ready \| no_rule \| unavailable` | Whether a rule was resolved or the lookup failed.                                        | `status`               |
| `formattedPrice`    | string or null                    | Unit price in the storefront money format; null when unknown.                            | `price.unitFormatted`  |
| `priceInCents`      | number                            | Displayed unit price in cents.                                                           | `price.unit`           |
| `originalPrice`     | number                            | Catalog unit price in cents.                                                             | `price.retail`         |
| `savingsPercentage` | number                            | Saving against the catalog price, rounded to one decimal place. Zero without a discount. | `price.savingsPercent` |

`ready` does not guarantee `hasDiscount: true`: the current quantity may not reach a discounted tier. A unit price is not a line total; see [`line`](#line).

### `DollarlabsGetNextTierWarning(variantId, currentQuantity)`

```js
const nudge = await window.DollarlabsGetNextTierWarning(variantId, quantity);
if (nudge) {
  messageElement.textContent = nudge.message;
}
```

Returns `Promise<NextTierWarning | null>`. Example: ten units at $10 each, with a $9 tier from twelve:

```json
{
  "itemsNeeded": 2,
  "nextTierQuantity": 12,
  "nextTierPrice": 9,
  "nextTierPriceFormatted": "$9.00",
  "currentQuantity": 10,
  "currentTierPrice": 10,
  "currentTierPriceFormatted": "$10.00",
  "savingsPerUnit": 1,
  "savingsPercentage": 10,
  "tag": "wholesale",
  "discountMode": "specific_price",
  "message": "Add 2 more for volume pricing: $9.00 per unit"
}
```

| Field                       | Type                                           | Meaning                                                             | New field                 |
| --------------------------- | ---------------------------------------------- | ------------------------------------------------------------------- | ------------------------- |
| `itemsNeeded`               | number                                         | Units to add to reach the next tier.                                | `nextTier.itemsNeeded`    |
| `nextTierQuantity`          | number                                         | Quantity to set to reach the tier. Always an orderable quantity.    | `nextTier.quantity`       |
| `nextTierPrice`             | number                                         | Next unit price in **currency units**, e.g. `9` means $9.           | `nextTier.unit` (cents)   |
| `nextTierPriceFormatted`    | string                                         | Formatted next unit price.                                          | `nextTier.unitFormatted`  |
| `currentQuantity`           | number                                         | Quantity supplied to the function.                                  | `quantity`                |
| `currentTierPrice`          | number                                         | Current unit price in currency units.                               | `price.unit` (cents)      |
| `currentTierPriceFormatted` | string                                         | Formatted current unit price.                                       | `price.unitFormatted`     |
| `savingsPerUnit`            | number                                         | Difference between current and next unit prices, in currency units. | —                         |
| `savingsPercentage`         | number                                         | Saving against the current tier, rounded to one decimal place.      | `nextTier.savingsPercent` |
| `tag`                       | string                                         | Selected pricing column.                                            | `column`                  |
| `discountMode`              | `specific_price \| percentage \| fixed_amount` | Rule's discount method.                                             | —                         |
| `message`                   | string                                         | English nudge; its ending follows the discount method.              | `nextTier.message`        |

Unlike the other functions, this response has **no `status` field**. It returns `null` when there is no cheaper tier to reach, no applicable rule, a missing variant or quantity, or a failed lookup. A null response alone cannot tell a failed lookup from "no next tier"; use another function's `status` for that.

### `DollarlabsGetPricingString(variantId)`

Returns `Promise<PricingStringResult>`. The response is identical to [`Dollarlabs.pricing.columns`](#dollarlabs.pricing.columns-variantid), including the empty and unavailable shapes.

### Empty and unavailable responses

For a $10 item at quantity two with no app rule:

| Function       | Result                                                                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Best price     | `status: "no_rule"`, `hasDiscount: false`, `priceInCents: 1000`, `originalPrice: 1000`, `formattedPrice: "$10.00"`, `savingsPercentage: 0`. |
| Next tier      | `null`.                                                                                                                                     |
| Pricing string | `found: false`, `status: "no_rule"`, `pricings: []`, `matchedTag: null`; `customerTags` still contains the buyer's tags.                    |

When the lookup is unavailable and the catalog price is unknown:

| Function       | Result                                                                                                                         |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Best price     | `status: "unavailable"`, `hasDiscount: false`, `formattedPrice: null`; numeric price and savings fields are zero placeholders. |
| Next tier      | `null`; this function does not report a failure status.                                                                        |
| Pricing string | `found: false`, `status: "unavailable"`, `pricings: []`, `matchedTag: null`; buyer tags are retained.                          |

Calling the function again retries a failed lookup.
