OpenReceive for Django

Accept Bitcoin & Stablecoin Payments With Django

Add OpenReceive as a Django app, mount its URLs, and implement one host class with three hooks. Migrations, settlement, admin visibility and management commands stay inside the project you already run.

Tested live · Oct 6, 2026 · 0.4.18

Copy agent directions — Paste into Claude Code, Codex or Cursor to add OpenReceive to your Django app.

Why OpenReceive

Try the checkout your customers will see

The live demo needs JavaScript: open the OpenReceive home page.

Set up

Requires Python ≥ 3.10, Django ≥ 5.2.

1. Get a receive-only NWC code — get one here. It can issue invoices and watch for payment, and it can never spend from your wallet.

2. Get an LSC code for swaps (optional) — from a swap provider, only if customers should be able to pay in USDT, USDC, ETH or SOL.

3. Set the environment variables — NWC_URI (required) and LSC_URI_PRIMARY / LSC_URI_BACKUP (optional) in the environment of your Django server process; see Environment variables.

4. Install

pip install "openreceive[django]"

5. Wire OpenReceive — quoted from the Django quickstart.

# shop/openreceive_host.py — generated by `manage.py openreceive_install shop`, then filled in.
from datetime import UTC, datetime

from openreceive.server import HookContext
from openreceive.storage import PaymentSettlement

from shop.models import Order  # YOUR model — it could be named anything. OpenReceive
# never sees it or touches its table; these hooks are the
# only bridge between the engine and your data.


class Host:
    # Your policy, called before every checkout/payment/swap request. `context`
    # has three attributes:
    #   context.action    — which route: "checkout.prepare", "checkout.create",
    #                       "payment.check", "swap.quote", "swap.create",
    #                       "swap.read", or "swap.refund"
    #   context.request   — the django.http.HttpRequest; read request.user,
    #                       request.session or cookies from it, as in a view
    #   context.resource  — {"reference": ..., "payment_hash": ...} copied from
    #                       the payer's JSON body. It names an order; it does
    #                       not prove this caller owns it. reference is always a
    #                       validated non-empty str (≤200 chars); payment_hash
    #                       is absent except on payment.check / swap.read /
    #                       swap.refund.
    # Return True to allow, False for a 403. Here: only the signed-in customer
    # who placed the order may act on it.
    def authorize(self, context: HookContext) -> bool:
        order = Order.objects.filter(pk=context.resource["reference"]).first()
        return order is not None and order.user_id == context.request.user.id

    # The price for a reference — here, your order id — from your own data;
    # None when there is nothing to pay for (a 404). `value` is a decimal
    # STRING from the order row, never a float and never a request param.
    # `description` is what the payer is buying, in your own words.
    def amount_for(self, reference: str) -> dict | None:
        order = Order.objects.filter(pk=reference).first()
        if order is None:
            return None
        return {
            "currency": "USD",
            "value": str(order.total),
            "description": f"{order.items.count()} items",
        }

    # Runs inside the settlement transaction, only for the order's first settled
    # attempt. The WHERE clause is the lock: a second fulfillment path of yours
    # (admin action, replayed job) updates zero rows and does nothing. Plain
    # ORM calls, because the engine WRAPS this method in transaction.atomic();
    # `settlement.connection` is None here. (The SQLAlchemy adapters instead
    # hand on_paid the transaction's Connection — the one shape difference.)
    def on_paid(self, settlement: PaymentSettlement) -> None:
        Order.objects.filter(pk=settlement.reference, state="awaiting_payment").update(
            state="paid", paid_at=datetime.fromtimestamp(settlement.paid_at, tz=UTC)
        )

6. Render the checkout

{# templates/orders/pay.html #}
{% load static %}
<meta name="csrf-token" content="{{ csrf_token }}">
<link rel="stylesheet" href="{% static 'openreceive/openreceive-checkout.css' %}">
<script type="module" src="{% static 'openreceive/openreceive-checkout.js' %}"></script>

<openreceive-checkout
  reference="{{ order.pk }}"
  csrf-header="X-CSRFToken"></openreceive-checkout>

Full Django quickstart · Example app

Practical questions

Which currencies can customers pay with?
Bitcoin over Lightning, always. With a swap provider configured, also USDT on Tron, Solana or Ethereum, USDC on Solana or Ethereum, ETH and SOL, each converted into the same Lightning payment. Automated swaps guide
Where does the Bitcoin go?
Into the wallet behind your NWC code. OpenReceive never holds funds: the invoice is minted in your wallet and the sats land there. Your server confirms settlement and runs your onPaid hook once per order. Payment storage guide
What wallet do I need?
Any wallet that issues receive-only NWC (Nostr Wallet Connect) codes, hosted or self-run. The code must be able to create invoices and read their status, and must not be able to spend. Get a receive-only NWC code
What does it cost?
OpenReceive is open source and free. Fees charged by your wallet provider, by a swap provider or by Lightning routing belong to those services and are outside OpenReceive's scope.
What do I need to deploy?
Python 3.10 or later, Django 5.2 or later, NWC_URI in the server environment, and the OpenReceive migration applied to your existing PostgreSQL, MySQL or SQLite database. Django quickstart

Add OpenReceive to your Django app

Copy agent directions · Full quickstart · Example repository · API reference · Guides