Skip to content
pricerulesv0.1.0
  • TypeScript types
  • 0 dependencies
  • Node 18+ · Edge

Node SDK

The official TypeScript client for the pricing API: typed responses, retries where they're safe, and a timeout. ESM and CommonJS, no runtime dependencies.

npm install pricerules
npmGitHubChangelog

Install

Run the install command above in your server project. Node 18 or later and edge runtimes such as Vercel Edge. Create an API key on the API page of the dashboard and set it as PRICERULES_API_KEY in your server's environment: the client reads it from there when you don't pass apiKey.

Quickstart

Acme's price on 120 valves, the request from the Quickstart:

server.tsTypeScript
import { PriceRules } from "pricerules";

const pricerules = new PriceRules({ apiKey: process.env.PRICERULES_API_KEY });

const { data } = await pricerules.resolve({
  customerId: "ACME-BUILD",
  products: [{ sku: "VLV-304", quantity: 120 }],
});

for (const item of data.items) {
  console.log(item.sku, item.final_price, item.applied_rule?.name, item.match_reason);
}

API keys stay on the server.

An API key can price every customer and product in your workspace, so use the SDK in server code only (API routes, backend services, edge functions), never in a browser page. The client refuses to start in a browser.

One customer: resolve

Up to 500 products per call. customerId is optional: without it, only rules for every customer apply. quantity defaults to 1, mode to business, and asOf (a YYYY-MM-DDday in your workspace's time zone, an RFC 3339 time with an offset, or a Date) to today. See rule dates and as_of.

resolveTypeScript
import { PriceRules } from "pricerules";

const pricerules = new PriceRules({ apiKey: process.env.PRICERULES_API_KEY });

const { data } = await pricerules.resolve({
  customerId: "ACME-BUILD",
  products: [
    { sku: "VLV-304", quantity: 120 },
    { sku: "FAS-1001", quantity: 10 },
  ],
  asOf: "2026-11-02",
});

for (const item of data.items) {
  console.log(item.sku, item.final_price, item.applied_rule?.name, item.match_reason);
}

Several customers: resolveBulk

Up to 10 requests per call, each with up to 500 products. data[i] answers requests[i], in order. Over a cap, the whole call is rejected (nothing priced or counted), never truncated.

resolveBulkTypeScript
import { PriceRules } from "pricerules";

const pricerules = new PriceRules({ apiKey: process.env.PRICERULES_API_KEY });

const { data } = await pricerules.resolveBulk({
  mode: "best_price",
  requests: [
    {
      customerId: "ACME-BUILD",
      products: [{ sku: "VLV-304", quantity: 120 }],
    },
    {
      customerId: "NORDIC-FAB",
      products: [
        { sku: "VLV-304", quantity: 10 },
        { sku: "FAS-1001", quantity: 50 },
      ],
    },
  ],
  asOf: "2026-11-02",
});

for (const entry of data) {
  console.log(entry.customer, entry.warnings, entry.items.length);
}

The response

Requests take camelCase parameters (customerId, asOf); responses come back exactly as the API sends them, in snake_case (final_price, applied_rule, match_reason), so the docs, the dashboard's API explorer and your logs all show the same names. Every field is typed; the API reference describes each one.

Reading the responseTypeScript
const { data, as_of, billing, meta } = await pricerules.resolve({
  customerId: "ACME-BUILD",
  products: [{ sku: "VLV-304", quantity: 120 }],
});

for (const item of data.items) {
  if (item.match_reason === "PRODUCT_NOT_FOUND") {
    // The SKU isn't in your workspace: base_price and final_price are null.
    continue;
  }
  item.final_price; // number
  item.applied_rule?.name; // the rule that set the price, or null
  item.applied_rule?.target; // who and what the rule was for
  item.applied_rule?.constraints; // minimum_quantity, valid_from, valid_to
}

data.customer; // { id, found, group }, or null without a customerId
data.warnings; // CUSTOMER_NOT_FOUND: an unknown code is priced as anonymous, not an error
as_of; // the day rule dates were checked against
billing; // { limit, usage, remaining } for this usage period
meta.request_id; // quote it to support

The value lists are exported too: MATCH_REASONS, ERROR_CODES, WARNING_CODES, RULE_TYPES, PRICING_MODES.

Errors

Every failure throws a PriceRulesError with the API's status, code, message, details and requestId (and retryAfter on a rate limit). An unknown customer (CUSTOMER_NOT_FOUND), an unknown SKU (PRODUCT_NOT_FOUND) or no rule (NO_RULE) isn't an error: the call succeeds, and the warning or match_reason says so.

Handling errorsTypeScript
import { PriceRules, PriceRulesError } from "pricerules";

const pricerules = new PriceRules({ apiKey: process.env.PRICERULES_API_KEY });

try {
  const { data } = await pricerules.resolve({ customerId: "ACME-BUILD", products: [{ sku: "VLV-304" }] });
  console.log(data.items);
} catch (error) {
  if (error instanceof PriceRulesError) {
    // Quote error.requestId to support.
    console.error(error.status, error.code, error.message, error.requestId);
    if (error.code === "VALIDATION_ERROR") console.error(error.details?.issues);
  }
  throw error;
}

The API's codes

  • INVALID_JSON400

    Retried: No: fix the request
  • VALIDATION_ERROR400

    Retried: No: fix the request
  • UNAUTHORIZED401

    Retried: No: check the key
  • MAX_PRODUCTS_EXCEEDED422

    Retried: No: split the request
  • MAX_REQUESTS_EXCEEDED422

    Retried: No: split the request
  • RATE_LIMIT_EXCEEDED429

    Retried: Yes, after Retry-After (a wait over 60 seconds is thrown with retryAfter set)
  • MONTHLY_LIMIT_REACHED429

    Retried: No: the period's quota is used up
  • INTERNAL_ERROR500

    Retried: No
  • PRICING_ENGINE_UNAVAILABLE503

    Retried: Yes, with backoff (nothing was priced or counted)

The SDK's own codes

  • TIMEOUT

    No response within timeout (PriceRulesTimeoutError). Retried: No
  • CONNECTION_ERROR

    The API couldn't be reached (PriceRulesConnectionError). Retried: Yes, with backoff
  • ABORTED

    Your signal aborted the call. Retried: No
  • UNEXPECTED_RESPONSE

    A response that isn't the API's format, e.g. from a proxy. Retried: Yes when 502/503/504, otherwise no
  • MISSING_API_KEY

    The constructor found no key. Retried: —
  • BROWSER_NOT_ALLOWED

    The constructor ran in a browser page. Retried: —

Retries and timeouts

Failed attempts are retried, up to maxRetries times (default 2), only when nothing was priced and waiting can help: after the Retry-After header on a rate limit, and with backoff (0.5 s, 1 s, 2 s…, at most 8 s, with jitter) on a 502, 503 or 504 or a connection error. Usage is only counted after a successful resolve, so a retried 503 is never charged.

Each attempt gets 10 seconds by default, reading the response included. Timeouts are not retried: the server may still be processing the request, and possibly counting it toward your usage. Raise timeout for large requests (500 products, or a bulk call).

Options

OptionsTypeScript
import { PriceRules } from "pricerules";

const pricerules = new PriceRules({
  apiKey: process.env.PRICERULES_API_KEY,
  timeout: 10_000, // milliseconds per attempt
  maxRetries: 2, // 0 turns retries off
});

// Per call: a shorter timeout and your own AbortSignal.
await pricerules.resolve({ products: [{ sku: "VLV-304" }] }, { timeout: 3_000, signal: AbortSignal.timeout(5_000) });
  • apiKeyDefault process.env.PRICERULES_API_KEY

    Your API key. Runtimes without process.env (such as Cloudflare Workers without nodejs_compat) must pass it.
  • baseUrlDefault https://pricerules.app/api/v1

    The API's base URL.
  • timeoutDefault 10000

    Milliseconds per attempt, reading the response included.
  • maxRetriesDefault 2

    Retries after a rate limit, a 502/503/504 or a connection error. 0 turns retries off.
  • fetchDefault the global fetch

    Your own fetch implementation, for example to go through a proxy.
  • dangerouslyAllowBrowserDefault false

    Lets the client start in a browser page. Don't: it would expose your API key.

Versions

Semantic versioning. The SDK is checked in CI against this API's published contract, so its types match what the API returns; the changelog is on GitHub.

Not using Node?

From any other language, call the REST API directly: it's one HTTPS POST with a JSON body, and every example in these docs has a curl version. See the API reference.