Node SDK (@zylopay/node)
Typed access to the ZyloPay API from Node.js and TypeScript, with auto-pagination, retries and webhook verification.
Pre-release
@zylopay/node is not published on npm yet. The interface below is the one the SDK ships with. Until it is published, use the curl examples or fetch.
Install
Coming soon on npm. Once published:
npm install @zylopay/nodeNode.js 18 or later (it uses the built-in fetch). No runtime dependencies; ESM and CommonJS; TypeScript types generated from the OpenAPI spec. Secret keys only: the constructor refuses a publishable key.
Configure
import ZyloPay from '@zylopay/node';
const zp = new ZyloPay(process.env.ZYLOPAY_SECRET_KEY!);Options, as a second argument:
| Option | Default | Meaning |
|---|---|---|
maxNetworkRetries | 2 | Automatic retries (see below). 0 disables them. |
timeout | 80000 | Per-attempt timeout in milliseconds (refunds wait up to 60 s for the chain). |
apiVersion | ZyloPay.API_VERSION (2026-09-28) | ZyloPay-Version sent with every request, so responses always match the SDK's types. |
baseUrl | https://zylopay-api.fly.dev | API origin. Override only to test against a local stub. |
telemetry | true | Sends ZyloPay-Client-Telemetry (the previous request's ID and latency, never payload data). |
appInfo | Your application's name and version, added to the User-Agent. | |
fetch | global fetch | A custom fetch, for proxies or tests. |
const zp = new ZyloPay(process.env.ZYLOPAY_SECRET_KEY!, { maxNetworkRetries: 3, timeout: 20_000, apiVersion: '2026-09-28' });The key's prefix sets the mode: a zp_test_sk_ key works on the testnet sandbox, a zp_live_sk_ key on mainnet.
Resources
Every method follows one pattern: (id?, params?, requestOptions?). Methods without parameters take the request options second. Responses are the API's JSON objects, with the same snake_case fields.
| Resource | Methods |
|---|---|
zp.paymentIntents | create(params, opts), retrieve(id), list(params), cancel(id, opts) |
zp.payments | retrieve(id), list(params) |
zp.refunds | create(params, opts), retrieve(id), list(params) |
zp.terminals | retrieve(id), list(params) |
zp.balance | retrieve() |
zp.events | retrieve(id), list(params) |
zp.webhookEndpoints | create(params, opts), retrieve(id), update(id, params, opts), del(id), list(), rotateSecret(id, params, opts), sendTest(id), listDeliveries(endpointId, params) |
zp.webhookDeliveries | retrieve(id), retry(id, opts) |
zp.webhooks | constructEvent(rawBody, signatureHeader, secret, toleranceSeconds = 300) |
Request options, the last argument of every method: idempotencyKey, apiVersion, timeout, maxNetworkRetries and signal (an AbortSignal; an aborted request throws ConnectionError with code: "aborted" and is never retried).
import { randomUUID } from 'node:crypto';
const intent = await zp.paymentIntents.create(
{ amount: 2500000, terminal: 'tml_ZP-4821-K', order_id: 'A7K2Q9XZ' },
{ idempotencyKey: `intent-${orderId}` },
);
const refund = await zp.refunds.create({ payment: 'pay_1042', amount: 1000000 }, { idempotencyKey: randomUUID() });Pagination
list methods can be awaited for one page, or iterated with for await to walk every page:
const page = await zp.payments.list({ limit: 50 });
console.log(page.data, page.has_more, page.next_cursor);
for await (const payment of zp.payments.list({ limit: 50 })) {
console.log(payment.id);
}Retries and timeouts
The SDK retries automatically, up to maxNetworkRetries times, with exponential backoff and jitter, on:
- network errors and timeouts;
409 idempotency_request_in_progress;429, honouringRetry-AfterandRateLimit-Reset;5xx.
Every POST, PATCH and DELETE gets an idempotency key generated by the SDK (a UUID v4) and reused across its retries, so retries never create a second object. Pass your own idempotencyKey when your application may re-submit the same operation later, for example after a crash: an SDK-generated key does not survive a restart.
Webhooks
const event = zp.webhooks.constructEvent(rawBody, req.headers['zylopay-signature'], process.env.ZYLOPAY_WEBHOOK_SECRET!);rawBody must be the body exactly as received (a string or Buffer). constructEvent checks the timestamp (5 minutes by default, or the fourth argument in seconds), accepts any matching v1 signature, and returns the parsed event. It throws ZyloPay.errors.SignatureVerificationError otherwise, with a code such as malformed, no_matching_signature or timestamp_out_of_tolerance. ZyloPay.webhooks.constructEvent(…) works without a client or API key. See Verify signatures.
To test your handler without ZyloPay, sign a payload yourself:
const header = ZyloPay.webhooks.generateTestHeaderString({ payload: rawBody, secret: 'whsec_test_secret' });
// Pass several secrets to simulate a rotation overlap: { payload, secret: [newSecret, oldSecret] }Errors
Every error extends ZyloPay.errors.ZyloPayError and has .type, .code, .param, .message, .requestId, .statusCode and .headers.
| Class | When |
|---|---|
AuthenticationError | 401 |
PermissionError | 403 |
InvalidRequestError | 400, 409 invalid_request_error |
NotFoundError | 404 (a subclass of InvalidRequestError) |
IdempotencyError | idempotency_error |
RateLimitError | 429, after retries |
APIError | 5xx after retries, or an unknown error type |
ConnectionError | No response. .code is network_error, timeout or aborted. |
SignatureVerificationError | A webhook failed verification. |
try {
await zp.paymentIntents.cancel(intentId, { idempotencyKey: `cancel-${intentId}` });
} catch (err) {
if (err instanceof ZyloPay.errors.InvalidRequestError && err.code === 'payment_intent_unexpected_state') {
// already paid: refund instead
} else {
throw err;
}
}Request IDs
Each result exposes the response metadata:
const payment = await zp.payments.retrieve('pay_1042');
console.log(payment.lastResponse.requestId, payment.lastResponse.statusCode);
const refund = await zp.refunds.create({ payment: 'pay_1042' }, { idempotencyKey: 'refund-R-2211' });
console.log(refund.lastResponse.idempotencyKey, refund.lastResponse.idempotentReplayed);lastResponse also carries the response headers and the apiVersion it was rendered with. It is non-enumerable, so it does not appear when you serialize the object.