/ 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).
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).