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

@intrpay/node

v0.5.0

Published

Node.js server SDK for the Intrpay API

Readme

@intrpay/node

Node.js server SDK for the Intrpay API. Use it from your backend to call the API, mint scoped embed sessions, and verify incoming webhooks.

This is a server-side SDK. Your API key is a secret and must never be exposed to the browser.

Install

npm install @intrpay/node

Requires Node.js 18+.

Usage

Create a client

import { createIntrpayClient } from '@intrpay/node';

const intrpay = createIntrpayClient({
  apiKey: process.env.INTRPAY_API_KEY!, // from the Intrpay dashboard
});

// Target the test environment (api.intrpay.dev):
const intrpayTest = createIntrpayClient({
  apiKey: process.env.INTRPAY_API_KEY!,
  environment: 'test',
});

Options:

| Option | Default | Description | | ------------- | -------------- | ------------------------------------------------------------------------------- | | apiKey | (required) | API key from the Intrpay dashboard. | | environment | 'production' | API environment: 'production' (api.intrpay.us) or 'test' (api.intrpay.dev). | | version | 2026-06 | Calver contract version sent as Intrpay-Version. | | timeoutMs | none | Per-request timeout in milliseconds. |

Every request is sent with the x-api-key and Intrpay-Version headers.

Products

await intrpay.products.list();
await intrpay.products.get(productId);
await intrpay.products.create({ name: 'Pro plan', unitPrice: 4900 });
await intrpay.products.update(productId, { unitPrice: 5900 });
await intrpay.products.updateInventory(productId, 42);
await intrpay.products.delete(productId);

Payment links

await intrpay.paymentLinks.list();
await intrpay.paymentLinks.get(id);
await intrpay.paymentLinks.create({ name: 'Donation', card: true });
await intrpay.paymentLinks.update(id, { title: 'Updated' });
await intrpay.paymentLinks.activate(id);
await intrpay.paymentLinks.deactivate(id);
await intrpay.paymentLinks.delete(id);

Contacts

await intrpay.contacts.list();

// `search` matches name, email, phone, or company name.
await intrpay.contacts.list({ search: 'acme.com', limit: 25, offset: 0 });

const contact = await intrpay.contacts.create({
  companyName: 'Acme Inc',
  email: '[email protected]',
});
await intrpay.contacts.get(contact.id);
await intrpay.contacts.update(contact.id, { phone: '5551234567' });

Contacts are unique by id. Display names must be unique per account (HTTP 409 on an explicit duplicate). Email and phone may repeat. upsert does not merge on email or phone. force still creates a contact backed by a stub user; display-name uniqueness still applies.

Payment methods (ACH)

Only ACH payment methods can be created through the API - cards go through the embed flow. Two ways to create one:

From raw bank details:

const method = await intrpay.contacts.paymentMethods.create(contactId, {
  type: 'ach',
  ach: {
    accountHolderName: 'Acme Inc',
    routingNumber: '021000021',
    accountNumber: '123456789',
    accountType: 'checking',
    accountOwnerType: 'company',
  },
  default: true,
});

From a Plaid connection (enables balance checks before charging):

// 1. Mint a link token and render Plaid Link in your own UI.
const { linkToken, plaidConnectionId } = await intrpay.plaid.createLinkToken({ contactId });

// 2. After Plaid Link succeeds, exchange the public token.
await intrpay.plaid.exchange({
  publicToken, // from Plaid Link onSuccess
  plaidConnectionId,
  plaidAccountId, // optional: the account the user picked
});

// 3. Create the payment method from the connection.
const method = await intrpay.contacts.paymentMethods.create(contactId, {
  plaidConnectionId,
  default: true,
});

List a contact's saved (masked) methods:

await intrpay.contacts.paymentMethods.list(contactId);

Change which method is the default, or remove one. Payment details cannot be replaced - add a new method instead. Deleting a method that has ever been charged keeps it internally so past transactions stay rechargeable; it simply stops listing and can no longer be used.

await intrpay.contacts.paymentMethods.update(contactId, paymentMethodId, { default: true });
await intrpay.contacts.paymentMethods.delete(contactId, paymentMethodId);

Subscriptions

await intrpay.subscriptions.list();
await intrpay.subscriptions.list({ status: 'active', contactId });

