Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions ADMIN_API_ENDPOINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,90 @@ Response:

Returns `404` if the user does not exist.

#### Get User Credit Balance

```
GET /api/admin/v1/users/{id}/credit
```

Required Permission: `account_credit::view`

The user's account credit balance.

```json
{
"data": {
"user_id": 42,
"balance": 5000000,
"currency": "BTC",
"auto_apply": true
}
}
```

- `balance`: minor units of `currency` (msat for BTC). Never negative.
- `currency`: the balance's base currency. A quote converts it into the
payment currency at the current rate.
- `auto_apply`: whether the customer opted into spending the balance against
quotes automatically.

A user who has never topped up returns a zero row, not a 404.

#### Adjust User Credit

```
PATCH /api/admin/v1/users/{id}/credit
```

Required Permission: `account_credit::update`

Shifts the balance and records the movement in the ledger. The request body
is `{ "delta": 5000000, "kind": "topup", "note": "goodwill grant" }`.
`delta` is signed msat (positive credits, negative claws back) and `kind` is
one of `topup`, `refund`, `admin`; a negative delta must use `admin`. Refuses
(`400`) a delta of 0, an unknown `kind`, a negative `topup`/`refund`, or a
clawback larger than the balance not held by pending orders. The response is the updated row in the same
shape as the GET.

#### List User Credit Ledger

```
GET /api/admin/v1/users/{id}/credit/events
```

Required Permission: `account_credit::view`

The append-only ledger behind the balance, newest first, paginated with the
standard `limit`/`offset`. Each row:

```json
{
"data": [
{
"id": 7,
"kind": "spend",
"amount": -1000000,
"currency": "BTC",
"subscription_payment_id": "a1b2c3…",
"note": null,
"created": "2026-10-15T12:00:00Z"
}
],
"total": 7,
"limit": 50,
"offset": 0
}
```

- `kind`: `topup`, `spend`, `refund` or `admin`. `amount` is the signed change
to the balance: `spend` rows are negative, `topup` and `refund` positive,
`admin` either.
- `subscription_payment_id`: the payment the row is about, for `spend`; null
otherwise. A VM refund that returns credit writes a `refund` row whose
`note` names the payment.
- The balance always equals the ledger sum, so a mismatch is a bug and worth
reporting.

#### Revoke User Passkey

