Entity Budgets
An entity budget caps the monthly credit spend of a single App or Dataset, independent of any per-actor usage profile cap. Budgets reset at the start of each calendar month.
Both budget routes resolve the row through a find-or-create, so a budget exists for an App or Dataset the moment you first read or write it. There is no separate "create budget" endpoint. The entityType is a tight enum: only app and dataset are accepted, and the entityId must be a UUID.
Budget fields
The response body is the serialized budget: the stored columns plus four computed getters serialized alongside them.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Budget row id |
entityId | string (UUID) | The App or Dataset id |
entityType | string | app or dataset |
monthlyBudget | integer | null | Monthly cap in credits. null = no cap |
creditsUsed | string | Credits consumed in the current period. A DECIMAL serialized as a string (for example "0.00") |
periodStart | date | Start of the current monthly period |
hasBudget | boolean | Computed. true when monthlyBudget is set |
budgetRemaining | number | null | Computed. monthlyBudget - creditsUsed, or null when uncapped |
budgetPercent | number | Computed. Percent of budget consumed (0-100) |
isOverBudget | boolean | Computed. true once creditsUsed >= monthlyBudget |
createdAt / updatedAt | date | Timestamps |
The response is a HAL object with a _links.self and a tenant field.
GET /api/usage-budgets/{entityType}/{entityId}
Get the budget for an App or Dataset. The row is created on first access via find-or-create, with monthlyBudget set to null and periodStart at the start of the current month. A second call returns the same row.
Authentication: Required
Pre-blocks: permission.tenant.superuser
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
entityType | string | app or dataset |
entityId | string (UUID) | The entity's id |
Response:
Responds 200 with the budget. On first access the getters report the uncapped state: hasBudget is false, budgetRemaining is null, budgetPercent is 0, and isOverBudget is false.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-budgets/app/00000000-0000-4000-8000-000000000001"
}
},
"hasBudget": false,
"budgetRemaining": null,
"budgetPercent": 0,
"isOverBudget": false,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"creditsUsed": "0.00",
"entityId": "00000000-0000-4000-8000-000000000001",
"entityType": "app",
"periodStart": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"monthlyBudget": null
}
entityType is a tight enum. Any other value fails Joi param validation with a 400 before the handler runs, so budgets cannot be created for arbitrary entity types.
{
"statusCode": 400,
"error": "Bad Request",
"message": "\"entityType\" must be one of [app, dataset]",
"validation": {
"source": "params",
"keys": [
"entityType"
]
},
"data": {
"defaultError": {
"data": null,
"isBoom": true,
"isServer": false,
"output": {
"statusCode": 400,
"payload": {
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid request params input"
},
"headers": {}
}
}
}
}
PUT /api/usage-budgets/{entityType}/{entityId}
Set or clear the monthly budget for an App or Dataset. The find-or-create resolves (or creates) the row, then updates monthlyBudget. Runs in a database transaction.
Authentication: Required
Pre-blocks: permission.usage.write (delegates to the billingManage global permission; superusers pass via the fallback)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
entityType | string | app or dataset |
entityId | string (UUID) | The entity's id |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
monthlyBudget | integer | null | No | Monthly cap in credits (must be >= 0), or null to remove the cap. Omitting the field is accepted as a no-op. |
Example Request:
{ "monthlyBudget": 2000 }
Response:
Responds 200 with the updated budget (the handler returns the row directly; it does not use .created(), so the status is 200, not 201). With a cap set, the getters update: hasBudget becomes true, and budgetRemaining and budgetPercent reflect creditsUsed.
{
"monthlyBudget": 2000
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-budgets/app/00000000-0000-4000-8000-000000000001"
}
},
"hasBudget": true,
"budgetRemaining": 2000,
"budgetPercent": 0,
"isOverBudget": false,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"entityId": "00000000-0000-4000-8000-000000000001",
"entityType": "app",
"monthlyBudget": 2000,
"creditsUsed": "0.00",
"periodStart": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
Send monthlyBudget: null to remove the cap. The getters then go uncapped: hasBudget is false, budgetRemaining is null, budgetPercent is 0, and isOverBudget is false.
{ "monthlyBudget": null }
{
"monthlyBudget": null
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-budgets/dataset/00000000-0000-4000-8000-000000000001"
}
},
"hasBudget": false,
"budgetRemaining": null,
"budgetPercent": 0,
"isOverBudget": false,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"creditsUsed": "0.00",
"entityId": "00000000-0000-4000-8000-000000000001",
"entityType": "dataset",
"periodStart": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"monthlyBudget": null
}