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

@r3pos/ecom

v0.1.0

Published

Server-side SDK for the R3 Ecom Platform API. Used with a SECRET key (r3_sk_…) from a merchant's backend: full catalog/inventory/order access, webhook endpoint management, and webhook signature verification. Built on @r3pos/ecom-core.

Readme

@r3pos/ecom

The server-side SDK for the R3 Ecom Platform API — the commerce API behind R3 POS. Use it from a merchant's backend with a secret key to read the catalog and inventory, place and manage orders, run checkout, register webhook endpoints, and verify webhook deliveries.

For it: developers and agencies building on top of an R3 POS merchant — a custom storefront's backend, an order-routing service, an ERP or accounting sync, a Slack bot that pings the kitchen.

Browser code needs the sibling package, @r3pos/ecom-storefront, which takes a publishable key and exposes only what one can do.

Install

npm install @r3pos/ecom

Ships ESM and CommonJS with TypeScript declarations and source maps, so it works in an ESM service, a CommonJS Express app, and everything in between. Node 20+.

Quickstart

import { createEcomClient } from '@r3pos/ecom';

const client = createEcomClient({
  apiKey: process.env.R3_SECRET_KEY!, // r3_sk_live_… (or r3_sk_test_…)
  baseUrl: 'https://api.r3pos.com', // origin only — /ecom/v1 is added for you
});

// Who am I talking to?
const merchant = await client.merchant.get();
console.log(merchant.name, merchant.currency);

// Browse the catalog
const { data: products, hasMore } = await client.catalog.products.list({
  q: 'latte',
  limit: 20,
});

// Or walk all of it, lazily — one round trip per page
for await (const product of client.catalog.products.listAll()) {
  console.log(product.id, product.name, product.priceCents);
}

// Place an order. Money is computed server-side from the merchant's own prices.
const order = await client.orders.create(
  { items: [{ productId: products[0]!.id, quantity: 2 }] },
  { idempotencyKey: `cart:${cartId}` },
);
console.log(order.number, order.totalCents, order.status); // "1042" 1130 "received"

// Take payment
const session = await client.orders.checkout.create(order.id);
// session.mode is 'stripe' | 'helcim' | 'mock' | 'free' — branch on it, not on your config

// Track it
const fresh = await client.orders.get(order.id);
const recent = await client.orders.list({ status: 'ready', limit: 50 });

Build one client per process and share it. It is stateless apart from its configuration; constructing one per request only re-parses the key and re-allocates the namespaces for no benefit.

What is on the client

| Namespace | Methods | | --------------------------- | -------------------------------------------------------------------------------------- | | client.merchant | get() | | client.catalog.products | list(), get(id), listAll() | | client.catalog.categories | list(), listAll() | | client.inventory | list(), listAll() | | client.orders | create(), get(id), list(), listAll(), cancel(id) | | client.orders.checkout | create(orderId), confirm(orderId, body) | | client.webhooks.endpoints | list(), create(), update(id), delete(id), rotateSecret(id), deliveries(id) | | client.webhooks | verify(), constructEvent() | | client.key | the parsed key — kind, mode (live/test), prefix | | client.transport | escape hatch for a /ecom/v1 route this SDK does not wrap yet |

client.key.mode is the cheap way to refuse to boot a production process against a test key:

if (process.env.NODE_ENV === 'production' && client.key.mode !== 'live') {
  throw new Error(`refusing to start on a ${client.key.mode} key (${client.key.prefix}…)`);
}

Key classes: secret here, publishable in the browser

An R3 Ecom API key is r3_<sk|pk>_<live|test>_<43 URL-safe base64 chars>.

  • r3_sk_… — secret. Every scope: catalog:read, inventory:read, orders:read, orders:write, webhooks:manage. This package requires one. Keep it on the server: never in a browser bundle, a mobile app, or a commit.
  • r3_pk_… — publishable. Browser-safe, limited to catalog:read, inventory:read and orders:write, with a per-key origin allowlist. It belongs in @r3pos/ecom-storefront.

createEcomClient refuses a publishable key at construction:

createEcomClient({ apiKey: 'r3_pk_live_…', baseUrl });
// R3EcomInvalidKeyError: @r3pos/ecom needs a SECRET key (r3_sk_live_… / r3_sk_test_…)
// but was given a PUBLISHABLE key (r3_pk_…). …
// Use your secret key here, and @r3pos/ecom-storefront for the publishable one.

If you mix them up: a publishable key on the server does not fail loudly — catalog reads and order creation keep working, and the first symptom is a 403 on orders.list or on webhook management, possibly weeks later and looking like a permissions bug. Failing at construction turns that into one obvious error on the developer's machine. In the other direction, a secret key that reaches a browser leaks the merchant's entire order history and control of their webhooks to every visitor, while the shop appears to work perfectly. A secret key that has ever been served to a browser is compromised — rotate it. Only a hash is stored server-side, so keys are rotated, never recovered.

Error handling

Every failure is an R3EcomError subclass carrying a literal kind, the HTTP status (0 when no response arrived), the server's code, and the X-Request-Id when there was one. Each has a type guard, so a catch block is one check rather than a status comparison.

import {
  isConflictError,
  isNotFoundError,
  isRateLimitError,
  isValidationError,
  isR3EcomError,
} from '@r3pos/ecom';

try {
  await client.orders.cancel(orderId);
} catch (err) {
  if (isNotFoundError(err)) return res.status(404).json({ error: 'no such order' });
  if (isConflictError(err)) return res.status(409).json({ error: 'order already picked up' });
  if (isValidationError(err)) return res.status(400).json({ error: err.message });
  if (isRateLimitError(err)) {
    res.setHeader('Retry-After', String(err.retryAfterSeconds ?? 60));
    return res.status(503).end();
  }
  if (isR3EcomError(err)) {
    logger.error({ kind: err.kind, status: err.status, code: err.code, requestId: err.requestId });
  }
  throw err;
}