```
Expand Down Expand Up @@ -5878,6 +5962,7 @@ The RBAC system uses the following permission format: `resource::action`
- `marketplace_operator` - Marketplace operator revenue share and payout configuration
- `support_agent` - Support-agent transcripts and the agent's memory of a customer
- `discount` - Discount campaigns: codes, their rules and redemption reporting
- `account_credit` - Customer account credit balances: read the balance and ledger, and adjust them

### Actions:

Expand Down
2 changes: 2 additions & 0 deletions API_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- **Account credit**: customers hold a balance (msat) and spend it against purchases, renewals and upgrades. `POST /api/v1/account/credit/topup {amount}` issues a Lightning invoice and credits the balance when it settles. `GET /api/v1/account/credit` returns the balance, currency and `auto_apply`; `PATCH /api/v1/account/credit` toggles it; `GET /api/v1/account/credit/events` lists the signed ledger. When opted in, renewal and upgrade quotes (`GET /api/v1/vm/{id}/renew/quote`, `GET /api/v1/subscriptions/{id}/renew/quote`, `POST /api/v1/vm/{id}/upgrade/quote`) and the payments they create carry a `credit` object with the amount covered in the bill's currency; the external invoice is for the remainder and the processing fee is charged only on it. A fully credit-funded payment settles immediately and its `data` is `"credit"`. Credit on an unpaid order is held until the order expires. Auto-renewal uses credit when it covers the whole renewal. Refunds of credit-funded VM payments return the credit share to the balance. Admins read and adjust balances through `GET`/`PATCH /api/admin/v1/users/{id}/credit` and `GET /api/admin/v1/users/{id}/credit/events`, gated by the `account_credit` permission (granted to `super_admin`).

- **Upgrade options**: `GET /api/v1/vm/{id}/upgrade/options` returns the `CustomTemplateParams` of the plan an upgrade of the VM is priced and limited by, so an upgrade form can bound its inputs without guessing the plan.

- **Bandwidth in custom pricing**: custom VMs can be ordered and upgraded with more monthly transfer and network speed. `POST /api/v1/vm/custom-template`, `POST /api/v1/vm/custom-template/price` and `POST /api/admin/v1/vms/custom` accept `transfer_gb` and `network_mbps`; `POST /api/v1/vm/{id}/upgrade` and `/upgrade/quote` accept the same two fields, and an upgrade that changes only bandwidth no longer restarts the VM. `CustomTemplateParams` on `GET /api/v1/vm/templates` gains `transfer` and `network` ranges. Custom pricing plans gain `min_transfer_gb`, `max_transfer_gb`, `transfer_cost_per_tb`, `min_network_mbps`, `max_network_mbps` and `network_cost_per_gbps` on `GET`/`POST`/`PATCH /api/admin/v1/custom_pricing` and the copy endpoint. Existing plans keep a fixed range at their current value and a cost of 0, so no price changes until one is set.
Expand Down
86 changes: 85 additions & 1 deletion API_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,90 @@ and React example. `{provider}` is one of the enabled tags (`google`, `github`,
- **Notes**: Removes the stored number, clears verification state and sets `contact_whatsapp` to `false`.
- **Response**: `null`

### Account Credit

A balance you top up and spend against purchases, renewals and upgrades, in the
same flow as a saved wallet. The balance is held in msat and converted into
the payment currency of whatever you buy at the current rate.

#### Get Credit Balance
- **GET** `/api/v1/account/credit`
- **Auth**: Required
- **Response**:
```json
{
"data": {
"balance": 5000000,
"currency": "BTC",
"auto_apply": true
}
}
```
- `balance`: your available credit, in msat. Never negative.
- `auto_apply`: when true, every quote automatically spends as much credit as
the bill allows; when false the balance waits for you to opt in.

#### Toggle Auto-Apply
- **PATCH** `/api/v1/account/credit`
- **Auth**: Required
- **Body**: `{ "auto_apply": bool }`
- **Response**: the balance object above, reflecting the new flag.

#### Top Up
- **POST** `/api/v1/account/credit/topup`
- **Auth**: Required
- **Body**: `{ "amount": 10000000 }` (msat, rounded up to a whole sat; minimum 1000)
- **Notes**: Issues a Lightning invoice. Pay it with any wallet; on settlement
the balance rises by the amount paid. A top-up is not tied to any
subscription and never appears on a VM's payment history.
- **Response**:
```json
{
"data": {
"pr": "lnbc...",
"payment_hash": "...",
"amount": 10000000,
"currency": "BTC",
"expires": "2026-10-15T12:10:00Z"
}
}
```

#### Credit Ledger
- **GET** `/api/v1/account/credit/events?limit=50&offset=0`
- **Auth**: Required
- **Notes**: Every movement of your balance, newest first. `kind` is one of
`topup`, `spend`, `refund`, `admin`; `amount` is the signed change to the
balance (`spend` is negative). `subscription_payment_id` is set on `spend`
rows to the payment the credit went to.
- **Response**: standard paginated wrapper of
`{ kind, amount, currency, subscription_payment_id, note, created }` rows.

#### How credit is spent

When `auto_apply` is on and you hold a balance, quotes for renewals, purchases
and upgrades show a `credit` line (renewal quotes) or a `credit` object on the
payment response:

```json
{ "amount": 500000, "currency": "BTC" }
```

That is the part of the bill your balance covers, in the bill's currency. The
invoice you are asked to pay covers the remainder, and the processing fee is
charged only on that remainder. A bill fully covered by credit needs no payment
at all: it settles the moment it is ordered and its payment `data` is
`"credit"`. If the remainder would fall below the payment method's minimum,
less credit is used so the remainder meets it.

Credit applied to an unpaid order is held until the order expires, so the same
credit is never offered to two orders at once. If an order is paid after it
expired and the credit has since gone elsewhere, only what is left is spent and
the rest of the bill buys less time (on-chain) or is absorbed. Auto-renewal
uses credit when it covers the whole renewal, otherwise your saved wallet or
card pays the remainder. A refund of a VM paid partly with credit returns the
credit share to your balance and the cash share to your invoice.

### Automatic Renewal with Nostr Wallet Connect

The LNVPS platform supports automatic VM renewal using Nostr Wallet Connect (NWC). This feature allows users to set up their Lightning wallets to automatically pay for VM renewals before expiration.
Expand Down Expand Up @@ -983,7 +1067,7 @@ historical range is needed; a usage bar can be rendered from this object alone.
- `method`: Optional payment method ('lightning' | 'revolut' | 'paypal'). Defaults to 'lightning'
- **Body**: `VmUpgradeRequest`
- **Response**: `VmUpgradeQuote`
- **Description**: Calculate the pro-rated upgrade cost for remaining VM time and the new monthly renewal cost after upgrade. Available for both standard template VMs and custom template VMs. Like the upgrade endpoint itself, the request must strictly increase at least one resource (no downgrades). Cost is calculated in the currency appropriate for the selected payment method. The response includes the upgrade cost (cost_difference), new renewal cost, and the discount amount representing the value of remaining time at the old pricing rate.
- **Description**: Calculate the pro-rated upgrade cost for remaining VM time and the new monthly renewal cost after upgrade. Available for both standard template VMs and custom template VMs. Like the upgrade endpoint itself, the request must strictly increase at least one resource (no downgrades). Cost is calculated in the currency appropriate for the selected payment method. The response includes the upgrade cost (cost_difference), new renewal cost, and the discount amount representing the value of remaining time at the old pricing rate. When account credit is auto-applied the response also carries `credit` (`{ amount, currency }`), and `processing_fee` is the fee on the remainder.

#### Create VM Upgrade Payment
- **POST** `/api/v1/vm/{id}/upgrade?method={payment_method}`
Expand Down
Loading
Loading