Appearance
Facturación
Generado desde
DialerDigital/corecommit0fc18008b3c8— especificaciónopenapi/dialer-v1.yamlv0.134.0. No se edita a mano; consulta Cómo se construye esta referencia. Descargar la especificación OpenAPI.
Traducción en curso
Esta referencia se genera desde la especificación OpenAPI. Las cadenas que todavía no tienen traducción aparecen en inglés.
Líneas facturables.
Endpoints
| Método | Ruta | Resumen |
|---|---|---|
GET | /v1/billing/account | Saldo, el tope vigente, el gasto frente a él y el modo de aplicación |
GET | /v1/billing/tier | El nivel vigente y el cambio de nivel pendiente, si lo hay |
POST | /v1/billing/tier/upgrade | Mejora de nivel (UPGRADE) en autoservicio: los derechos aplican ya, el precio en el próximo cierre de periodo |
GET | /v1/billing/tiers | Los niveles a la venta, con su precio de catálogo: lo que costaría una mejora ANTES de pedirla |
GET | /v1/billing/alerts | Cruces de umbral de alerta de gasto registrados en la época de facturación actual |
GET | /v1/billing/summary | Líneas facturables sobre una ventana elegida por quien llama (NO es la factura) |
GET | /v1/billing/cost | Coste bruto del libro mayor por campaña, carrier o agente en una ventana acotada (NO es la factura) |
GET | /v1/billing/ledger | El libro mayor del monedero del tenant, asiento por asiento, del más reciente al más antiguo |
GET | /v1/billing/invoice | La factura de un mes natural, con el periodo fijado por el SERVIDOR |
Saldo, el tope vigente, el gasto frente a él y el modo de aplicación
http
GET /v1/billing/accountLa lectura «¿estoy a punto de dejar de marcar?» y la contrapartida de una aplicación que es incondicional desde el ADR #39: un tenant que se queda sin saldo deja de marcar, y esta es la superficie de cara al tenant que puede avisarlo antes.
Cada número de aquí es una LECTURA de algo que ya existe, nunca una segunda fuente. El dinero sale del monedero en el que la protección de gasto netea sus retenciones: las retenciones abiertas cuentan como gastadas, así que available_usd es conservador por adelantado. cap sale de la misma aritmética desde la que dispara el escritor de alertas de gasto, y used_pct se redondea hacia abajo, de modo que este campo nunca anuncia un umbral que no se haya cruzado realmente.
enforcement_mode y alert_thresholds son de solo lectura aquí. Se escriben desde la superficie del runbook de administración (PUT /v1/admin/tenants/{id}/plan/enforcement-mode y .../plan/alerts): el cliente ajusta CUÁNDO se le avisa, nunca SI se le bloquea.
Un tenant con la facturación desactivada responde 200 con billing_enabled: false y dinero nulo, no 404, que haría indistinguible «la facturación no está activada para ti» de «tu token apunta a la nada». Además, cap es nulo siempre que el carril esté inerte: plan no efectivamente activo, sin monedero, sin asiento de fondeo o, en suscripción, sin concesión todavía.
Respuestas
200
La lectura de la cuenta. balance y cap admiten null de forma independiente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tenant_id | string (uuid) | sí | |
currency | "USD" | sí | |
billing_enabled | boolean | sí | False cuando el tenant no tiene ninguna fila de plan. |
scheme | prepaid · postpaid · subscription · null | no | |
plan_status | string,null | no | |
enforcement_mode | graduated · hard_block · null | no | graduated notifica al tenant en un cruce de umbral; hard_block registra el cruce en silencio. Ninguno decide SI el bloqueo ocurre: eso es incondicional (ADR #39). De solo lectura en esta ruta. |
alert_thresholds | arreglo de integer | sí | Puntos porcentuales en los que se avisa al tenant. Vacío cuando la facturación está desactivada. |
balance | object,null | sí | |
balance.cash_usd | string | sí | Decimal como cadena. |
balance.grant_usd | string | sí | Decimal como cadena. |
balance.deposit_usd | string | sí | Decimal como cadena. |
balance.credit_limit_usd | string | sí | Decimal como cadena. |
balance.available_usd | string | sí | Decimal como cadena. cash + grant + credit_limit − Σ(open holds): gastable ahora mismo. |
cap | object,null | sí | |
cap.scheme | prepaid · postpaid · subscription | sí | |
cap.period_key | string | sí | La época contra la que se arman los umbrales. Cambia cuando el tenant se fondea (prepago/pospago) o en un nuevo periodo de suscripción, que es lo que rearma los avisos sin borrar nada. |
cap.limit_usd | string | sí | Decimal como cadena. |
cap.spent_usd | string | sí | Decimal como cadena. |
cap.used_pct | string,null | sí | Decimal como cadena. spent / limit * 100, redondeado hacia abajo a un decimal, nunca hacia arriba, de modo que este número queda del mismo lado de cada umbral que el emisor de alertas. Null cuando el límite no es positivo: eso significa «no hay tope que usar», no «100 % usado». Se sirve en lugar de dejarlo al cliente porque el umbral de alerta es en sí mismo un porcentaje, y dos redondeos de la misma razón son la forma en que un banner acaba afirmando 80 % junto a una barra que no lo ha alcanzado. |
json
{
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"currency": "USD",
"billing_enabled": true,
"scheme": "prepaid",
"plan_status": "active",
"enforcement_mode": "graduated",
"alert_thresholds": [
80,
90,
100
],
"balance": {
"cash_usd": "412.5000",
"grant_usd": "0.0000",
"deposit_usd": "0.0000",
"credit_limit_usd": "0.0000",
"available_usd": "387.5000"
},
"cap": {
"scheme": "prepaid",
"period_key": "wallet:9f1c2f7e-77aa-4a1e-9a2b-6c0d5e4f3a21",
"limit_usd": "1000.0000",
"spent_usd": "612.5000",
"used_pct": "61.2"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}El nivel vigente y el cambio de nivel pendiente, si lo hay
http
GET /v1/billing/tierLa lectura que hace VISIBLE la brecha entre derecho y precio. Un cliente que pulsa «mejorar plan», ve las capacidades del nuevo nivel y NO ve moverse la factura concluirá que la mejora falló: este endpoint es lo que permite a la UI decir «pediste scale; empieza a facturarse el 2026-10-01; hasta entonces estás en growth a $69».
tier es lo que el tenant tiene AHORA MISMO: los derechos de producto leen el nivel vivo, así que este es el que gobierna lo que la cuenta puede hacer.
pending_change es null cuando no hay nada pendiente, y eso es una respuesta real y no un hueco: un 404 haría indistinguible «no has pedido nada» de «tu token apunta a la nada» (mismo criterio que /v1/billing/account). Se DERIVA del nivel congelado del periodo abierto y del vivo, nunca de un registro aparte que pudiera desincronizarse de cualquiera de los dos.
Ambos precios son cadenas: un numeric serializado como float JSON pierde céntimos, y esta es la cifra que el dashboard muestra junto a «lo que pagas este mes».
Respuestas
200
El nivel vigente, más el cambio pendiente o null.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tenant_id | string (uuid) | sí | |
tier | string | sí | El nivel vigente AHORA MISMO, el que leen los derechos de producto. |
pending_change | TierPendingChange | null | sí | Null cuando no hay nada pendiente. Eso es una respuesta, no un hueco. |
json
{
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"tier": "scale",
"pending_change": {
"requested_tier": "scale",
"requested_price_usd": "119.0000",
"billing_tier": "growth",
"billing_price_usd": "69.0000",
"period_start": "2026-09-01",
"period_end": "2026-09-30",
"effective_from": "2026-10-01"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Mejora de nivel (UPGRADE) en autoservicio: los derechos aplican ya, el precio en el próximo cierre de periodo
http
POST /v1/billing/tier/upgradeSube al tenant un nivel con su propio bearer. Ocurren dos cosas, y ocurren en momentos distintos a propósito:
- los derechos son inmediatos: el nivel vivo cambia, de modo que la cuenta puede usar las capacidades del nuevo nivel desde este minuto;
- el precio espera al próximo cierre de periodo: el periodo abierto ya estaba congelado en el nivel en el que EMPEZÓ, así que la factura de este mes no se mueve, ni un céntimo.
Esa división es decisión del founder (macro-tasks#158, 2026-09-07): un cambio de plan se aplica «cuando el mes del plan actual ya se ha cerrado», y el exceso dentro del mes se factura mediante las líneas de exceso que ya existen en lugar de un reprecio a mitad de mes. Por tanto no hay cargo prorrateado ni línea de factura nueva: véase el ADR #130.
SOLO mejoras. El destino debe estar en el vocabulario asignable, estar a la venta en el catálogo y costar estrictamente MÁS que el nivel vigente. Una rebaja se aplica en el próximo límite de periodo, sin reembolso, y no se sirve aquí: responde 422 not_an_upgrade, y lo mismo una llamada repetida (el tenant ya está en el destino).
La ruta NO recibe ningún identificador de tenant: opera sobre el tenant del bearer y solo sobre él, de modo que esta superficie no puede mover el nivel de otro tenant.
Quién puede llamarla (core#1098, ADR #143). La propia clave de API dd_ del tenant, o un login del dashboard cuyo usuario sea account_admin: la misma regla que GET /v1/billing/tiers. Un login member recibe 403 role_forbidden y no se escribe nada; el rol se comprueba ANTES del cuerpo, así que un member recibe ese 403 envíe lo que envíe (nunca el 400 / 422 de abajo). Un token de widget (ddw_) recibe 403 forbidden.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tier | string | sí | El nivel de destino. Debe estar a la venta y costar estrictamente más que el actual. |
Respuestas
200
El nivel cambió. pending_change lleva lo que se pidió, a qué sigue facturando el periodo abierto y la fecha en que empieza el nuevo precio.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tenant | object (Tenant) | sí | |
tenant.id | string (uuid) | no | |
tenant.name | string | no | |
tenant.account_id | string (uuid) | no | La EMPRESA a la que pertenece este tenant (sede) (DA-2). |
tenant.billing_account_id | string (uuid) | no | La cuenta de facturación bajo la que se factura la sede (DA-3). Compartida por todas las sedes de una empresa consolidated; una por sede cuando es per_site. |
tenant.status | active · suspended | no | |
tenant.synthetic | boolean | no | true para un tenant de demostración. No origina tráfico: la marcación manual, el envío de SMS, la pierna externa de una transferencia y el sandbox responden 403 synthetic_tenant. Siempre presente, nunca nulo, para que una consola pueda avisar ANTES de que la acción falle (core#2512). |
tenant.retention_months | integer | no | |
tenant.seats | integer | no | |
tenant.tier | starter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shield | no | |
tenant.voice_ai_enabled | boolean | no | |
tenant.ai_monthly_budget_usd | number | null | no | Techo de integridad de costes (lo fija el admin; null = sin tope). |
tenant.ai_max_call_seconds | integer | null | no | Techo estricto de duración por llamada de IA (segundos) fijado por el admin. null significa que se aplica el valor por defecto de la PLATAFORMA, nunca "sin techo" (core#922, ADR #115). |
tenant.ai_max_call_seconds_effective | integer | no | El techo que realmente se entrega a FreeSWITCH para las piernas de IA de este tenant: ai_max_call_seconds cuando está definido; si no, el valor por defecto de plataforma de la celda (AI_MAX_CALL_SECONDS_DEFAULT, valor de reserva compilado 900). Siempre presente, nunca nulo. Solo lectura (derivado). |
tenant.sms_monthly_budget_usd | number | null | no | Tope de gasto en SMS acumulado en el mes hasta la fecha. |
tenant.human_initiated_enabled | boolean | no | Postura de consentimiento D17 de iniciativa humana (la fija el admin; por defecto false). Cuando es true, una marcación de voz EN VIVO de vista previa/manual/CTI sin voz artificial y sin voz de IA no requiere consentimiento expreso previo (sin ATDS). Todas las demás compuertas (DNC, horas de silencio, Reg F, matriz estatal, voz artificial) siguen aplicándose sin cambios. |
tenant.max_cps | integer | null | no | Techo de CPS por tenant (lo fija el admin; null = sin techo). ADR #12. |
tenant.max_concurrent_channels | integer | null | no | Techo de llamadas concurrentes por tenant, una unidad por llamada con o sin agente (lo fija el admin; null = sin techo). ADR #12, ADR #185. |
tenant.abandon_seller_name | string | null | no | Nombre del vendedor para el mensaje de identificación de llamada abandonada de la FTC TSR (lo fija el admin; null = no configurado, el sobremarcado predictivo permanece apagado). ADR #14. |
tenant.abandon_seller_phone | string | null | no | Número de rellamada E.164 para el mensaje de identificación de llamada abandonada (lo fija el admin; null = no configurado). ADR #14. |
tenant.tax_country | string | null | no | País ISO-3166-1 alpha-2 de la jurisdicción de impuesto sobre ventas del cliente (lo fija el admin; null = no declarado). ADR #77. |
tenant.tax_region | string | null | no | Código de estado/provincia de la jurisdicción de impuesto sobre ventas del cliente (lo fija el admin; null = no declarado). ADR #77. |
tenant.tax_postal_code | string | null | no | Código postal de la jurisdicción de impuesto sobre ventas del cliente (lo fija el admin; null = no declarado). ADR #77. |
tenant.kyc_verified_at | string (date-time) | null | no | Cuándo verificó la plataforma a este cliente (lo fija el admin; null = aún no). Una cuenta de 1 asiento que no está verificada no puede crear ni iniciar una campaña que se ejecute sin agente (403 verification_required, ADR #279). |
tenant.created_at | string (date-time) | no | |
tenant.updated_at | string (date-time) | no | |
tier | string | sí | El nivel ahora vigente: el que se acaba de pedir. |
pending_change | TierPendingChange | null | sí | Null en un caso real: el periodo abierto YA estaba congelado en el destino (una ida y vuelta dentro del mismo mes), así que el mes ya se factura a lo que el tenant tiene ahora. |
json
{
"tenant": {
"id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"name": "Acme Collections",
"status": "active",
"tier": "scale"
},
"tier": "scale",
"pending_change": {
"requested_tier": "scale",
"requested_price_usd": "119.0000",
"billing_tier": "growth",
"billing_price_usd": "69.0000",
"period_start": "2026-09-01",
"period_end": "2026-09-30",
"effective_from": "2026-10-01"
}
}400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: un login del dashboard (ddu_) cuyo usuario no es account_admin; el nivel no cambia. forbidden: un token de widget (ddw_), o un tenant suspendido.
json
{
"error": {
"code": "role_forbidden",
"message": "a dashboard login needs the account_admin role to change the tier"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422
not_an_upgrade (una rebaja, un movimiento lateral al mismo precio o una llamada repetida), unknown_tier (no está en el vocabulario asignable), tier_not_sellable (está en el catálogo pero retirado / sin precio) o tier_not_in_catalog (el nivel vigente no se puede tarificar, así que no hay nada con lo que comparar: falla cerrado).
json
{
"error": {
"code": "not_an_upgrade",
"message": "tier must cost strictly more than the current one; downgrades take effect at the next period and are not served here"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Los niveles a la venta, con su precio de catálogo: lo que costaría una mejora ANTES de pedirla
http
GET /v1/billing/tiersEl catálogo del que un tenant puede comprar, leído ANTES de POST /v1/billing/tier/upgrade: cada nivel a la venta con su precio de catálogo, su moneda y sus derechos, cuál es el del tenant y cuáles son una mejora respecto a él. Existe para que la confirmación previa a una mejora muestre un precio servido por core (el mismo catálogo del que sale el precio de la factura) en lugar de uno escrito en un cliente.
Qué está a la venta. Para cada clave de nivel, la versión de catálogo MÁS NUEVA, conservada solo cuando esa versión es vendible Y tiene precio (un nivel retirado se descarta aunque existan versiones anteriores, que son inmutables), y solo cuando la clave está en el vocabulario al que se puede mover a un tenant. Ordenado por precio, ascendente. Nunca se lista un nivel que no esté a la venta.
upgrade es la propia regla de mejora, evaluada sin escribir: el nivel está a la venta, es asignable y cuesta estrictamente más que current_price_usd. Un nivel marcado upgrade: true es uno que el POST acepta; false es uno al que responde con 422. Cuando el nivel vigente no tiene entrada de catálogo, current_price_usd es null y nada es una mejora (el POST responde 422 tier_not_in_catalog).
De quién es el precio. Los precios de nivel son de toda la plataforma: no hay precio de nivel por tenant en el catálogo, así que aquí no puede aparecer el precio de otro tenant. Un descuento de design partner (ADR #111) se aplica a la línea del complemento de IA de voz, nunca al precio del nivel, de modo que el precio listado para un nivel de destino es el que este tenant empieza a pagar por él en el próximo periodo. Lo que factura el periodo ABIERTO es GET /v1/billing/tier: puede diferir del precio de lista del nivel vigente, porque el periodo abierto está congelado al precio con el que empezó.
Quién puede leerlo. La propia clave de API dd_ del tenant, o un login del dashboard cuyo usuario sea account_admin. Un login member recibe 403 role_forbidden; un token de widget (ddw_) recibe 403 forbidden. La ruta no recibe identificador de tenant.
El precio es por asiento y por mes: la unidad por la que la línea seats de la factura multiplica los asientos facturables. El dinero es una cadena para que los céntimos sobrevivan a JSON.
Respuestas
200
El nivel vigente, su precio de catálogo y los niveles a la venta.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tenant_id | string (uuid) | sí | |
tier | string | sí | El nivel vigente AHORA MISMO. Aparece en tiers solo si sigue a la venta. |
current_price_usd | string,null | sí | Precio de catálogo del nivel vigente: el precio que una mejora debe superar estrictamente. No es necesariamente lo que factura el periodo abierto (véase GET /v1/billing/tier). Null cuando el nivel vigente no tiene entrada de catálogo; entonces ningún nivel es una mejora. |
tiers | arreglo de BillingTierOffer | sí | Cada nivel a la venta al que se puede mover al tenant, del más barato al más caro. |
tiers[].tier | string | sí | La clave de nivel que nombra el cuerpo de la mejora. |
tiers[].label | string | sí | |
tiers[].version | integer | sí | La versión de catálogo a la venta (el catálogo es solo de anexado; un cambio de precio es una versión nueva). |
tiers[].monthly_price_usd | string | sí | Precio de catálogo de la versión a la venta, POR ASIENTO y por mes (la línea seats de la factura lo multiplica por los asientos facturables). Cadena, para que los céntimos sobrevivan a JSON. |
tiers[].currency | USD | sí | |
tiers[].effective_from | string (date) | sí | La fecha en que entró en vigor la versión de catálogo a la venta. |
tiers[].entitlements | object | sí | La capacidad que otorga el nivel, tal como la almacena el catálogo. En max_seats y max_concurrent_channels null significa ILIMITADO; en included_ai_minutes null significa que el complemento de IA de voz no se ofrece. |
tiers[].entitlements.max_seats | integer,null | sí | Null significa ilimitado. |
tiers[].entitlements.max_concurrent_channels | integer,null | sí | Null significa ilimitado. |
tiers[].entitlements.included_minutes | string,null | sí | |
tiers[].entitlements.included_sms_segments | integer,null | sí | |
tiers[].entitlements.included_ai_minutes | string,null | sí | Null significa que el complemento de IA de voz no se ofrece en este nivel; un número (cero incluido) significa que sí. |
tiers[].entitlements.included_dids | integer,null | sí | |
tiers[].current | boolean | sí | Este es el nivel vigente. |
tiers[].upgrade | boolean | sí | POST /v1/billing/tier/upgrade con este nivel sería aceptado: a la venta y estrictamente más caro que current_price_usd. |
pay_as_you_go | BillingPayAsYouGo | null | no | El modo de entrada de pago por uso: UN asiento y TODO el consumo cargado al saldo prepagado, sin bolsa de franquicia. Es el esquema prepaid sobre el nivel starter, así que seat_price_usd es el precio de catálogo de esa entrada por asiento y mes (leído de la fila, nunca reescrito) y seats es su mínimo de asientos. Null cuando el nivel de entrada no está a la venta. |
json
{
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"tier": "growth",
"current_price_usd": "69.0000",
"tiers": [
{
"tier": "starter",
"label": "Starter",
"version": 1,
"monthly_price_usd": "25.0000",
"currency": "USD",
"effective_from": "2026-07-07",
"entitlements": {
"max_seats": 5,
"max_concurrent_channels": null,
"included_minutes": "500",
"included_sms_segments": 200,
"included_ai_minutes": null,
"included_dids": 1
},
"current": false,
"upgrade": false
},
{
"tier": "growth",
"label": "Growth",
"version": 1,
"monthly_price_usd": "69.0000",
"currency": "USD",
"effective_from": "2026-07-07",
"entitlements": {
"max_seats": 25,
"max_concurrent_channels": null,
"included_minutes": "1500",
"included_sms_segments": 500,
"included_ai_minutes": "100",
"included_dids": 2
},
"current": true,
"upgrade": false
},
{
"tier": "scale",
"label": "Scale",
"version": 1,
"monthly_price_usd": "119.0000",
"currency": "USD",
"effective_from": "2026-07-07",
"entitlements": {
"max_seats": 100,
"max_concurrent_channels": null,
"included_minutes": "2500",
"included_sms_segments": 1500,
"included_ai_minutes": "150",
"included_dids": 3
},
"current": false,
"upgrade": true
},
{
"tier": "enterprise",
"label": "Enterprise",
"version": 1,
"monthly_price_usd": "199.0000",
"currency": "USD",
"effective_from": "2026-07-07",
"entitlements": {
"max_seats": null,
"max_concurrent_channels": null,
"included_minutes": "4000",
"included_sms_segments": 2000,
"included_ai_minutes": "300",
"included_dids": 5
},
"current": false,
"upgrade": true
}
]
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Cruces de umbral de alerta de gasto registrados en la época de facturación actual
http
GET /v1/billing/alertsEl historial detrás del porcentaje que GET /v1/billing/account reporta en vivo: qué umbrales se han disparado realmente para este tenant en la época contra la que se arman las alertas.
Cada fila se sirve tal como se registró, nunca recalculada. spent_usd, limit_usd y enforcement_mode son la instantánea EN el cruce, que es la razón por la que el libro mayor subyacente es de solo anexado: un aviso del 80 % sigue siendo un aviso del 80 % aunque el tenant haya recargado después. Rederivar esto desde el saldo de hoy respondería «cómo se vería esta alerta si se disparara ahora», una pregunta distinta, y que dejaría que esta lista contradijera la notificación que el cliente ya recibió.
period_key es la MISMA época que GET /v1/billing/account reporta en cap.period_key, de modo que las dos superficies nunca pueden describir periodos distintos. Cambia cuando el tenant se fondea o en un nuevo periodo de suscripción, lo que rearma cada umbral sin borrar nada; así que una lista vacía justo después de una recarga significa «nada ha cruzado TODAVÍA en la nueva época», no «nunca ha cruzado nada».
Un tenant sin plan activo, o cuyo carril aún no tiene época, responde 200 con period_key: null y una lista vacía, nunca 404, por la misma razón que /billing/account: un 404 haría indistinguible «no has cruzado nada» de «tu token apunta a la nada».
Respuestas
200
Los cruces de la época actual, del más reciente al más antiguo. Vacío cuando el carril está inerte o todavía no ha cruzado nada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tenant_id | string (uuid) | sí | |
period_key | string,null | sí | La época a la que pertenecen los cruces, idéntica a cap.period_key de GET /v1/billing/account. Null cuando el carril no tiene época (sin plan activo, sin monedero, sin asiento de fondeo o (suscripción) sin concesión todavía), en cuyo caso alerts está vacío. |
alerts | arreglo de object | sí | |
alerts[].threshold_pct | integer | sí | El porcentaje que se disparó, tal como estaba configurado en los alert_thresholds del plan cuando se armó la época. |
alerts[].scheme | subscription · prepaid · postpaid | sí | |
alerts[].enforcement_mode | graduated · hard_block | sí | El modo vigente EN el cruce. Puede diferir del modo actual del tenant: ese es el sentido de una instantánea. |
alerts[].spent_usd | string | sí | Decimal como cadena. El gasto EN el cruce, no el de hoy. |
alerts[].limit_usd | string | sí | Decimal como cadena. El tope EN el cruce, no el de hoy. |
alerts[].crossed_at | string (date-time) | sí | Cuándo se registró el cruce (UTC, microsegundos). |
json
{
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"period_key": "wallet:9f1c2f7e-77aa-4a1e-9a2b-6c0d5e4f3a21",
"alerts": [
{
"threshold_pct": 90,
"scheme": "prepaid",
"enforcement_mode": "graduated",
"spent_usd": "902.0000",
"limit_usd": "1000.0000",
"crossed_at": "2026-08-30T18:22:04.518423Z"
},
{
"threshold_pct": 80,
"scheme": "prepaid",
"enforcement_mode": "graduated",
"spent_usd": "812.5000",
"limit_usd": "1000.0000",
"crossed_at": "2026-08-29T11:04:51.201884Z"
}
]
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Líneas facturables sobre una ventana elegida por quien llama (NO es la factura)
http
GET /v1/billing/summaryEl modelo de lectura exploratorio: quien llama elige [from, to), así que esto es lo que parecen las líneas facturables en una ventana arbitraria: útil para inspeccionar, y NO es la factura. La factura es GET /v1/billing/invoice, cuyo periodo fija el SERVIDOR y el cliente no puede mover.
Asientos por nivel, exceso de DID ($25 / paquete de 10 DID) y uso medido de IA de voz (max(metered, $200) mientras el complemento está activado, $0 cuando está desactivado), tarificados desde el catálogo de planes en código (Dialer.Billing.Plan). El mínimo de facturación de asientos es POR PLAN y una línea seats live publica el que aplica como seat_minimum: 1 en starter y growth, 3 en scale, 25 en enterprise (así que un tenant enterprise con 10 asientos ocupados factura billable_seats: 25). Una línea seats ledger_snapshot (un mes cerrado) lleva seat_minimum: null, igual que occupied_seats: null: el cierre sella la cantidad, no el mínimo que aplicó, y los meses cerrados antes de que el mínimo pasara a ser por plan aplicaron 3 en todos los planes. Valores por defecto: los últimos 30 días UTC. La ventana [from, to) es semiabierta (to EXCLUSIVO); las líneas del libro diario (minutos house/entrantes, renta de DID, SMS) derivan su rango de días UTC cerrados de ella, así que un to en la medianoche UTC exacta es un corte de facturación y ese día pertenece solo a la vista del periodo SIGUIENTE. Por tanto, una ventana intradía suma DÍAS UTC ENTEROS en esas líneas, y la respuesta lo indica: period.day_ledger lleva el rango de días cerrados que sumaron junto a los from/to que se pidieron (core#1383; el CSV lo lleva como filas de metadatos day_ledger_from_day/day_ledger_to_day/ day_ledger_kinds). ?format=csv / Accept: text/csv para el artefacto de facturación (con X-Artifact-SHA256).
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
from | query | string (date-time) | no | Límite inferior RFC 3339 (campo específico del endpoint; por defecto los últimos 30 días UTC donde se indica). |
to | query | string (date-time) | no | Límite superior RFC 3339. |
format | query | csv | no | csv para el artefacto descargable (equivalente: Accept: text/csv). |
Respuestas
200
Las líneas facturables (JSON) o el artefacto CSV.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tenant_id | string (uuid) | no | |
tier | starter · growth · scale · enterprise · dialer_core · compliance_pro · audit_shield | no | |
tier_label | string | no | |
voice_ai_enabled | boolean | no | |
currency | "USD" | no | |
period | object | no | |
period.from | string (date-time) | no | El from que se pidió (inclusivo). |
period.to | string (date-time) | no | El to que se pidió (EXCLUSIVO). |
period.day_ledger | object | no | Lo que las líneas del libro diario sumaron realmente (core#1383). Esas líneas leen billable_events, que está indexado por día UTC, así que una ventana que no empieza y termina en medianoche UTC suma sus días ENTEROS: from=16:49Z&to=17:00Z lee ese día completo. Este es el rango cerrado que leen, derivado de [from, to) (un to en la medianoche UTC exacta es un corte, así que su día no está incluido). Se aplica solo a kinds: voice_ai mide exactamente [from, to), y las líneas mensuales se resuelven a su final. |
period.day_ledger.from_day | string (date) | sí | Primer día UTC sumado (inclusivo). |
period.day_ledger.to_day | string (date) | sí | Último día UTC sumado (inclusivo). |
period.day_ledger.kinds | arreglo de house_minutes · inbound_minutes · did_rent · sms | sí | Los tipos de línea leídos sobre este rango de días. |
line_items | arreglo de object | no | |
line_items[].kind | seats · voice_ai · house_minutes · inbound_minutes · did_rent · did_overage · did_overage_customer · sms · wav_surcharge | sí | |
line_items[].cadence | monthly · metered | sí | |
line_items[].amount_usd | number | sí | |
line_items[].settled_from_balance_usd | number | no | Presente solo en las cuatro líneas MEDIDAS (house_minutes, inbound_minutes, sms, voice_ai): lo que se ha cargado al saldo del tenant por el medidor de esta línea durante sus días (core#2199, ADR #276). Es el hecho del libro mayor hasta ahora, no un segundo precio: amount_usd es la venta y no cambia. La factura congelada lo lleva a amount_usd en el cierre. |
line_items[].fiscal_bucket | saas · telecom · pass_through · tax | no | Cómo se clasifica la línea fiscalmente (ADR #73): saas son ingresos de software, telecom son ingresos de telecomunicaciones, pass_through es un recargo regulatorio que la empresa remite por su propia obligación, y tax es dinero cobrado por cuenta de una jurisdicción. Publicado desde MT-D de macro-tasks#448, cuando el founder confirmó el planteamiento; un documento congelado antes de eso puede no llevar clasificación, y no se reclasifica. |
line_items[].allowance_pool | minutes · sms_segments · ai_minutes | no | Presente solo en las cuatro líneas MEDIDAS (house_minutes, inbound_minutes, sms, voice_ai). Nombra la COLUMNA del catálogo plan_tiers de la que salió la franquicia de esta línea, que es como un lector puede saber que house_minutes e inbound_minutes COMPARTEN un único included_minutes entre ellas en lugar de llevar dos (core#219, ADR #110). |
line_items[].included_allowance | number | no | Unidades de este medidor que el nivel ya incluye para el periodo, tal como se aplican a ESTA línea: para las dos líneas de minutos es la parte del included_minutes agrupado proporcional a lo que consumió cada una, y las dos partes suman la columna. 0 significa que el nivel no incluye nada en este eje, que es como se lee una columna NULL del catálogo (nunca ilimitado), y también a lo que se resuelve todo esquema de plan que no sea subscription, ya que prepaid y postpaid nunca compraron una versión de nivel. Con 0 la línea se cobra exactamente como antes de que existiera este campo. |
line_items[].consumed_quantity | number | no | Lo que midió el medidor durante el periodo, antes de la franquicia. La misma cifra que lleva el campo de medidor de la propia línea (billable_minutes / segments). |
line_items[].overage_quantity | number | no | Las unidades FACTURABLES: max(consumed_quantity - included_allowance, 0). Nunca negativas: un medidor no puede producir un crédito. |
line_items[].gross_amount_usd | number | no | La suma INTACTA del LIBRO MAYOR de esta línea, antes de aplicar la franquicia. En las tres líneas de libro mayor (house_minutes, inbound_minutes, sms) amount_usd es la misma proporción de ella que overage_quantity es de consumed_quantity, así que los dos lado a lado son la auditoría de la deducción, y la evidencia de que ninguna fila de billable_events se retarificó. En voice_ai esa proporción es un MÍNIMO, no una igualdad: mientras el complemento está activado, amount_usd es max(deducted, monthly_minimum_usd) y por tanto puede superar gross_amount_usd. |
total_usd | number | no | |
balance_settled_usd | number | no | La suma de los settled_from_balance_usd de las líneas (core#2199, ADR #276). |
amount_due_usd | number | no | total_usd - balance_settled_usd, impuestos incluidos. total_usd no cambia: la factura sigue siendo la venta. |
json
{
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"tier": "scale",
"tier_label": "Scale",
"voice_ai_enabled": true,
"currency": "USD",
"period": {
"from": "2026-06-05T00:00:00Z",
"to": "2026-07-05T00:00:00Z",
"day_ledger": {
"from_day": "2026-06-05",
"to_day": "2026-07-04",
"kinds": [
"house_minutes",
"inbound_minutes",
"did_rent",
"sms"
]
}
},
"line_items": [
{
"kind": "seats",
"cadence": "monthly",
"source": "live",
"occupied_seats": 5,
"billable_seats": 5,
"seat_minimum": 3,
"unit_price_usd": 119,
"amount_usd": 595
},
{
"kind": "did_overage",
"cadence": "monthly",
"source": "live",
"monitored_dids": 8,
"included_dids": 3,
"overage_dids": 1,
"pack_size": 10,
"pack_price_usd": 25,
"amount_usd": 25
},
{
"kind": "did_overage_customer",
"cadence": "monthly",
"source": "live",
"monitored_dids": 8,
"included_dids": 3,
"overage_dids": 4,
"pack_size": 10,
"pack_price_usd": 25,
"amount_usd": 0
},
{
"kind": "wav_surcharge",
"cadence": "monthly",
"source": "live",
"recording_format": "wav",
"billable_seats": 5,
"unit_price_usd": 5,
"amount_usd": 25
},
{
"kind": "voice_ai",
"cadence": "metered",
"enabled": true,
"billable_minutes": 900,
"minute_rate_usd": 0.25,
"metered_usd": 225,
"monthly_minimum_usd": 200,
"minimum_applied": false,
"amount_usd": 225
}
],
"total_usd": 870
}400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422 — Unprocessable
La forma es correcta, la semántica no (regla de dominio incumplida).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Coste bruto del libro mayor por campaña, carrier o agente en una ventana acotada (NO es la factura)
http
GET /v1/billing/cost«Qué campaña / carrier / agente está gastando ahora mismo»: el LIBRO MAYOR de uso (billable_events) sumado sobre [from, to) y agrupado por UNA dimensión por petición (ADR #218, core#1647). Los días ya cerrados se leen del acumulado diario (ADR #232, core#1751); solo los dos días UTC más recientes (hoy y ayer) se suman directamente del libro mayor unido al CDR. sources indica cuántos días de la ventana vinieron de cada lugar.
Detrás de un flag. La ruta solo responde para tenants con billing_cost_enabled ACTIVADO, que está DESACTIVADO por defecto: con él DESACTIVADO todo llamante, de cualquier rol, recibe el mismo 404 not_found, como si la ruta no existiera. No se borra ni se oculta nada cuando está DESACTIVADO.
Es la suma bruta del libro mayor, no la factura. La franquicia incluida, el nivel por volumen, el descuento de design partner y el mínimo mensual se aplican en la factura y NO se reparten entre los grupos; el bloque reconciliation pone gross_total junto a las líneas de GET /v1/billing/summary sobre la misma ventana y lista lo que no está asignado (not_allocated). Quien necesite «lo que se facturó» lee la factura.
No se descarta nada. Cada fila del libro mayor cae en exactamente un grupo: la campaña / carrier / agente resuelto, o el cubo con key: null y unattributed: true (sin campaña, un CDR sin carrier, un agente no asignado, una llamada sin fila de CDR, un SMS). Los grupos de cada moneda suman gross_total; si alguna vez no lo hicieran, la respuesta es 500 cost_unreconciled, nunca un número parcial. Las monedas nunca se mezclan ni se convierten: cada grupo y cada total lleva la suya. billed_by indica quién factura los MINUTOS de telecomunicaciones de esas llamadas: platform (troncal house), tenant_carrier (el propio carrier del cliente; el importe del libro mayor se mantiene entero) o null.
Ventana. from y to son instantes RFC 3339 OBLIGATORIOS, from < to, con un máximo de 31 días de separación (400 en caso contrario), semiabierta como /v1/billing/summary: un to en la medianoche UTC exacta pertenece a la siguiente ventana. El libro mayor está indexado por día UTC, así que una ventana intradía suma sus DÍAS UTC ENTEROS; ledger_days indica cuáles.
Quién. Una clave de API dd_ o un login del dashboard con el rol account_admin. Cualquier otro recibe el mismo 403 role_forbidden envíe lo que envíe y tenga lo que tenga el tenant: una denegación nunca dice si hay algún coste que leer. El aislamiento entre tenants es el de la base de datos (RLS): las filas de otro tenant nunca se leen.
Cuando no puede responder. Si parte de la ventana no está cerrada en el acumulado ni es uno de los dos días más recientes, o el acumulado de un día cerrado no cuadra con el libro mayor, la respuesta es 503 unavailable: nunca se publica un número parcial. Reintenta más tarde.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
group_by | query | campaign · carrier · agent | sí | La UNA dimensión por la que agrupar. |
from | query | string (date-time) | sí | Cota inferior RFC 3339, inclusiva. |
to | query | string (date-time) | sí | Cota superior RFC 3339, EXCLUSIVA; como máximo 31 días después de from. |
Respuestas
200
Los grupos, el total bruto y el bloque de conciliación.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
group_by | campaign · carrier · agent | sí | |
from | string (date-time) | sí | |
to | string (date-time) | sí | |
ledger_days | object | sí | El rango de días UTC cerrados sobre el que se leyó el libro mayor. |
ledger_days.from | string (date) | sí | |
ledger_days.to | string (date) | sí | |
sources | object | sí | Cuántos días UTC de ledger_days se leyeron del acumulado diario (días cerrados), se sumaron directamente del libro mayor (los dos más recientes) o estaban vacíos (sin eventos). |
sources.rollup | integer | sí | |
sources.direct | integer | sí | |
sources.empty | integer | sí | |
groups | arreglo de BillingCostGroup | sí | |
groups[].key | string,null | sí | El id de la campaña / carrier / agente; null en el cubo sin atribuir. |
groups[].unattributed | boolean | sí | |
groups[].currency | string | sí | |
groups[].billed_by | platform · tenant_carrier · null | sí | Quién factura los minutos de telecomunicaciones de esas llamadas. |
groups[].amount | string | sí | |
groups[].events | integer | sí | |
gross_total | arreglo de BillingCostTotal | sí | La suma del libro mayor por moneda. |
gross_total[].currency | string | sí | |
gross_total[].amount | string | sí | |
gross_total[].events | integer | sí | |
reconciliation | object | sí | |
reconciliation.basis | ledger_gross | sí | |
reconciliation.groups_sum | arreglo de BillingCostTotal | sí | La suma de groups por moneda; igual a gross_total en cada 200. |
reconciliation.groups_sum[].currency | string | sí | |
reconciliation.groups_sum[].amount | string | sí | |
reconciliation.groups_sum[].events | integer | sí | |
reconciliation.invoice | object | sí | Las líneas de GET /v1/billing/summary sobre la misma ventana, o status: unavailable con el motivo cuando la ventana no se puede tarificar (el coste en sí no depende de ello). |
reconciliation.invoice.status | available · unavailable | sí | |
reconciliation.invoice.reason | string | no | Solo cuando es unavailable. |
reconciliation.invoice.currency | string | no | |
reconciliation.invoice.total_usd | string | no | |
reconciliation.invoice.lines | arreglo de object | no | |
reconciliation.invoice.lines[].kind | string | sí | |
reconciliation.invoice.lines[].amount_usd | string | sí | |
reconciliation.invoice.lines[].gross_amount_usd | string,null | sí | La suma intacta del libro mayor, en las líneas que llevan franquicia; null en las demás. |
reconciliation.not_allocated | arreglo de string | sí | Lo que la factura aplica sobre el bruto y que este recurso NO reparte entre los grupos. |
reconciliation.note | string | sí |
json
{
"group_by": "campaign",
"from": "2026-10-01T00:00:00Z",
"to": "2026-10-08T00:00:00Z",
"ledger_days": {
"from": "2026-10-01",
"to": "2026-10-07"
},
"sources": {
"rollup": 5,
"direct": 2,
"empty": 0
},
"groups": [
{
"key": "6f1d2c3a-5b7e-4f0a-9c1d-2e3f4a5b6c7d",
"unattributed": false,
"currency": "USD",
"billed_by": "platform",
"amount": "41.2500",
"events": 310
},
{
"key": null,
"unattributed": true,
"currency": "USD",
"billed_by": null,
"amount": "8.7500",
"events": 52
}
],
"gross_total": [
{
"currency": "USD",
"amount": "50.0000",
"events": 362
}
],
"reconciliation": {
"basis": "ledger_gross",
"groups_sum": [
{
"currency": "USD",
"amount": "50.0000",
"events": 362
}
],
"invoice": {
"status": "available",
"currency": "USD",
"total_usd": "870.0",
"lines": [
{
"kind": "seats",
"amount_usd": "595",
"gross_amount_usd": null
},
{
"kind": "house_minutes",
"amount_usd": "0",
"gross_amount_usd": "41.2500"
}
]
},
"not_allocated": [
"included_allowance",
"volume_tier",
"discount",
"monthly_minimum"
],
"note": "gross_total is the sum of the usage ledger, not the amount billed; the included allowance, volume tier, discount and monthly minimum are applied on the invoice and are not split across groups"
}
}400
group_by ausente o distinto de campaign, carrier, agent; from o to ausentes o que no son RFC 3339; from no anterior a to; o una ventana de más de 31 días.
json
{
"error": {
"code": "bad_request",
"message": "date range must be <= 31 days"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: un login del dashboard sin el rol account_admin. El mismo cuerpo para toda petición de ese login, así que no revela nada sobre el coste del tenant. Un widget u otro ámbito que no sea de tenant recibe forbidden.
json
{
"error": {
"code": "role_forbidden",
"message": "a dashboard login needs the account_admin role to read billing cost"
}
}404
not_found: el tenant tiene billing_cost_enabled DESACTIVADO (el valor por defecto). Igual para todo rol y todo parámetro: el flag responde antes que cualquier otra cosa.
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}500
cost_unreconciled: los grupos no sumaron el total del libro mayor; no se publica nada.
503
unavailable: parte de la ventana no está cerrada en el acumulado (y no es uno de los dos días más recientes) o un día cerrado no concilia; no se publica nada.
json
{
"error": {
"code": "unavailable",
"message": "the cost of part of this window is not closed yet; nothing is published, retry later"
}
}El libro mayor del monedero del tenant, asiento por asiento, del más reciente al más antiguo
http
GET /v1/billing/ledger«Por qué bajó mi saldo»: las filas del libro mayor del monedero del tenant (balance_entries), de la más reciente a la más antigua y paginadas por keyset (core#2573). Cada movimiento indica su entry_type, los deltas con signo por cubo, el día UTC al que pertenece (occurred_on) y las dos claves que lo unen a su origen: ref_uuid (el id del evento facturable para un debit, la clave de idempotencia del proveedor de pago para un topup, YYYY-MM para una concesión de periodo) y billable_event_id. La fila NO lleva texto libre ni datos personales.
Detrás de un flag. La ruta solo responde para tenants con billing_ledger_enabled ACTIVADO, que está DESACTIVADO por defecto: con él DESACTIVADO todo llamante, de cualquier rol, recibe el mismo 404 not_found, como si la ruta no existiera. No se borra ni se oculta nada cuando está DESACTIVADO.
Paginación. limit (por defecto 50, máximo 200) y cursor (el next_cursor de la página anterior; null en la última página). Las filas vienen ordenadas por recorded_at y luego id, ambos descendentes, de modo que una página nunca repite ni omite una fila que ya existía cuando se leyó la primera página. from / to acotan recorded_at ([from, to), ambos opcionales); entry_type conserva un solo tipo de movimiento.
Quién. Una clave de API dd_ o un login del dashboard con el rol account_admin. Cualquier otro recibe el mismo 403 role_forbidden envíe lo que envíe. El aislamiento entre tenants es el de la base de datos (RLS): las filas de otro tenant nunca se leen.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
from | query | string (date-time) | no | Cota inferior RFC 3339 de recorded_at, inclusiva. |
to | query | string (date-time) | no | Cota superior RFC 3339 de recorded_at, EXCLUSIVA; debe ser posterior a from. |
entry_type | query | topup · debit · refund · adjustment · subscription_grant · grant_expiry · deposit_hold · deposit_release · deposit_apply | no | Conserva un solo tipo de movimiento. |
limit | query | integer | no | Tamaño de página, por defecto 50, con tope de 200. |
cursor | query | string | no | El next_cursor de la página anterior. |
Respuestas
200
Una página de asientos del libro mayor.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
entries | arreglo de BillingLedgerEntry | sí | |
entries[].id | string (uuid) | sí | |
entries[].entry_type | topup · debit · refund · adjustment · subscription_grant · grant_expiry · deposit_hold · deposit_release · deposit_apply | sí | |
entries[].event_type | string,null | sí | El tipo de evento facturable para un debit; null para los demás movimientos. |
entries[].ref_uuid | string | sí | El id del evento facturable para un debit, la clave de idempotencia del pago para un topup, YYYY-MM para una concesión de periodo. |
entries[].billable_event_id | string,null (uuid) | sí | El evento de uso que causó un debit; null para los demás movimientos. |
entries[].cash_delta | string | sí | Decimal con signo, como cadena. |
entries[].grant_delta | string | sí | Decimal con signo, como cadena. |
entries[].deposit_delta | string | sí | Decimal con signo, como cadena. |
entries[].occurred_on | string (date) | sí | El día UTC al que pertenece el movimiento. |
entries[].recorded_at | string (date-time) | sí | Cuándo se asentó la entrada. |
next_cursor | string,null | sí | Pásalo como cursor para la página siguiente; null en la última. |
json
{
"entries": [
{
"id": "0b7e9f2a-1c3d-4e5f-8a6b-7c8d9e0f1a2b",
"entry_type": "debit",
"event_type": "sms_segment",
"ref_uuid": "3f1d2c3a-5b7e-4f0a-9c1d-2e3f4a5b6c7d",
"billable_event_id": "3f1d2c3a-5b7e-4f0a-9c1d-2e3f4a5b6c7d",
"cash_delta": "-0.5000",
"grant_delta": "0.0000",
"deposit_delta": "0.0000",
"occurred_on": "2026-10-24",
"recorded_at": "2026-10-24T15:04:05.123456Z"
}
],
"next_cursor": null
}400
from o to que no son RFC 3339, from no anterior a to, un entry_type desconocido, o un limit / cursor no válido.
json
{
"error": {
"code": "bad_request",
"message": "invalid limit or cursor"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: un login del dashboard sin el rol account_admin. El mismo cuerpo para toda petición de ese login. Un widget u otro ámbito que no sea de tenant recibe forbidden.
json
{
"error": {
"code": "role_forbidden",
"message": "a dashboard login needs the account_admin role to read the billing ledger"
}
}404
not_found: el tenant tiene billing_ledger_enabled DESACTIVADO (el valor por defecto). Igual para todo rol y todo parámetro: el flag responde antes que cualquier otra cosa.
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}La factura de un mes natural, con el periodo fijado por el SERVIDOR
http
GET /v1/billing/invoiceLas mismas líneas que GET /v1/billing/summary, sobre una ventana que el CLIENTE NO PUEDE ELEGIR: el ciclo de facturación es el mes natural (la misma clave con la que el cierre mensual sella su fila de libro mayor seat_month) y la única opción es qué mes.
period es YYYY-MM y por defecto es el mes en curso. Cualquier cosa que no sea un mes real es un 400, nunca un mes cercano.
from y to se RECHAZAN aquí con un 400 que los nombra, en lugar de ignorarlos: un llamante que los envió cree que acotó la factura, y facturar en silencio una ventana distinta sería indistinguible de haberlos respetado. Usa /v1/billing/summary para un rango libre.
Leer dos veces el mismo mes cerrado devuelve el mismo documento: ese es el sentido de anclar el periodo en el servidor.
Una vez que el cierre mensual ha EMITIDO el documento del mes, esta lectura sirve ESE: las filas congeladas de invoices/invoice_lines, numeradas, inmutables, nunca un recálculo, con la forma FrozenInvoice (source: frozen_invoice, con invoice_number). Un mes sin documento emitido (el mes en curso, o un mes pasado cuyo cierre aún no ha convergido) responde con el modelo de lectura vivo BillingSummary, que no tiene número de factura.
?format=csv / Accept: text/csv para el artefacto de facturación (con X-Artifact-SHA256). La fila report del bloque meta discrimina los dos casos, igual que source en el JSON: un mes congelado son las filas del documento tal cual (report,invoice, y el meta lleva invoice_number e issued_at), mientras que un mes sin documento emitido es el modelo de lectura (report,invoice_draft, sin número de factura). Los nombres de fichero siguen la misma división: invoice_<period_key>_n<num>.csv una vez emitida, invoice_<period_key>.csv mientras no lo está.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
period | query | string | no | El mes natural a facturar, YYYY-MM. Por defecto, el mes UTC actual. |
format | query | csv | no | csv para el artefacto descargable (equivalente: Accept: text/csv). |
Respuestas
200
Las líneas de la factura (JSON) o el artefacto CSV.
frozen
json
{
"source": "frozen_invoice",
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"invoice_number": 7,
"period_key": "2026-06",
"period_from": "2026-06-01",
"period_to": "2026-06-30",
"currency": "USD",
"issued_at": "2026-07-01T00:00:07.412331Z",
"total": "595.0000",
"balance_settled_usd": "0.0000",
"amount_due_usd": "595.0000",
"line_items": [
{
"position": 1,
"line_type": "seats",
"description": "5 billable seats (closed-month snapshot) x $119",
"quantity": "5.0000",
"unit_price": "119.0000",
"amount": "595.0000",
"settled_from_balance_usd": null
}
]
}live
json
{
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"tier": "scale",
"tier_label": "Scale",
"voice_ai_enabled": true,
"currency": "USD",
"period": {
"from": "2026-06-01T00:00:00Z",
"to": "2026-07-01T00:00:00Z",
"day_ledger": {
"from_day": "2026-06-01",
"to_day": "2026-06-30",
"kinds": [
"house_minutes",
"inbound_minutes",
"did_rent",
"sms"
]
}
},
"line_items": [
{
"kind": "seats",
"cadence": "monthly",
"source": "ledger_snapshot",
"occupied_seats": null,
"billable_seats": 5,
"seat_minimum": null,
"unit_price_usd": 119,
"amount_usd": 595
}
],
"total_usd": 595
}400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422 — Unprocessable
La forma es correcta, la semántica no (regla de dominio incumplida).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}