@nexum-reviews/client
v0.3.0
Published
TypeScript client for the review-site API
Downloads
402
Readme
@nexum-reviews/client
TypeScript client for the review-site API. Native fetch, Node 18+. Ships ESM and CJS — drop into Next.js, NestJS, plain Node, Bun, or Deno without a bundler shim.
Install
npm install @nexum-reviews/client zodzod is a peer dependency (^3.23 || ^4). The SDK uses it for the webhook payload schema; you bring your own version so you don't end up with two copies in your bundle.
Quick start
import { ReviewsClient } from "@nexum-reviews/client";
const client = new ReviewsClient({
apiKey: process.env.REVIEWS_API_KEY!, // rk_live_...
baseUrl: "https://api.mynexum.ai",
});
// Manage your account
const account = await client.account.get();
await client.account.update({ webhook_url: "https://aftercard.example/hooks" });
// Manage merchants
const shop = await client.merchants.create({
slug: "downtown-cafe",
display_name: "Downtown Cafe",
});
for await (const m of client.merchants.iterAll()) {
console.log(m.slug);
}
// Submit a review (public — no auth needed, but it's on the same client)
await client.reviews.submit("downtown-cafe", {
reviewer_name: "Jane",
reviewer_identifier: "[email protected]",
rating: 5,
body: "Great coffee!",
});
// List and moderate
const page = await client.reviews.list("downtown-cafe", { verified_only: true });
await client.reviews.delete("downtown-cafe", page.data[0]!.id);Error handling
The SDK throws two distinct error classes so you can discriminate transport problems from server responses without string-matching on a code.
import {
ReviewsAPIError,
ReviewsNetworkError,
ReviewsError, // common base, if you want to catch both
} from "@nexum-reviews/client";
try {
await client.merchants.create({ slug: "BAD", display_name: "x" });
} catch (err) {
if (err instanceof ReviewsNetworkError) {
// Never reached the server: DNS, connection refused, TLS, abort, timeout.
if (err.timedOut) console.log("retry later, our timeout fired");
else console.log("transport blew up:", err.cause);
} else if (err instanceof ReviewsAPIError) {
// Reached the server, server said no.
console.log(err.code); // "validation_error"
console.log(err.status); // 400
console.log(err.details); // { field: "slug" }
} else {
throw err;
}
}ReviewsAPIError.code values: validation_error, unauthorized, forbidden, not_found, conflict, rate_limited, internal_error, or http_error (fallback when the response wasn't a recognised error envelope).
Key rotation
rotateKey() automatically updates the client's in-memory key so subsequent calls keep working. Persist the returned key so your next process start can reuse it.
const { api_key } = await client.account.rotateKey();
await saveSecret("REVIEWS_API_KEY", api_key);Verifying webhooks
When a review is submitted, the server POSTs to your webhook_url with an X-Webhook-Signature: sha256=… header. Verify it against the raw body, then parse the body through the exported Zod schema so a malformed payload throws instead of silently typing as anything.
import express from "express";
import {
verifyWebhookSignature,
WebhookPayloadSchema,
} from "@nexum-reviews/client";
const app = express();
app.post(
"/hooks/verify",
express.raw({ type: "application/json" }),
async (req, res) => {
const ok = verifyWebhookSignature({
secret: process.env.REVIEWS_WEBHOOK_SECRET!,
body: req.body, // Buffer — MUST be the raw bytes, not re-serialized JSON
signature: req.header("x-webhook-signature"),
});
if (!ok) return res.status(401).end();
const payload = WebhookPayloadSchema.parse(JSON.parse(req.body.toString("utf8")));
// payload.reviewer_identifier is the exact value the reviewer submitted —
// do a direct indexed lookup against your user table.
const user = await db.users.findOne({ publicId: payload.reviewer_identifier });
res.json({ verified: !!user });
},
);Respond within 5 seconds with { "verified": true | false }. Timeouts are treated as failed verification.
The schema is the source of truth: WebhookPayload is z.infer<typeof WebhookPayloadSchema>, so the runtime validator and the TS type can never drift.
Pagination
Both merchants.list() and reviews.list() return a Page<T> with { data, next_cursor }. The iterAll() async iterators walk every page for you:
for await (const review of client.reviews.iterAll("downtown-cafe")) {
console.log(review.rating, review.reviewer_name);
}Options
new ReviewsClient({
apiKey: "rk_live_...",
baseUrl: "https://api.mynexum.ai",
timeoutMs: 10_000, // default 30_000
fetch: customFetch, // default globalThis.fetch
});ESM and CJS
Both formats ship in dist/. Node resolves the right one via the exports map. Concretely:
import { ReviewsClient } from "@nexum-reviews/client";— works in ESM (Next.js app router, modern Node).const { ReviewsClient } = require("@nexum-reviews/client");— works in CJS (NestJS default, older Node, Jest's default transform).
Both expose the same surface and the same .d.ts / .d.cts types.
