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

@ratio-app-qa/sdk

v0.1.0-alpha.2

Published

Official Ratio TypeScript SDK

Readme

@ratio-app-qa/sdk

Official TypeScript SDK for the Ratio app platform. Build merchant-facing agents that access orders, products, loyalty points, and more — authenticated with a single access token, typed end-to-end.

Alpha release: this package is currently in alpha (0.1.0-alpha.x). The API surface may change before v1.0. Install with the @alpha tag and pin exact versions in production.


Table of contents

  1. Install
  2. Quickstart
  3. Client configuration
  4. Resources
  5. OAuth helpers
  6. Webhook verification
  7. Error handling
  8. Pagination
  9. Compatibility
  10. License

Install

npm install @ratio-app-qa/sdk
yarn add @ratio-app-qa/sdk
pnpm add @ratio-app-qa/sdk

Node.js 18 or later is required.


Quickstart

import { RatioClient } from '@ratio-app-qa/sdk';

const client = new RatioClient({ accessToken: process.env.RATIO_TOKEN! });
const orders = await client.orders.list();
console.log(orders);

Client configuration

All 8 constructor options:

| Option | Type | Default | Notes | |---|---|---|---| | accessToken | string | — (required) | Non-empty, non-whitespace; never logged. | | baseUrl | string | 'https://api-gw-v4.dev.gokwik.in/qa' | Override for staging or self-hosted deployments. | | timeout | number ms | 30_000 | Per-attempt; does not include retry backoff. | | maxRetries | number | 3 | 5xx / 429 / network errors only. | | maxRetryDelayMs | number | 30_000 | Cap on Retry-After + exponential backoff. | | fetch | FetchFn | globalThis.fetch | Pass a polyfill or test double. | | hooks | HttpRequestHooks | undefined | onAttemptStart / onAttemptEnd callbacks. | | logger | Logger | undefined | {debug?,info?,warn?,error?} seam. |

const client = new RatioClient({
  accessToken: process.env.RATIO_TOKEN!,
  timeout: 10_000,
  maxRetries: 2,
});

Observability: Hook payloads have Authorization and other sensitive headers redacted by the core. Resolved URLs in hook events may carry PII path segments (e.g. phone numbers substituted by loyalty methods). Use {endpointGroup, endpointKey} from hook payloads for metric dimensions.


Resources

The RatioClient exposes 10 resource properties, all eagerly instantiated:

orders · products · variants · reviews · loyalty · customers · checkout · cart · discounts · webhooks


orders

const list     = await client.orders.list({ page: 1, limit: 20 });
const order    = await client.orders.get('ord_xxx');
const created  = await client.orders.create({ /* OrderCreateInput */ });
const updated  = await client.orders.update('ord_xxx', { /* OrderUpdateInput */ });
const cancelled = await client.orders.cancel('ord_xxx'); // input? optional
const ext       = await client.orders.updateExternalId('ord_xxx', { external_id: 'my-id' });

// Async iterator
for await (const o of client.orders.listAll()) { console.log(o.id); }

products

const list    = await client.products.list({ page: 1, limit: 50 });
const product = await client.products.get('prod_xxx');
const created = await client.products.create({ /* ProductCreateInput */ });
const updated = await client.products.update('prod_xxx', { /* ProductUpdateInput */ }); // PUT
await client.products.remove('prod_xxx');

for await (const p of client.products.listAll()) { console.log(p.id); }

variants

const list = await client.variants.list('prod_xxx', { limit: 50 });
const v    = await client.variants.get('var_xxx');

// create: productId is used as the URL path segment and is not sent in the body
const created = await client.variants.create({ productId: 'prod_xxx', title: 'Blue / L' });

const updated = await client.variants.update('var_xxx', { price: '24.99' }); // PUT
await client.variants.remove('var_xxx');

for await (const v of client.variants.listAll('prod_xxx')) { console.log(v.id); }

reviews

const page = await client.reviews.getByProductId('prod_xxx', { page: 1, limit: 20 });

