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

pay-with-zend-sdk

v0.1.2

Published

Typed TypeScript SDK for Pay with Zend — programmatic payment requests, webhook signature verification, and return-token verification against a Zend user's own account.

Readme

pay-with-zend-sdk

A fully typed TypeScript SDK for Pay with Zend — create payment requests against your own Zend account, verify webhook signatures, and confirm post-payment return tokens, all from your own backend or scripts.

This SDK is not a merchant integration. There is no separate "merchant" account type here — a Developer is just a regular Zend user who has paired a CLI (or otherwise obtained a User API Key) to their own account. Every request this SDK makes settles directly to that user's own Zend account.

Looking for the CLI instead? See pay-with-zend-cli — it uses this SDK internally and is usually the fastest way to get an API key in the first place.

Installation

npm install pay-with-zend-sdk
# or
pnpm add pay-with-zend-sdk
# or
yarn add pay-with-zend-sdk

Requires Node.js 18 or later.

Getting an API key

You cannot create a User API Key by typing a secret anywhere — it's issued through a device-pairing flow, the same pattern used by tools like the GitHub CLI. The easiest way to get one is:

npx pay-with-zend-cli login

This prints an approval link (and a QR code) — open it on your phone, approve it inside the Zend App with your existing PIN/biometrics, and the CLI receives your key automatically. The key is never typed into a terminal.

If you'd rather drive the pairing flow yourself (e.g. from your own onboarding UI), see Programmatic pairing below.

Quick start

import { createZendClient } from "pay-with-zend-sdk";

const zend = createZendClient({
  apiKey: process.env.ZEND_API_KEY!, // zdev_live_...
});

const payment = await zend.createZendPayment({
  amount: 25.0,
  description: "Order #1024",
  redirectUrl: "https://yourapp.com/checkout/return",
  webhookUrl: "https://yourapp.com/webhooks/zend", // optional per-request override
});

console.log(payment.linkUrl);
// https://zdfi.me/@yourtag/abc123def0

Share payment.linkUrl with your customer. On a phone with the Zend App installed, it deep-links straight into a native payment confirmation sheet. Without the app, it falls back to the hosted zdfi.me web checkout. Same link, both paths — you don't need to detect which one applies.

There's also a plain convenience function if you don't want to hold onto a client instance:

import { createZendPayment } from "pay-with-zend-sdk";

const payment = await createZendPayment(
  { apiKey: process.env.ZEND_API_KEY! },
  { amount: 25.0 },
);

API reference

createZendClient(config)

interface ZendClientConfig {
  apiKey: string;
  baseUrl?: string; // defaults to https://api-v2.zendfi.tech
}

Returns a ZendClient with the methods below. Every method throws ZendPaymentError on failure (see Error handling).

Payment requests

createZendPayment(input)

interface CreatePaymentRequestInput {
  amount: number;                // USD, 0.01 - 100,000
  description?: string;         // max 500 characters
  expiresInMinutes?: number;    // 1 - 60, defaults to 15
  redirectUrl?: string;         // HTTPS, max 2048 characters
  webhookUrl?: string;          // HTTPS, max 2048 characters — overrides your global webhook URL for this request only
}

Returns a CreatePaymentRequestResult:

interface CreatePaymentRequestResult {
  id: string;
  linkUrl: string;             // the zdfi.me link — share this with your customer
  status: "pending";
  amount: number;                // USD
  description: string | null;
  expiresAt: string;           // ISO 8601
  source: "api";
}

redirectUrl only matters for mobile web checkout flows: after the payer confirms inside the Zend App, they're redirected back to this URL with a single-use zend_return_token query parameter attached. Verify that token server-side with verifyReturnToken before trusting the redirect (see Return tokens).

getPaymentRequest(id)

const request = await zend.getPaymentRequest(payment.id);
// { id, status, amount, description, expiresAt, paidAt, hasRedirectUrl, linkUrl }

status is one of "pending" | "paid" | "expired" | "cancelled". Requesting a payment request that doesn't exist, or belongs to a different account, returns an identical "not found" error in both cases — the SDK (and the API underneath it) never reveals which.

listPaymentRequests(params?)

const { requests } = await zend.listPaymentRequests({ status: "paid", limit: 20 });

limit is clamped to 1-50 (defaults to 20).

testPaymentRequest(input) — Sandbox Mode

Takes the exact same input as createZendPayment, runs the exact same validation server-side, but never creates a request, moves funds, or fires a webhook:

const result = await zend.testPaymentRequest({ amount: 25.0 });
// { sandbox: true, valid: true, amount: 25, description: null, expiresInMinutes: 15, redirectUrl: null, webhookUrl: null }

Useful in CI or local dev to confirm your integration is sending well-formed requests without touching a real balance.

Webhooks

Configure where events are delivered, and validate them when they arrive.

getWebhookConfig() / setWebhookUrl(url)

await zend.setWebhookUrl("https://yourapp.com/webhooks/zend");
const config = await zend.getWebhookConfig();
// { webhookUrl: "https://yourapp.com/webhooks/zend", hasSecret: true }

A webhook secret is generated automatically the first time you set a URL — you never generate or upload one yourself. Get the secret's value from zend config get webhookUrl in the CLI, or from your account settings; it's never returned by this call for security (only whether one exists).

verifyWebhookSignature(payload, signatureHeader, secret, toleranceSeconds?)