// One product per subscription. A start on/before today bills immediately.
const subscription = await intrpay.subscriptions.create({
  contactId,
  productId,
  frequency: 'monthly',
  paymentMethodId: method.id, // auto-charge with the saved ACH method
});

await intrpay.subscriptions.get(subscription.id);

Billing everything on one day

By default each subscription bills on its own start-date anniversary, so a contact with three products can end up with three separate charges a month. alignBillingDate puts a new subscription on the day the contact already bills, and prorate charges only the days between signup and that day.

await intrpay.subscriptions.create({
  contactId,
  productId,
  frequency: 'monthly',
  paymentMethodId: method.id,
  alignBillingDate: true, // adopt this contact's existing monthly billing day
  prorate: true, // charge only the partial period up to it
});

The contact's first subscription has nothing to align to, so it bills a full period immediately and becomes the day everything added later lands on. Pass an ISO date instead of true to pick the day yourself. Subscriptions sharing a contact and billing day are consolidated into one invoice.

// Pricing/settings only. Schedule fields (frequency, billingInterval,
// startDate) are immutable - cancel and create a new subscription instead.
await intrpay.subscriptions.update(subscription.id, {
  unitPrice: 15,
  prorateChange: true, // settle the mid-cycle price change now
});

await intrpay.subscriptions.pause(subscription.id, { reason: 'Customer request' });
await intrpay.subscriptions.resume(subscription.id);
await intrpay.subscriptions.cancel(subscription.id, { reason: 'Churned' });
await intrpay.subscriptions.archive(subscription.id);

// Invoice the current billing period now instead of waiting for the daily cron
await intrpay.subscriptions.invoiceNow(subscription.id);

Invoices

Create, list, update, and manage invoices. action defaults to send: a bare create / update publishes the invoice AND emails the customer. Pass action: 'save' to skip the email, or action: 'draft' on create to save a draft.

const { invoices, total } = await intrpay.invoices.list({ contactId });
await intrpay.invoices.list({ subscriptionId: subscription.id, status: 'open' });
await intrpay.invoices.get(invoiceId);

// Publishes and emails (action defaults to 'send')
const invoice = await intrpay.invoices.create({
  contactId,
  lineItems: [{ productId, name: 'Consulting', quantity: 1, unitPrice: 150 }],
  terms: 'net_30',
  paymentCollection: { type: 'none' },
});

// Publish without emailing
await intrpay.invoices.create({
  contactId,
  lineItems: [{ productId, name: 'Fee', quantity: 1, unitPrice: 50 }],
  action: 'save',
});

await intrpay.invoices.update(invoice.id, {
  comments: 'Updated note',
  action: 'save',
});

await intrpay.invoices.send(invoice.id);
await intrpay.invoices.void(invoice.id);
await intrpay.invoices.delete(invoice.id);

Credit memos

Create, list, update, apply, and void credit memos. Unlike invoice line items, credit memo line items may omit productId for a free-text line.

const { creditMemos, total } = await intrpay.creditMemos.list({ contactId });
await intrpay.creditMemos.get(creditMemoId);

// Defaults to status 'draft'; pass status: 'open' to make it immediately
// available to apply.
const creditMemo = await intrpay.creditMemos.create({
  contactId,
  creditMemoDate: new Date().toISOString(),
  lineItems: [{ name: 'Refund - damaged item', quantity: 1, unitPrice: 25 }],
  status: 'open',
});

await intrpay.creditMemos.update(creditMemo.id, { note: 'Approved by support' });

const application = await intrpay.creditMemos.apply(creditMemo.id, {
  invoiceId,
  amount: 25,
});
await intrpay.creditMemos.unapply(creditMemo.id, application.id);
await intrpay.creditMemos.void(creditMemo.id);

Statements

A statement is an immutable snapshot of a contact's activity for a period, plus a link the customer can pay their open invoices from. Statements have no status: sentAt is the only send signal, and deleting one never touches the ledger, since any payment taken through its link lives on the invoices.

Numbers are plain ('1000') and come from their own sequence, so a statement number may coincide with an invoice number.

const { statements, total } = await intrpay.statements.list({ contactId });
await intrpay.statements.get(statementId);