for await (const r of client.reviews.getByProductIdAll('prod_xxx')) { console.log(r.id); }

loyalty

phone is URL-encoded when placed in request paths. Redact it in your logs — the SDK core redacts headers only, not URL segments.

credit / debit have two separate idempotency layers:

  • opts.idempotencyKey → HTTP header Idempotency-Key (transport).
  • dto.idempotency_key (in-body) → durable ledger-row idempotency.
await client.loyalty.credit(
  { phone: '+911234567890', points: 100, idempotency_key: 'dedup-001' },
  { idempotencyKey: 'http-dedup-001', requestId: 'req_xxx' },
);
await client.loyalty.debit(
  { phone: '+911234567890', points: 50, idempotency_key: 'dedup-002' },
);
const balance = await client.loyalty.getBalance('+911234567890');
const history = await client.loyalty.getHistory('+911234567890', { page: 1, limit: 20 });

for await (const entry of client.loyalty.getHistoryAll('+911234567890')) {
  console.log(entry.points);
}

customers

One method. opts.requestId → header gk-request-id.

const customer = await client.customers.get('cust_xxx', { requestId: 'req_xxx' });

checkout

customerIdgk-customer-id; requestIdgk-request-id.

const list     = await client.checkout.listByCartId('cart_xxx', { customerId: 'cust_xxx' });
const checkout = await client.checkout.get('chk_xxx', { customerId: 'cust_xxx' });

cart

One method. cartToken is required and maps to x-cart-token.

| Opt | Wire header | Required? | |---|---|---| | cartToken | x-cart-token | yes | | customerId | gk-customer-id | no | | requestId | gk-request-id | no | | relatedVariant | related-variant | no |

const cart = await client.cart.getByToken({
  cartToken: process.env.CART_TOKEN!,
  customerId: 'cust_xxx',
});

cartToken is a session-scoped sensitive key. Redact it in logs and hook payloads.


discounts

opts.idempotencyKeyIdempotency-Key; opts.requestIdgk-request-id.

const d       = await client.discounts.create({ /* DiscountCreateInput */ }, { idempotencyKey: 'idemp_xxx' });
const list    = await client.discounts.list({ page: 1, limit: 20 });
const single  = await client.discounts.get('disc_xxx');
const updated = await client.discounts.update('disc_xxx', { /* DiscountUpdateInput */ }); // PUT
await client.discounts.remove('disc_xxx');

for await (const d of client.discounts.listAll()) { console.log(d.id); }

webhooks (lifecycle)

8 lifecycle methods. opts accepts {requestId?} only — no idempotencyKey (the upstream API does not accept it on these endpoints).

const wh     = await client.webhooks.create({ webhookUrl: 'https://…', eventName: 'order.created' });
const bulk   = await client.webhooks.createBulk({ webhookUrl: 'https://…', eventNames: ['order.created'] });
const list   = await client.webhooks.list({ appId: 'app_xxx' });
const single = await client.webhooks.get('wh_xxx');
const upd    = await client.webhooks.update('wh_xxx', { webhookUrl: 'https://new…' });
const tog    = await client.webhooks.toggle('wh_xxx');    // enable ↔ disable

// rotateSecret: returned ONCE — store in a secrets manager immediately
const { secret: newSecret } = await client.webhooks.rotateSecret('wh_xxx');

await client.webhooks.remove('wh_xxx');

verify and constructEvent are not on client.webhooks. They are in the @ratio-app-qa/sdk/webhooks sub-entry — see Webhook verification.


OAuth helpers

OAuth helpers run before you have an access token and therefore cannot use RatioClient. Import from the @ratio-app-qa/sdk/oauth sub-entry:

import { buildAuthorizeUrl, exchangeToken, refreshToken, revokeToken } from '@ratio-app-qa/sdk/oauth';

buildAuthorizeUrl — pure / sync / no network:

