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

monzo-sdk

v1.0.0

Published

A complete, type-safe, production-grade Node.js/TypeScript SDK for the Monzo API (OAuth2, Accounts, Balance, Pots, Transactions, Feed Items, Attachments, Receipts, Webhooks).

Readme

monzo-sdk

A complete, type-safe, production-grade Node.js/TypeScript SDK for the Monzo API.

  • Full API coverage — OAuth2, Accounts, Balance, Pots, Transactions, Feed Items, Attachments, Transaction Receipts, Webhooks
  • Strict TypeScript — every request and response is fully typed, built with exactOptionalPropertyTypes and strict mode
  • Dual ESM/CJS — works in Node.js require(), ESM import, and modern bundlers
  • Resilient by default — automatic exponential-backoff retries on transient errors, automatic token refresh on 401s
  • Zero runtime dependencies — built entirely on the standard fetch API

Note on scope: the Monzo API is intended for personal use or a small, explicitly-allowed set of users — not for public multi-tenant applications. See Monzo's docs for details. This SDK doesn't change that policy; it's just a client for it.

Install

npm install monzo-sdk

Requires Node.js >= 18.17 (for global fetch/crypto.getRandomValues), or any modern browser/edge runtime.

Quickstart

import { MonzoClient } from "monzo-sdk";

const client = new MonzoClient({
  accessToken: process.env.MONZO_ACCESS_TOKEN,
  refreshToken: process.env.MONZO_REFRESH_TOKEN, // optional, enables auto-refresh
  clientId: process.env.MONZO_CLIENT_ID,
  clientSecret: process.env.MONZO_CLIENT_SECRET,
});

const accounts = await client.accounts.list();
const balance = await client.balance.read({ account_id: accounts[0].id });

console.log(`Balance: ${balance.balance / 100} ${balance.currency}`);

The OAuth2 flow

Monzo access tokens are obtained via a standard OAuth2 authorization-code flow, with one wrinkle: the token you get back from the code exchange is not usable until the user approves it inside the Monzo app (a push notification + PIN/biometric prompt). Calls will fail with 403 until that happens.

import { MonzoClient, generateStateToken } from "monzo-sdk";

const client = new MonzoClient();

// 1. Send the user to Monzo to authorize your app.
//    Persist `state` (e.g. in a signed cookie/session) to verify on callback.
const state = generateStateToken();
const url = client.auth.buildAuthorizationUrl({
  clientId: process.env.MONZO_CLIENT_ID!,
  redirectUri: "https://yourapp.com/oauth/callback",
  state,
});
// redirect the user's browser to `url`

// 2. In your callback handler, verify `state` matches, then exchange the code.
const tokens = await client.auth.exchangeAuthorizationCode({
  clientId: process.env.MONZO_CLIENT_ID!,
  clientSecret: process.env.MONZO_CLIENT_SECRET!,
  redirectUri: "https://yourapp.com/oauth/callback",
  code: req.query.code as string,
});

client.setTokens({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token });

// 3. Tell the user to check their phone and approve access in the Monzo app
//    before making further calls.

Automatic token refresh

If you construct the client with refreshToken, clientId, and clientSecret, it will transparently refresh an expired access token on a 401 and retry the original request once — no extra code needed. Use onTokenRefresh to persist the rotated tokens (Monzo refresh tokens are single-use):

const client = new MonzoClient({
  accessToken: user.accessToken,
  refreshToken: user.refreshToken,
  clientId: process.env.MONZO_CLIENT_ID!,
  clientSecret: process.env.MONZO_CLIENT_SECRET!,
  onTokenRefresh: async (tokens) => {
    await db.users.update(user.id, tokens);
  },
});

Usage examples

Accounts & balance

const accounts = await client.accounts.list({ account_type: "uk_retail" });
const balance = await client.balance.read({ account_id: accounts[0].id });

Pots

const pots = await client.pots.list({ current_account_id: accountId });

await client.pots.deposit({
  potId: pots[0].id,
  sourceAccountId: accountId,
  amount: 5000, // minor units — 5000 = £50.00
  dedupeId: `deposit-${orderId}`, // keep static across retries of the same logical transfer
});

await client.pots.withdraw({
  potId: pots[0].id,
  destinationAccountId: accountId,
  amount: 2500,
  dedupeId: `withdraw-${orderId}`,
});

Transactions

// A single page
const transactions = await client.transactions.list({ account_id: accountId, limit: 50 });

// A specific transaction, with merchant details expanded inline
const tx = await client.transactions.retrieve({ transactionId: "tx_00008zIcpb1TB4yfVsE6EY", expand: ["merchant"] });

// Every transaction, transparently paginated (async iterator)
for await (const tx of client.transactions.listAll({ account_id: accountId })) {
  console.log(tx.id, tx.amount, tx.description);
}