// See what a period would say without creating anything.
const preview = await intrpay.statements.preview({
  contactId,
  periodStart: '2026-07-01',
  periodEnd: '2026-07-31',
});

// `include` only filters which rows are listed - opening and closing balances
// always come from the full ledger. Transactions and estimates are off by
// default.
const statement = await intrpay.statements.create({
  contactId,
  periodStart: '2026-07-01',
  periodEnd: '2026-07-31',
  include: { transaction: true },
  message: 'Thanks for your business.',
  allowPartialPayments: true,
  send: true,
});

await intrpay.statements.send(statement.id, { cc: ['[email protected]'] });
await intrpay.statements.delete(statement.id);

Payment plans

Split one or more existing invoices into scheduled installments. recurringSchedule is required once a plan has 2 or more lines.

const { paymentPlans, total } = await intrpay.paymentPlans.list({ contactId });
await intrpay.paymentPlans.get(paymentPlanId);

const plan = await intrpay.paymentPlans.create({
  invoiceIds: [invoice.id],
  contactId,
  paymentMethodId: method.id,
  recurringSchedule: 'monthly',
  lines: [
    { amount: 50, scheduledDate: '2026-08-01' },
    { amount: 50, scheduledDate: '2026-09-01' },
  ],
});

// Update top-level fields, or pass `lines` to replace the schedule entirely.
await intrpay.paymentPlans.update(plan.id, { description: 'Renamed plan' });

await intrpay.paymentPlans.pause(plan.id);
await intrpay.paymentPlans.cancel(plan.id);

// Charge a scheduled/failed installment. Pass idempotencyKey to safely retry.
await intrpay.paymentPlans.chargeLine(plan.id, plan.lines[0].id, {
  idempotencyKey: 'unique-retry-key',
});

Payments

Record and delete manual (non-gateway) invoice payments - cash, check, wire, ACH, card, DAF, Zelle, or other. Gateway charges are a separate resource; see Transactions.

const { payments, total } = await intrpay.payments.list({ contactId });
await intrpay.payments.get(paymentId);

const payment = await intrpay.payments.create({
  contactId,
  type: 'check',
  refNumber: '1234',
  paymentDate: new Date().toISOString(),
  amount: 100,
  paymentLines: [{ invoiceId: invoice.id, amount: 100 }],
});

await intrpay.payments.delete(payment.id);

Transactions

Ad-hoc charges against a contact's stored payment method, plus refund and void. paymentMethodId must reference a payment method already on file - raw card/ACH details are never accepted here.

const { transactions, total } = await intrpay.transactions.list({ contactId });
await intrpay.transactions.get(transactionId);

// Filter to what one payment link collected. Worth knowing for a `checkout`
// link: its invoice is raised only after the charge succeeds and carries its
// own pay-link, so the transaction is the one record of which link was paid.
const { transactions: collected } = await intrpay.transactions.list({ paymentLinkId });

// Pass idempotencyKey to safely retry without double-charging
const transaction = await intrpay.transactions.charge(
  {
    contactId,
    paymentMethodId: method.id,
    amount: 50,
    note: 'Manual charge for services rendered',
  },
  { idempotencyKey: 'unique-retry-key' }
);

await intrpay.transactions.refund(transaction.id, { amount: 20, note: 'Partial refund' });
await intrpay.transactions.void(transaction.id, { note: 'Entered in error' });

Projects

Job-costing projects group a customer's invoices and estimates under a budget/timeline. A project's contactId cannot be changed after creation.

const { projects } = await intrpay.projects.list({ contactId });

const project = await intrpay.projects.create({
  contactId,
  name: 'Kitchen remodel',
  budget: 15000,
  startDate: '2026-08-01',
  endDate: '2026-10-01', // cannot be before startDate
});

await intrpay.projects.get(project.id);
await intrpay.projects.update(project.id, { status: 'completed' });

// Both the document and the project must belong to the same contact.
await intrpay.projects.assignInvoice(project.id, invoice.id);
await intrpay.projects.unassignInvoice(project.id, invoice.id);
await intrpay.projects.assignEstimate(project.id, estimate.id);
await intrpay.projects.unassignEstimate(project.id, estimate.id);

await intrpay.projects.archive(project.id);

Checkout sessions

Sell to one of your contacts in the browser. You build the cart on your own backend, we price it, and the invoice is raised from the charge:

