/ billing infra for the obtuse labs portfolio

Subscription billing, modelled correctly.

Plans, customers, subscriptions and invoices, backed by idempotent payment recording and an append-only ledger. A REST API that records money movement — it never touches the money itself.

FastAPIPython 3.14SQLAlchemy 2.xPostgreSQL 18RedisCelery + BeatPydantic v2Docker Compose
POST /api/v1/payments/record
POST /api/v1/payments/record
Authorization: Bearer <jwt>
Idempotency-Key: 8f14e45f-ceea-4b2a-9c1e-2b6a1d9f7a02

{
	"invoice_id": "inv_9f3a2c81",
	"amount": "499.00",
	"currency": "INR",
	"status": "success",
	"provider_reference": "razorpay_pay_Nk3xQ2"
}

Subledger is the brain, not the wallet.

It orchestrates charges on your own payment gateway and records every attempt, success and failure. Money moves customer → your gateway → your bank. Subledger never holds, pools, escrows, routes, or settles a rupee of it.

/ core primitives

Six resources, nothing hidden.

01
Plans
Price, currency and billing cycle that a subscription snapshots from.
02
Customers
The people and organisations being billed.
03
Subscriptions
Links a customer to a plan; moves through active, paused, cancelled, expired.
04
Invoices
Subscription or one-time — a discriminated union enforced at the DB level.
05
Payments
Recorded attempts against an invoice, made idempotent by a UNIQUE key.
06
Ledger
The append-only record of every money-flow event, insert-only.
/ integrity

Retries can't double-charge. History can't be rewritten.

1. Idempotency is a database constraint
Every payment record carries an Idempotency-Key, enforced by a Postgres UNIQUE constraint on the attempt row — not a cache. A repeated key replays the original outcome; it never charges twice.
2. The ledger is append-only
Entries are insert-only. Nothing is ever mutated or deleted — corrections post as new entries, so the history stays intact.
3. Balances are derived, not stored
There's one source of truth. Anything you'd call a "balance" is computed by reading the ledger, not by trusting a mutable counter.
ledger_entries
GET /api/v1/ledger?invoice_id=inv_9f3a2c81

[
    { "entry_type": "invoice_created", "amount": "499.00", "currency": "INR" },
    { "entry_type": "payment_success", "amount": "499.00", "currency": "INR" },
    { "entry_type": "payment_failure", "amount": "499.00", "currency": "INR" }
]
/ getting started

Run it yourself.

terminal
git clone https://github.com/utkarsh-vats/subledger_backend.git && cd subledger_backend
cp .env.example .env.local
docker compose --env-file .env.local up -d		# api · postgres 18 · redis · celery
docker compose exec web alembic upgrade head
# open http://localhost:8001/docs				# interactive OpenAPI schema

Read the code, not the deck.

Early access — built in public. No signup, no waitlist. Clone it and run it.

View on GitHub