// Annotate with custom metadata (empty string deletes a key)
await client.transactions.annotate({
  transactionId: tx.id,
  metadata: { my_app_category: "business_expense" },
});

Full-history sync window: Monzo only allows fetching a user's complete transaction history during the first 5 minutes after authentication. After that, only the last 90 days are available. If you need full history, run your listAll() backfill immediately after the OAuth callback completes.

Feed items

await client.feedItems.create({
  accountId,
  type: "basic",
  params: {
    title: "Order shipped!",
    image_url: "https://example.com/icon.png",
    body: "Your order #1234 is on its way.",
  },
  url: "https://example.com/orders/1234",
});

Attachments

const { upload_url, file_url } = await client.attachments.upload({
  fileName: "receipt.jpg",
  fileType: "image/jpeg",
  contentLength: fileBuffer.byteLength,
});

// Uploads directly to Monzo's storage (not the Monzo API itself)
await client.attachments.uploadFileToUrl(upload_url, fileBuffer, "image/jpeg");

const attachment = await client.attachments.register({
  externalId: tx.id,
  fileUrl: file_url,
  fileType: "image/jpeg",
});

// Later, if needed:
await client.attachments.deregister({ id: attachment.id });

Transaction receipts

Unlike the rest of the API, this endpoint takes a JSON body. external_id is your own idempotency key — calling create again with the same external_id updates the receipt.

await client.receipts.create({
  external_id: `order-${orderId}`,
  transaction_id: tx.id,
  total: 1299,
  currency: "GBP",
  items: [{ description: "Flat White", quantity: 1, amount: 1299, currency: "GBP" }],
});

const receipt = await client.receipts.retrieve({ externalId: `order-${orderId}` });
await client.receipts.delete({ externalId: `order-${orderId}` });

Webhooks

const webhook = await client.webhooks.register({ accountId, url: "https://yourapp.com/hooks/monzo" });
const webhooks = await client.webhooks.list({ accountId });
await client.webhooks.delete({ webhookId: webhook.id });

Handling an inbound webhook (e.g. in an Express/Fastify route):

import { parseWebhookEvent, assertExpectedAccount } from "monzo-sdk";

app.post("/hooks/monzo", express.text({ type: "*/*" }), (req, res) => {
  const event = parseWebhookEvent(req.body);
  assertExpectedAccount(event, [expectedAccountId]);

  if (event.type === "transaction.created") {
    console.log("New transaction:", event.data.id, event.data.amount);
  }

  res.sendStatus(200);
});

On webhook security: as of this writing, Monzo does not document a cryptographic signature (HMAC or similar) on webhook deliveries, so parseWebhookEvent cannot verify authenticity beyond shape-checking the payload. Serve your webhook endpoint over HTTPS, treat the URL itself as a secret, validate the account id against an allowlist (as above), and re-fetch anything financially sensitive via client.transactions.retrieve(...) rather than trusting the webhook body outright.

Error handling

Every error thrown by the SDK extends MonzoError, so you can catch broadly or narrowly:

import { MonzoApiError, MonzoTimeoutError, MonzoNetworkError, MonzoError } from "monzo-sdk";

try {
  await client.pots.withdraw({ potId, destinationAccountId, amount, dedupeId });
} catch (err) {
  if (err instanceof MonzoApiError) {
    if (err.isRateLimited) {
      // back off
    } else if (err.isForbidden) {
      // token hasn't been approved in-app yet, or lacks required scope
    }
    console.error(err.status, err.body, err.requestId);
  } else if (err instanceof MonzoTimeoutError || err instanceof MonzoNetworkError) {
    // transient — the SDK already retried idempotent requests automatically
  } else if (err instanceof MonzoError) {
    // MonzoValidationError, MonzoWebhookVerificationError, etc.
  }
  throw err;
}

Configuration reference

new MonzoClient({
  accessToken: string,          // current user access token
  refreshToken: string,         // enables auto-refresh, together with clientId/clientSecret
  clientId: string,
  clientSecret: string,
  baseUrl: string,              // default: https://api.monzo.com — override for testing
  fetch: typeof fetch,          // default: global fetch — override for testing/custom runtimes
  timeoutMs: number,            // default: 15000
  retry: {                      // default: { maxRetries: 2, baseDelayMs: 250, maxDelayMs: 4000 }
    maxRetries: number,
    baseDelayMs: number,
    maxDelayMs: number,
  },
  logger: {                     // default: no-op
    debug(message, meta?), warn(message, meta?), error(message, meta?)
  },
  userAgent: string,
  onTokenRefresh: (tokens) => void | Promise<void>,
});

Development

npm install
npm run build         # tsup — dual ESM/CJS + .d.ts
npm test              # vitest run (built on Vite)
npm run test:coverage
npm run lint
npm run typecheck

License

MIT