16. The operator surface
/internal/v1/* requires an internal_ key and refuses pk_live_ outright.
Customer keys are refused here and internal keys are refused on /v1/*; there
is no overlap.
| Endpoint | Purpose |
|---|---|
GET /internal/v1/health/full | every dependency (postgres, OpenObserve, Dokploy) with latency and error. 503 when any fails, for load balancers; ?soft=1 returns the same body with 200 for dashboards, the verdict staying in status (ready | not_ready) |
GET /internal/v1/providers | every provider row, enabled or not |
PATCH /internal/v1/providers/{name} | enable/disable, timeout, capability overrides |
| `GET | PUT /internal/v1/catalog/models/{name}/pricing` |
POST /internal/v1/catalog/models | seed catalog rows |
| `GET | POST /internal/v1/admin/model-bindings` |
POST /internal/v1/admin/byok/reseal | carry every sealed BYOK credential onto the current master-key version (rotation step two; needs internal:credentials:reseal) |
| `GET | POST /internal/v1/admin/byok/platform-credentials` |
| `PATCH | DELETE /internal/v1/admin/byok/platform-credentials/{id}·POST …/{id}/verify` |
POST /internal/v1/blueprints | author a self-hosting recipe |
GET /internal/v1/rtr/providers | what the router actually registered at boot |
POST /internal/v1/db/query | read-mostly SQL |
GET /internal/v1/forensics/* | orphan pods, instance composites |
POST /internal/v1/scenarios/{name}/run | end-to-end gates |
Operator scopes are separated on purpose: internal:catalog:manage files a
model (inert), internal:pricing:manage changes a rate (moves money),
internal:providers:manage decides whether an upstream serves at all,
internal:credentials:manage decides which upstream accounts the platform
spends from. A key trusted with one is not thereby trusted with the others.
Platform keys live in the database, not the compose env. A provider with
any platform key row is served from that pool; its api_key_env is only the
fallback for a provider with none.
Provider changes apply without a restart. The router fingerprints the
provider rows every 30 seconds and hot-swaps its registry when they change;
every patch response says applies_within_secs: 30.
Doors for operators managing tenants:
| Endpoint | Purpose |
|---|---|
GET /internal/v1/admin/tenants?q=&limit=&offset= | every tenant: plan, overrides, organization / project / active-key counts, month-to-date spend in µ¢; q matches name, slug or id. Scope internal:tenants:read |
GET /internal/v1/admin/tenants/{tenant_id} | one tenant with its organizations, projects, entitlement in force, keys (previews only) and month-to-date spend. Scope internal:tenants:read |
DELETE /internal/v1/admin/tenants/{tenant_id} | hard-delete a tenant and all of its data in one transaction. Dry run by default (empty body or {"dry_run": true} returns per-table row counts); a real run needs {"dry_run": false, "confirm_slug": "<slug>", "confirmation_token": "<catastrophic token>"}. Refused while an instance is not stopped, and refused naming any tenant table the door does not know. api.audit_log is kept; users are never deleted. Scope internal:db:query |
GET /internal/v1/tenant-keys · PATCH /internal/v1/tenant-keys/{id} | read a tenant's keys; set a key's scope set (absolute, audited) — the only way to grant keys:write |
POST /internal/v1/db/query | the current path for custom_overrides on an entitlement, e.g. {"monthly_credits": null, "max_concurrent_instances": null} for metered-but-unbounded |
The operator surface answers on https://ops.opennozzle.com/internal/v1. Use
that host: https://api.opennozzle.com/internal/v1 and the plain-HTTP
http://api.49-12-240-8.traefik.me still answer on the current server but do
not survive the move off it, and an internal_ key on the plain-HTTP host
crosses the network in the clear.