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

@backendfree/payments

v0.1.1

Published

Typed access to a BackendFree project's money: what has been taken, and opening a payment page.

Readme

@backendfree/payments

A project's money: what has been taken, and taking some. Built on @backendfree/core.

npm install @backendfree/core @backendfree/payments
import { Client } from '@backendfree/core';
import { Payments } from '@backendfree/payments';

const payments = new Payments(
  new Client({ origin: 'https://backendfree.com', key: process.env.BACKENDFREE_SECRET_KEY! }),
);

const payment = await payments.checkout({
  amount: 3500,                              // minor units: CHF 35.00
  description: 'Deposit',
  success_url: 'https://shop.example/thanks?payment={id}',
  cancel_url: 'https://shop.example/cart',
  metadata: { order: 'A-1042' },             // your own references, returned on every read and event
});

redirect(payment.checkout_url);

{id} in either address becomes the payment's id, which does not exist until this call returns, so the page after paying can read the payment it was for. metadata comes back on the payment and in every payment.* event about it, so the webhook can find the order it paid for: at most 20 keys, each value text.

Server only

Every method here needs a secret key, and constructing Payments with a publishable one throws. A payment carries the paying customer's name and email, and a checkout is a charge on a bank account, so none of it belongs in a bundle whoever loads the site can read.

That is also why the mistake is refused at construction rather than by a 403 in production: the stack trace points at the line that is wrong.

Why there is no refund method

Refunding is the one act that moves money out of the business's own account, so it lives in the payments screen in the dashboard, behind a capability only the account owner holds.

A secret key lives in server code, deploy settings and CI, which is further than the account owner's own screen. A leaked key that can read payment records is one incident; one that could refund would drain the account. Reading payment records with a key is deliberate and bounded, so a developer can render a receipt; issuing money back is not the same thing and is not in the API.

Nothing here says a payment was made

A checkout is a page that exists. The provider's webhook is what says money moved, and the page the customer lands on afterwards says nothing at all.

Take the platform's payment.succeeded webhook and verify it:

import { verifyWebhook } from '@backendfree/core';

const event = await verifyWebhook({ body: await request.text(), headers, secret });
if (event.name === 'payment.succeeded') {
  markPaid(event.data.metadata.order, event.data.amount, event.data.currency);
}

When the provider refuses

checkout() does not throw for a provider that would not open a page. The payment comes back with an empty checkout_url and the reason on it, so the attempt is on record and you have an id to try again with.

if (!payment.checkout_url) {
  payment = await payments.offerAgain(payment.id, { success_url, cancel_url });
}

offerAgain is keyed on the payment, so asking twice is one page, and what is owed comes off the payment rather than off the call. Calling checkout() a second time would record a second payment for one order, and a customer handed both pages could pay twice.

Reading what has been taken

await payments.list({ status: 'paid' });
await payments.payment(id);
await payments.forSubject('bookings.booking', bookingId);   // was it paid for?

A payment says what it settled, how much, what has gone back and what is left to give back. It never says which provider account the money landed in: that is the business's own and appears nowhere in this API.

refundable is published rather than left to you to work out, because the arithmetic is not what it looks like. A refund that is still settling is money already spent, so subtracting only the settled ones shows more left than there is.

Money

Always minor units and a currency. 3500 with CHF is 35 francs.

import { formatMoney } from '@backendfree/payments';

formatMoney({ amount: 6500, currency: 'CHF' }, 'de-CH');   // CHF 65.00
formatMoney({ amount: 6500, currency: 'JPY' }, 'en-US');   // ¥6,500

Dividing by a hundred yourself is wrong in both directions: JPY has no minor unit and KWD has three places. The number of places comes from the currency rather than from a constant.

Reading a status

import { hasPaid, isOver } from '@backendfree/payments';

hasPaid(payment);   // the money arrived, refunded payments included
isOver(payment);    // nothing further will happen, so do not offer a page

A status only ever moves forward. A provider redelivers events for days and promises no order, so the platform applies each one as a maximum and a late "still pending" cannot undo a payment that arrived.