const checkout = await intrpay.checkout.sessions.create({
  contactId,
  title: 'Pro plan + onboarding',
  items: [
    { productId: 'prod_pro_plan' },
    // An add-on the customer can tick, and buy more than one of.
    { productId: 'prod_onboarding', optional: true },
    // Sell the same product at a negotiated price on this cart only.
    { productId: 'prod_seats', quantity: 5, unitPrice: 12 },
  ],
  methods: { card: true, ach: true },
});

// The token is already bound to this cart - no id reaches the browser.
res.json({ token: checkout.token });
import { Checkout } from '@intrpay/react';

<Checkout token={token} onSuccess={(r) => console.log(r.checkoutSessionId)} />;

Every line names a catalog product, so the invoice the charge raises carries productId and the sale lands in product revenue and inventory rather than as a loose line of text. Prices are resolved and frozen when the session is created: the browser only ever sends back line ids and add-on quantities, so a compromised host page cannot name its own total.

Read the outcome back from your backend rather than trusting the browser:

const session = await intrpay.checkout.sessions.get(checkout.id);

if (session.status === 'paid' && session.paymentStatus === 'approved') {
  grantAccess(session.contactId, { invoiceId: session.invoiceId });
}

status is paid as soon as the charge lands, but ACH takes days to clear - paymentStatus is pending until it does. If you are granting something the customer paid for and cannot easily take it back, wait for approved.

No invoice exists until the charge lands, so an abandoned or declined checkout costs you an expired session rather than an unpaid document in your books. A session stops being payable after ttlSeconds (an hour by default), or when you give up on it early:

await intrpay.checkout.sessions.expire(checkout.id);

A paid session cannot be paid again, and cannot be expired - abandoning one can never bury a sale.

Embed sessions

Mint a short-lived, scoped token for the browser embed (e.g. @intrpay/react):

const { token, expiresAt } = await intrpay.embed.sessions.create({
  contactId,
  scope: ['invoices:read:contact', 'payment_methods:write:contact'],
  ttlSeconds: 900,
});

contactId is required when any requested scope ends with :contact.

Request only what the surface you are rendering needs:

| Scope | What it allows | | ------------------------------- | ------------------------------------------------------------------------------------- | | invoices:read:contact | Read the contact's invoices (<InvoiceList>, <InvoiceDetail>) | | invoices:pay:contact | Pay one of them (<Checkout invoiceId>), including collecting the method used to pay | | payment_methods:read:contact | List the contact's saved methods | | payment_methods:write:contact | Save a new one without charging (<AddPaymentMethod>) |

Selling from your catalog is not one of these. A token minted so a customer can settle a bill they already owe should not also be able to transact against your catalog, so that power only comes with a cart it is bound to - create a checkout session and use the token it returns.

Webhooks

Intrpay signs every outbound webhook with HMAC-SHA256 over the raw request body using the endpoint's signing secret, sent in the X-Webhook-Signature header.

import { constructEvent, verifyWebhook } from '@intrpay/node';

// Express example: use the raw body, not the parsed JSON.
app.post('/webhooks/intrpay', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = constructEvent({
      body: req.body, // Buffer
      signature: req.get('X-Webhook-Signature') ?? '',
      secret: process.env.INTRPAY_WEBHOOK_SECRET!,
    });
    // event => { event, data, timestamp }
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);
  }
});

verifyWebhook(...) returns a boolean if you'd rather verify and parse yourself.

Deliverable events (see the WebhookEventName type): bill.created, bill.approved, invoice.created, invoice.paid, invoice.voided, payment.created, payment.deleted, subscription.created, subscription.updated, subscription.paused, subscription.resumed, subscription.cancelled, subscription.overdue, subscription.payment_failed, subscription.expired, and payment_method.created. Subscription event payloads carry subscriptionId, contactId and accountId (subscription.payment_failed adds invoiceId and errorMessage); payment_method.created carries paymentMethodId, contactId and accountId.

Errors

Non-2xx responses throw an IntrpayError with status and body:

import { IntrpayError } from '@intrpay/node';

try {
  await intrpay.products.get('missing');
} catch (err) {
  if (err instanceof IntrpayError) {
    console.error(err.status, err.body);
  }
}