@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@alphatag and pin exact versions in production.
Table of contents
- Install
- Quickstart
- Client configuration
- Resources
- OAuth helpers
- Webhook verification
- Error handling
- Pagination
- Compatibility
- License
Install
npm install @ratio-app-qa/sdk
yarn add @ratio-app-qa/sdk
pnpm add @ratio-app-qa/sdkNode.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
Authorizationand 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 headerIdempotency-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
customerId → gk-customer-id; requestId → gk-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',
});
cartTokenis a session-scoped sensitive key. Redact it in logs and hook payloads.
discounts
opts.idempotencyKey → Idempotency-Key; opts.requestId → gk-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');
verifyandconstructEventare not onclient.webhooks. They are in the@ratio-app-qa/sdk/webhookssub-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: urlNote: 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. Useverifyplus your own parsing if you need to handle the bare-payload format;constructEventsupport 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.
