Freedom technology for inbound payments.
Accept Bitcoin payments on your website, app, or point of sale, straight into a wallet you control.
Bitcoin by default. Use the internet's neutral settlement currency. Your server issues a QR code. The payer pays the QR code, and your server approves delivery of the purchase.
Deposit-only by design. OpenReceive exposes no payment-sending API and never holds a key: it connects with only a spec-compliant receive-only NWC code. Choose an existing NWC service to receive payments, or build your own NWC Service.
To run the wallet on your own hardware, use an NWC service you host yourself, like Alby Hub.
Optionally swap in other currencies. Not every customer holds Bitcoin. Configure any swap provider to receive altcoins. Use any swap provider that implements the FixedFloat / Lightning-Swap API, or build your own.
Optional Inbound Currencies:
| Pay with | On network | Settles in |
|---|---|---|
# Node: an HTTP adapter for your framework, plus the checkout UI for yours
npm install @openreceive/express @openreceive/reactSwap @openreceive/express for @openreceive/fastify or @openreceive/next,
and @openreceive/react for @openreceive/vue, @openreceive/svelte,
@openreceive/angular, or @openreceive/elements (framework-free custom
element). On Rails:
# Gemfile
gem "openreceive-rails"Pick your stack:
| Stack | Quickstart |
|---|---|
| Node.js | Node quickstart |
| Ruby on Rails | Rails quickstart |
| BTCPay Server | Coming soon |
On all platforms: Your server owns the price and the order, the payer
gets a QR code to pay, and your onPaid hook runs once inside the settlement
transaction.
- OpenReceive does not transmit money or hold customer funds. OpenReceive only helps your backend create payment QR codes and safely verify settlement.
- OpenReceive cannot spend your funds.
- An attacker who gains control of your server gets no reward: A receive-only NWC code cannot spend.
- Every payment settles as a private, immutable Bitcoin Lightning payment, swapped from other currencies as necessary. Accept ETH, SOL, USDT, and USDC without censorship risks.
- See Security guide.
- Your app owns business state. Your application owns orders; the library
owns the
openreceive_paymentsrows (they live in your database) — see Payment storage. OpenReceive never owns orders, users, prices, or fulfillment, and never requires a separate database, Redis, or migration runner: you pass a database handle, and the library owns the schema, locking, settlement write-once, and reconciliation.
OpenReceive is three server objects plus an optional browser package. Each one talks to a different side of your app, and each has an obvious home:
| Piece | You build it with | It talks to | It lives |
|---|---|---|---|
| Wallet client | createOpenReceive() |
your wallet — mints invoices, reads settlement, holds the NWC code | server-only, one per process |
| Host | createHost() |
your database — your hooks, plus the openreceive_payments table |
server-only, next to your models |
| HTTP routes | openReceiveExpress() (or Fastify / Next / Rails) |
the browser — the endpoints the checkout UI calls | mounted on your app by default at /openreceive |
| Checkout UI (optional) | @openreceive/react (or vue/svelte/angular/elements) |
the HTTP routes above — creates the checkout, polls until paid | your browser bundle |
Only that last row is genuinely optional. Take the drop-in components, build
your own on the semver-stable @openreceive/browser/headless engine, or skip
our browser packages altogether and call the routes yourself — the server side
is identical either way. See
Headless checkout.
The three server pieces:
import { openReceiveExpress } from "@openreceive/express";
import { createHost } from "@openreceive/http";
import { createOpenReceive } from "@openreceive/node";
// 1. The wallet client. Reads NWC_URI; never let this reach client code.
const service = await createOpenReceive();
// 2. The host: your database and your price.
const host = createHost({
db, // pg Pool/Client, node:sqlite, better-sqlite3, or a custom adapter
// The authoritative price for a reference (here, your order id) — never
// taken from payer input.
// OpenReceive converts this exact decimal into the invoice amount; null
// means there is nothing to pay for (404).
amountFor: async (reference) => {
const order = await orders.find(reference);
return order
? { currency: order.currency, value: order.total.toString() }
: null;
},
onPaid: async ({ reference, query }) => {
// Runs inside the settlement transaction, only for the order's first
// settled attempt. Update the order or insert an outbox row here.
await query("UPDATE orders SET state = 'paid' WHERE id = ?", [reference]);
},
});
// 3. The HTTP routes. `authorize` is your own access check: it runs on every
// reference-scoped request, because a reference alone does not prove ownership.
app.use(
openReceiveExpress({
service,
host,
authorize: async ({ action, request, resource }) =>
orders.authorize({ request, reference: resource.reference, action }),
}),
);The host is the server object between your app and OpenReceive's payment
attempts: it calls the two hooks you hand it — amountFor, onPaid — and
owns one table, openreceive_payments, inside your database. You run that
table's migration (npx openreceive scaffold payments emits it
for your ORM); the library owns everything else: schema, per-reference locking,
write-once settlement, reconciliation.
The reference is a string you choose, and it is the fulfillment identity:
your order id — one per thing you fulfill, created before checkout, kept
across retries, never reused. OpenReceive never looks inside it, but onPaid
runs once per reference, a new checkout under a reference that already
settled is refused with 409, and a fresh id per page load lets one order be
paid twice. Each row is one invoice or swap attempt under a reference. A row
commits before the payer sees an invoice, settles once, and fulfills at most
once per reference; to your app an order is simply unpaid or paid.
Schema, the attempt state machine, live-attempt rules, and the custom-repository escape hatch: Payment storage.
createOpenReceive() reads the receive-only wallet
code from NWC_URI; optional swap providers come from LSC_URI_PRIMARY and
LSC_URI_BACKUP. Those are OpenReceive's only secret environment variables.
See Environment variables.
The routes never inspect your session. You write one callback,
authorize (step 3 above), and it runs on every reference-scoped
request — a reference identifies a row but does not prove the caller owns it.
The context carries the action (checkout.create, payment.check, …), the
Web-standard request, and the untrusted resource selectors the payer sent;
return false for 403.
A create request carries a reference, never a price: your amountFor hook
resolves the amount. A refused attempt (order already paid, competing
live attempt) is a 409 with no invoice attached.
Authorization covers the context object,
framework sessions, and guest orders.
OpenReceive checks your wallet for payment while serving the checkout routes,
so no background process is required. An order is marked paid only when the
wallet itself reports the payment final — never from a preimage the payer
presents, never from a swap provider reporting "complete" — and an unpaid
invoice is closed only once the wallet confirms it went unpaid, not on your
server's clock. Optional notification workers (Rails:
rake openreceive:notifications) settle faster
under the same rule.
How settlement is driven, multi-instance behavior, and workers:
Deploying. Swap recovery, swap_data, and refunds:
Automated swaps.
Most applications should not — mounting the adapter gives you the routes, and the shipped checkout components work against them with no glue. If you need a flow the routes do not offer: Writing your own checkout route.
One shop, four stacks. You add buttons to a cart, check out to create an order,
and pay that order with a real Lightning invoice from your own wallet or a
stablecoin swap; the download unlocks only after onPaid marks the order paid.
npm run demo node # Buy a Button — Express + React/Vue/Svelte/Angular http://localhost:3000
npm run demo static # Buy a Button — static HTML, no framework http://localhost:3001
npm run demo nextjs # Buy a Button — Next.js app router http://localhost:3002
npm run demo buttons # Buy a Button — Rails + host Postgres http://localhost:3003Buy a Button is the persistence story: a products table, a
visitor remembered by a signed cookie, an orders table, and a public feed of
every paid order on the site, with three lambdas as the entire bridge to
OpenReceive. The four stacks share one shop — the UI, the wire types and the
Node server live once in examples/buttons/shared/ and each stack under
server/ is a thin host with its own routing, database idiom and build.
They differ in exactly two interesting ways, and both are on purpose: Rails
pushes settlement over ActionCable while the Node stacks poll, and node-express
plugs the packaged <Checkout> into the shared shop behind React / Vue /
Svelte / Angular tabs while the others render the keystone-driven checkout.
Every demo needs a receive-only NWC_URI in the root .env. The
Buy a Button README explains what each command
starts and which parts of the code belong to the shop and which to the library.
npm test # the JS suite
npm run check # contracts and secret-safety checks
npm run test:ci # the full deterministic gate, including Ruby and demosCONTRIBUTING has setup, ground rules, and the repository layout; the test command map lists every command.
Start with the developer guides:
- Node quickstart
- Node ORM recipes
- Rails quickstart
- Frontend checkout
- Headless checkout
- Writing your own checkout route
- Price feeds
- Automated swaps
- Lightning Swap Connect
- Environment variables
- Provider registry
- Authorization
- Rate limiting
- Payment storage
- Deploying OpenReceive
- Testing your OpenReceive integration
- Security
- API reference
- React + Material UI recipe
- Normative HTTP contract
- Contributor and operator docs