Skip to content

Admin (platform operators)

Generated from DialerDigital/core commit 6e9f026f0a72 — spec openapi/dialer-v1.yaml v0.36.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Separate trust domain

The /v1/admin/* surface authenticates with the platform-operator bearer (ADMIN_API_TOKEN), not with tenant API keys. Tenants cannot call these endpoints; they are documented for completeness.

Platform-admin surface (separate trust domain, ADMIN_API_TOKEN).

Endpoints

MethodPathSummary
POST/v1/admin/tenantsProvision a tenant (onboarding backbone)
PATCH/v1/admin/tenants/{id}Change plan / billing-bearing fields
POST/v1/admin/tenants/{id}/didsAssign a HOUSE number to a tenant
GET/v1/admin/tenants/{id}/planThe tenant's plan anchor
POST/v1/admin/tenants/{id}/planOpen the tenant's plan and choose its billing scheme
PUT/v1/admin/tenants/{id}/plan/alertsSet the tenant's spend-alert thresholds and destinations
PUT/v1/admin/tenants/{id}/plan/enforcement-modeSet the plan's enforcement posture
POST/v1/admin/tenants/{id}/paymentsRecord a MANUAL payment (wire/cash/check) for a tenant
GET/v1/admin/tenants/{id}/configEffective governed config for a tenant
PUT/v1/admin/tenants/{id}/config/{key}Set a per-tenant override for a governed key
DELETE/v1/admin/tenants/{id}/config/{key}Drop a per-tenant override (back to the catalog default)
GET/v1/admin/tenants/{id}/config-auditWho changed which governed knob, from what to what
GET/v1/admin/tenants/{id}/design-partner-discountsThe tenant's design-partner discount history, plus the grant in force
POST/v1/admin/tenants/{id}/design-partner-discountsGrant the design-partner discount on the voice-AI add-on
GET/v1/admin/tax-nexusEvery tax-nexus declaration, plus the subset in force today
POST/v1/admin/tax-nexusDeclare tax nexus in a jurisdiction

Provision a tenant (onboarding backbone)

http
POST /v1/admin/tenants

Provisions a new tenant + its first admin agent + a bootstrap API key in one call. SEPARATE trust domain (it predates any tenant), gated by the platform-admin bearer (ADMIN_API_TOKEN). FAIL-CLOSED: when the admin token is unset, every /v1/admin/* request is 401. The returned token is the tenant's first credential — plaintext shown ONCE. 400/422 on an invalid payload (no tenant is created).

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Request body (JSON, required)

FieldTypeRequiredDescription
namestringyes
tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
voice_ai_enabledbooleannoOffered on the tiers whose catalog entry declares a voice-AI allowance (growth, scale, enterprise, and the retired compliance_pro / audit_shield); enabling it on a tier that does not offer it — starter or dialer_core — is rejected.
seatsintegerno
adminobjectyes
admin.namestringyes
admin.emailstring (email)yes
admin.sip_extensionstringno
json
{
  "name": "Acme Collections",
  "tier": "scale",
  "voice_ai_enabled": false,
  "seats": 10,
  "admin": {
    "name": "Owner",
    "email": "[email protected]"
  }
}

Responses

201

Tenant + first admin agent + bootstrap key (plaintext ONCE).

FieldTypeRequiredDescription
tenantobject (Tenant)yes
tenant.idstring (uuid)no
tenant.namestringno
tenant.statusactive · suspendedno
tenant.retention_monthsintegerno
tenant.seatsintegerno
tenant.tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
tenant.voice_ai_enabledbooleanno
tenant.ai_monthly_budget_usdnumber | nullnoCost-integrity ceiling (admin-set; null = no cap).
tenant.ai_max_call_secondsinteger | nullnoPer-AI-call hard duration ceiling (seconds) set by the admin. null means the PLATFORM default applies — never "no ceiling" (core#922, ADR #115).
tenant.ai_max_call_seconds_effectiveintegernoThe ceiling FreeSWITCH is actually handed for this tenant's AI legs: ai_max_call_seconds when set, else the cell's platform default (AI_MAX_CALL_SECONDS_DEFAULT, compiled fallback 900). Always present, never null. Read-only (derived).
tenant.sms_monthly_budget_usdnumber | nullnoMonth-to-date SMS spend cap.
tenant.human_initiated_enabledbooleannoD17 human-initiated consent posture (admin-set; default false). When true, a LIVE preview/manual/CTI voice dial with no artificial and no AI voice does not require prior express consent (no ATDS). Every other gate — DNC, quiet hours, Reg F, state matrix, artificial voice — still applies unchanged.
tenant.max_cpsinteger | nullnoPer-tenant CPS ceiling (admin-set; null = no ceiling). ADR
tenant.max_concurrent_channelsinteger | nullnoPer-tenant concurrent-channel ceiling (admin-set; null = no ceiling). ADR
tenant.abandon_seller_namestring | nullnoSeller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14.
tenant.abandon_seller_phonestring | nullnoE.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14.
tenant.tax_countrystring | nullnoISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_regionstring | nullnoState/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_postal_codestring | nullnoPostal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.created_atstring (date-time)no
tenant.updated_atstring (date-time)no
admin_agentobject (Agent)yes
admin_agent.idstring (uuid)no
admin_agent.tenant_idstring (uuid)no
admin_agent.namestringno
admin_agent.emailstringno
admin_agent.roleagent · supervisor · adminno
admin_agent.statusactive · inactivenoRoster flag — NOT presence.
admin_agent.sip_extensionstring | nullno
admin_agent.device_modebrowser · externalno
admin_agent.login_enabledbooleannoWidget self-login state — true iff a password credential is set. The password hash itself is never exposed.
admin_agent.presenceoffline · available · ringing · on_call · wrap_up · pausednoLive seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11).
admin_agent.created_atstring (date-time)no
admin_agent.updated_atstring (date-time)no
api_keyobjectyes
api_key.tokenstringyesBootstrap bearer, shown exactly once.
json
{
  "tenant": {
    "id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
    "name": "Acme Collections",
    "status": "active",
    "tier": "scale",
    "voice_ai_enabled": false,
    "seats": 10,
    "retention_months": 84,
    "ai_monthly_budget_usd": null,
    "ai_max_call_seconds": null,
    "ai_max_call_seconds_effective": 900,
    "sms_monthly_budget_usd": null,
    "created_at": "2026-07-05T10:00:00Z",
    "updated_at": "2026-07-05T10:00:00Z"
  },
  "admin_agent": {
    "id": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
    "tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
    "name": "Owner",
    "email": "[email protected]",
    "role": "admin",
    "status": "active",
    "sip_extension": null,
    "device_mode": "external",
    "presence": "offline",
    "created_at": "2026-07-05T10:00:00Z",
    "updated_at": "2026-07-05T10:00:00Z"
  },
  "api_key": {
    "token": "dd_example_bootstrap_shown_once"
  }
}

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Change plan / billing-bearing fields

http
PATCH /v1/admin/tenants/{id}

The billing-bearing fields a tenant must NOT be able to self-edit (under-pay vector): tier, voice_ai_enabled, seats, plus the cost-integrity ceilings (ai_monthly_budget_usd, ai_max_call_seconds, sms_monthly_budget_usd; null = no cap — except ai_max_call_seconds, where null = the platform default applies, core#922). The Tenant changeset still enforces the tier vocabulary and the voice-AI eligibility rule.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
namestringno
tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
voice_ai_enabledbooleanno
seatsintegerno
ai_monthly_budget_usdnumber | nullno
ai_max_call_secondsinteger | nullnonull clears the tenant's own ceiling and puts it back on the platform default (the response's ai_max_call_seconds_effective shows the number that will apply). There is no "no ceiling".
sms_monthly_budget_usdnumber | nullno
human_initiated_enabledbooleannoD17 human-initiated consent posture (default false). ADMIN-ONLY: it is compliance-loosening, so it is never writable on the tenant self-edit PATCH /v1/tenant.
max_cpsinteger | nullnoPer-tenant CPS ceiling (ADR
max_concurrent_channelsinteger | nullnoPer-tenant concurrent-channel ceiling (ADR
abandon_seller_namestring | nullnoSeller name spoken by the FTC TSR §310.4(b)(4)(iii) abandoned-call identification message (ADR #14). Admin-only. Null = not configured; the predictive over-dial stays dark (seat-first) until BOTH abandon_seller_name and abandon_seller_phone are set.
abandon_seller_phonestring | nullnoE.164 callback number spoken by the abandoned-call identification message (must accept do-not-call requests during business hours). Admin-only; null = not configured (over-dial stays dark).
tax_countrystring | nullnoISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (MT-BIL-07 (3), ADR #77). Admin-only: a tenant that could declare its own jurisdiction could pick its own tax. The shape is validated, the list of countries is NOT. Captured only — no invoice amount changes because of this field.
tax_regionstring | nullnoState/province code of the sales-tax jurisdiction (ADR #77). Admin-only; null = not declared.
tax_postal_codestring | nullnoPostal code of the sales-tax jurisdiction (ADR #77). Admin-only; null = not declared.
json
{
  "tier": "enterprise",
  "voice_ai_enabled": true,
  "seats": 25,
  "abandon_seller_name": "Acme Recovery LLC",
  "abandon_seller_phone": "+18005550123"
}

Responses

200

Updated tenant.

FieldTypeRequiredDescription
tenantobject (Tenant)yes
tenant.idstring (uuid)no
tenant.namestringno
tenant.statusactive · suspendedno
tenant.retention_monthsintegerno
tenant.seatsintegerno
tenant.tierstarter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shieldno
tenant.voice_ai_enabledbooleanno
tenant.ai_monthly_budget_usdnumber | nullnoCost-integrity ceiling (admin-set; null = no cap).
tenant.ai_max_call_secondsinteger | nullnoPer-AI-call hard duration ceiling (seconds) set by the admin. null means the PLATFORM default applies — never "no ceiling" (core#922, ADR #115).
tenant.ai_max_call_seconds_effectiveintegernoThe ceiling FreeSWITCH is actually handed for this tenant's AI legs: ai_max_call_seconds when set, else the cell's platform default (AI_MAX_CALL_SECONDS_DEFAULT, compiled fallback 900). Always present, never null. Read-only (derived).
tenant.sms_monthly_budget_usdnumber | nullnoMonth-to-date SMS spend cap.
tenant.human_initiated_enabledbooleannoD17 human-initiated consent posture (admin-set; default false). When true, a LIVE preview/manual/CTI voice dial with no artificial and no AI voice does not require prior express consent (no ATDS). Every other gate — DNC, quiet hours, Reg F, state matrix, artificial voice — still applies unchanged.
tenant.max_cpsinteger | nullnoPer-tenant CPS ceiling (admin-set; null = no ceiling). ADR
tenant.max_concurrent_channelsinteger | nullnoPer-tenant concurrent-channel ceiling (admin-set; null = no ceiling). ADR
tenant.abandon_seller_namestring | nullnoSeller name for the FTC TSR abandoned-call identification message (admin-set; null = not configured, predictive over-dial stays dark). ADR #14.
tenant.abandon_seller_phonestring | nullnoE.164 callback number for the abandoned-call identification message (admin-set; null = not configured). ADR #14.
tenant.tax_countrystring | nullnoISO-3166-1 alpha-2 country of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_regionstring | nullnoState/province code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.tax_postal_codestring | nullnoPostal code of the client's sales-tax jurisdiction (admin-set; null = not declared). ADR #77.
tenant.created_atstring (date-time)no
tenant.updated_atstring (date-time)no

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Assign a HOUSE number to a tenant

http
POST /v1/admin/tenants/{id}/dids

Same metadata surface as the tenant-facing POST /v1/dids EXCEPT origin/status: origin is pinned to house by the context and a fresh assignment is always active. The tenant-facing route can only ever create origin: customer — rentable inventory is admin-minted ONLY. A number already active in ANY sede is a clean 409 (global partial unique index).

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
e164stringyes
npastringno
us_statestringno
attestationA · B · C · unknownno
labelsarray of stringno
notesstringno
json
{
  "e164": "+13125550600",
  "us_state": "IL",
  "attestation": "A",
  "labels": [
    "house-pool"
  ]
}

Responses

201

Assigned house DID (origin: house, status: active).

FieldTypeRequiredDescription
didobject (DID)yes
did.idstring (uuid)no
did.tenant_idstring (uuid)no
did.e164stringnoImmutable after creation.
did.npastring | nullno
did.us_statestring | nullno
did.attestationA · B · C · unknownnoSTIR/SHAKEN level the carrier signs.
did.statusactive · quarantine · retiredno
did.originhouse · customernohouse rows are admin-minted platform inventory; the tenant surface only creates customer.
did.labelsarray of stringno
did.notesstring | nullno
did.created_atstring (date-time)no
did.updated_atstring (date-time)no

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

The tenant's plan anchor

http
GET /v1/admin/tenants/{id}/plan

status is the field that decides whether the tenant may place traffic: anything other than active is dark. The paid-period fields stay null until a payment lands, so "opened but not paid" is distinguishable from "paid" at a glance. 404 no_plan when the plan has never been opened.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The plan anchor row.

FieldTypeRequiredDescription
planobject (TenantPlan)yesThe plan anchor (ADR #42). status is what PlanGate reads: only active lets the tenant place traffic.
plan.schemesubscription · prepaid · postpaidno
plan.statusdraft · pending_approval · pending_deposit · pending_payment · active · suspendedno
plan.tier_row_idstring (uuid) | nullnoThe SELLABLE tier version this plan committed to (subscription only).
plan.current_period_startstring (date) | nullno
plan.current_period_endstring (date) | nullno
plan.period_price_usdstring | nullnoDecimal STRING (money is never a JSON float).
plan.auto_renewbooleanno
plan.enforcement_modehard_block · graduatednoWhat a spend-threshold crossing does: graduated notifies the tenant, hard_block only records it. Neither posture changes WHETHER the tenant is blocked at 100% — that is unconditional.
plan.alert_thresholdsarray of integerno% points of the plan limit at which the tenant is warned.
plan.alert_emailstring (email) | nullnoWhere spend alerts are e-mailed. null ⇒ no e-mail destination (the emitter falls back to log-only), which is the state of every plan opened before this column existed.
plan.alert_webhook_urlstring (uri) | nullnoPOST target for graduated-mode notifications. null ⇒ log-only. Admin surface only — it can carry a secret path token.

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Open the tenant's plan and choose its billing scheme

http
POST /v1/admin/tenants/{id}/plan

Creates the plan anchor if absent (idempotent draft) and commits the billing scheme. subscription and prepaid land on pending_payment; postpaid opens an operator approval and lands on pending_approval. The plan is NOT active yet — POST /v1/admin/tenants/{id}/payments activates a pending_payment plan.

tier_key is required for subscription and must name a SELLABLE, priced tier; a retired or unpriced key is refused 422 rather than silently selecting a different one.

Admin-only, like every plan-bearing surface: a tenant must never choose its own billing posture.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
schemesubscription · prepaid · postpaidyes
tier_keystringnoRequired for subscription; ignored otherwise.
json
{
  "scheme": "subscription",
  "tier_key": "scale"
}

Responses

201

Plan opened and scheme committed.

FieldTypeRequiredDescription
planobject (TenantPlan)yesThe plan anchor (ADR #42). status is what PlanGate reads: only active lets the tenant place traffic.
plan.schemesubscription · prepaid · postpaidno
plan.statusdraft · pending_approval · pending_deposit · pending_payment · active · suspendedno
plan.tier_row_idstring (uuid) | nullnoThe SELLABLE tier version this plan committed to (subscription only).
plan.current_period_startstring (date) | nullno
plan.current_period_endstring (date) | nullno
plan.period_price_usdstring | nullnoDecimal STRING (money is never a JSON float).
plan.auto_renewbooleanno
plan.enforcement_modehard_block · graduatednoWhat a spend-threshold crossing does: graduated notifies the tenant, hard_block only records it. Neither posture changes WHETHER the tenant is blocked at 100% — that is unconditional.
plan.alert_thresholdsarray of integerno% points of the plan limit at which the tenant is warned.
plan.alert_emailstring (email) | nullnoWhere spend alerts are e-mailed. null ⇒ no e-mail destination (the emitter falls back to log-only), which is the state of every plan opened before this column existed.
plan.alert_webhook_urlstring (uri) | nullnoPOST target for graduated-mode notifications. null ⇒ log-only. Admin surface only — it can carry a secret path token.

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Set the tenant's spend-alert thresholds and destinations

http
PUT /v1/admin/tenants/{id}/plan/alerts

The tenant is warned as its spend crosses thresholds (% points of the plan limit). The customer tunes WHEN it is warned, never WHETHER it is blocked — 100 is always implied by the emitter even if removed here, and blocking is decided by the enforcement posture, not by this route.

thresholds is REQUIRED. Each destination is OPTIONAL and the asymmetry is deliberate:

  • omit email / webhook_url → the stored value is PRESERVED, so retuning thresholds can never blind a tenant's alerting;
  • send it as null → the destination is CLEARED;
  • send an address / URL → it is written.

Both destinations null means log-only, which is the state of every plan opened before the alert e-mail landed. A malformed address is refused 422 and NOTHING is written — not the thresholds either.

Only an active plan can be tuned; anything else is 409. Note the request/response asymmetry: the body names the destinations email and webhook_url, while the plan echoes the stored columns alert_email and alert_webhook_url.

Admin-only, like every plan-bearing surface. There is no screen for it: the dashboard proxy denies the whole /v1/admin prefix, so this is runbook surface.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
thresholdsarray of integeryesNon-empty, strictly ascending, every element in 1..100.
emailstring (email) | nullnoOmit to preserve; null to clear.
webhook_urlstring (uri) | nullnoOmit to preserve; null to clear.
json
{
  "thresholds": [
    80,
    90,
    100
  ],
  "email": "[email protected]"
}

Responses

200

Alert config stored; the plan echoes it back.

FieldTypeRequiredDescription
planobject (TenantPlan)yesThe plan anchor (ADR #42). status is what PlanGate reads: only active lets the tenant place traffic.
plan.schemesubscription · prepaid · postpaidno
plan.statusdraft · pending_approval · pending_deposit · pending_payment · active · suspendedno
plan.tier_row_idstring (uuid) | nullnoThe SELLABLE tier version this plan committed to (subscription only).
plan.current_period_startstring (date) | nullno
plan.current_period_endstring (date) | nullno
plan.period_price_usdstring | nullnoDecimal STRING (money is never a JSON float).
plan.auto_renewbooleanno
plan.enforcement_modehard_block · graduatednoWhat a spend-threshold crossing does: graduated notifies the tenant, hard_block only records it. Neither posture changes WHETHER the tenant is blocked at 100% — that is unconditional.
plan.alert_thresholdsarray of integerno% points of the plan limit at which the tenant is warned.
plan.alert_emailstring (email) | nullnoWhere spend alerts are e-mailed. null ⇒ no e-mail destination (the emitter falls back to log-only), which is the state of every plan opened before this column existed.
plan.alert_webhook_urlstring (uri) | nullnoPOST target for graduated-mode notifications. null ⇒ log-only. Admin surface only — it can carry a secret path token.

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Set the plan's enforcement posture

http
PUT /v1/admin/tenants/{id}/plan/enforcement-mode

Chooses what happens when the tenant's spend crosses one of its alert thresholds:

  • graduated → the crossing NOTIFIES the tenant (e-mail / webhook, per PUT .../plan/alerts);
  • hard_block → the crossing is recorded and nothing is sent.

The threshold history is written in BOTH postures; only the notification differs. What this route does NOT do is as important as what it does: it never moves a limit, never retunes a threshold and never softens the 100% verdict — spend enforcement blocks at the limit in both postures. There is no "off".

mode is REQUIRED and the vocabulary is closed: anything else — an absent key, a wrong word, a non-string — is refused 422 and the plan is left untouched. Only an active plan has a posture to set; anything else is 409, and a tenant whose plan was never opened is 404 no_plan.

Admin-only, like every plan-bearing surface. There is no screen for it: the dashboard proxy denies the whole /v1/admin prefix, so this is runbook surface.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
modehard_block · graduatedyes
json
{
  "mode": "graduated"
}

Responses

200

Posture stored; the plan echoes it back.

FieldTypeRequiredDescription
planobject (TenantPlan)yesThe plan anchor (ADR #42). status is what PlanGate reads: only active lets the tenant place traffic.
plan.schemesubscription · prepaid · postpaidno
plan.statusdraft · pending_approval · pending_deposit · pending_payment · active · suspendedno
plan.tier_row_idstring (uuid) | nullnoThe SELLABLE tier version this plan committed to (subscription only).
plan.current_period_startstring (date) | nullno
plan.current_period_endstring (date) | nullno
plan.period_price_usdstring | nullnoDecimal STRING (money is never a JSON float).
plan.auto_renewbooleanno
plan.enforcement_modehard_block · graduatednoWhat a spend-threshold crossing does: graduated notifies the tenant, hard_block only records it. Neither posture changes WHETHER the tenant is blocked at 100% — that is unconditional.
plan.alert_thresholdsarray of integerno% points of the plan limit at which the tenant is warned.
plan.alert_emailstring (email) | nullnoWhere spend alerts are e-mailed. null ⇒ no e-mail destination (the emitter falls back to log-only), which is the state of every plan opened before this column existed.
plan.alert_webhook_urlstring (uri) | nullnoPOST target for graduated-mode notifications. null ⇒ log-only. Admin surface only — it can carry a secret path token.

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Record a MANUAL payment (wire/cash/check) for a tenant

http
POST /v1/admin/tenants/{id}/payments

An operator records an off-platform payment the tenant made; the durable payments audit fact lands (exactly-once on payment_ref) and, if the plan accepts it, the plan activates / the wallet is credited (Dialer.Payments.confirm_manual/3). Idempotent on payment_ref — a replay neither double-records nor double-credits. amount_usd MUST be a decimal STRING (a JSON number is refused 400). A refused edge (below the plan minimum; a renewal on an already-active subscription) still records the audit fact and returns 422 — never a wrong money movement.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
payment_refstringyesOUR-side idempotency key (the wire/SPEI/check reference).
amount_usdstringyesDecimal STRING (never a JSON number).
kindsubscription_period · prepaid_topup · postpaid_deposityes
currencystringno
json
{
  "payment_ref": "wire-2026-07-07-001",
  "amount_usd": "50.00",
  "kind": "prepaid_topup"
}

Responses

201

Payment recorded and applied.

FieldTypeRequiredDescription
paymentobjectyes
payment.statusstringno
payment.plan_statusstringno

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Effective governed config for a tenant

http
GET /v1/admin/tenants/{id}/config

For every key declared in the governed catalog: the value its CONSUMER actually sees, with its provenance (source), WHERE it is read (consumer), whether a per-tenant override applies at all (override_honoured), and any stored override that is NOT being applied (ignored_override).

Not every declared key resolves through the override table. A key whose consumer still reads application config reports the application-config value, and a row stored against it is reported as ignored_override rather than as the effective value — so a knob can never read back as armed while the code acting on it sees something else. For a key that IS resolved here, precedence is catalog default <- segment <- tenant.

The catalog default is also the FAIL-SAFE value: if the config table cannot be read, resolution falls back to it rather than raising or enabling anything.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yes

Responses

200

The effective catalog for this tenant.

FieldTypeRequiredDescription
configarray of ConfigEntryyes
config[].keystringyes
config[].valueboolean | integer | string | nullyesThe value the code acting on this key actually sees. For a key whose consumer is resolver that is the resolved override chain; for a key read from application config it is the application-config value, reported RAW — a misconfigured deployment shows up here rather than being sanitised into a value nothing acts on. NEVER a stored override that the consumer does not read.
config[].sourcedefault · override · app_configyesdefault = the compile-time platform default; override = a stored per-tenant value that IS honoured; app_config = an explicitly set application-config value on the deployment.
config[].typeboolean · integeryes
config[].defaultboolean | integeryesThe platform default, which is also the fail-safe value.
config[].sensitivitycompliance_loosening · money · security · operationalyesWho may write this override. Everything except operational is admin-only forever: a tenant must never grant itself a laxer compliance posture or a higher spend ceiling.
config[].consumerstringyesWHERE this key is really read. resolver = through the governed catalog, so a per-tenant override is honoured. app_config:<key> = the consumer reads that application-config key directly, so an override would be inert — and <key> is the exact name to set on the deployment instead.
config[].override_honouredbooleanyesWhether writing a per-tenant override for this key would change anything. false means PUT on this key is refused with 409.
config[].ignored_overrideobject,nullnoA row that EXISTS in the override table and is NOT reflected in value — residue to clean up with DELETE. Null when there is none.
config[].ignored_override.valueboolean | integer | string | nullyesThe stored value that is not being applied.
config[].ignored_override.reasonconsumer_not_migrated · invalid_valueyesconsumer_not_migrated — the consumer reads application config, so nothing reads this row. invalid_value — the stored value does not match the declared type, so resolution falls back to the default.
config[].docstringno

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Set a per-tenant override for a governed key

http
PUT /v1/admin/tenants/{id}/config/{key}

Stores a tenant override for a DECLARED catalog key and records the change in the append-only audit trail (the write and its audit row commit together: if the trail cannot be written, the override does not happen).

The key must exist in the catalog (422 unknown_config_key), the value must match its declared type (422 invalid_config_value), and the key's consumer must actually READ the override table (409 config_override_not_honoured).

That last one is the point: storing an override nobody reads is not a harmless no-op — it made this endpoint report a money brake as armed while the code spending the money still saw it off. A write that cannot change behaviour is refused rather than accepted with a caveat. Check override_honoured on the GET before writing; when it is false, set the application-config key named in consumer on the deployment instead.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yes
keypathstringyesA key declared in the governed catalog.

Request body (JSON, required)

FieldTypeRequiredDescription
valueboolean | integeryesMust match the key's declared type (boolean or integer).
json
{
  "value": true
}

Responses

200

Override stored; the whole effective catalog is returned.

FieldTypeRequiredDescription
configarray of ConfigEntryyes
config[].keystringyes
config[].valueboolean | integer | string | nullyesThe value the code acting on this key actually sees. For a key whose consumer is resolver that is the resolved override chain; for a key read from application config it is the application-config value, reported RAW — a misconfigured deployment shows up here rather than being sanitised into a value nothing acts on. NEVER a stored override that the consumer does not read.
config[].sourcedefault · override · app_configyesdefault = the compile-time platform default; override = a stored per-tenant value that IS honoured; app_config = an explicitly set application-config value on the deployment.
config[].typeboolean · integeryes
config[].defaultboolean | integeryesThe platform default, which is also the fail-safe value.
config[].sensitivitycompliance_loosening · money · security · operationalyesWho may write this override. Everything except operational is admin-only forever: a tenant must never grant itself a laxer compliance posture or a higher spend ceiling.
config[].consumerstringyesWHERE this key is really read. resolver = through the governed catalog, so a per-tenant override is honoured. app_config:<key> = the consumer reads that application-config key directly, so an override would be inert — and <key> is the exact name to set on the deployment instead.
config[].override_honouredbooleanyesWhether writing a per-tenant override for this key would change anything. false means PUT on this key is refused with 409.
config[].ignored_overrideobject,nullnoA row that EXISTS in the override table and is NOT reflected in value — residue to clean up with DELETE. Null when there is none.
config[].ignored_override.valueboolean | integer | string | nullyesThe stored value that is not being applied.
config[].ignored_override.reasonconsumer_not_migrated · invalid_valueyesconsumer_not_migrated — the consumer reads application config, so nothing reads this row. invalid_value — the stored value does not match the declared type, so resolution falls back to the default.
config[].docstringno

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

409

config_override_not_honoured — the key is declared and the value is well typed, but this key's consumer reads application config, so the override would be inert and is refused. Distinct from the 422s on purpose: nothing is wrong with the request, the target's current state cannot accept it. Migrating the consumer makes the identical request succeed.

json
{
  "error": {
    "code": "config_override_not_honoured",
    "message": "this key's consumer reads application config (:spend_guard_enforce), not the per-tenant override table, so the override would be inert and is refused. Set it through the deployment's application config instead."
  }
}

422

unknown_config_key — the key is not declared in the catalog, or invalid_config_value — the value does not match its declared type.

json
{
  "error": {
    "code": "unknown_config_key",
    "message": "no such key in the governed catalog"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Drop a per-tenant override (back to the catalog default)

http
DELETE /v1/admin/tenants/{id}/config/{key}

Deactivates the override so the key resolves to its catalog default. The row is DEACTIVATED, never deleted (DELETE is revoked for the app role): the record that this tenant once carried an override is itself evidence. Idempotent — dropping an absent override is a 200.

Unlike PUT, this is allowed even when override_honoured is false: it is the only way to clear a row that is reported as ignored_override, and gating removal on the same condition that makes a row inert would strand exactly the rows that need clearing.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yes
keypathstringyes

Responses

200

Override dropped (or already absent); effective catalog returned.

FieldTypeRequiredDescription
configarray of ConfigEntryyes
config[].keystringyes
config[].valueboolean | integer | string | nullyesThe value the code acting on this key actually sees. For a key whose consumer is resolver that is the resolved override chain; for a key read from application config it is the application-config value, reported RAW — a misconfigured deployment shows up here rather than being sanitised into a value nothing acts on. NEVER a stored override that the consumer does not read.
config[].sourcedefault · override · app_configyesdefault = the compile-time platform default; override = a stored per-tenant value that IS honoured; app_config = an explicitly set application-config value on the deployment.
config[].typeboolean · integeryes
config[].defaultboolean | integeryesThe platform default, which is also the fail-safe value.
config[].sensitivitycompliance_loosening · money · security · operationalyesWho may write this override. Everything except operational is admin-only forever: a tenant must never grant itself a laxer compliance posture or a higher spend ceiling.
config[].consumerstringyesWHERE this key is really read. resolver = through the governed catalog, so a per-tenant override is honoured. app_config:<key> = the consumer reads that application-config key directly, so an override would be inert — and <key> is the exact name to set on the deployment instead.
config[].override_honouredbooleanyesWhether writing a per-tenant override for this key would change anything. false means PUT on this key is refused with 409.
config[].ignored_overrideobject,nullnoA row that EXISTS in the override table and is NOT reflected in value — residue to clean up with DELETE. Null when there is none.
config[].ignored_override.valueboolean | integer | string | nullyesThe stored value that is not being applied.
config[].ignored_override.reasonconsumer_not_migrated · invalid_valueyesconsumer_not_migrated — the consumer reads application config, so nothing reads this row. invalid_value — the stored value does not match the declared type, so resolution falls back to the default.
config[].docstringno

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

422

unknown_config_key — the key is not declared in the catalog.

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Who changed which governed knob, from what to what

http
GET /v1/admin/tenants/{id}/config-audit

The append-only trail of config changes for this tenant, newest first. Append-only at the DATABASE level (a trigger rejects UPDATE/DELETE and the grants revoke them), so the process that writes the trail cannot rewrite it.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yes

Responses

200

The change trail.

FieldTypeRequiredDescription
auditarray of ConfigAuditEntryyes
audit[].keystringyes
audit[].old_valueboolean | integer | nullno
audit[].new_valueboolean | integer | nullno
audit[].activebooleanyesfalse = this change DEACTIVATED the override (back to the default).
audit[].actorstring | nullno
audit[].changed_atstring (date-time)yes

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

The tenant's design-partner discount history, plus the grant in force

http
GET /v1/admin/tenants/{id}/design-partner-discounts

Every grant ever appended for this tenant, newest first, plus the one in force today (null when there is none).

The superseded rows ARE the audit trail — a month that already closed was billed against the grant in force then — so the history is served whole rather than filtered down to what applies now. in_force is DERIVED on every read and never stored, so it cannot drift from it.

Admin-only, like every surface that decides an invoice.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The grants.

FieldTypeRequiredDescription
discountsarray of DesignPartnerDiscountyes
discounts[].idstring (uuid)yes
discounts[].tenant_idstring (uuid)yes
discounts[].percentstringyesPercentage points off the voice-AI subtotal, as a decimal string.
discounts[].effective_fromstring (date)yesThe first day the grant applies.
discounts[].effective_tostring (date)yesThe LAST day the grant applies (inclusive).
discounts[].granted_bystringyesWho granted it, and on what basis. Never machine-inferred.
discounts[].notestringno
discounts[].inserted_atstring (date-time)yes
in_forceDesignPartnerDiscount | nullyesThe grant applying today, or null.
as_ofstring (date)yesThe date in_force was resolved at.

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Grant the design-partner discount on the voice-AI add-on

http
POST /v1/admin/tenants/{id}/design-partner-discounts

APPENDS a discount grant on the voice-AI add-on (core#220). The pricing page promises design partners «20% off for 6 months»; this is the only surface that can give it to anybody.

ADMIN-ONLY, and for the reason the whole /v1/admin/tenants/{id} family exists: a discount decides the invoice as directly as the tier does, so a tenant that could write its own would be under-paying by API. PATCH /v1/tenant accepts name and nothing else, and a discount field sent there is ignored like every other billing-bearing key.

There is deliberately no PUT and no DELETE: ending a grant EARLY is a new row with percent: 0 and a later effective_from, because a month that already closed has to keep reading the grant that applied to it.

The window is closed at BOTH ends and it is REQUIRED. Send exactly one of effective_to (an explicit end, for a negotiated term) or months (the published duration counted from effective_from); neither is a 400 rather than a grant with no end, and both is a 400 rather than one of the two silently winning.

Auth: platform-admin bearer (ADMIN_API_TOKEN) — not a tenant API key.

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
percentstringyesPercentage points off the voice-AI subtotal, as a DECIMAL STRING (never a JSON float: binary floats are refused outright by the changeset, as everywhere else money is handled). 0 is legal and is how a grant is ended early; outside [0, 100] is a 400.
effective_fromstring (date)noThe day the grant starts applying. Defaults to today. A future date schedules it without retro-applying it.
effective_tostring (date)noThe LAST day the grant applies (inclusive). Mutually exclusive with months.
monthsintegernoThe grant's duration in calendar months from effective_from, the way the published offer states it (6). Mutually exclusive with effective_to. No maximum is declared because the real ceiling is not a constant: effective_from + months must land inside the range a date column holds (PostgreSQL: 4713 BC .. 5874897 AD), so how large months may be depends on the start date. A value that pushes effective_to past it is a 400 invalid_months naming the year it refused.
granted_bystringyesWho granted it and on what basis. Required: a discount moves money, so no row is unsigned and the machine never grants one.
notestringno

Responses

201

The grant as stored.

FieldTypeRequiredDescription
discountobject (DesignPartnerDiscount)yesOne append-only design-partner discount grant (core#220): from effective_from through effective_to INCLUSIVE, the tenant's voice-AI subtotal is reduced by percent points. Rows are never edited or deleted; ending a grant early appends a percent: 0 row with a later effective_from, and the superseded rows are the audit trail.
discount.idstring (uuid)yes
discount.tenant_idstring (uuid)yes
discount.percentstringyesPercentage points off the voice-AI subtotal, as a decimal string.
discount.effective_fromstring (date)yesThe first day the grant applies.
discount.effective_tostring (date)yesThe LAST day the grant applies (inclusive).
discount.granted_bystringyesWho granted it, and on what basis. Never machine-inferred.
discount.notestringno
discount.inserted_atstring (date-time)yes

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

404 — NotFound

Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Every tax-nexus declaration, plus the subset in force today

http
GET /v1/admin/tax-nexus

The jurisdictions where the company has declared tax nexus (ADR #80 decision 3). Returns the WHOLE append-only history in nexus, newest first, and the derived subset in force today in in_force.

The superseded rows are the audit trail — a month that already closed was closed against the declaration in force then, so hiding them would defeat the reason the table is append-only. in_force is DERIVED on every read, never stored, so it cannot drift from the history.

Admin-only, with no tenant-facing counterpart even for reading: nexus is a fact about the COMPANY, not about a customer, so exposing it per tenant would publish the platform's fiscal posture to every account.

An EMPTY list means "no nexus declared anywhere", which is the truth until the founder declares the first state — never "unknown".

Responses

200

The declarations.

FieldTypeRequiredDescription
nexusarray of TaxNexusStateyes
nexus[].idstring (uuid)yes
nexus[].countrystringyesISO-3166 alpha-2, stored upcased.
nexus[].regionstringyesState/province code, stored upcased.
nexus[].effective_fromstring (date)yesThe day this declaration starts applying.
nexus[].activebooleanyesfalse = nexus ENDED on effective_from.
nexus[].declared_bystringyesWho declared it, and on what basis. Never machine-inferred.
nexus[].notestringno
nexus[].inserted_atstring (date-time)yes
in_forcearray of TaxNexusStateyes
in_force[].idstring (uuid)yes
in_force[].countrystringyesISO-3166 alpha-2, stored upcased.
in_force[].regionstringyesState/province code, stored upcased.
in_force[].effective_fromstring (date)yesThe day this declaration starts applying.
in_force[].activebooleanyesfalse = nexus ENDED on effective_from.
in_force[].declared_bystringyesWho declared it, and on what basis. Never machine-inferred.
in_force[].notestringno
in_force[].inserted_atstring (date-time)yes
as_ofstring (date)yesThe date in_force was resolved at.

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Declare tax nexus in a jurisdiction

http
POST /v1/admin/tax-nexus

APPENDS a declaration (ADR #80 decision 3). There is deliberately no PUT and no DELETE: ending a nexus is a NEW row with active: false and a later effective_from, because a month that already closed has to keep reading the declaration that was in force when it closed.

The machine never infers nexus from revenue thresholds — that would be a tax engine that is wrong silently — so declared_by is required and must name the human or the ruling behind the row.

country and region are upcased before storing, so ca and CA are the same jurisdiction and collide instead of becoming two rows.

Request body (JSON, required)

FieldTypeRequiredDescription
countrystringyesISO-3166 alpha-2. Shape, not a closed list.
regionstringyesState/province code as the jurisdiction writes it.
effective_fromstring (date)noThe day the declaration starts applying. Defaults to today. A future date schedules the change without retro-applying it.
activebooleannofalse declares that nexus ENDED on effective_from.
declared_bystringyesWho declared it, and on what basis.
notestringno

Responses

201

The declaration as stored.

FieldTypeRequiredDescription
nexusobject (TaxNexusState)yesOne append-only tax-nexus declaration (ADR #80 decision 3). A row says that on effective_from the company DOES (active: true) or NO LONGER DOES (active: false) have nexus in country/region. Rows are never edited or deleted; the superseded ones are the audit trail.
nexus.idstring (uuid)yes
nexus.countrystringyesISO-3166 alpha-2, stored upcased.
nexus.regionstringyesState/province code, stored upcased.
nexus.effective_fromstring (date)yesThe day this declaration starts applying.
nexus.activebooleanyesfalse = nexus ENDED on effective_from.
nexus.declared_bystringyesWho declared it, and on what basis. Never machine-inferred.
nexus.notestringno
nexus.inserted_atstring (date-time)yes

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "error": {
    "code": "bad_request",
    "message": "from must be RFC-3339"
  }
}

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

429 — AdminAuthThrottled

Too many FAILED platform-admin authentications from this origin inside the fixed window (retryable later). code rate_limited. Keyed on the calling origin, not on the token, and evaluated BEFORE the token is compared — so a 429 never reveals whether the presented token was the right one. A request carrying the correct token is not counted, so a healthy operator cannot reach this state.

json
{
  "error": {
    "code": "rate_limited",
    "message": "too many failed admin authentications; retry later"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.