Describe the bug
Several resources can be modified after reaching a terminal state (voided or terminated) that should make them immutable. The API accepts mutations that change payment status, add metadata, and update fields on resources that are supposed to be frozen.
-
Payment status changeable on voided invoices — After voiding an invoice, PUT /api/v1/invoices/{id} with payment_status: "succeeded" returns 200. The same works with "failed". This triggers webhook delivery (invoice.payment_status_updated) on a cancelled invoice. Note: void-again and retry-payment are correctly blocked with 405.
-
Terminated wallets can be updated — After terminating a wallet via DELETE, PUT /api/v1/wallets/{id} with name, priority, or expiration_at changes all return 200. Note: topping up a terminated wallet with paid_credits or granted_credits correctly returns 422.
-
Metadata writable on voided invoices — Adding metadata to a voided invoice via PUT returns 200. No state check on metadata updates.
To Reproduce
Environment:
- Lago API (
getlago/api:v1.48.1)
- PostgreSQL 15 with pg_partman
- Redis
- Env vars:
LAGO_DISABLE_SEGMENT=true, LAGO_DISABLE_WALLET_REFRESH=true
Setup:
Organization with API key, a customer, and an add-on (for one-off invoices).
Control — voided invoice blocks void-again and retry:
Create a one-off invoice and void it:
POST /api/v1/invoices/{invoice_id}/void
Authorization: Bearer {api_key}
Returns 200. Then:
POST /api/v1/invoices/{invoice_id}/void
Authorization: Bearer {api_key}
Returns 405. Correctly rejects double-void.
POST /api/v1/invoices/{invoice_id}/retry_payment
Authorization: Bearer {api_key}
Returns 405. Correctly rejects retry on voided invoice.
Bug 1 — payment status updated on voided invoice:
PUT /api/v1/invoices/{invoice_id}
Authorization: Bearer {api_key}
Content-Type: application/json
{
"invoice": {
"payment_status": "succeeded"
}
}
Returns 200. The invoice is now voided with payment_status: succeeded.
Bug 2 — terminated wallet updated:
Create and terminate a wallet:
DELETE /api/v1/wallets/{wallet_id}
Authorization: Bearer {api_key}
Returns 200. Then:
PUT /api/v1/wallets/{wallet_id}
Authorization: Bearer {api_key}
Content-Type: application/json
{
"wallet": {
"name": "Zombie Wallet Name"
}
}
Returns 200. Also works for priority and expiration_at.
Control — terminated wallet balance is correctly protected:
POST /api/v1/wallet_transactions
Authorization: Bearer {api_key}
Content-Type: application/json
{
"wallet_transaction": {
"wallet_id": "{wallet_id}",
"paid_credits": "50.0"
}
}
Returns 422. Correctly rejects top-up on terminated wallet.
Bug 3 — metadata added to voided invoice:
PUT /api/v1/invoices/{invoice_id}
Authorization: Bearer {api_key}
Content-Type: application/json
{
"invoice": {
"metadata": [
{
"key": "ghost-key",
"value": "ghost-value"
}
]
}
}
Returns 200.
Reproducing with Dokkimi:
Dokkimi is an open source testing tool that stands up isolated Docker environments from declarative YAML definitions — services, databases, seed data, test steps, and assertions all in one file. The definition for this bug is in dokkimi/dokkimi-in-the-wild/.dokkimi/lago — dokkimi run reproduces it from scratch.
Expected behavior
- Voided invoices should reject all mutations via the update endpoint — payment status, metadata, and any other field. The 405 enforcement on void-again and retry-payment should extend to the PUT endpoint.
- Terminated wallets should reject all mutations. The 422 enforcement on balance top-ups should extend to name, priority, and expiration updates.
Screenshots
N/A
Additional context
- A voided invoice marked as
succeeded triggers webhook delivery and payment processing jobs, potentially causing reconciliation errors
- Terminated wallets with mutable priority can affect wallet selection logic for customers with multiple wallets
- Metadata added to voided invoices modifies the audit trail of a cancelled document, which may violate compliance requirements for immutable financial records
Version
Describe the bug
Several resources can be modified after reaching a terminal state (voided or terminated) that should make them immutable. The API accepts mutations that change payment status, add metadata, and update fields on resources that are supposed to be frozen.
Payment status changeable on voided invoices — After voiding an invoice,
PUT /api/v1/invoices/{id}withpayment_status: "succeeded"returns 200. The same works with"failed". This triggers webhook delivery (invoice.payment_status_updated) on a cancelled invoice. Note: void-again and retry-payment are correctly blocked with 405.Terminated wallets can be updated — After terminating a wallet via DELETE,
PUT /api/v1/wallets/{id}with name, priority, or expiration_at changes all return 200. Note: topping up a terminated wallet withpaid_creditsorgranted_creditscorrectly returns 422.Metadata writable on voided invoices — Adding metadata to a voided invoice via PUT returns 200. No state check on metadata updates.
To Reproduce
Environment:
getlago/api:v1.48.1)LAGO_DISABLE_SEGMENT=true,LAGO_DISABLE_WALLET_REFRESH=trueSetup:
Organization with API key, a customer, and an add-on (for one-off invoices).
Control — voided invoice blocks void-again and retry:
Create a one-off invoice and void it:
Returns 200. Then:
Returns 405. Correctly rejects double-void.
Returns 405. Correctly rejects retry on voided invoice.
Bug 1 — payment status updated on voided invoice:
Returns 200. The invoice is now voided with
payment_status: succeeded.Bug 2 — terminated wallet updated:
Create and terminate a wallet:
Returns 200. Then:
Returns 200. Also works for
priorityandexpiration_at.Control — terminated wallet balance is correctly protected:
Returns 422. Correctly rejects top-up on terminated wallet.
Bug 3 — metadata added to voided invoice:
Returns 200.
Reproducing with Dokkimi:
Dokkimi is an open source testing tool that stands up isolated Docker environments from declarative YAML definitions — services, databases, seed data, test steps, and assertions all in one file. The definition for this bug is in dokkimi/dokkimi-in-the-wild/.dokkimi/lago —
dokkimi runreproduces it from scratch.Expected behavior
Screenshots
N/A
Additional context
succeededtriggers webhook delivery and payment processing jobs, potentially causing reconciliation errorsVersion