Every Developer Webhook Event is delivered with an X-Zend-Signature header in the form t=<unix_seconds>,v1=<hex_hmac_sha256>. Verify it like this:

import { verifyWebhookSignature } from "pay-with-zend-sdk";
import express from "express";

const app = express();

app.post("/webhooks/zend", express.text({ type: "*/*" }), (req, res) => {
  const signatureHeader = req.header("X-Zend-Signature") ?? "";
  const isValid = verifyWebhookSignature(req.body, signatureHeader, process.env.ZEND_WEBHOOK_SECRET!);

  if (!isValid) {
    return res.status(401).send("invalid signature");
  }

  const event = JSON.parse(req.body) as import("pay-with-zend-sdk").DeveloperWebhookEvent;
  // handle event.event, event.payment_request, ...
  res.status(200).send("ok");
});

Important: verify against the raw request body string, not a re-serialized object — the signature is computed over the exact bytes sent, and re-serializing (even to logically identical JSON) can produce a different byte sequence and fail verification.

toleranceSeconds defaults to 300 (5 minutes), matching the backend's replay window exactly. Signatures older than that are rejected regardless of correctness.

Event types

type DeveloperWebhookEventType =
  | "payment_request_created"
  | "payment_request_pending"
  | "payment_request_succeeded"
  | "payment_request_failed"
  | "payment_request_expired"
  | "payment_request_cancelled";

Events are only ever fired for requests you created via this SDK/the CLI (source: "api") — peer-to-peer payments made from inside the Zend App never trigger a Developer Webhook Event.

listWebhookDeliveries(limit?)

const deliveries = await zend.listWebhookDeliveries(50);
// [{ eventType, webhookUrl, status, attempts, responseCode, createdAt }, ...]

Useful for debugging delivery failures without needing to check zend logs tail in the CLI.

Return tokens

If you set redirectUrl on a payment request and the payer confirms via the native app, they're redirected back to that URL with ?zend_return_token=... appended. Verify it before resuming their session:

const result = await zend.verifyReturnToken({
  token: req.query.zend_return_token as string,
  paymentRequestId: payment.id,
});

if (result.valid) {
  // resume the customer's checkout session
}

Return tokens are single-use and expire 5 minutes after issuance. Every failure mode — expired, already used, unknown, or bound to a different request — is reported identically as valid: false; the API deliberately does not distinguish between them, so don't try to branch on why a token failed.

API key management

const created = await zend.createApiKey({ scopes: ["create_payment_request", "read"] });
console.log(created.apiKey); // shown exactly once — store it now, it cannot be retrieved again

const keys = await zend.listApiKeys();
// [{ id, displayPrefix, scopes, createdAt, lastUsedAt }, ...] — never includes plaintext keys

await zend.revokeApiKey(created.id);

Scopes are "create_payment_request" | "read" | "manage_webhook". A key can only ever create another key with a subset of its own scopes — you can't use a read-only key to mint yourself broader access.

Error handling

Every SDK method throws ZendPaymentError on failure:

import { ZendPaymentError } from "pay-with-zend-sdk";

try {
  await zend.createZendPayment({ amount: -5 });
} catch (err) {
  if (err instanceof ZendPaymentError) {
    console.error(err.code);       // e.g. "INVALID_AMOUNT"
    console.error(err.message);    // human-readable
    console.error(err.statusCode); // HTTP status, if the error came from a response
  }
}

Network failures (timeouts, DNS errors, connection refused) also throw ZendPaymentError, with code: "NETWORK_ERROR" and no statusCode.

Programmatic pairing

Most integrations should just run zend login from the CLI. If you're building your own onboarding flow and want to drive the device-pairing handshake directly, the client also exposes the underlying pairing primitives:

const anon = createZendClient({ apiKey: "" }); // no key needed yet

const session = await anon.createPairingSession({ cliDisplayName: "My Integration" });
console.log(session.approvalUrl); // show this as a link or QR code to the user

// Poll no faster than every 2 seconds
let status = "pending";
while (status === "pending") {
  await new Promise((r) => setTimeout(r, 2000));
  ({ status } = await anon.getPairingSessionStatus(session.sessionId));
}

if (status === "approved") {
  const { apiKey } = await anon.retrievePairingKey(session.sessionId); // succeeds exactly once
  // persist apiKey securely
}

retrievePairingKey can only be called once per session — a second call returns an error, by design.

Types

Every request, response, and webhook payload used by this SDK is exported as a named type — the package has no any in its public surface. Import what you need directly:

import type {
  ZendClientConfig,
  CreatePaymentRequestInput,
  CreatePaymentRequestResult,
  GetPaymentRequestResult,
  DeveloperWebhookEvent,
  DeveloperWebhookEventType,
  VerifyReturnTokenInput,
  VerifyReturnTokenResult,
  WebhookConfig,
  WebhookDeliveryRecord,
  ApiKeyMetadata,
  CreateApiKeyInput,
  CreateApiKeyResult,
  TestPaymentRequestResult,
} from "pay-with-zend-sdk";

Security notes

  • Treat your API key like any other secret — it's scoped to your own account and can create payment requests, read your data, and (depending on scopes) manage your webhook config.
  • Always verify webhook signatures before acting on a webhook payload. Never trust an unsigned or incorrectly-signed request.
  • redirect_url and webhook_url must be HTTPS — plain HTTP is rejected by the API.
  • This SDK never touches merchant infrastructure, merchant API keys, or merchant endpoints. It is a distinct, user-scoped system by design.

License

MIT