const url = buildAuthorizeUrl({
  clientId:    process.env.RATIO_CLIENT_ID!,
  merchantId:  'mer_xxx',
  scopes:      ['orders:read', 'products:read'],
  redirectUri: 'https://your-app.example.com/oauth/callback',
  state:       'csrf-token',
});
// Redirect the merchant's browser to: url

Note: Scope names must not contain commas. The SDK joins the scopes array with a comma; a scope name containing a comma would be split into two tokens by the backend.

exchangeToken — POST oauth/token (authorization_code grant):

const token = await exchangeToken({
  code:         req.query.code as string,
  clientId:     process.env.RATIO_CLIENT_ID!,
  clientSecret: process.env.RATIO_CLIENT_SECRET!,
  redirectUri:  'https://your-app.example.com/oauth/callback',
});

refreshToken — POST oauth/token (refresh_token grant). The SDK translates camelCase refreshToken to the wire field refresh_token:

const refreshed = await refreshToken({
  refreshToken: storedRefreshToken,
  clientId:     process.env.RATIO_CLIENT_ID!,
  clientSecret: process.env.RATIO_CLIENT_SECRET!,
});

revokeToken — POST oauth/revoke, 204 → void:

await revokeToken({
  token:        accessTokenToRevoke,
  clientId:     process.env.RATIO_CLIENT_ID!,
  clientSecret: process.env.RATIO_CLIENT_SECRET!,
});

All helpers accept optional baseUrl and fetch for non-default environments.


Webhook verification

Import from the @ratio-app-qa/sdk/webhooks sub-entry:

import { verify, constructEvent } from '@ratio-app-qa/sdk/webhooks';

Raw-body requirement

The HMAC is computed over the exact bytes received off the wire. Framework body parsers that parse and re-serialize JSON change the byte sequence; the HMAC will not match. Preserve raw bytes:

Express — put express.raw before the handler:

import express from 'express';

app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  // req.body is a Buffer here — do NOT call JSON.parse before verify
  verify({
    rawBody:   req.body as Buffer,
    signature: req.headers['x-ratio-signature'] as string,
    secret:    process.env.RATIO_WEBHOOK_SECRET!,
  });
  res.status(200).send('OK');
});

Fastify:

fastify.addContentTypeParser('application/json', { parseAs: 'buffer' },
  (_req, body, done) => done(null, body));

Next.js API routes:

export const config = { api: { bodyParser: false } };

Then read raw bytes via raw-body or a stream helper.

verify

import { verify } from '@ratio-app-qa/sdk/webhooks';

// Returns undefined on success; throws RatioWebhookError on any failure.
verify({
  rawBody:      req.body as Buffer,
  signature:    req.headers['x-ratio-signature'] as string,
  secret:       process.env.RATIO_WEBHOOK_SECRET!,
  // toleranceSec: 300,  // default ±300 s replay window
  // nowSec: Math.floor(Date.now() / 1000),  // injectable for tests
});

RatioWebhookError codes thrown by verify:

| Code | Cause | |---|---| | missing_secret | secret is empty or nullish | | invalid_payload | signature header missing, empty, or malformed | | signature_mismatch | HMAC length or value mismatch | | replay_window_exceeded | timestamp outside ±300 s window |

constructEvent

Calls verify first, then decodes UTF-8 JSON and validates {id, type, createdAt, data}.

import { constructEvent, type WebhookEvent } from '@ratio-app-qa/sdk/webhooks';
import { RatioWebhookError } from '@ratio-app-qa/sdk';

app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  let event: WebhookEvent;
  try {
    event = constructEvent({
      rawBody:   req.body as Buffer,
      signature: req.headers['x-ratio-signature'] as string,
      secret:    process.env.RATIO_WEBHOOK_SECRET!,
    });
  } catch (err) {
    if (err instanceof RatioWebhookError) {
      console.error('Webhook rejected:', err.code);
      return res.status(400).send('Bad webhook');
    }
    throw err;
  }
  console.log('Event:', event.type, event.id);
  res.status(200).send('OK');
});

