- 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.
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:
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.
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.
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.
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.
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 supportThe 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.
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_JSON400Retried: No: fix the requestVALIDATION_ERROR400Retried: No: fix the requestUNAUTHORIZED401Retried: No: check the keyMAX_PRODUCTS_EXCEEDED422Retried: No: split the requestMAX_REQUESTS_EXCEEDED422Retried: No: split the requestRATE_LIMIT_EXCEEDED429Retried: Yes, after Retry-After (a wait over 60 seconds is thrown with retryAfter set)MONTHLY_LIMIT_REACHED429Retried: No: the period's quota is used upINTERNAL_ERROR500Retried: NoPRICING_ENGINE_UNAVAILABLE503Retried: Yes, with backoff (nothing was priced or counted)
The SDK's own codes
TIMEOUTNo response within timeout (PriceRulesTimeoutError). Retried: NoCONNECTION_ERRORThe API couldn't be reached (PriceRulesConnectionError). Retried: Yes, with backoffABORTEDYour signal aborted the call. Retried: NoUNEXPECTED_RESPONSEA response that isn't the API's format, e.g. from a proxy. Retried: Yes when 502/503/504, otherwise noMISSING_API_KEYThe constructor found no key. Retried: —BROWSER_NOT_ALLOWEDThe 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
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_KEYYour API key. Runtimes without process.env (such as Cloudflare Workers without nodejs_compat) must pass it.baseUrlDefault https://pricerules.app/api/v1The API's base URL.timeoutDefault 10000Milliseconds per attempt, reading the response included.maxRetriesDefault 2Retries after a rate limit, a 502/503/504 or a connection error. 0 turns retries off.fetchDefault the global fetchYour own fetch implementation, for example to go through a proxy.dangerouslyAllowBrowserDefault falseLets 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?