/ api reference

Endpoints

A predictable, resource-oriented REST API — JSON in, JSON out. Auth is a JWT bearer token.

https://api.subledger.obtuse.in/api/v1early access
Auth & idempotency
Every mutation requires Authorization: Bearer <jwt>. POST /api/v1/payments/record additionally requires an Idempotency-Key header (a UUID).
AuthPlansCustomersSubscriptionsInvoicesPaymentsLedger

Auth

Obtain a JWT bearer token to authenticate every mutating request.

POST/api/v1/auth/login
Form-encoded username + password. Returns an access token.
POST /api/v1/auth/login
POST /api/v1/auth/login
Content-Type: application/x-www-form-urlencoded

username=founder@obtuse.in&password=********

→ 200
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer"
}

Plans

Billing plans customers subscribe to — price, currency, cycle.

POST/api/v1/plans
Create a plan.
POST /api/v1/plans
POST /api/v1/plans
Authorization: Bearer <jwt>
{
  "name": "Pro Monthly",
  "billing_cycle": "monthly",
  "price": "499.00",
  "currency": "INR",
  "status": "active"
}

→ 201
{
  "id": "plan_7a21",
  "name": "Pro Monthly",
  "billing_cycle": "monthly",
  "price": "499.00",
  "currency": "INR",
  "status": "active"
}
GET/api/v1/plans
List plans. Filter by ?status=
GET /api/v1/plans
GET /api/v1/plans?status=active

→ 200
[
  { "id": "plan_7a21", "name": "Pro Monthly", "price": "499.00", "currency": "INR", "status": "active" }
]
GET/api/v1/plans/{id}
Fetch a single plan.
GET /api/v1/plans/{id}
GET /api/v1/plans/plan_7a21

→ 200
{
  "id": "plan_7a21",
  "name": "Pro Monthly",
  "billing_cycle": "monthly",
  "price": "499.00",
  "currency": "INR",
  "status": "active"
}
PATCH/api/v1/plans/{id}
Update a plan.
PATCH /api/v1/plans/{id}
PATCH /api/v1/plans/plan_7a21
Authorization: Bearer <jwt>
{
  "status": "archived"
}

→ 200
{
  "id": "plan_7a21",
  "status": "archived"
}

Customers

The people and organisations being billed.

POST/api/v1/customers
Create a customer.
POST /api/v1/customers
POST /api/v1/customers
Authorization: Bearer <jwt>
{
  "name": "Aarav Shah",
  "email": "aarav@example.com"
}

→ 201
{
  "id": "cus_8f2a91d0",
  "name": "Aarav Shah",
  "email": "aarav@example.com"
}
GET/api/v1/customers
List customers.
GET /api/v1/customers
GET /api/v1/customers

→ 200
[
  { "id": "cus_8f2a91d0", "name": "Aarav Shah", "email": "aarav@example.com" }
]
GET/api/v1/customers/{id}
Fetch a customer.
GET /api/v1/customers/{id}
GET /api/v1/customers/cus_8f2a91d0

→ 200
{
  "id": "cus_8f2a91d0",
  "name": "Aarav Shah",
  "email": "aarav@example.com"
}
PATCH/api/v1/customers/{id}
Update a customer.
PATCH /api/v1/customers/{id}
PATCH /api/v1/customers/cus_8f2a91d0
Authorization: Bearer <jwt>
{
  "email": "aarav.shah@example.com"
}

→ 200
{
  "id": "cus_8f2a91d0",
  "email": "aarav.shah@example.com"
}
GET/api/v1/customers/{id}/ledger
Ledger entries for this customer.
GET /api/v1/customers/{id}/ledger
GET /api/v1/customers/cus_8f2a91d0/ledger

→ 200
[
  { "entry_type": "invoice_created", "amount": "499.00", "currency": "INR" },
  { "entry_type": "payment_success", "amount": "499.00", "currency": "INR" }
]

Subscriptions

Links a customer to a plan and tracks lifecycle state.

POST/api/v1/subscriptions
Create a subscription.
POST /api/v1/subscriptions
POST /api/v1/subscriptions
Authorization: Bearer <jwt>
{
  "customer_id": "cus_8f2a91d0",
  "plan_id": "plan_7a21"
}

→ 201
{
  "id": "sub_b1f2c3d4",
  "customer_id": "cus_8f2a91d0",
  "plan_id": "plan_7a21",
  "status": "active"
}
GET/api/v1/subscriptions
List subscriptions. Filter by ?status=
GET /api/v1/subscriptions
GET /api/v1/subscriptions?status=active

→ 200
[
  { "id": "sub_b1f2c3d4", "status": "active" }
]
GET/api/v1/subscriptions/{id}
Fetch a subscription.
GET /api/v1/subscriptions/{id}
GET /api/v1/subscriptions/sub_b1f2c3d4

→ 200
{
  "id": "sub_b1f2c3d4",
  "customer_id": "cus_8f2a91d0",
  "plan_id": "plan_7a21",
  "status": "active"
}
POST/api/v1/subscriptions/{id}/pause
Pause an active subscription.
POST /api/v1/subscriptions/{id}/pause
POST /api/v1/subscriptions/sub_b1f2c3d4/pause
Authorization: Bearer <jwt>

