@zhexio/node
v0.2.0
Published
Official Node.js SDK for the Zhex API. Typed, retry-safe, idempotent by default.
Maintainers
Readme
@zhexio/node
Official Node.js SDK for the Zhex API.
npm install @zhexio/nodeimport Zhex from '@zhexio/node';
const zhex = new Zhex(process.env.ZHEX_SECRET_KEY!);
const customer = await zhex.customers.create({
email: '[email protected]',
name: 'Jane Doe',
});
const intent = await zhex.paymentIntents.create({
amount: 19900,
currency: 'brl',
customer: customer.id,
payment_method_types: ['pix'],
products: [{ product: 'prod_…', price: 'price_…', quantity: 1 }],
});Authentication
Pass your secret key (zsk_live_… or zsk_test_…) to the constructor. Never check it into source — the SDK will refuse to send requests if the key looks malformed, but a leaked key is a leaked key.
const zhex = new Zhex(process.env.ZHEX_SECRET_KEY!);The SDK derives the mode (test vs. live) from the key prefix. There's no livemode toggle — a zsk_test_* key returns only sandbox data, a zsk_live_* key only production. Mismatched IDs across modes return 404, never 200.
Idempotency
Every POST and DELETE automatically gets an Idempotency-Key (UUIDv4). Retries on 429 / 5xx replay the same key, so you never double-charge a customer because of a flaky network. Pass your own key when you want the dedup boundary to match your business id:
await zhex.paymentIntents.create(
{ amount: 9700, currency: 'brl', customer: 'cus_…' },
{ idempotencyKey: `mensalidade:${customerId}:2026-04` },
);If you replay the same key with a different body within 24h, the API returns 409 and the SDK throws ZhexIdempotencyError — fix the caller, don't reuse keys across logical operations.
Auto-pagination
Every list() returns a single page synchronously and exposes autoPagingEach() to walk every page lazily:
// page-at-a-time (Stripe-style)
const page = await zhex.customers.list({ limit: 100 });
for (const c of page.data) { /* … */ }
if (page.has_more) { /* fetch next */ }
// or iterate everything
for await (const c of zhex.customers.list({}).autoPagingEach()) {
/* one request per 100 customers, transparently */
}Cursor pagination uses starting_after / ending_before under the hood — no offsets, no page numbers, no skipped records under concurrent writes.
Webhook verification
Verify the Zhex-Signature header on every delivery — never trust a webhook payload that hasn't been verified.
import Zhex from '@zhexio/node';
app.post(
'/webhooks/zhex',
express.raw({ type: 'application/json' }), // RAW body — JSON-parsed bodies break the HMAC
(req, res) => {
let event;
try {
event = Zhex.webhooks.constructEvent(
req.body,
req.headers['zhex-signature'] as string,
process.env.ZHEX_WEBHOOK_SECRET!,
);
} catch (err) {
return res.status(400).send('invalid signature');
}
if (event.type === 'payment_intent.succeeded') {
// …
}
res.json({ received: true });
},
);constructEvent enforces a 5-minute replay tolerance and uses timingSafeEqual to compare HMACs. If you need a wider window for a debugging session, pass a third arg — but don't ship that to prod, you'll defeat the anti-replay guarantee.
Errors
Every failure is one of these, all extending ZhexError:
| Class | Status | When |
|---|---|---|
| ZhexInvalidRequestError | 400 | Bad input — fix the call, don't retry |
| ZhexAuthenticationError | 401 / 403 | Bad key, wrong key type, or revoked |
| ZhexNotFoundError | 404 | Resource doesn't exist (or wrong mode) |
| ZhexIdempotencyError | 409 | Same Idempotency-Key, different body |
| ZhexRateLimitError | 429 | Surfaced after the SDK exhausts retries |
| ZhexCardError | 402 | Card-side decline — card_declined, insufficient_funds, etc. |
| ZhexAPIError | 5xx | Our problem; retry with the same idempotency key |
| ZhexConnectionError | — | DNS / socket / fetch threw |
Each carries code, statusCode, requestId, and raw. Always include requestId when filing support tickets — we look up the request in the audit log straight from there.
try {
await zhex.paymentIntents.create({ /* … */ });
} catch (err) {
if (err instanceof ZhexCardError) {
// show err.code to the customer ('card_declined', 'expired_card', …)
} else if (err instanceof ZhexInvalidRequestError) {
// log err.message + err.requestId, fix your call
} else {
throw err;
}
}Per-request options
Override retries, timeouts, or headers on a single call:
await zhex.refunds.create(
{ payment_intent: 'pi_…' },
{ idempotencyKey: 'refund:order-42', timeoutMs: 30_000, maxRetries: 1 },
);What's covered
| Resource | Methods |
|---|---|
| customers | create, retrieve, update, list |
| paymentIntents | create, retrieve, confirm, cancel, list |
| paymentMethods | create (from token), retrieve, detach, list |
| refunds | create, retrieve, list |
| tokens | create (server-side; for browser tokenization use @zhexio/zhex-js) |
| webhooks | constructEvent (static) |
The SDK doesn't try to wrap every resource Zhex exposes — only the ones you reach for in a charge flow. For raw access to anything else (events, balance, webhook endpoints, customer subscriptions), use fetch against the documented endpoints; the SDK is a convenience layer, not a wall.
Compatibility
- Node 18+ (uses built-in
fetch). - ESM and CJS both supported via dual exports.
- TypeScript 5+ for the bundled
.d.ts.
Status
0.1.0-alpha — the API surface above is stable, but we're still polishing some edges (better typed event narrowing, automatic Connect account propagation). Pin to a specific minor while we hit 1.0.0.
License
MIT.