Note: The {id, type, createdAt, data} envelope is a forward contract defined by this SDK. Webhook deliveries may currently arrive as a bare payload without this wrapper. Use verify plus your own parsing if you need to handle the bare-payload format; constructEvent support will be announced once the platform emits the enveloped format.


Error handling

All SDK errors extend RatioAPIError extends Error.

import { RatioAPIError, RatioAuthError, RatioTimeoutError, RatioWebhookError } from '@ratio-app-qa/sdk';

| Class | Codes | .retryable | |---|---|---| | RatioAPIError | 'bad_request' \| 'not_found' \| 'conflict' \| 'validation_error' \| 'rate_limited' \| 'server_error' \| 'network_error' | varies | | RatioAuthError | 'unauthorized' \| 'forbidden' \| 'invalid_scope' | false | | RatioTimeoutError | 'timeout' | true | | RatioWebhookError | 'signature_mismatch' \| 'replay_window_exceeded' \| 'invalid_payload' \| 'missing_secret' | false |

Common fields on RatioAPIError: .code · .statusCode · .requestId? · .retryable · .response? · .cause?

.retryable semantics: when true and maxRetries > 0, the SDK auto-retries. RatioTimeoutError (code 'timeout') and network_error are retryable. RatioAuthError and RatioWebhookError are never retried.

try {
  const order = await client.orders.get('ord_xxx');
} catch (err) {
  if (err instanceof RatioAuthError) {
    console.error('Auth error:', err.code, err.statusCode);
  } else if (err instanceof RatioTimeoutError) {
    console.error('Timed out. retryable:', err.retryable);
  } else if (err instanceof RatioAPIError) {
    console.error('API error:', err.code, 'requestId:', err.requestId);
  } else {
    throw err;
  }
}

Pagination

Six resources expose *All async iterators for full-dataset traversal:

  • client.orders.listAll(params?, opts?)
  • client.products.listAll(params?, opts?)
  • client.variants.listAll(productId?, params?, opts?)
  • client.reviews.getByProductIdAll(productId, params?, opts?)
  • client.loyalty.getHistoryAll(phone, params?, opts?)
  • client.discounts.listAll(params?, opts?)

No *All iterator on: customers, checkout, cart, webhooks.

for await (const order of client.orders.listAll()) {
  console.log(order.id);
}

maxPages cap — default 1000. The check fires BEFORE the (maxPages+1)th fetch (safety budget, not post-fact detection):

import { MaxPagesExceededError } from '@ratio-app-qa/sdk';

try {
  for await (const o of client.orders.listAll({}, { maxPages: 10 })) {
    console.log(o.id);
  }
} catch (err) {
  if (err instanceof MaxPagesExceededError) {
    // err.name    === 'MaxPagesExceededError'
    // err.maxPages === 10
    // err.message  === 'Pagination exceeded maxPages=10'
    console.warn('Hit page cap:', err.maxPages);
  }
}

variants.listAll paginates with an offset+limit cursor internally. The public variants.list() signature stays {limit?} only.


Compatibility

| Requirement | Value | |---|---| | Node.js | >= 18 (built-in globalThis.fetch + globalThis.crypto.randomUUID) | | Module format | ESM + CJS — both supported via the package.json exports map | | TypeScript | Strict — .d.ts declarations ship with each entry point |

On older runtimes without a global fetch, pass a polyfill:

import fetch from 'node-fetch';

const client = new RatioClient({
  accessToken: process.env.RATIO_TOKEN!,
  fetch: fetch as unknown as typeof globalThis.fetch,
});

Entry points

| Import path | Exports | |---|---| | @ratio-app-qa/sdk | RatioClient, resource classes, error classes, MaxPagesExceededError | | @ratio-app-qa/sdk/webhooks | verify, constructEvent, VerifyInput, ConstructEventInput, WebhookEvent | | @ratio-app-qa/sdk/oauth | buildAuthorizeUrl, exchangeToken, refreshToken, revokeToken |

For a migration guide from raw HTTP calls to this SDK, see MIGRATION-from-raw-API.md.


License

MIT — see LICENSE.