{
  "info": {
    "_postman_id": "b1f0c6a2-7d41-4d8e-9a3b-000000000002",
    "name": "Trivora Admin — Control Plane",
    "description": "**Internal only.** The management surface Trivora's own Salesforce org calls to onboard tenants, amend contracts, move credits and read usage across every organisation.\n\nThis is a different credential from the tenant collection, on the same host:\n\n| | Scope | `org_scope` | Reaches |\n|---|---|---|---|\n| This collection | `admin` | **must be null** | every organisation, the control plane |\n| Tenant collection | `tenant` | pinned to one org | runtime operations for that org only |\n\n⚠️ **Never issue `admin` together with an `org_scope`.** `require_write` only checks that the admin scope is present, and `org_scope` is an independent column — so that combination could amend its *own* contract: seats to 10,000, the reasoning entitlement switched on, the seat price rewritten. `require_unscoped()` now guards the control-plane routes, but the rule stands at issuance too.\n\n**The credential behind this collection is powerful.** The admin API is the only process besides the gateway that holds the LiteLLM master key, which reads every tenant and mints unlimited keys. Keep this collection and its environment out of shared workspaces.\n\n**Three guards on money-moving writes**, and all three are enforced:\n- an `Idempotency-Key` header\n- an `X-Trivora-Actor` header naming the acting human\n- an audit row, written whether the call succeeds or not\n\nA missing `X-Trivora-Actor` is a 400 on every write. That is deliberate: without it the ledger's actor columns go null the moment one integration forgets the header, and the audit trail is only as good as the least careful caller.\n\n`actor_user_id` is **attested, not authenticated** — client credentials identify the calling *system*, and the human arrives in a header the system sets. In an audit conversation, say \"attributed by the calling system\".",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{access_token}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Fetch and cache the OAuth token. Tokens last 15 minutes.",
          "const path = pm.request.url.getPath();",
          "if (path.indexOf('/admin/oauth/token') === 0) { return; }",
          "if (path.indexOf('/admin/activate') === 0) { return; }",
          "",
          "const now = Math.floor(Date.now() / 1000);",
          "if (pm.environment.get('access_token') &&",
          "    Number(pm.environment.get('token_expires_at') || 0) > now + 30) { return; }",
          "",
          "const id = pm.environment.get('client_id');",
          "const secret = pm.environment.get('client_secret');",
          "if (!id || !secret) { console.warn('set client_id / client_secret in the environment'); return; }",
          "",
          "pm.sendRequest({",
          "  url: pm.environment.get('admin_base') + '/admin/oauth/token',",
          "  method: 'POST',",
          "  header: { 'Content-Type': 'application/x-www-form-urlencoded' },",
          "  body: { mode: 'urlencoded', urlencoded: [",
          "    { key: 'grant_type',    value: 'client_credentials' },",
          "    { key: 'client_id',     value: id },",
          "    { key: 'client_secret', value: secret }",
          "  ]}",
          "}, (err, res) => {",
          "  if (err || res.code !== 200) { console.error('token request failed', err || res.text()); return; }",
          "  const body = res.json();",
          "  pm.environment.set('access_token', body.access_token);",
          "  pm.environment.set('token_expires_at', now + body.expires_in);",
          "});"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "admin_base",
      "value": "https://admin.trivora.tech"
    }
  ],
  "item": [
    {
      "name": "Auth",
      "item": [
        {
          "name": "Get access token",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const b = pm.response.json();",
                  "  pm.environment.set('access_token', b.access_token);",
                  "  pm.environment.set('token_expires_at', Math.floor(Date.now()/1000) + b.expires_in);",
                  "}",
                  "pm.test('token issued', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "grant_type",
                  "value": "client_credentials"
                },
                {
                  "key": "client_id",
                  "value": "{{client_id}}"
                },
                {
                  "key": "client_secret",
                  "value": "{{client_secret}}"
                }
              ]
            },
            "url": {
              "raw": "{{admin_base}}/admin/oauth/token",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "oauth",
                "token"
              ]
            },
            "description": "Form-encoded client credentials, matching what a Salesforce External Credential sends.\n\nAn unknown `client_id` and a wrong secret take the same time and return the same 401 — the endpoint is deliberately not a client-id oracle.\n\nRun automatically by the collection pre-request script."
          }
        },
        {
          "name": "Who am I",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/whoami",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "whoami"
              ]
            },
            "description": "For an admin credential, `org_scope` **must be null**. If it is not, this credential can amend the contract of the organisation it is pinned to. Revoke and re-issue."
          }
        },
        {
          "name": "Health",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{admin_base}}/admin/health",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "health"
              ]
            },
            "description": "Unauthenticated liveness for the load balancer. Reports dependency state without revealing anything about tenants.\n\n⚠️ **This returns 503 unless the admin API can also reach the gateway** — and it is admin's own ALB health check path. So the admin service cannot start until the gateway's domain resolves, and a gateway outage takes admin down with it even though its own reads are fine. Attach `ai.trivora.tech` before creating or rolling the admin service."
          }
        }
      ]
    },
    {
      "name": "Organisations",
      "item": [
        {
          "name": "List all organisations",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs"
              ]
            },
            "description": "Every tenant with its balance, ordered by how much of its allowance is consumed.\n\nWatch two columns:\n- `pct_consumed` — approaching 100 means a hard stop is coming. Phone them before it lands, never after.\n- `over_licensed` — more seats in use than the contract sold. LiteLLM counts users that exist, not seats purchased, so this is how you see the breach happening."
          }
        },
        {
          "name": "Create organisation (seat billing)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) {",
                  "  pm.environment.set('org_ref', pm.response.json().organization_id);",
                  "}",
                  "pm.test('created', () => pm.response.to.have.status(201));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "REQUIRED on every money-moving write."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"display_name\": \"Acme Corp\",\n  \"billing_mode\": \"seat\",\n  \"seats\": 50,\n  \"seat_price_usd\": 39,\n  \"salesforce_org_id\": \"00D000000000000EAA\",\n  \"included_credits_per_seat\": 500,\n  \"term_months\": 12,\n  \"reasoning_enabled\": true,\n  \"transcribe_enabled\": true,\n  \"plan_label\": \"Advanced\"\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs"
              ]
            },
            "description": "Creates the gateway organisation, its budget, the contract row and the first monthly grant in one call.\n\n⚠️ **`included_credits_per_seat` must be set per contract, not defaulted, for any tenant that records calls.** The 500 default was derived from chat traffic alone and buys about 5.9 audio-hours after chat. Audio swamps everything else at realistic volume:\n\n| per seat / month | credits | audio share |\n|---|---:|---:|\n| chat only, 150 turns | 58 | — |\n| + 5 audio-hours | 433 | 87% |\n| + 15 audio-hours | 1,183 | **95%** |\n\nSizing, with 30% headroom: **~75** for chat-only, **~565** at 5 audio-hours/rep, **~1,540** at 15. \"How many hours of calls per user per month?\" is a discovery question you cannot skip — it is 95% of what you are selling them.\n\n⚠️ **`reasoning_enabled` is an entitlement flag, not a synonym for the Advanced plan.** It defaults from the plan but is independently settable, so it can be trialled on a Standard contract or withheld from an Advanced one without a price-book change or a deploy. Enforcement is unchanged: it is the organisation's model list, which session keys inherit, which is the only control point that survives a user reading their key out of devtools.\n\nThere is no default `seat_price_usd`. A silent default would put a number on an invoice nobody agreed to."
          }
        },
        {
          "name": "Create organisation (credit billing)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"display_name\": \"Globex Custom Implementation\",\n  \"billing_mode\": \"credit\",\n  \"included_credits_per_seat\": 0,\n  \"term_months\": 12,\n  \"reasoning_enabled\": true,\n  \"transcribe_enabled\": true,\n  \"plan_label\": \"Custom Implementation\",\n  \"tpm_limit\": 200000\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs"
              ]
            },
            "description": "Org-wide prepaid credits, no seats, no per-user attribution. Used by backend integrations and batch workloads rather than the Salesforce package.\n\n**Omit `seats` and `seat_price_usd`** — sending either is a 400. Credit mode is org-wide by definition; a seat price on it would be meaningless.\n\nThese tenants use **project keys**, not sessions. `POST /sessions` returns 409 for them: a per-user session key would create a membership row that means nothing and inflate an \"active seats\" count nobody is billed on.\n\n⚠️ **`billing_mode` cannot be changed later.** Switching a live tenant between seats and prepaid credits changes what its ledger *means* — grants, seat rows and the budget baseline all reinterpret — and there is no honest migration for the history. Create a new organisation."
          }
        },
        {
          "name": "Get one organisation",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}"
              ],
              "description": "org_ref accepts the organisation id or the display name."
            },
            "description": "Contract, the entitlement in force **today**, pending amendments, balance and seats in use.\n\nThe entitlement is date-filtered. Ordering by term start alone would return a December row in August and report a contract the customer is not yet on — pending rows come back separately so a console can show both without confusing them."
          }
        },
        {
          "name": "Amend the contract",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"seats\": 75,\n  \"included_credits_per_seat\": 800,\n  \"reasoning_enabled\": true,\n  \"effective_from\": \"2026-12-01\"\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}"
              ]
            },
            "description": "**Amendments are dated, and the sweep applies them.** A December step-up signed in August is written in August — the row exists so the deal is recorded when it is signed — and nothing reaches the gateway until the date arrives. Omit `effective_from` to apply today.\n\nThe hourly sweep then does three things per org, all convergent and all no-ops once satisfied: opens the month's billing period, grants the monthly allowance, applies any due amendment.\n\n**Seat increases are prorated from the amendment's effective date**, clamped to the month start — not from the day the sweep happens to reach it. An amendment effective the 1st that lands on the 3rd is still owed the whole month.\n\nTwo amendments on the same day are allowed; the later row wins deterministically.\n\n⚠️ Requires an **unscoped** admin credential. A client pinned to one organisation is refused here, because otherwise a tenant credential could rewrite the contract it is billed under.\n\n`billing_mode` is deliberately absent from this endpoint — see *Create organisation (credit billing)*.\n\n---\n\n### Two new amendment types\n`term_end` renews or extends the contract term. `seat_terms` moves individual seats. Both were impossible before: every amendment inherited the term it amended, and the guard that refused an effective date past it said *\"renew the term first\"* pointing at a path that did not exist.\n\nSee **Extend seats**, **Split seat terms**, **Preview a renewal** and **Renew a lapsed contract** below.\n\n⚠️ A **seat decrease** is now refused while seats are assigned, rather than silently logging people out. Release them first, or name the slots in `seat_terms`."
          }
        },
        {
          "name": "List entitlement history",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/entitlements",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "entitlements"
              ]
            },
            "description": "Every contract row for this tenant, past, present and scheduled.\n\nThis is a **contract history**, which is why amendments create rows rather than rewriting one. Overwriting the entitlement monthly would destroy the term-bounded record the whole design exists for."
          }
        },
        {
          "name": "Cancel a pending amendment",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/entitlements/123",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "entitlements",
                "123"
              ]
            },
            "description": "Withdraws a signed-but-not-yet-effective amendment. Only rows that have not been applied can be removed — an entitlement already in force is history, not a plan."
          }
        },
        {
          "name": "Renew / extend the term",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}"
              ]
            },
            "description": "**Extend time.** Writes a new entitlement row carrying the later term; history is preserved as always.\n\nThis also works on a **LAPSED** contract. Previously `update_org` called `current_entitlement()` first, which 409s when no row covers today — so a customer who renewed three days after expiry could not be renewed through the API at all, and the only way back was editing the database.\n\nExtending the term does **not** by itself extend the seats. Seats carry their own dates; use `seat_terms` in the same call or a following one. The reverse is automatic: extending a seat past `term_end` pushes `term_end` with it, because `current_entitlement()` gates the whole org on the term and a seat beyond it would be refused along with everyone else.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"term_end\": \"2028-02-01\"\n}"
            }
          }
        },
        {
          "name": "Extend seats to a date",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}"
              ]
            },
            "description": "Extend 40 seats to an absolute date.\n\n### ⚠️ Absolute dates, never `+6 months`\nThere is no duration form, deliberately. The selector is `license_end <= target`, so **a seat already at the target cannot match** — which makes the operation idempotent and makes the classic double-extension bug unrepresentable.\n\nExtend 40 seats, then extend 10 more to the same date, and the second call can only reach the 10 left behind. With an offset it would push ten already-extended seats twice:\n\n```\nrelative:  10 @ +12mo,  30 @ +6mo,  10 lapsed on the original date\nabsolute:  50 @ the date you asked for          ← verified end to end\n```\n\n### Which seats\n**Assigned first.** The seats *not* selected are the ones that lapse, so ordering decides who gets cut off. Ordering by id would renew empty licences and cut off working reps, and nothing in the request would look wrong.\n\nFilters: `ending_on_or_before`, `assigned` (true/false), `limit`. Or pass `slots: [1,2,3]` to name them exactly.\n\n### The selector can only EXTEND\nAsking for an earlier date matches zero rows, because the seat is already past it. Shortening requires naming slots explicitly plus `allow_shorten`. An extension that picks slightly the wrong set costs money; a shortening that does logs people out, so the destructive direction is not reachable from a convenience filter.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"seat_terms\": [\n    {\n      \"ending_on_or_before\": \"2026-12-31\",\n      \"limit\": 40,\n      \"license_end\": \"2027-06-30\"\n    }\n  ]\n}"
            }
          }
        },
        {
          "name": "Split seat terms (30 + 10)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}"
              ]
            },
            "description": "Different end dates for different groups, in one atomic amendment.\n\nEvery group resolves to an explicit slot id list **before** anything is written, so the amendment records what it actually did.\n\nValidation:\n- a seat may appear in **at most one group** — overlap is a `400`, not last-write-wins\n- slots must belong to this org and not be retired; a named id that does not is a `404`\n- an end date in the past is a `400`\n- **all groups apply in one transaction** — half-applying is worse than failing\n\nResponse echoes per group: `slots`, `assigned`, `shortened`, `no_op`. A high `no_op` count means the group was already at that date — usually a re-run, not a mistake.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"seat_terms\": [\n    { \"slots\": [1,2,3], \"license_end\": \"2027-06-30\" },\n    { \"slots\": [4,5],   \"license_end\": \"2027-12-31\" }\n  ]\n}"
            }
          }
        },
        {
          "name": "Preview a renewal (dry run)",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}?dry_run=true",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}"
              ],
              "query": [
                {
                  "key": "dry_run",
                  "value": "true"
                }
              ]
            },
            "description": "**Run this before every renewal.** Changes nothing, takes no Idempotency-Key, and returns exactly what the real call would do — same resolver, same ids.\n\nThe field that matters is `would_lapse`: the seats this amendment leaves behind, **with the users sitting in them**.\n\n```json\n{\n  \"dry_run\": true,\n  \"seat_terms\": [ { \"slots\": [1,…,40], \"assigned\": 38, \"no_op\": 0,\n                     \"license_end\": \"2027-06-30\" } ],\n  \"would_lapse\": [\n    { \"slot\": 47, \"on\": \"2026-12-31\", \"assigned_to\": \"005…\" }\n  ]\n}\n```\n\nRenewals get signed off by someone reading a number in an email. Showing the names of the people who stop working is the difference between a decision and a surprise.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"seat_terms\": [\n    {\n      \"ending_on_or_before\": \"2026-12-31\",\n      \"limit\": 40,\n      \"license_end\": \"2027-06-30\"\n    }\n  ]\n}"
            }
          }
        }
      ]
    },
    {
      "name": "Credits",
      "item": [
        {
          "name": "Balance and ledger",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/credits?limit=50",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "credits"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "Append-only, and it records **grants and purchases only — never consumption**. Consumption is derived from the gateway's daily spend rollup at read time. Writing usage into a ledger is how ledgers drift from the system of record."
          }
        },
        {
          "name": "Sell credits (purchase)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "REQUIRED. Reserved in the SAME transaction as the ledger write — see the description."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"event_type\": \"purchase\",\n  \"credits\": 10000,\n  \"invoice_ref\": \"INV-2026-0842\",\n  \"note\": \"Overage top-up, Q3\"\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/credits",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "credits"
              ]
            },
            "description": "Adds purchased credits, then recomputes the gateway budget from the ledger. In that order — the ledger is the record, the budget is derived from it.\n\n**Purchased credits are the shared pool.** Included credits belong to a seat and cannot cross to another; purchased credits are org-wide and any seat may draw on them once its own allowance is gone.\n\n`invoice_ref` is required. An unreferenced sale is unauditable.\n\n**Why the Idempotency-Key is reserved before the write, in the same transaction.** Found by crashing, not by reasoning: the handler died between the ledger insert and storing the key, the client retried, and the ledger took a **second 10,000-credit purchase** under the key meant to prevent exactly that. In an append-only ledger both rows are valid and nothing can tell them apart afterwards. Now: crash before commit and both roll back; crash after and both are durable, so the retry resumes rather than re-executing.\n\nIf the budget sync fails the ledger still stands and the response is a **207**. The credits are real. Run *Resync budget* to reconcile."
          }
        },
        {
          "name": "Grant included credits",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"event_type\": \"included_grant\",\n  \"note\": \"Manual grant — sweep missed 2026-08\"\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/credits",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "credits"
              ]
            },
            "description": "The monthly allowance. Omit `credits` and it is computed as `included_credits_per_seat × seats`.\n\n**You should rarely need this.** The hourly sweep grants automatically, and `GET /admin/maintenance/status` reports `orgs_missing_grant` — which should be zero at all times. A non-zero value on the 2nd means the scheduler did not run, and *that* is the thing to investigate before reaching for a manual grant.\n\nThe sweep runs hourly rather than as a cron on the 1st because a once-a-month job has exactly one chance to work. This one self-heals within the hour."
          }
        },
        {
          "name": "Adjustment (signed)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"event_type\": \"adjustment\",\n  \"credits\": -2500,\n  \"note\": \"Goodwill reversal — duplicate batch job on 2026-08-03, ref TICKET-119\"\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/credits",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "credits"
              ]
            },
            "description": "A signed correction. Negative claws back, positive gives.\n\n`note` is required and it is the only explanation anyone will ever have. Write it for the person reading the ledger in a year, not for yourself today."
          }
        },
        {
          "name": "Resync budget from ledger",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/credits/sync",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "credits",
                "sync"
              ]
            },
            "description": "Recomputes the gateway budget from the ledger. Idempotent and safe to run at any time.\n\nRun it after a 207 from a credit write, or whenever the balance and the enforced ceiling disagree.\n\n**Two budgets, two meanings:**\n\n| | Set to | Fires when |\n|---|---|---|\n| `soft_budget` | baseline + included credits | fair use crossed → **alert to us** |\n| `max_budget` | soft + purchased credits still unspent | credits exhausted → **hard stop** |\n\nPhone the customer on soft. Never let a hard stop surface mid-conversation.\n\n**No `budget_duration` anywhere.** Spend must accumulate monotonically or the ceiling silently resets and the ledger goes out of sync. The monthly rebase is done by moving the *baseline* instead — see the maintenance folder."
          }
        }
      ]
    },
    {
      "name": "Project keys",
      "description": "Long-lived gateway credentials for a Custom Implementation. Admin-only, and unscoped-admin-only.",
      "item": [
        {
          "name": "List project keys",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/keys",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "keys"
              ]
            },
            "description": "Masked name, models, limits, and whether the key is still active. The secret itself is stored only as a hash — enough to list and revoke, useless to anyone who reads the table."
          }
        },
        {
          "name": "Issue a project key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"Nightly lead-scoring batch\",\n  \"models\": [\"trivora-extract\"],\n  \"max_budget_usd\": 250,\n  \"tpm_limit\": 100000\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/keys",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "keys"
              ]
            },
            "description": "⚠️ **The secret is returned exactly once and is not recoverable.** Capture it before closing the response.\n\n**Only an unscoped admin credential can issue one.** A project key is long-lived and reaches every model the organisation is entitled to, so issuing one is a commercial act, not a runtime one. A tenant credential minting its own durable keys would make the session key's fifteen-minute limit decorative.\n\nOmit `models` and the key gets everything the contract entitles them to. **Narrow it** — giving a batch job the extract tier only is the usual case, and it is the difference between a runaway loop costing $30 and costing $800.\n\nWhy long-lived at all: a custom implementation is a backend service, a batch job or a cron. There is no browser, no end user to attribute to, and re-minting every fifteen minutes would buy nothing while adding a failure mode to every call."
          }
        },
        {
          "name": "Revoke a project key",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/keys/1",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "keys",
                "1"
              ]
            },
            "description": "Takes effect at the gateway immediately. Usage already billed is unaffected — revoking a key does not rewrite history."
          }
        }
      ]
    },
    {
      "name": "Activation codes",
      "description": "How a customer's managed package gets a credential without anyone emailing a secret.",
      "item": [
        {
          "name": "List codes",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/activation-codes",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "activation-codes"
              ]
            },
            "description": "Codes issued for this organisation, with `usable` telling you whether one is still outstanding. The code itself is stored as a SHA-256 digest and is never recoverable — only a hint of its last characters."
          }
        },
        {
          "name": "Issue an activation code",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"note\": \"Sent to Acme IT during onboarding call\",\n  \"ttl_days\": 14\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/activation-codes",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "activation-codes"
              ]
            },
            "description": "⚠️ **The code is shown once and never again.** Re-issuing revokes any code still outstanding for this organisation.\n\nThe customer's package redeems it at `POST /admin/activate` and the long-lived secret is generated **inside** that exchange, returned once, to the org that redeemed it. It never exists before the package asks for it and never passes through a human inbox, Slack thread or support ticket.\n\nThe code is 128 bits of CSPRNG output, single-use, expiring, stored as SHA-256, and bound to the tenant's Salesforce org id — so a leaked code is useless anywhere else.\n\nSHA-256 rather than bcrypt here on purpose: a client secret is looked up *by* `client_id` and then verified, but a code arrives with no identifier and must be found *by* its digest, which needs a deterministic hash."
          }
        },
        {
          "name": "Revoke a code",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/activation-codes/1",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "activation-codes",
                "1"
              ]
            },
            "description": "Use this the moment a code is believed to have gone somewhere it should not have. An unredeemed code is the only window in the onboarding flow where a secret-equivalent exists outside our systems."
          }
        }
      ]
    },
    {
      "name": "Seats",
      "item": [
        {
          "name": "List licences (pick seats to extend)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/licenses?status=active",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "licenses"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "active",
                  "description": "active | scheduled | lapsed | retired | free | assigned"
                },
                {
                  "key": "ending_on_or_before",
                  "value": "2026-12-31",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "500",
                  "disabled": true
                }
              ]
            },
            "description": "**Run this before any extension.** It is the only place licence slot ids are visible, and `seat_terms` needs them to extend a named set.\n\n`GET /seats` cannot do this job: it is built on LiteLLM's membership table, so it shows only slots someone is sitting in. Free and future-dated licences do not appear there at all — which meant \"extend 40 of 50\" silently meant \"40 of the assigned ones\".\n\n### The order is the resolver's order\nRows come back **assigned first, then by `license_end`, then id** — exactly how a `seat_terms` group with `limit` selects. So the first N rows here are precisely the N seats that group would take. Sorting it any other way would make the preview answer a different question from the amendment.\n\n```\nid=98   active  ends 2027-08-01  assigned  R. Kumar\nid=99   active  ends 2027-08-01  assigned  P. Nair\nid=101  active  ends 2027-08-01  FREE\n```\n\nThen either hand `[98, 99]` to **Split seat terms**, or use `limit: 2` and get the same two.\n\nEach row carries `status`, `license_start`, `license_end`, the occupant, and that slot's `credits_used_mtd` / `included_remaining` — so you can see what a seat has consumed before deciding whether to renew it.\n\n`inventory` is the same aggregate block `GET /seats` returns; **`next_expiry`** is the date the org first loses capacity."
          }
        },
        {
          "name": "List seats",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/seats",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "seats"
              ]
            },
            "description": "Visibility only. **Trivora stopped being the writer here.**\n\nSeats are created by the customer's org, inside the session mint. Any design where seats arrive later than keys has a permanent window in which a user transacts, bills the organisation, and attributes to nobody — and the seat cap can only be enforced at mint time anyway; a sync discovers the breach afterwards.\n\nNow also returns `inventory`: `seats_live`, `seats_assigned`, `seats_available`, `seats_scheduled`, `seats_lapsed`, `seats_retired`, and **`next_expiry`** — the date the org first loses capacity. Put `next_expiry` on the renewal dashboard."
          }
        },
        {
          "name": "Get a seat's allowance",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/seats/005000000000001AAA/allowance",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "seats",
                "005000000000001AAA",
                "allowance"
              ]
            },
            "description": "The seat's own included allowance, what is left of it, and the shared purchased pool it may draw on afterwards.\n\n**Two ceilings doing different jobs.** The seat cap stops one seat consuming another's included credits. The org cap still stops the shared purchased pool being spent twice — every seat is authorised for the *whole* pool individually, so without it two heavy seats could each draw it down in full."
          }
        },
        {
          "name": "Override a seat's allowance",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"included_credits_override\": 2000,\n  \"note\": \"Heavy call recorder — approved by AM, ref TICKET-204\"\n}"
            },
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/seats/005000000000001AAA/allowance",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "seats",
                "005000000000001AAA",
                "allowance"
              ]
            },
            "description": "Raises one SLOT above the contract allowance. `null` clears the override.\n\n**Keyed on the slot, not the user**, so it survives a change of occupant — the same rule the allowance itself follows. Address it by the current occupant's user id; the API resolves the slot.\n\nPushed to the gateway immediately, not on the next hourly sweep: waiting would mean raising a blocked rep's allowance and telling them to come back in an hour.\n\nNo Idempotency-Key — this sets an absolute value, so a duplicate call is a no-op."
          }
        },
        {
          "name": "Release a seat",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/seats/005000000000001AAA",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "seats",
                "005000000000001AAA"
              ]
            },
            "description": "Usage history is retained. Deleting it would rewrite past invoices."
          }
        }
      ]
    },
    {
      "name": "Usage",
      "item": [
        {
          "name": "Monthly usage",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/usage?month=2026-08",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "usage"
              ],
              "query": [
                {
                  "key": "month",
                  "value": "2026-08"
                }
              ]
            },
            "description": "Credits used, tier mix, error rate, balance.\n\n⚠️ **A reasoning turn costs 28× a premium turn.** `reasoning_pct` is the single number that explains a surprising bill, and any scheme that treats actions equally gets arbitraged."
          }
        },
        {
          "name": "Per-seat usage",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/usage/seats?month=2026-08",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "usage",
                "seats"
              ],
              "query": [
                {
                  "key": "month",
                  "value": "2026-08"
                }
              ]
            },
            "description": "**Track per seat, enforce per org.** Per-seat visibility is what tells you the pool is sized right, and it is the only way to tell a heavy human from a script.\n\nRead `pct_of_org_consumption`, not `pct_of_notional_share`, when hunting for who is eating a pool. The two diverge hard: a seat can sit at 31% of its notional share while being 89% of all consumption.\n\n`avg_prompt_tokens` / `avg_completion_tokens` validate the 3k/400 assumption that every cost figure rests on. **That assumption is still unvalidated.** If real Salesforce context turns out to be 10k input, everything roughly triples — check this against real traffic before printing a price list."
          }
        },
        {
          "name": "Usage trend",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/usage/trend?months=12",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "usage",
                "trend"
              ],
              "query": [
                {
                  "key": "months",
                  "value": "12"
                }
              ]
            },
            "description": "**Size the allowance from `p95_seat_credits`, not the mean.**"
          }
        },
        {
          "name": "Unattributed spend",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/usage/unattributed?month=2026-08",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "usage",
                "unattributed"
              ],
              "query": [
                {
                  "key": "month",
                  "value": "2026-08"
                }
              ]
            },
            "description": "**The tripwire. It should always be zero.**\n\nNon-zero means a session key was minted for a user with no organisation membership. That spend bills the org and belongs to no seat.\n\nWhy membership is the only durable seat→org edge, verified against the live build: `/key/generate` with `user_id` + `organization_id` creates no user row; `/user/new` with `organization_id` silently ignores it; `/organization/member_add` writes the membership and *still* leaves the user table's `organization_id` null. And do not attribute through the key table instead — it *is* populated, which is what makes it a trap: session keys are pruned after ~7 days, so all seat history would orphan itself."
          }
        }
      ]
    },
    {
      "name": "Logs & audit",
      "item": [
        {
          "name": "Request log",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/requests?month=2026-08&limit=100",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "requests"
              ],
              "query": [
                {
                  "key": "month",
                  "value": "2026-08"
                },
                {
                  "key": "status",
                  "value": "failure",
                  "disabled": true
                },
                {
                  "key": "model",
                  "value": "trivora-premium",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "100"
                },
                {
                  "key": "before",
                  "value": "",
                  "disabled": true
                }
              ]
            },
            "description": "Keyset pagination — pass the previous page's `next_before`. Offset would degrade linearly against an unpruned log and skip or repeat rows when new requests land mid-page.\n\nNo prompt or completion content exists to read: message logging is off, so those columns hold `{}` on every row. That is provable to a customer with a single count query, and it is what makes a customer-scoped read-only client viable without touching a route."
          }
        },
        {
          "name": "Error summary",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/errors?month=2026-08&limit=20",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "errors"
              ],
              "query": [
                {
                  "key": "month",
                  "value": "2026-08"
                },
                {
                  "key": "limit",
                  "value": "20"
                }
              ]
            },
            "description": "`exception_type` is usually null — LiteLLM's error log table is empty on this build. `status` is the reliable signal.\n\nCross-check against `GET /usage`'s `error_rate_pct` for the same month. They come from different tables, so a persistent disagreement means requests are escaping one of them."
          }
        },
        {
          "name": "Audit trail for one organisation",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/orgs/{{org_ref}}/audit?limit=50",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "orgs",
                "{{org_ref}}",
                "audit"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                },
                {
                  "key": "action",
                  "value": "credits.create",
                  "disabled": true
                }
              ]
            },
            "description": "Who granted credits, who changed the contract, who removed a seat — and whether it succeeded.\n\n**status_code 207 on a credits row means the ledger committed but the budget sync did not.** The credits are real; run *Credits → Resync budget*.\n\n`actor_user_id` is attested by the calling system, not authenticated."
          }
        },
        {
          "name": "Audit trail, all organisations",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/audit?limit=100",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "audit"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "100"
                }
              ]
            },
            "description": "Cross-tenant. Unscoped admin credentials only."
          }
        }
      ]
    },
    {
      "name": "Maintenance",
      "description": "The hourly sweep, and the ability to inspect and trigger it. A background loop you cannot inspect is a background loop you will not trust on the one day it matters.",
      "item": [
        {
          "name": "What the next sweep would do",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/maintenance/status",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "maintenance",
                "status"
              ]
            },
            "description": "Pending amendments, amendments now due, and organisations missing this month's grant — without doing any of it.\n\n**`orgs_missing_grant` is the number that matters. It should be zero at all times, and any non-zero value on the 2nd of the month means the scheduler did not run.** Put a monitor on it.\n\nCredit-mode tenants are excluded from that count: they are prepaid, nothing is ever granted to them, and listing them would make the alert fire forever and teach everyone to ignore it."
          }
        },
        {
          "name": "Run the sweep now",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/maintenance/run",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "maintenance",
                "run"
              ]
            },
            "description": "The same code path the hourly scheduler runs, under the same advisory lock. Safe to run at any time.\n\nPer organisation it does three things, all convergent and all no-ops once satisfied: open the month's billing period, grant the monthly allowance, apply any due amendment.\n\n⚠️ **Order matters: activate BEFORE granting.** A seat true-up compensates for a grant already made at a lower seat count. If both are outstanding on one sweep and the grant runs first, it is issued at the *new* seat count — because the current entitlement is selected by date and already returns the amended row — and the true-up then grants the same seats a second time. A 30-seat org billed 15,000 + 4,516 instead of 15,000. Found by a test, not in production.\n\n**No Idempotency-Key**, because every step converges on desired state rather than applying a delta.\n\n**Why the baseline is rebased monthly, not once per term.** Included credits expire monthly, so a ceiling of `term-start baseline + unexpired credits` is coherent for exactly one month — from month 2 the sum is `month-1 spend + month-2 allowance`, and a customer who used month 1 in full hard-stops on the 1st. Demonstrated on a 10-seat org: cumulative spend $50, ceiling $50, **zero headroom with 3,000 of 5,000 credits unused**. The sweep captures the counter as each month opens instead."
          }
        },
        {
          "name": "Reconcile one organisation",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-Trivora-Actor",
                "value": "{{actor_user_id}}"
              },
              {
                "key": "X-Trivora-Actor-Name",
                "value": "{{actor_name}}"
              }
            ],
            "url": {
              "raw": "{{admin_base}}/admin/maintenance/orgs/{{org_ref}}/reconcile",
              "host": [
                "{{admin_base}}"
              ],
              "path": [
                "admin",
                "maintenance",
                "orgs",
                "{{org_ref}}",
                "reconcile"
              ]
            },
            "description": "The sweep, for one tenant. Also re-pushes every seat's ceiling.\n\nSeat authorisation can be briefly stale as the shared pool drains — the sweep skips pushes that have not changed, so a 200-seat org does not make 200 gateway calls an hour forever. That staleness is safe: over-authorisation is caught by the org cap, while under-authorisation would block a seat legitimately inside its allowance."
          }
        }
      ]
    }
  ]
}