→ 200
{
  "id": "sub_b1f2c3d4",
  "status": "paused"
}
POST/api/v1/subscriptions/{id}/resume
Resume a paused subscription.
POST /api/v1/subscriptions/{id}/resume
POST /api/v1/subscriptions/sub_b1f2c3d4/resume
Authorization: Bearer <jwt>

→ 200
{
  "id": "sub_b1f2c3d4",
  "status": "active"
}
POST/api/v1/subscriptions/{id}/cancel
Cancel a subscription (terminal).
POST /api/v1/subscriptions/{id}/cancel
POST /api/v1/subscriptions/sub_b1f2c3d4/cancel
Authorization: Bearer <jwt>

→ 200
{
  "id": "sub_b1f2c3d4",
  "status": "cancelled"
}

Invoices

Subscription and one-time invoices — a discriminated union enforced by a DB check constraint.

POST/api/v1/invoices
Create a one-time invoice. Requires description.
POST /api/v1/invoices
POST /api/v1/invoices
Authorization: Bearer <jwt>
{
  "customer_id": "cus_8f2a91d0",
  "invoice_type": "one_time",
  "description": "Setup fee",
  "amount": "150.00",
  "currency": "INR"
}

→ 201
{
  "id": "inv_4c7e1a90",
  "invoice_type": "one_time",
  "description": "Setup fee",
  "amount": "150.00",
  "currency": "INR",
  "status": "open"
}
POST/api/v1/invoices/generate
Generate a subscription invoice from the plan's snapshotted price.
POST /api/v1/invoices/generate
POST /api/v1/invoices/generate
Authorization: Bearer <jwt>
{
  "subscription_id": "sub_b1f2c3d4"
}

→ 201
{
  "id": "inv_9f3a2c81",
  "invoice_type": "subscription",
  "subscription_id": "sub_b1f2c3d4",
  "amount": "499.00",
  "currency": "INR",
  "status": "open"
}
GET/api/v1/invoices
List invoices. Filter by status, invoice_type, customer_id, subscription_id, period_start.
GET /api/v1/invoices
GET /api/v1/invoices?status=open

→ 200
[
  { "id": "inv_9f3a2c81", "amount": "499.00", "currency": "INR", "status": "open" }
]
GET/api/v1/invoices/{id}
Fetch an invoice.
GET /api/v1/invoices/{id}
GET /api/v1/invoices/inv_9f3a2c81

→ 200
{
  "id": "inv_9f3a2c81",
  "invoice_type": "subscription",
  "amount": "499.00",
  "currency": "INR",
  "status": "open"
}
GET/api/v1/invoices/{id}/ledger
Ledger entries for this invoice.
GET /api/v1/invoices/{id}/ledger
GET /api/v1/invoices/inv_9f3a2c81/ledger

→ 200
[
  { "entry_type": "invoice_created", "amount": "499.00", "currency": "INR" }
]

Payments

Recorded payment attempts against an invoice — idempotent by design.

POST/api/v1/payments/record
Record a payment attempt. Requires an Idempotency-Key header.
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"
}

→ 201
{
  "id": "pay_3d5f9a10",
  "invoice_id": "inv_9f3a2c81",
  "status": "success"
}
GET/api/v1/payments
List payments. Filter by ?invoice_id= or ?status=
GET /api/v1/payments
GET /api/v1/payments?invoice_id=inv_9f3a2c81

→ 200
[
  { "id": "pay_3d5f9a10", "status": "success", "amount": "499.00", "currency": "INR" }
]
GET/api/v1/payments/{id}
Fetch a payment.
GET /api/v1/payments/{id}
GET /api/v1/payments/pay_3d5f9a10

→ 200
{
  "id": "pay_3d5f9a10",
  "invoice_id": "inv_9f3a2c81",
  "amount": "499.00",
  "currency": "INR",
  "status": "success",
  "provider_reference": "razorpay_pay_Nk3xQ2"
}

Ledger

The append-only record of every money-flow event.

GET/api/v1/ledger
Global ledger. Filter by customer_id, invoice_id, entry_type.
GET /api/v1/ledger
GET /api/v1/ledger?entry_type=payment_success

→ 200
[
  { "id": "led_2c81f9a3", "entry_type": "payment_success", "amount": "499.00", "currency": "INR" }
]
GET/api/v1/ledger/{id}
Fetch a single ledger entry.
GET /api/v1/ledger/{id}
GET /api/v1/ledger/led_2c81f9a3

→ 200
{
  "id": "led_2c81f9a3",
  "entry_type": "payment_success",
  "customer_id": "cus_8f2a91d0",
  "invoice_id": "inv_9f3a2c81",
  "amount": "499.00",
  "currency": "INR",
  "created_at": "2026-08-06T02:00:00Z"
}
Prefer exploring live? The running instance serves interactive OpenAPI docs at /docs (early access).