Usage Profiles
A usage profile bundles an AI credit budget with a set of allowed model tiers, then is assigned to teams (or agents) to govern what their members can do. See Usage & Credits for how profiles fit into enforcement.
These routes do not require AI to be enabled on the tenant. They gate purely on permission. Reading profiles (GET /api/usage-profiles and GET /api/usage-profiles/{id}) requires permission.tenant.superuser. Creating, updating, and deleting profiles require permission.usage.write (which delegates to the billingManage global permission; superusers pass via the global-permission fallback). The per-team assignment routes require permission.billing.manage (also billingManage).
Profile fields
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Profile identifier |
name | string | Human-readable name |
slug | string | Machine identifier, format ^[a-z][a-z0-9-]*$, immutable after creation |
description | string | Description of the profile's intent (NOT NULL) |
creditCapPerMonth | integer | null | Per-actor monthly credit ceiling. null = unlimited, 0 = hard stop, positive = capped |
allowedModelTiers | string[] | Subset of everyday, advanced, strategic |
isUnlimited | boolean | Derived getter: true when creditCapPerMonth is null |
tenant | string | Owning tenant slug |
createdAt / updatedAt | date | Timestamps |
A user's effective profile is resolved from their team memberships. When a user belongs to multiple teams with different profiles, the result is merged to the most permissive: the union of allowedModelTiers and the highest creditCapPerMonth (null/unlimited always wins). If no team assignment applies, the tenant's default profile is used.
GET /api/usage-profiles
List all usage profiles for the tenant.
Authentication: Required (session)
Pre-blocks: permission.tenant.superuser
Response:
A paged envelope { _links, items, start, count, total } (with a HAL _links.self), not a bare array and not a HAL _embedded collection. Each row carries the isUnlimited getter (true when creditCapPerMonth is null).
The route uses the model default scope (row-level security only, no explicit ORDER BY), so callers should not rely on row order. System-seeded Standard and Premium profiles are inserted before any tenant-created profile, but applying a stable sort is the caller's job.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-profiles"
}
},
"items": [
{
"isUnlimited": false,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Scratch Profile",
"slug": "scratch-profile",
"description": "Disposable profile used only for the delete example",
"creditCapPerMonth": 1000,
"allowedModelTiers": [
"everyday"
],
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"isUnlimited": false,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"name": "Standard",
"slug": "standard",
"description": "Everyday + advanced AI models for routine and harder tasks",
"creditCapPerMonth": 5000,
"allowedModelTiers": [
"everyday",
"advanced"
],
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"isUnlimited": false,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000003",
"name": "Premium",
"slug": "premium",
"description": "Adds strategic-tier models for complex analysis",
"creditCapPerMonth": 15000,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
],
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
"… 2 more items (5 total) — omitted from docs"
],
"start": 0,
"count": 5,
"total": 5
}
POST /api/usage-profiles
Create a new usage profile.
Authentication: Required (session)
Pre-blocks: permission.usage.write
Request Body:
The payload is stripUnknown, so unrecognized fields are dropped rather than rejected.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Profile name |
slug | string | Yes | Machine identifier, ^[a-z][a-z0-9-]*$ |
description | string | Yes | Profile description |
creditCapPerMonth | integer | null | No | Monthly credit cap (default null = unlimited) |
allowedModelTiers | string[] | No | Allowed tiers (default []; valid tiers: everyday, advanced, strategic) |
Example Request:
{
"name": "Analysts",
"slug": "analysts",
"description": "Strategic models, no monthly cap",
"creditCapPerMonth": null,
"allowedModelTiers": ["everyday", "advanced", "strategic"]
}
Response:
Responds 200 with the created profile (the handler returns the row directly; it does not use .created(), so the status is 200, not 201).
{
"name": "Analysts",
"slug": "analysts",
"description": "Strategic models, no monthly cap",
"creditCapPerMonth": null,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
]
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-profiles"
}
},
"isUnlimited": true,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Analysts",
"slug": "analysts",
"description": "Strategic models, no monthly cap",
"creditCapPerMonth": null,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
],
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z"
}
The route checks the slug before creating. A slug already used within the tenant answers 409 Conflict.
{
"name": "Analysts (dupe)",
"slug": "analysts",
"description": "Second profile reusing an existing slug"
}
{
"statusCode": 409,
"error": "Conflict",
"message": "Profile with slug 'analysts' already exists"
}
GET /api/usage-profiles/{id}
Retrieve a single usage profile.
Authentication: Required (session)
Pre-blocks: permission.tenant.superuser, usageProfile.lookup (a missing id answers 404)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (UUID) | Profile id |
Response:
Returns the profile row (including the isUnlimited getter) with its HAL inf:usage-profile link.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-profiles/00000000-0000-4000-8000-000000000001"
}
},
"isUnlimited": true,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Analysts",
"slug": "analysts",
"description": "Strategic models, no monthly cap",
"creditCapPerMonth": null,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
],
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
PUT /api/usage-profiles/{id}
Update a usage profile. All fields are optional; only the supplied fields are changed.
Authentication: Required (session)
Pre-blocks: permission.usage.write, usageProfile.lookup
Request Body:
The body is stripUnknown. slug is immutable and silently dropped.
| Field | Type | Description |
|---|---|---|
name | string | Profile name |
description | string | Profile description (must be a real value if included; the column is NOT NULL) |
creditCapPerMonth | integer | null | Monthly credit cap |
allowedModelTiers | string[] | Allowed tiers |
Example Request:
{
"name": "Senior Analysts",
"creditCapPerMonth": 20000,
"allowedModelTiers": ["everyday", "advanced", "strategic"]
}
Response:
Responds 200 with the reloaded profile. Runs in a database transaction.
{
"name": "Senior Analysts",
"creditCapPerMonth": 20000,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
]
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-profiles/00000000-0000-4000-8000-000000000001"
}
},
"isUnlimited": false,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Senior Analysts",
"slug": "analysts",
"description": "Strategic models, no monthly cap",
"creditCapPerMonth": 20000,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
],
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
DELETE /api/usage-profiles/{id}
Delete a usage profile.
Authentication: Required (session)
Pre-blocks: permission.usage.write, usageProfile.lookup
Response:
Responds 200 with the body { success: true }, not 204.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/usage-profiles/00000000-0000-4000-8000-000000000001"
}
},
"success": true
}
A profile cannot be deleted while it is in use. The route answers 409 Conflict if the profile is assigned to any team or agent, or if it is the tenant default (Tenant.defaultAiProfileId).
GET /api/teams/{id}/ai/profile
Get the usage profile currently assigned to a team.
Authentication: Required (session)
Pre-blocks: team.lookup, permission.billing.manage
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Team id (teams use string ids, not UUIDs) |
Response:
Returns { profileId, profile } (with a HAL _links.self). When no profile is assigned, both fields are null. When a profile is assigned, profile is the full Usage Profile row.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/teams/order-desk/ai/profile"
}
},
"profileId": null,
"profile": null
}
After a profile is assigned:
{
"_links": {
"self": {
"href": "https://informer.example.com/api/teams/order-desk/ai/profile"
}
},
"profileId": "00000000-0000-4000-8000-000000000001",
"profile": {
"isUnlimited": true,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Premium",
"slug": "premium-team",
"description": "Adds strategic-tier models, no monthly cap",
"creditCapPerMonth": null,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
],
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
}
PUT /api/teams/{id}/ai/profile
Assign a usage profile to a team, or unassign it.
Authentication: Required (session)
Pre-blocks: team.lookup, permission.billing.manage
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
profileId | string (UUID) | null | Yes | Profile to assign, or null to unassign |
Response:
Responds 200 with the assigned { profileId, profile }. A non-null profileId is validated.
{
"profileId": "00000000-0000-4000-8000-000000000001"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/teams/order-desk/ai/profile"
}
},
"profileId": "00000000-0000-4000-8000-000000000001",
"profile": {
"isUnlimited": true,
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"name": "Premium",
"slug": "premium-team",
"description": "Adds strategic-tier models, no monthly cap",
"creditCapPerMonth": null,
"allowedModelTiers": [
"everyday",
"advanced",
"strategic"
],
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
}
Passing { profileId: null } deletes the assignment and returns { profileId: null, profile: null }:
{
"profileId": null
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/teams/order-desk/ai/profile"
}
},
"profileId": null,
"profile": null
}
A non-null profileId that matches no profile answers 404 Not Found with the message Usage profile not found.
{
"profileId": "00000000-0000-4000-8000-000000000001"
}
{
"statusCode": 404,
"error": "Not Found",
"message": "Usage profile not found"
}