A production-grade, zero-runtime-dependency TypeScript SDK for the Moov payments API — accounts, transfers, cards, ACH, issuing, wallets, disputes, and 30+ more resource areas covering the full API surface.
- Zero runtime dependencies — built entirely on the Web Fetch API, Web Crypto, and
FormData, so it runs unmodified on Node.js 18+, browsers, Deno, Bun, Cloudflare Workers, and other Fetch-compatible edge runtimes. - Dual ESM/CJS —
importandrequire()both work out of the box, with full type declarations for each. - Fully typed — every request and response shape is a hand-derived TypeScript
interface, not
any. - Idempotent by default — mutation-safe endpoints (like
transfers.create) auto-generate a UUID v4 idempotency key that's reused across retries. - Automatic retries — transient failures (429, 5xx, network errors) are retried with
exponential backoff and full jitter, honoring the API's
Retry-Afterhints. - Webhook verification — constant-time HMAC-SHA512 signature verification via a
separate
moov-client/webhooksentry point (keepscryptocode out of your main bundle unless you need it).
npm install moov-clientimport { Moov } from "moov-client";
const moov = new Moov({
publicKey: process.env.MOOV_PUBLIC_KEY!,
privateKey: process.env.MOOV_PRIVATE_KEY!,
});
const account = await moov.accounts.create({
accountType: "business",
profile: {
business: {
legalBusinessName: "Whole Foods",
businessType: "llc",
},
},
});
const transfer = await moov.transfers.create(account.accountID, {
source: { paymentMethodID: sourcePaymentMethodID },
destination: { paymentMethodID: destinationPaymentMethodID },
amount: { currency: "USD", value: 2500 },
});
console.log(transfer.transferID, transfer.status);Client-side (e.g. with a Moov.js-issued token) instead of a public/private key pair:
const moov = new Moov({ token: shortLivedAccessToken });Every method rejects with one of three typed errors:
| Class | When |
|---|---|
MoovAPIError |
Moov returned a non-2xx response |
MoovNetworkError |
The request never reached Moov (DNS, TCP, TLS, timeout, abort) |
MoovDecodeError |
A 2xx response body wasn't valid JSON |
import { isNotFound, isValidationError, MoovAPIError } from "moov-client";
try {
return await moov.accounts.get(accountID);
} catch (err) {
if (isNotFound(err)) return null;
if (isValidationError(err)) {
console.error("validation failed:", err.body);
}
if (err instanceof MoovAPIError) {
console.error(err.statusCode, err.requestID, err.message);
}
throw err;
}Other guards: isUnauthorized, isForbidden, isConflict, isRateLimited,
isTransientError.
transfers.create() is idempotent by nature on Moov's API, so the SDK auto-generates a
UUID v4 X-Idempotency-Key and reuses the same key across every retry of a single
logical call. To stay deduplicated across process restarts (e.g. a webhook handler that
might be invoked twice for the same order), pass your own key:
await moov.transfers.create(accountID, req, {
idempotencyKey: `order-${orderID}`,
});const moov = new Moov({
publicKey,
privateKey,
maxRetries: 3, // additional attempts beyond the first (default: 3)
retryBaseDelayMs: 250, // default: 250
retryMaxDelayMs: 8000, // default: 8000
timeoutMs: 30_000, // per-request timeout; 0 disables it (default: 30000)
onRetry: (attempt, delayMs, err) => console.warn(`retry ${attempt} in ${delayMs}ms`, err),
});Every call also accepts a signal (AbortSignal) as part of its per-request options:
const controller = new AbortController();
await moov.accounts.get(accountID, { signal: controller.signal });Import from the dedicated moov-client/webhooks entry point so signature-verification code
(and its Web Crypto usage) stays out of bundles that don't need it:
import { parseEvent, InvalidSignatureError } from "moov-client/webhooks";
// Express example — req.headers works directly; for raw bytes/text, pass those instead.
app.post("/webhooks/moov", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await parseEvent(req.headers, req.body, process.env.MOOV_WEBHOOK_SECRET!);
switch (event.type) {
case "transfer.updated":
// event.data is `unknown` — validate/cast to your own payload shape
break;
}
res.sendStatus(200);
} catch (err) {
if (err instanceof InvalidSignatureError) return res.sendStatus(401);
throw err;
}
});parseEvent/verifySignature accept a native Headers, a Map, or a plain object as
the header source, so they work the same in Express, Fastify, Next.js Route Handlers, or
Cloudflare Workers.
Every one of Moov's resource areas is available as a property on the Moov instance:
| Property | Covers |
|---|---|
accounts, capabilities, representatives, underwriting, billing, files, onboarding, resolution, partner |
Account lifecycle, KYC/KYB, billing, partner tooling |
bankAccounts, cards, applePay, googlePay, paymentMethods, terminal, wallets |
Payment sources & rails |
transfers, sweeps, refunds, disputes, issuing, invoices, paymentLinks, receipts, schedules |
Money movement |
images, products, support |
Account-level tooling |
branding, enrichment, institutions |
Enrichment & lookups |
auth, e2ee |
OAuth2 tokens & end-to-end encryption keys |
Every method also accepts a final RequestOptions argument (idempotencyKey, waitFor,
apiVersion, maxRetries, signal, headers) to override defaults for a single call.
- Node.js 18+ (for native
fetch,FormData, andcrypto.subtle), or any modern browser / edge runtime with the same APIs. - TypeScript 5+ if you want the bundled type declarations (the package works fine from plain JavaScript too).
MIT