The full family: R3EcomAuthError (401), R3EcomForbiddenError (403 — not licensed for ecom, or missing a scope), R3EcomNotFoundError (404), R3EcomValidationError (400/422), R3EcomConflictError (409), R3EcomRateLimitError (429, with retryAfterSeconds), R3EcomServerError (5xx), R3EcomTimeoutError, R3EcomNetworkError, R3EcomInvalidKeyError, R3EcomSignatureVerificationError and R3EcomPaginationError. Every one is re-exported from this package, along with its guard and the catch-all isR3EcomError / isRetryableError.

Idempotency

orders.create always sends an Idempotency-Key; if you do not supply one, the SDK mints a UUID for that call. That protects you from the transport's own retries — without a key, a POST is never replayed, so a dropped connection surfaces as an error on a request that may already have created the order.

It does not protect you from a retry you make later, because a new call mints a new key. For end-to-end safety, derive the key from something stable:

await client.orders.create(body, { idempotencyKey: `cart:${cart.id}` });

Replaying the same key with the same body returns the same order. Replaying it with a different body is a 409 (R3EcomConflictError).

Webhooks

Register an endpoint

const endpoint = await client.webhooks.endpoints.create({
  url: 'https://shop.example.com/webhooks/r3',
  enabledEvents: ['order.paid', 'order.status_changed', 'inventory.updated'],
  description: 'production receiver',
});

// This is the ONLY time the signing secret exists in plaintext. Store it now.
await secrets.put('r3_webhook_secret', endpoint.signingSecret);

The secret is stored sealed server-side. It cannot be read back by any route or recovered by support — a lost secret is rotated with client.webhooks.endpoints.rotateSecret(id), which returns a new plaintext secret exactly once. During a rotation the platform signs with both secrets, so a receiver verifying with either keeps working while you deploy.

Event types: order.created, order.paid, order.status_changed, order.cancelled, order.refunded, product.updated, inventory.updated. The API is additive-only — tolerate a type you do not recognise rather than crashing on it.

Verify a delivery

import express from 'express';
import { createEcomClient, isSignatureVerificationError } from '@r3pos/ecom';

const app = express();
const client = createEcomClient({ apiKey: process.env.R3_SECRET_KEY!, baseUrl });

// ⚠️ RAW body parser on this route — see the warning below
app.post('/webhooks/r3', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = await client.webhooks.constructEvent<{ orderId: string }>({
      payload: req.body.toString('utf8'), // the exact bytes R3 sent
      header: req.get('r3-signature'),
      secret: process.env.R3_WEBHOOK_SECRET!,
    });
  } catch (err) {
    if (isSignatureVerificationError(err)) {
      // err.reason: 'malformed_header' | 'timestamp_out_of_tolerance'
      //           | 'no_matching_signature' | 'invalid_payload'
      console.warn('rejected webhook:', err.reason);
      return res.status(400).send('bad signature'); // 4xx stops the retries
    }
    throw err;
  }

  if (await alreadyHandled(event.id)) return res.sendStatus(200);

  switch (event.type) {
    case 'order.paid':
      await fulfil(event.data.orderId);
      break;
    default:
      break; // unknown types are not an error
  }

  await markHandled(event.id);
  res.sendStatus(200);
});

⚠️ The payload must be the RAW request body. The signature is an HMAC over the exact bytes R3 sent, so verifying a re-serialized body — JSON.stringify(req.body) after express.json(), or anything else that has round-tripped through a parser — always fails, because key order and whitespace do not survive. Mount a raw body parser on the webhook route (express.raw), or read await request.text() in a fetch-style handler, before parsing.

Two more rules the platform enforces:

  • Answer with a 2xx only when you have accepted the event. A 2xx marks the delivery done; anything else is retried at 0s, 30s, 2m, 10m, 1h and 6h, then marked failed. Reject an unverified delivery with a 4xx so it stops.
  • Delivery is at-least-once. De-duplicate on event.id before acting.

Use client.webhooks.verify() instead of constructEvent() when you only want a boolean and will parse the body yourself. To answer "did my endpoint actually receive order.paid?" without adding logging, read client.webhooks.endpoints.deliveries(id).

Pagination

Every list method returns a Page<T>{ data, nextCursor, hasMore } — and has a listAll() twin that walks the cursor lazily:

// one page
const page = await client.orders.list({ status: 'ready', limit: 100 });

// every page, one round trip at a time; breaking out early stops the walk
for await (const order of client.orders.listAll({ updatedSince: since })) {
  await sync(order);
}

Prefer updatedSince over re-walking the whole history on a schedule: order history grows without bound and the retry budget does not.

Configuration

createEcomClient accepts everything the transport does:

| Option | Default | | | -------------------------------------- | -------------- | ----------------------------------------- | | apiKey | — | required; must be r3_sk_… | | baseUrl | — | required; API origin, no /ecom/v1 | | fetch | global fetch | inject your own instrumented one | | timeoutMs | 30000 | per request | | maxRetries | 2 | retries after the first attempt | | userAgent | — | e.g. acme-sync/1.4.0 | | defaultHeaders | — | merged into every request | | retryBaseDelayMs / retryMaxDelayMs | 250 / 8000 | full-jitter backoff | | maxRetryAfterMs | 60000 | longest Retry-After actually waited out |

Nothing is read from the environment — your application decides where the key comes from, which is what lets one process serve several tenants.

Reference

The wire contract — routes, scopes, error codes, pagination, idempotency and the webhook event catalogue — is documented in docs/ecom/CONTRACT.md.

License

MIT © R3 Lab