# TruoCloud > API publica para operar infraestructura cloud: VPS, DNS, bases de datos administradas, > contenedores, balanceadores, Object Storage y Mail Gateway. Base: https://api.truo.cloud/v1 · OpenAPI: https://api.truo.cloud/v1/openapi.json Autenticacion: `Authorization: Bearer tc_live_...` (API key con scopes). ## Como leer las respuestas - Recurso: `{ "object": "vps", "id": "svc_10432", ... }` — sin envoltorio. - Coleccion: `{ "object": "list", "data": [...], "has_more": true, "next_cursor": "..." }`. El cursor es **opaco**: no lo construyas ni lo parsees. - Error (cualquier >=400): `{ "error": { "type", "code", "message", "param", "request_id" } }`. Ramifica por `code`, nunca por el texto de `message`. - Lo que tarda devuelve `202` + un objeto `operation`: hay que consultar `/v1/operations/{id}` hasta `succeeded` o `failed`. ## Reglas - Toda mutacion acepta `Idempotency-Key`. Usala: un reintento sin ella crea dos veces. - Un servicio que no existe **o que tu credencial no puede ver** devuelve 404, nunca 403. Un 403 confirmaria que existe. - Los ids llevan prefijo tipado (`svc_10432`). Se acepta el numero pelado, se devuelve el prefijado. ## Documentacion - [Introduccion](https://docs.truo.cloud/) - [Autenticacion](https://docs.truo.cloud/empezar/autenticacion/) - [El contrato](https://docs.truo.cloud/empezar/contrato/) - [Errores](https://docs.truo.cloud/errores/) - [Limites de uso](https://docs.truo.cloud/limites/) - [Politica de deprecacion](https://docs.truo.cloud/deprecation/) - [Para agentes de IA](https://docs.truo.cloud/agentes/) ## Operaciones (103) ### Meta - `meta.capabilities` — GET /v1/meta/capabilities: Qué soporta esta instancia de la API ### Account - `account.get` — GET /v1/account (scope `account:read`): Obtener la cuenta y la credencial actual ### Services - `services.get` — GET /v1/services/{id} (scope `services:read`): Obtener un servicio - `services.list` — GET /v1/services (scope `services:read`): Listar los servicios de la cuenta ### VPS - `vps.backups.create` — POST /v1/vps/{id}/backups (scope `vps:write`) · asincrona: Crear un backup - `vps.backups.delete` — DELETE /v1/vps/{id}/backups/{backup_id} (scope `vps:write`) ⚠️ destructiva: Borrar un backup - `vps.backups.list` — GET /v1/vps/{id}/backups (scope `vps:read`): Backups del VPS - `vps.backups.restore` — POST /v1/vps/{id}/backups/{backup_id}/restore (scope `vps:write`) ⚠️ destructiva · asincrona: Restaurar un backup - `vps.config.get` — GET /v1/vps/{id}/config (scope `vps:read`): Configuración de la máquina - `vps.console.create` — POST /v1/vps/{id}/console (scope `vps:console`): Abrir una consola - `vps.get` — GET /v1/vps/{id} (scope `vps:read`): Obtener un VPS con su estado real - `vps.ips.list` — GET /v1/vps/{id}/ips (scope `vps:read`): IPs asignadas al VPS - `vps.list` — GET /v1/vps (scope `vps:read`): Listar los VPS de la cuenta - `vps.metrics.list` — GET /v1/vps/{id}/metrics (scope `vps:read`): Serie de uso de CPU, memoria, disco y red - `vps.power` — POST /v1/vps/{id}/power (scope `vps:power`) · asincrona: Encender, apagar o reiniciar - `vps.reinstall` — POST /v1/vps/{id}/reinstall (scope `vps:write`) ⚠️ destructiva · asincrona: Reinstalar el sistema operativo - `vps.templates.list` — GET /v1/vps/{id}/templates (scope `vps:read`): Sistemas operativos disponibles para reinstalar - `vps.update` — PATCH /v1/vps/{id} (scope `vps:write`): Renombrar un VPS ### DNS - `dns.records.delete` — DELETE /v1/dns/zones/{zone}/records/{name}/{type} (scope `dns:write`) ⚠️ destructiva: Borrar un RRset - `dns.records.get` — GET /v1/dns/zones/{zone}/records/{name}/{type} (scope `dns:read`): Obtener un RRset - `dns.records.list` — GET /v1/dns/zones/{zone}/records (scope `dns:read`): Listar los registros de una zona - `dns.records.patch` — PATCH /v1/dns/zones/{zone}/records (scope `dns:write`) ⚠️ destructiva: Aplicar varios cambios a la vez - `dns.records.put` — PUT /v1/dns/zones/{zone}/records/{name}/{type} (scope `dns:write`): Crear o reemplazar un RRset - `dns.zones.export` — GET /v1/dns/zones/{zone}/export (scope `dns:read`): Exportar la zona en formato BIND - `dns.zones.get` — GET /v1/dns/zones/{zone} (scope `dns:read`): Obtener una zona - `dns.zones.list` — GET /v1/dns/zones (scope `dns:read`): Listar las zonas DNS de la cuenta ### DBaaS - `dbaas.backups.create` — POST /v1/dbaas/{id}/backups (scope `dbaas:write`) · asincrona: Crear un backup - `dbaas.backups.list` — GET /v1/dbaas/{id}/backups (scope `dbaas:read`): Backups del servicio - `dbaas.connection.get` — GET /v1/dbaas/{id}/connection (scope `dbaas:read`): Datos de conexión, sin la credencial - `dbaas.credentials.create` — POST /v1/dbaas/{id}/credentials (scope `dbaas:credentials`): Revelar la credencial de administración - `dbaas.databases.create` — POST /v1/dbaas/{id}/databases (scope `dbaas:write`): Crear una base - `dbaas.databases.delete` — DELETE /v1/dbaas/{id}/databases/{name} (scope `dbaas:write`) ⚠️ destructiva: Borrar una base - `dbaas.databases.list` — GET /v1/dbaas/{id}/databases (scope `dbaas:read`): Listar las bases del servicio - `dbaas.instances.get` — GET /v1/dbaas/{id} (scope `dbaas:read`): Obtener una base de datos con su estado real - `dbaas.instances.list` — GET /v1/dbaas (scope `dbaas:read`): Listar las bases de datos gestionadas de la cuenta - `dbaas.instances.restart` — POST /v1/dbaas/{id}/restart (scope `dbaas:write`): Reiniciar el motor - `dbaas.logs.get` — GET /v1/dbaas/{id}/logs (scope `dbaas:read`): Últimas líneas del log del motor - `dbaas.stats.get` — GET /v1/dbaas/{id}/stats (scope `dbaas:read`): Métricas de la instancia - `dbaas.users.create` — POST /v1/dbaas/{id}/users (scope `dbaas:write`): Crear un usuario - `dbaas.users.delete` — DELETE /v1/dbaas/{id}/users/{username} (scope `dbaas:write`) ⚠️ destructiva: Borrar un usuario - `dbaas.users.list` — GET /v1/dbaas/{id}/users (scope `dbaas:read`): Listar los usuarios del motor - `dbaas.users.set_password` — POST /v1/dbaas/{id}/users/{username}/password (scope `dbaas:write`): Cambiar la password de un usuario ### CaaS - `caas.apps.create` — POST /v1/caas/{id}/apps (scope `caas:write`): Crear una app - `caas.apps.delete` — DELETE /v1/caas/{id}/apps/{app_id} (scope `caas:write`) ⚠️ destructiva: Borrar una app - `caas.apps.deploy` — POST /v1/caas/{id}/apps/{app_id}/deploy (scope `caas:deploy`) · asincrona: Desplegar una app - `caas.apps.get` — GET /v1/caas/{id}/apps/{app_id} (scope `caas:read`): Obtener una app - `caas.apps.list` — GET /v1/caas/{id}/apps (scope `caas:read`): Listar las apps del servicio - `caas.apps.logs` — GET /v1/caas/{id}/apps/{app_id}/logs (scope `caas:read`): Logs de una app - `caas.apps.restart` — POST /v1/caas/{id}/apps/{app_id}/restart (scope `caas:write`): Reiniciar una app - `caas.databases.create` — POST /v1/caas/{id}/databases (scope `caas:write`): Crear una base de datos - `caas.databases.list` — GET /v1/caas/{id}/databases (scope `caas:read`): Bases de datos del servicio - `caas.deployments.list` — GET /v1/caas/{id}/apps/{app_id}/deployments (scope `caas:read`): Historial de despliegues de una app - `caas.domains.create` — POST /v1/caas/{id}/apps/{app_id}/domains (scope `caas:write`): Agregar un dominio a una app - `caas.domains.delete` — DELETE /v1/caas/{id}/apps/{app_id}/domains/{host} (scope `caas:write`) ⚠️ destructiva: Quitar un dominio de una app - `caas.domains.list` — GET /v1/caas/{id}/apps/{app_id}/domains (scope `caas:read`): Dominios de una app - `caas.env.list` — GET /v1/caas/{id}/apps/{app_id}/env (scope `caas:read`): Nombres de las variables de entorno - `caas.env.replace` — PUT /v1/caas/{id}/apps/{app_id}/env (scope `caas:write`) ⚠️ destructiva: Reemplazar las variables de entorno - `caas.instances.get` — GET /v1/caas/{id} (scope `caas:read`): Obtener un servicio CaaS con su estado real - `caas.instances.list` — GET /v1/caas (scope `caas:read`): Listar los servicios CaaS de la cuenta ### Load Balancer - `lb.backends.create` — POST /v1/load-balancers/{id}/backends (scope `lb:write`): Agregar un destino a un listener - `lb.backends.delete` — DELETE /v1/load-balancers/{id}/backends/{listener}/{ip}/{port} (scope `lb:write`) ⚠️ destructiva: Quitar un destino de un listener - `lb.backends.list` — GET /v1/load-balancers/{id}/backends (scope `lb:read`): Listar los destinos - `lb.instances.get` — GET /v1/load-balancers/{id} (scope `lb:read`): Obtener un load balancer con su estado real - `lb.instances.list` — GET /v1/load-balancers (scope `lb:read`): Listar los load balancers de la cuenta - `lb.listeners.list` — GET /v1/load-balancers/{id}/listeners (scope `lb:read`): Listar los listeners - `lb.listeners.replace` — PUT /v1/load-balancers/{id}/listeners (scope `lb:write`) ⚠️ destructiva: Reemplazar la configuración de listeners - `lb.stats.get` — GET /v1/load-balancers/{id}/stats (scope `lb:read`): Estado y tráfico por listener ### Object Storage - `objectstorage.buckets.create` — POST /v1/object-storage/buckets (scope `objectstorage:write`): Crear un bucket - `objectstorage.buckets.delete` — DELETE /v1/object-storage/buckets/{bucket} (scope `objectstorage:write`) ⚠️ destructiva: Borrar un bucket - `objectstorage.buckets.empty` — POST /v1/object-storage/buckets/{bucket}/empty (scope `objectstorage:write`) ⚠️ destructiva · asincrona: Vaciar un bucket - `objectstorage.buckets.get` — GET /v1/object-storage/buckets/{bucket} (scope `objectstorage:read`): Obtener un bucket - `objectstorage.buckets.list` — GET /v1/object-storage/buckets (scope `objectstorage:read`): Listar los buckets - `objectstorage.buckets.metrics` — GET /v1/object-storage/buckets/{bucket}/metrics (scope `objectstorage:read`): Métricas de un bucket - `objectstorage.buckets.update` — PATCH /v1/object-storage/buckets/{bucket} (scope `objectstorage:write`): Cambiar la visibilidad de un bucket - `objectstorage.keys.create` — POST /v1/object-storage/keys (scope `objectstorage:keys`): Emitir una llave de acceso - `objectstorage.keys.delete` — DELETE /v1/object-storage/keys/{key_id} (scope `objectstorage:keys`) ⚠️ destructiva: Revocar una llave de acceso - `objectstorage.keys.list` — GET /v1/object-storage/keys (scope `objectstorage:read`): Listar las llaves de acceso - `objectstorage.objects.delete` — POST /v1/object-storage/buckets/{bucket}/objects/delete (scope `objectstorage:write`) ⚠️ destructiva: Borrar objetos - `objectstorage.objects.list` — GET /v1/object-storage/buckets/{bucket}/objects (scope `objectstorage:read`): Listar objetos de un bucket - `objectstorage.objects.presign` — POST /v1/object-storage/buckets/{bucket}/presign (scope `objectstorage:read`): Firmar una URL temporal - `objectstorage.tenant.get` — GET /v1/object-storage (scope `objectstorage:read`): Obtener el Object Storage de la cuenta ### Mail Gateway - `mailgateway.domains.create` — POST /v1/mail-gateway/domains (scope `mailgateway:write`): Agregar un dominio de envío - `mailgateway.domains.delete` — DELETE /v1/mail-gateway/domains/{domain} (scope `mailgateway:write`) ⚠️ destructiva: Quitar un dominio de envío - `mailgateway.domains.list` — GET /v1/mail-gateway/domains (scope `mailgateway:read`): Listar los dominios de envío - `mailgateway.domains.verify` — POST /v1/mail-gateway/domains/{domain}/verify (scope `mailgateway:write`): Consultar la verificación de un dominio - `mailgateway.keys.create` — POST /v1/mail-gateway/keys (scope `mailgateway:send`): Crear una API key de envío - `mailgateway.keys.delete` — DELETE /v1/mail-gateway/keys/{key_id} (scope `mailgateway:write`) ⚠️ destructiva: Revocar una API key de envío - `mailgateway.keys.list` — GET /v1/mail-gateway/keys (scope `mailgateway:read`): Listar las API keys de envío - `mailgateway.messages.list` — GET /v1/mail-gateway/messages (scope `mailgateway:read`): Listar los mensajes enviados - `mailgateway.metrics.get` — GET /v1/mail-gateway/metrics (scope `mailgateway:read`): Métricas de entrega y reputación - `mailgateway.smtp.get` — GET /v1/mail-gateway/smtp (scope `mailgateway:read`): Ver la configuración SMTP - `mailgateway.smtp.reveal` — POST /v1/mail-gateway/smtp (scope `mailgateway:send`): Revelar la password SMTP - `mailgateway.smtp.rotate` — POST /v1/mail-gateway/smtp/rotate (scope `mailgateway:send`) ⚠️ destructiva: Rotar la credencial SMTP - `mailgateway.tenant.get` — GET /v1/mail-gateway (scope `mailgateway:read`): Obtener el Mail Gateway de la cuenta - `mailgateway.usage.get` — GET /v1/mail-gateway/usage (scope `mailgateway:read`): Uso del mes en curso ### Operations - `operations.get` — GET /v1/operations/{id} (scope `operations:read`): Estado de una operación - `operations.list` — GET /v1/operations (scope `operations:read`): Listar operaciones recientes de la cuenta ### API Keys - `apiKeys.create` — POST /v1/api-keys (scope `apikeys:write`): Crear una API key - `apiKeys.get` — GET /v1/api-keys/{id} (scope `apikeys:read`): Obtener una API key - `apiKeys.list` — GET /v1/api-keys (scope `apikeys:read`): Listar las API keys de la cuenta - `apiKeys.revoke` — POST /v1/api-keys/{id}/revoke (scope `apikeys:write`) ⚠️ destructiva: Revocar una API key - `apiKeys.update` — PATCH /v1/api-keys/{id} (scope `apikeys:write`): Modificar una API key ### Audit - `auditLogs.list` — GET /v1/audit-logs (scope `audit:read`): Listar la actividad de API de la cuenta --- # Detalle de cada operacion ## account.get GET /v1/account → 200 Obtener la cuenta y la credencial actual Scope: `account:read` ## apiKeys.create POST /v1/api-keys → 201 Crear una API key Devuelve el token en claro **una sola vez**. Guardalo en el momento: solo se almacena su hash SHA-256 y no hay forma de recuperarlo después. Scope: `apikeys:write` Body: ```json {} ``` ## apiKeys.get GET /v1/api-keys/{id} → 200 Obtener una API key Scope: `apikeys:read` Parametros: - `id` (requerido): string ## apiKeys.list GET /v1/api-keys → 200 Listar las API keys de la cuenta Solo con sesión. Nunca devuelve tokens: solo prefijo y últimos 4. Scope: `apikeys:read` Parametros: - `limit`: string - `cursor`: string ## apiKeys.revoke POST /v1/api-keys/{id}/revoke → 200 Revocar una API key Irreversible. La revocación se propaga a todas las réplicas por pub/sub en menos de un segundo; el peor caso, con Redis caído, es 60 segundos (el TTL de la caché en proceso). Scope: `apikeys:write` Parametros: - `id` (requerido): string ## apiKeys.update PATCH /v1/api-keys/{id} → 200 Modificar una API key Los scopes y la allowlist solo se pueden **estrechar**. Ampliar devuelve 403: sin esa regla, una key con `vps:read` se auto-promueve a `vps:write` con un PATCH y el scope deja de significar algo. Scope: `apikeys:write` Parametros: - `id` (requerido): string Body: ```json {} ``` ## auditLogs.list GET /v1/audit-logs → 200 Listar la actividad de API de la cuenta Incluye los intentos **denegados** (4xx), no solo lo que funcionó: una credencial probando endpoints que no le corresponden es precisamente la señal que hay que poder ver. Scope: `audit:read` Parametros: - `limit`: string - `cursor`: string - `status`: string - `denied_only`: string ## caas.apps.create POST /v1/caas/{id}/apps → 201 Crear una app Crea la app y configura su origen, pero **no la despliega**: queda en `idle` hasta que llames a `POST /v1/caas/{id}/apps/{app_id}/deploy`. Separar las dos cosas es lo que permite crear la app, cargarle las variables y recién ahí desplegar — el orden inverso arrancaría la aplicación sin su configuración. Scope: `caas:write` Parametros: - `id` (requerido): string Body: ```json { "name": { "type": "string", "minLength": 1, "maxLength": 63, "description": "Nombre visible de la app. El backend deriva de acá un identificador interno.", "example": "api" }, "source": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "git", "docker_image" ] }, "ref": { "type": "string", "minLength": 1, "maxLength": 512, "description": "URL del repositorio para `git`, referencia de la imagen para `docker_image`.", "example": "https://github.com/acme/api.git" }, "branch": { "type": "string", "minLength": 1, "maxLength": 255, "description": "Solo para `git`. Default `main`." } }, "required": [ "type", "ref" ], "description": "Se puede omitir y configurar después, pero una app sin origen no se puede desplegar." } } ``` ## caas.apps.delete DELETE /v1/caas/{id}/apps/{app_id} → 204 Borrar una app **Destructivo.** Borra la app, sus variables y sus dominios. Los datos de las bases del servicio no se tocan: viven aparte. Scope: `caas:write` Parametros: - `id` (requerido): string - `app_id` (requerido): string ## caas.apps.deploy POST /v1/caas/{id}/apps/{app_id}/deploy → 202 Desplegar una app Devuelve `202` en cuanto el despliegue arranca. La operación se resuelve buscando ese despliegue en el historial de la app, que es el único lugar donde el backend reporta en qué quedó. Esperá con `GET /v1/operations/{id}`; el detalle de un fallo está en `GET /v1/caas/{id}/apps/{app_id}/logs`. Vive en su propio scope (`caas:deploy`) porque desplegar ejecuta el código que haya en el origen configurado — es distinto de editar la configuración de la app. Scope: `caas:deploy` Parametros: - `id` (requerido): string - `app_id` (requerido): string ## caas.apps.get GET /v1/caas/{id}/apps/{app_id} → 200 Obtener una app Devuelve **solo** los campos declarados. El backend responde con el objeto interno completo del motor de despliegue —que incluye las variables de entorno en claro—; nada de eso sale por acá. Para los nombres de las variables, `GET /v1/caas/{id}/apps/{app_id}/env`. Scope: `caas:read` Parametros: - `id` (requerido): string - `app_id` (requerido): string ## caas.apps.list GET /v1/caas/{id}/apps → 200 Listar las apps del servicio `source` viene en `null`: el backend no lo trae en el listado. Scope: `caas:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## caas.apps.logs GET /v1/caas/{id}/apps/{app_id}/logs → 200 Logs de una app **Es una foto, no un stream.** Devuelve lo que el backend tenga en el momento de la llamada y no hay forma de pedir "lo que vino después": el backend acepta un cursor pero nunca emite el siguiente, así que este endpoint no publica ninguno. Para seguir una aplicación en vivo, volvé a llamar. Scope: `caas:read` Parametros: - `id` (requerido): string - `app_id` (requerido): string ## caas.apps.restart POST /v1/caas/{id}/apps/{app_id}/restart → 202 Reiniciar una app Reinicia el proceso sin volver a construir la imagen: toma las variables de entorno actuales pero **no** trae código nuevo. Para eso es `deploy`. Scope: `caas:write` Parametros: - `id` (requerido): string - `app_id` (requerido): string ## caas.databases.create POST /v1/caas/{id}/databases → 201 Crear una base de datos La contraseña la genera la plataforma y **no se devuelve acá ni en ningún otro endpoint de `/v1`**: no hay forma de recuperarla por esta API. Conectate desde una app del mismo servicio, donde la cadena de conexión ya está disponible. Borrar una base no está en esta versión: el backend todavía no lo implementa y publicar un endpoint que siempre falla sería publicar roadmap. Scope: `caas:write` Parametros: - `id` (requerido): string Body: ```json { "engine": { "type": "string", "enum": [ "postgres", "mysql", "mariadb", "mongo", "redis" ] }, "name": { "type": "string", "minLength": 1, "maxLength": 63, "example": "principal" } } ``` ## caas.databases.list GET /v1/caas/{id}/databases → 200 Bases de datos del servicio Son del servicio, no de una app: varias apps del mismo servicio pueden usar la misma base. **Las credenciales no se devuelven** por ningún endpoint de esta API. Scope: `caas:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## caas.deployments.list GET /v1/caas/{id}/apps/{app_id}/deployments → 200 Historial de despliegues de una app Del más reciente al más viejo, según lo devuelve el backend. Scope: `caas:read` Parametros: - `id` (requerido): string - `app_id` (requerido): string - `limit`: string - `cursor`: string ## caas.domains.create POST /v1/caas/{id}/apps/{app_id}/domains → 201 Agregar un dominio a una app El DNS del host tiene que estar apuntado a la IP del servicio **antes** de llamar: la emisión del certificado se valida por HTTP. Dos cosas más que hay que saber: - **No es atómico.** El alta registra el dominio y después reconstruye el ruteo de entrada; si lo segundo falla, la llamada devuelve error con el dominio ya creado. Reintentar es seguro y es lo correcto — el alta es idempotente por host. - **El certificado se emite después**, de forma asíncrona y sin ningún estado ni id que consultar. Por eso `certificate_type` viene en `null` acá. La única verificación real es una petición HTTPS al host. Scope: `caas:write` Parametros: - `id` (requerido): string - `app_id` (requerido): string Body: ```json { "host": { "type": "string", "minLength": 1, "maxLength": 253, "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$", "description": "Tiene que resolver a la IP del servicio **antes** de crearlo: el certificado se valida por HTTP y sin el DNS apuntado la emisión falla en silencio.", "example": "app.ejemplo.com" } } ``` ## caas.domains.delete DELETE /v1/caas/{id}/apps/{app_id}/domains/{host} → 204 Quitar un dominio de una app Borrar un host que no está en la app no es un error: el ruteo de entrada se reconstruye igual, que es lo que hace que reintentar sea seguro. Scope: `caas:write` Parametros: - `id` (requerido): string - `app_id` (requerido): string - `host` (requerido): string ## caas.domains.list GET /v1/caas/{id}/apps/{app_id}/domains → 200 Dominios de una app Scope: `caas:read` Parametros: - `id` (requerido): string - `app_id` (requerido): string ## caas.env.list GET /v1/caas/{id}/apps/{app_id}/env → 200 Nombres de las variables de entorno **Devuelve los nombres, nunca los valores.** No hay una versión de este endpoint que los devuelva: una vez escrito, un valor solo lo lee la aplicación. El backend enmascara aplicando una regex al nombre de la clave, lo que deja pasar en claro cualquier cosa que no se llame como un secreto (`DATABASE_URL`, `SENTRY_DSN`); eso no es una política de clasificación y no se publica. Scope: `caas:read` Parametros: - `id` (requerido): string - `app_id` (requerido): string ## caas.env.replace PUT /v1/caas/{id}/apps/{app_id}/env → 200 Reemplazar las variables de entorno **Reemplaza el conjunto entero**: lo que no venga en `vars` se borra. No es una limitación, es la semántica del backend, que escribe el bloque completo de una. Como `GET /env` no devuelve valores, el set tiene que salir de tu lado — de tu gestor de secretos o de tu repositorio de configuración. Eso es lo natural para infraestructura declarativa, y de paso elimina el modo de fallo del panel, donde guardar sin volver a escribir los secretos los borraba. Los cambios toman efecto en el próximo `deploy` o `restart`. Scope: `caas:write` Parametros: - `id` (requerido): string - `app_id` (requerido): string Body: ```json { "vars": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string", "minLength": 1, "maxLength": 255, "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", "example": "DATABASE_URL" }, "value": { "type": "string", "maxLength": 8192, "pattern": "^[^\\r\\n]*$", "description": "Se guarda tal cual. No se devuelve nunca." } }, "required": [ "key", "value" ] }, "maxItems": 500, "description": "El conjunto **completo**. Lo que no esté acá se borra: mandar `[]` deja la app sin ninguna variable. Como `GET /env` no devuelve valores, el set entero tiene que salir de tu lado —de tu gestor de secretos o de tu repositorio de configuración—, que es como funciona cualquier infraestructura declarativa." } } ``` ## caas.instances.get GET /v1/caas/{id} → 200 Obtener un servicio CaaS con su estado real Consulta el control plane. Si no responde, `provisioning_state` y `machine` vuelven en `null` en vez de fallar: un hipo del control plane no debería impedirte leer el resto del recurso ni sus `capabilities`. Scope: `caas:read` Parametros: - `id` (requerido): string ## caas.instances.list GET /v1/caas → 200 Listar los servicios CaaS de la cuenta Sale de la base, sin consultar el control plane: `provisioning_state` y `machine` vienen en `null`. Traerlos costaría dos llamadas por elemento de la página. Una página puede venir con menos elementos que el `limit` aunque haya más: todos los productos del control plane comparten un mismo módulo de aprovisionamiento, así que el filtro por familia solo puede aplicarse después de leer la página. `has_more` sigue siendo la señal correcta de si queda algo por traer. Scope: `caas:read` Parametros: - `limit`: string - `cursor`: string ## dbaas.backups.create POST /v1/dbaas/{id}/backups → 202 Crear un backup Devuelve `202` en cuanto la tarea arranca, no cuando el archivo está listo: un dump puede tardar minutos, muy por encima de cualquier timeout HTTP. La operación **se resuelve contra la lista de backups** —aparece uno nuevo o no— y no contra el resultado del POST, así que sobrevive a que la llamada expire con el backup corriendo. Esperala con `GET /v1/operations/{id}`. Scope: `dbaas:write` Parametros: - `id` (requerido): string ## dbaas.backups.list GET /v1/dbaas/{id}/backups → 200 Backups del servicio Del más nuevo al más viejo. Un servicio cuyo motor no tiene backups gestionados devuelve una lista vacía, no un error. Scope: `dbaas:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## dbaas.connection.get GET /v1/dbaas/{id}/connection → 200 Datos de conexión, sin la credencial Host, puerto, base, usuario administrador, modo TLS y la CA del servicio — que es **pública** y sirve para verificar al servidor. **No incluye la password ni ninguna URI que la contenga**: la credencial sale de `POST /v1/dbaas/{id}/credentials`, que exige el scope `dbaas:credentials`. Scope: `dbaas:read` Parametros: - `id` (requerido): string ## dbaas.credentials.create POST /v1/dbaas/{id}/credentials → 200 Revelar la credencial de administración Devuelve la password del administrador **en claro**. No rota nada: es la credencial que ya está en uso. Es un POST y no un GET a propósito. Un GET queda en el historial del navegador, en los logs de cualquier proxy y en cachés intermedias, y se puede disparar sin querer desde un link; un POST obliga a una acción deliberada y entra al audit log como mutación, así que revelar la credencial de una base deja rastro. Por lo mismo vive en su propio scope (`dbaas:credentials`): `dbaas:write` crea bases y usuarios acotados, esto da acceso total a los datos y sobrevive a revocar la key. Scope: `dbaas:credentials` Parametros: - `id` (requerido): string ## dbaas.databases.create POST /v1/dbaas/{id}/databases → 201 Crear una base `charset` y `collation` son de MySQL; `owner`, de PostgreSQL. El resto de los motores los ignora. La respuesta no trae tamaño ni conteo de tablas: la base nace vacía y releerla costaría otra llamada para informar un cero. Scope: `dbaas:write` Parametros: - `id` (requerido): string Body: ```json { "name": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_][A-Za-z0-9_$-]*$", "example": "appdb" }, "charset": { "type": "string", "maxLength": 64, "description": "Solo MySQL. Default `utf8mb4`.", "example": "utf8mb4" }, "collation": { "type": "string", "maxLength": 64, "description": "Solo MySQL. Default `utf8mb4_unicode_ci`." }, "owner": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_][A-Za-z0-9_$-]*$", "description": "Solo PostgreSQL. Usuario dueño de la base; por defecto el administrador." } } ``` ## dbaas.databases.delete DELETE /v1/dbaas/{id}/databases/{name} → 204 Borrar una base **Destructivo e irreversible**: se van los datos y no hay papelera. Lo único que queda es lo que haya en `GET /v1/dbaas/{id}/backups`. Scope: `dbaas:write` Parametros: - `id` (requerido): string - `name` (requerido): string ## dbaas.databases.list GET /v1/dbaas/{id}/databases → 200 Listar las bases del servicio Scope: `dbaas:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## dbaas.instances.get GET /v1/dbaas/{id} → 200 Obtener una base de datos con su estado real Consulta el backend. Si no responde, los campos de estado vuelven en `null` y `capabilities` queda sin `databases`/`users` en vez de fallar: que el backend tenga un hipo no debería impedirte leer el resto del recurso. Scope: `dbaas:read` Parametros: - `id` (requerido): string ## dbaas.instances.list GET /v1/dbaas → 200 Listar las bases de datos gestionadas de la cuenta Sale de la base, sin consultar el backend: `engine`, `state`, `host` y `plan` vienen en `null`, y `capabilities` **omite** `databases` y `users` porque saber si el motor las tiene costaría una llamada por elemento de la página. Una clave ausente es "no se consultó", que no es lo mismo que `false`. Para el estado real de una, `GET /v1/dbaas/{id}`. Scope: `dbaas:read` Parametros: - `limit`: string - `cursor`: string ## dbaas.instances.restart POST /v1/dbaas/{id}/restart → 202 Reiniciar el motor Corta las conexiones abiertas: las transacciones en vuelo se pierden. Devuelve `202` con una operación ya terminada —el reinicio es síncrono en los dos backends— para que el cliente trate todas las mutaciones largas igual, y para que el día que deje de serlo no cambie el contrato sino la columna `backend` de la operación. Scope: `dbaas:write` Parametros: - `id` (requerido): string ## dbaas.logs.get GET /v1/dbaas/{id}/logs → 200 Últimas líneas del log del motor La cola del log del proceso del motor, de la más vieja a la más nueva. No es un log de consultas ni de auditoría: son los mensajes de arranque, errores y avisos del motor. Scope: `dbaas:read` Parametros: - `id` (requerido): string - `lines`: string ## dbaas.stats.get GET /v1/dbaas/{id}/stats → 200 Métricas de la instancia Instantánea, no serie temporal. Qué campos vienen llenos depende del backend del servicio: unos miden el contenedor y otros el motor. Scope: `dbaas:read` Parametros: - `id` (requerido): string ## dbaas.users.create POST /v1/dbaas/{id}/users → 201 Crear un usuario La password no se guarda de nuestro lado ni se devuelve después: si se pierde, se cambia con `POST /v1/dbaas/{id}/users/{username}/password`. Scope: `dbaas:write` Parametros: - `id` (requerido): string Body: ```json { "username": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_][A-Za-z0-9_$-]*$", "example": "app" }, "password": { "type": "string", "minLength": 12, "maxLength": 128, "description": "No se guarda ni se devuelve: si se pierde, se cambia con `POST /v1/dbaas/{id}/users/{username}/password`." }, "host": { "type": "string", "maxLength": 60, "pattern": "^[A-Za-z0-9_.:%/-]+$", "description": "Solo MySQL. Default `%` (cualquier origen).", "example": "%" }, "databases": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_][A-Za-z0-9_$-]*$" }, "maxItems": 50, "description": "Bases sobre las que se le otorgan permisos." }, "privileges": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9_]{1,64}$" }, "maxItems": 20, "description": "MySQL: privilegios de SQL (`SELECT`, `INSERT`, …); default `ALL` sobre `databases`. PostgreSQL: se usa el primero como rol (`readwrite`, `readonly`). Una palabra por elemento: los privilegios compuestos (`ALL PRIVILEGES`) no se aceptan porque el valor termina dentro de un `GRANT` que el motor arma por concatenación." } } ``` ## dbaas.users.delete DELETE /v1/dbaas/{id}/users/{username} → 204 Borrar un usuario **Corta el acceso de todo lo que estuviera conectado con ese usuario.** No borra datos: las bases que creó siguen ahí. Scope: `dbaas:write` Parametros: - `id` (requerido): string - `username` (requerido): string - `host`: string ## dbaas.users.list GET /v1/dbaas/{id}/users → 200 Listar los usuarios del motor Incluye al administrador. En MySQL el mismo nombre puede aparecer con varios `host`: el par `usuario@host` es lo que identifica al usuario, y por eso es el `id` del recurso. Scope: `dbaas:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## dbaas.users.set_password POST /v1/dbaas/{id}/users/{username}/password → 204 Cambiar la password de un usuario Toma efecto de inmediato: las aplicaciones que sigan usando la anterior van a fallar al reconectar. Sirve también para el usuario administrador. Scope: `dbaas:write` Parametros: - `id` (requerido): string - `username` (requerido): string Body: ```json { "password": { "type": "string", "minLength": 12, "maxLength": 128 } } ``` ## dns.records.delete DELETE /v1/dns/zones/{zone}/records/{name}/{type} → 204 Borrar un RRset Borra todos los valores de ese nombre y tipo. Scope: `dns:write` Parametros: - `zone` (requerido): string - `name` (requerido): string - `type` (requerido): string ## dns.records.get GET /v1/dns/zones/{zone}/records/{name}/{type} → 200 Obtener un RRset Scope: `dns:read` Parametros: - `zone` (requerido): string - `name` (requerido): string - `type` (requerido): string ## dns.records.list GET /v1/dns/zones/{zone}/records → 200 Listar los registros de una zona Agrupados en RRsets: un `A` con dos IPs es **un** registro con dos valores. La respuesta trae un `ETag`; pasalo como `If-Match` al escribir y ningún cambio concurrente se pierde. Scope: `dns:read` Parametros: - `zone` (requerido): string - `limit`: string - `cursor`: string ## dns.records.patch PATCH /v1/dns/zones/{zone}/records → 200 Aplicar varios cambios a la vez Cada elemento reemplaza su RRset; `values: []` lo borra. Es la forma de aplicar un cambio coherente —mover un sitio y su correo juntos— sin que quede a medias entre dos llamadas. **No es atómico en el backend**: si un cambio falla, los anteriores ya se aplicaron y la respuesta dice cuál cortó. Scope: `dns:write` Parametros: - `zone` (requerido): string Body: ```json { "records": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 }, "type": { "type": "string", "enum": [ "A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA", "PTR" ], "description": "El `SOA` y el `NS` de la delegación los administra la plataforma y no se editan." }, "ttl": { "type": "integer", "minimum": 60, "maximum": 604800, "description": "Segundos, 60–604800. Default 3600." }, "values": { "type": "array", "items": { "type": "string" }, "description": "Lista vacía = borrar el RRset." } }, "required": [ "name", "type", "values" ] }, "minItems": 1, "maxItems": 100 } } ``` ## dns.records.put PUT /v1/dns/zones/{zone}/records/{name}/{type} → 200 Crear o reemplazar un RRset Reemplaza el RRset **entero**: los valores que no vengan en `values` se borran. Es la semántica del DNS y la del backend — no existe "agregar una IP" sin reescribir el conjunto. Leé el RRset, agregá el valor a la lista y mandá la lista completa con el `ETag` en `If-Match`. Scope: `dns:write` Parametros: - `zone` (requerido): string - `name` (requerido): string - `type` (requerido): string Body: ```json { "ttl": { "type": "integer", "minimum": 60, "maximum": 604800, "description": "Segundos, 60–604800. Default 3600." }, "values": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "description": "Reemplaza el RRset **entero**. Los valores que no estén acá se borran: para agregar uno, leé el RRset, agregalo a la lista y mandá la lista completa." } } ``` ## dns.zones.export GET /v1/dns/zones/{zone}/export → 200 Exportar la zona en formato BIND El archivo de zona tal como lo emite el backend. Útil para respaldo o migración. Scope: `dns:read` Parametros: - `zone` (requerido): string ## dns.zones.get GET /v1/dns/zones/{zone} → 200 Obtener una zona Scope: `dns:read` Parametros: - `zone` (requerido): string ## dns.zones.list GET /v1/dns/zones → 200 Listar las zonas DNS de la cuenta Sale de la base, sin consultar el backend de DNS: `record_count` y `serial` vienen en `null`. Traerlos costaría una llamada por zona y hay cuentas con decenas. Scope: `dns:read` Parametros: - `limit`: string - `cursor`: string ## lb.backends.create POST /v1/load-balancers/{id}/backends → 201 Agregar un destino a un listener Atajo sobre `PUT /listeners` para el caso frecuente de sumar una máquina. Revalida y aplica la configuración completa, así que hereda la misma garantía: o el destino queda recibiendo tráfico, o no cambió nada. Scope: `lb:write` Parametros: - `id` (requerido): string Body: ```json { "listener": { "type": "string", "minLength": 1, "description": "Nombre de un listener existente." }, "ip": { "type": "string", "minLength": 1, "maxLength": 253 }, "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, "weight": { "type": "integer", "minimum": 0, "maximum": 256 } } ``` ## lb.backends.delete DELETE /v1/load-balancers/{id}/backends/{listener}/{ip}/{port} → 204 Quitar un destino de un listener Los tres valores que identifican al destino van en la ruta. El backend los espera en el cuerpo de un `DELETE`, cosa que proxies y CDNs descartan y que varios clientes HTTP no mandan; el cuerpo se arma de este lado. **Un listener no puede quedarse sin destinos.** Quitar el último devuelve `400 validation_failed`: para eliminar el listener entero usá `PUT /listeners` sin él. Scope: `lb:write` Parametros: - `id` (requerido): string - `listener` (requerido): string - `ip` (requerido): string - `port` (requerido): string ## lb.backends.list GET /v1/load-balancers/{id}/backends → 200 Listar los destinos Los destinos de todos los listeners, aplanados, cada uno con el listener al que pertenece. Es una vista sobre la misma configuración que devuelve `GET /listeners`. Scope: `lb:read` Parametros: - `id` (requerido): string ## lb.instances.get GET /v1/load-balancers/{id} → 200 Obtener un load balancer con su estado real Consulta el control plane, que a su vez sondea el balanceador. Si no responde, los campos de estado vuelven en `null` en vez de fallar. Scope: `lb:read` Parametros: - `id` (requerido): string ## lb.instances.list GET /v1/load-balancers → 200 Listar los load balancers de la cuenta Sale de la base, sin consultar el control plane: `provisioning_state`, `healthy` y `listener_count` vienen en `null`. Traerlos costaría una llamada por elemento de la página. Una página puede venir con menos elementos que el `limit` aunque haya más: todos los productos del control plane comparten un mismo módulo de aprovisionamiento, así que el filtro por familia solo puede aplicarse después de leer la página. `has_more` sigue siendo la señal correcta de si queda algo por traer. Scope: `lb:read` Parametros: - `limit`: string - `cursor`: string ## lb.listeners.list GET /v1/load-balancers/{id}/listeners → 200 Listar los listeners La configuración completa del balanceador, incluidos los destinos de cada listener. Es lo que hay que leer, modificar y volver a mandar en `PUT`. Scope: `lb:read` Parametros: - `id` (requerido): string ## lb.listeners.replace PUT /v1/load-balancers/{id}/listeners → 200 Reemplazar la configuración de listeners **Reemplaza el conjunto entero**: los listeners que no vengan en `listeners` se borran, con sus destinos. Mandar `[]` deja el balanceador sin nada escuchando y corta el tráfico. Leé `GET /listeners`, modificá y mandá todo de vuelta. **El cambio se aplica dentro de la llamada**: cuando esto devuelve, la configuración nueva ya está sirviendo tráfico. Si la configuración resultante es inválida no se aplica nada y la respuesta es `400 validation_failed` — el servicio nunca queda a medias. Scope: `lb:write` Parametros: - `id` (requerido): string Body: ```json { "listeners": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$", "description": "Identifica al listener dentro del servicio. Único.", "example": "web" }, "protocol": { "type": "string", "enum": [ "http", "tcp" ], "description": "`tcp` balancea a nivel de conexión y sirve para cualquier protocolo. `http` entiende el pedido y es lo que habilita enrutar por dominio." }, "port": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "Puerto de entrada. Único dentro del servicio." }, "tls": { "type": "string", "enum": [ "none", "passthrough", "terminate" ], "description": "Default `none`." }, "domain": { "type": "string", "minLength": 1, "maxLength": 253 }, "algorithm": { "type": "string", "enum": [ "roundrobin", "leastconn", "source" ], "description": "Default `roundrobin`." }, "health_check": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "tcp", "http" ], "description": "Default `tcp`." }, "path": { "type": "string", "minLength": 1, "maxLength": 255, "description": "Obligatorio si `http`." }, "interval_ms": { "type": "integer", "minimum": 200, "maximum": 60000, "description": "Cada cuánto se sondea cada destino. Default 2000." } } }, "backends": { "type": "array", "items": { "type": "object", "properties": { "ip": { "type": "string", "minLength": 1, "maxLength": 253, "description": "IP o nombre de host." }, "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, "weight": { "type": "integer", "minimum": 0, "maximum": 256 } }, "required": [ "ip", "port" ] }, "minItems": 1, "maxItems": 64, "description": "Un listener sin destinos no es representable: mínimo uno." } }, "required": [ "name", "protocol", "port", "backends" ] }, "maxItems": 32, "description": "El conjunto **completo**. Lo que no esté acá se borra, incluidos sus destinos; mandar `[]` deja el balanceador sin nada escuchando. Nombres y puertos son únicos dentro del conjunto." } } ``` ## lb.stats.get GET /v1/load-balancers/{id}/stats → 200 Estado y tráfico por listener Una foto del momento: conexiones en curso y bytes acumulados desde el último arranque del balanceador, más la salud de cada destino según el último sondeo. No hay serie histórica. Si el balanceador no contesta el sondeo, los listeners igual aparecen —salen de la configuración guardada— con `state: unknown` y los contadores en cero. Un listener que existe y no responde y uno que existe sin tráfico no se distinguen por los contadores: miralos por `state`. Scope: `lb:read` Parametros: - `id` (requerido): string ## mailgateway.domains.create POST /v1/mail-gateway/domains → 201 Agregar un dominio de envío Devuelve **los registros DNS que hay que publicar** en la zona del dominio. Eso es lo importante de esta llamada: hasta que estén publicados y SES los vea, el dominio no verifica y no se puede enviar desde él. Cada registro trae `purpose`, `type`, `host`, `value` y `status`; `purpose` es la clave estable con la que automatizar la publicación. La verificación es **asíncrona y del lado de SES**: esta llamada no espera. Consultá el estado con `POST /v1/mail-gateway/domains/{domain}/verify`. Es idempotente: repetirla sobre un dominio ya dado de alta reusa el mismo par de llaves DKIM y devuelve los mismos registros, así que un reintento no invalida lo ya publicado. Scope: `mailgateway:write` Body: ```json { "domain": { "type": "string", "minLength": 4, "maxLength": 253, "description": "Dominio de envío. Se normaliza a minúsculas. Tenés que poder editar su DNS: el alta devuelve los registros a publicar.", "example": "ejemplo.com" } } ``` ## mailgateway.domains.delete DELETE /v1/mail-gateway/domains/{domain} → 204 Quitar un dominio de envío Corta el envío desde ese dominio: sale de la política del SMTP y de las keys. Los registros DNS quedan publicados en tu zona; borrarlos es cosa tuya. Volver a agregarlo genera llaves DKIM nuevas, así que el TXT viejo deja de servir. Scope: `mailgateway:write` Parametros: - `domain` (requerido): string ## mailgateway.domains.list GET /v1/mail-gateway/domains → 200 Listar los dominios de envío Cada dominio viene con los registros DNS que le corresponden y el estado de cada uno. Solo se puede enviar desde un dominio `verified`. Scope: `mailgateway:read` Parametros: - `limit`: string - `cursor`: string ## mailgateway.domains.verify POST /v1/mail-gateway/domains/{domain}/verify → 200 Consultar la verificación de un dominio **Consulta, no fuerza.** SES revisa el DNS público por su cuenta y a su ritmo; esto lee ese resultado y actualiza el estado del dominio y el de cada registro. Que vuelva `pending` no es un error: significa que SES todavía no vio los registros, sea porque no propagaron o porque falta publicarlos. Los registros `dkim` y `mail_from_mx` son los que SES verifica. `spf`, `mail_from_spf` y `dmarc` quedan siempre en `info`: mejoran la entrega, pero no hay quien los chequee. `verified_at` viene en `null` en esta respuesta aunque el estado sea `verified`; el dato está en `GET /v1/mail-gateway/domains`. Scope: `mailgateway:write` Parametros: - `domain` (requerido): string ## mailgateway.keys.create POST /v1/mail-gateway/keys → 201 Crear una API key de envío Devuelve la key completa en `secret`, **una sola vez**: guardamos su hash, así que no hay forma de volver a mostrarla. Si se pierde, se crea otra y se revoca esta. Esta key **no sirve contra esta API**: se usa en el plano de envío, `POST {api_endpoint}/emails` (hoy `https://mg.truo.cloud/v1/emails`), con `Authorization: Bearer mg_live_…`. El envío no pasa por `api.truo.cloud` a propósito: un salto de más en el camino del correo es un modo de falla de más. Pide `mailgateway:send` y no `mailgateway:write` porque emitir esta credencial **es** poder enviar en nombre de la cuenta, y eso sobrevive a que revoquen la key de esta API. Scope: `mailgateway:send` ## mailgateway.keys.delete DELETE /v1/mail-gateway/keys/{key_id} → 204 Revocar una API key de envío Efecto casi inmediato: la key sale del índice del gateway. Lo que ya se aceptó, se entrega. Revocar es `write` y no `send` a propósito: quitarle capacidad de envío a la cuenta no debería exigir el scope que **otorga** capacidad de envío. Scope: `mailgateway:write` Parametros: - `key_id` (requerido): string ## mailgateway.keys.list GET /v1/mail-gateway/keys → 200 Listar las API keys de envío Incluye las revocadas, para que se pueda auditar qué hubo. `secret` siempre es `null`: de la key guardamos su hash y no hay forma de recuperarla. Scope: `mailgateway:read` Parametros: - `limit`: string - `cursor`: string ## mailgateway.messages.list GET /v1/mail-gateway/messages → 200 Listar los mensajes enviados Un elemento por mensaje, del más reciente al más viejo, con el estado agregado del evento de mayor severidad que se vio (`bounced` gana a `delivered`). Se retienen 90 días. Sin `total`: el backend no sabe cuántos mensajes matchean sin recorrer la historia entera, y ninguna colección de `/v1` publica totales. Paginá con `next_cursor`. Scope: `mailgateway:read` Parametros: - `limit`: string - `cursor`: string - `recipient`: string - `days`: string ## mailgateway.metrics.get GET /v1/mail-gateway/metrics → 200 Métricas de entrega y reputación Tasas de entrega, apertura, rebote y queja del rango, con la serie diaria, la latencia envío→entrega y el desglose por dominio. Las tasas son fracciones (0–1), no porcentajes. `bounce_rate_limit` y `complaint_rate_limit` son los umbrales de SES: cruzarlos suspende el envío para proteger la reputación compartida. Scope: `mailgateway:read` Parametros: - `range`: string — uno de `7d`, `30d`, `90d` ## mailgateway.smtp.get GET /v1/mail-gateway/smtp → 200 Ver la configuración SMTP Host, puerto, usuario y estado, **sin la password**. Es lo que hace falta para configurar o revisar un cliente de correo sin manipular el secreto. Para la password, `POST /v1/mail-gateway/smtp`. Scope: `mailgateway:read` ## mailgateway.smtp.reveal POST /v1/mail-gateway/smtp → 200 Revelar la password SMTP Devuelve la password SMTP en claro. **Es un POST a propósito, aunque no cambie nada.** El backend la expone en un GET, y un GET que devuelve un secreto queda en el historial del navegador, en la caché de cualquier proxy y en el `curl` de ayer. Un POST obliga a una acción deliberada, no es cacheable, y entra al audit log como mutación — que es exactamente cómo hay que poder auditar "quién sacó la clave de envío y cuándo". La password es recuperable (se guarda cifrada, no hasheada) porque un servidor de correo la necesita entera en cada conexión. Si la comprometieron, no alcanza con dejar de mirarla: rotala con `POST /v1/mail-gateway/smtp/rotate`. Scope: `mailgateway:send` ## mailgateway.smtp.rotate POST /v1/mail-gateway/smtp/rotate → 200 Rotar la credencial SMTP Emite usuario y password nuevos y devuelve los dos. La credencial anterior queda desactivada, no borrada, para que una aplicación que todavía la tenga en memoria no se caiga en el instante de la rotación — pero dejará de funcionar, así que actualizá tus sistemas. No hay vuelta atrás: la vieja no se puede reactivar desde acá. Scope: `mailgateway:send` ## mailgateway.tenant.get GET /v1/mail-gateway → 200 Obtener el Mail Gateway de la cuenta No lleva id: hay un solo Mail Gateway por cuenta. Trae el estado, el uso del mes y cuántos dominios y keys hay; el detalle de cada uno tiene su propio endpoint. Si la cuenta no tiene el servicio, devuelve 404. Scope: `mailgateway:read` ## mailgateway.usage.get GET /v1/mail-gateway/usage → 200 Uso del mes en curso Envíos aceptados y rechazados del mes calendario UTC en curso. Es el número que factura. Los rechazados no se cobran: son los que el gateway frenó antes de SES. Scope: `mailgateway:read` ## meta.capabilities GET /v1/meta/capabilities → 200 Qué soporta esta instancia de la API No requiere autenticación. Devuelve los recursos disponibles, la taxonomía de scopes y los límites vigentes, para que un cliente no tenga que descubrirlos a los golpes. ## objectstorage.buckets.create POST /v1/object-storage/buckets → 201 Crear un bucket Devuelve el mismo recurso que `GET /v1/object-storage/buckets/{bucket}`. El alta del backend responde la fila cruda del registro —otra forma, con otro formato de fecha— así que se relee antes de contestar: cuesta una llamada y compra que el alta y la lectura devuelvan el mismo objeto. Scope: `objectstorage:write` Body: ```json { "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$", "example": "respaldos" }, "access": { "type": "string", "enum": [ "private", "public" ], "description": "Default `private`." } } ``` ## objectstorage.buckets.delete DELETE /v1/object-storage/buckets/{bucket} → 204 Borrar un bucket Un bucket con objetos no se borra: el request falla y no toca nada. `?purge=true` lo borra con todo el contenido, y eso **no se puede deshacer** — no hay papelera ni versiones. Si querés saber cuántos objetos se van a perder, vaciálo primero con `POST .../empty`, que devuelve la cuenta. Scope: `objectstorage:write` Parametros: - `bucket` (requerido): string - `purge`: string — uno de `true`, `false` ## objectstorage.buckets.empty POST /v1/object-storage/buckets/{bucket}/empty → 200 Vaciar un bucket Borra todos los objetos y conserva el bucket con su configuración. **No se puede deshacer.** Sobre un bucket grande puede tardar: el borrado va objeto por objeto contra el almacenamiento. Scope: `objectstorage:write` Parametros: - `bucket` (requerido): string ## objectstorage.buckets.get GET /v1/object-storage/buckets/{bucket} → 200 Obtener un bucket Scope: `objectstorage:read` Parametros: - `bucket` (requerido): string ## objectstorage.buckets.list GET /v1/object-storage/buckets → 200 Listar los buckets Incluye los buckets creados directamente por el protocolo S3, que no tienen fila de registro: se listan igual —ocultarlos escondería datos que existen— con `created_at` en `null` y acceso privado. Scope: `objectstorage:read` Parametros: - `limit`: string - `cursor`: string ## objectstorage.buckets.metrics GET /v1/object-storage/buckets/{bucket}/metrics → 200 Métricas de un bucket Almacenamiento, egress y requests del rango pedido. Las series traen un punto por día UTC y vienen vacías mientras no haya datos, en vez de rellenarse con ceros que se confundirían con un día sin tráfico. Scope: `objectstorage:read` Parametros: - `bucket` (requerido): string - `range`: string — uno de `7d`, `30d`, `90d` ## objectstorage.buckets.update PATCH /v1/object-storage/buckets/{bucket} → 200 Cambiar la visibilidad de un bucket Publicar el bucket le acuña una URL de lectura anónima (`public_url`) y la conserva si después se vuelve privado: republicar devuelve la misma URL, no una nueva. Scope: `objectstorage:write` Parametros: - `bucket` (requerido): string Body: ```json { "access": { "type": "string", "enum": [ "private", "public" ], "description": "`public` publica el bucket en una URL de solo lectura (`public_url`). `private` la retira: los objetos siguen accesibles con llave o con una URL prefirmada." } } ``` ## objectstorage.keys.create POST /v1/object-storage/keys → 201 Emitir una llave de acceso Es el **único** endpoint que devuelve `secret_access_key`, y lo devuelve una sola vez: no se guarda en claro de nuestro lado y no hay forma de recuperarlo después. Si se pierde, la salida es borrar la llave y emitir otra. Las llaves conviven: emitir una no revoca las anteriores. Acotá cada una a un bucket con `scope` para que perder una no comprometa el resto. Scope: `objectstorage:keys` Body: ```json { "name": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Para reconocerla después. No tiene efecto sobre los permisos.", "example": "backups-produccion" }, "scope": { "type": "string", "description": "Nombre de bucket para acotar la llave, o `*` (default) para todos. Una llave por bucket es lo que hace que perder una no comprometa el resto.", "example": "respaldos" }, "permission": { "type": "string", "enum": [ "read", "readwrite", "full" ], "description": "Default `readwrite`." } } ``` ## objectstorage.keys.delete DELETE /v1/object-storage/keys/{key_id} → 204 Revocar una llave de acceso La revocación es inmediata. Revocar una llave **invalida también las URLs prefirmadas que se firmaron con ella**, aunque no hayan expirado: la firma se valida contra la llave, y una llave revocada ya no existe. Es la única forma de cortar una URL prefirmada antes de tiempo. Scope: `objectstorage:keys` Parametros: - `key_id` (requerido): string ## objectstorage.keys.list GET /v1/object-storage/keys → 200 Listar las llaves de acceso Solo las activas, y nunca el secreto. Scope: `objectstorage:read` Parametros: - `limit`: string - `cursor`: string ## objectstorage.objects.delete POST /v1/object-storage/buckets/{bucket}/objects/delete → 200 Borrar objetos Borrado en lote por key. Es un `POST` y no un `DELETE` porque la lista de keys va en el cuerpo: un `DELETE` con body no lo mandan igual todos los clientes HTTP. **No se puede deshacer.** `deleted` puede ser menor que la cantidad de keys pedidas: las que no existían no cuentan. Scope: `objectstorage:write` Parametros: - `bucket` (requerido): string Body: ```json { "keys": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "maxItems": 1000, "description": "Keys relativas al bucket. Una key que no existe no es un error: no se cuenta.", "example": [ "fotos/logo.png" ] } } ``` ## objectstorage.objects.list GET /v1/object-storage/buckets/{bucket}/objects → 200 Listar objetos de un bucket Un nivel a la vez, como un explorador de archivos: las entradas con `is_folder: true` son prefijos, y se navegan pasando su `key` como `prefix`. No acepta `limit`: el backend fija el tamaño de página (hasta 1000 entradas) y recortar acá perdería objetos en silencio al avanzar el cursor. Scope: `objectstorage:read` Parametros: - `bucket` (requerido): string - `prefix`: string - `cursor`: string ## objectstorage.objects.presign POST /v1/object-storage/buckets/{bucket}/presign → 200 Firmar una URL temporal Devuelve un link que funciona sin credenciales hasta que expira. `method: "GET"` para descargar (requiere `objectstorage:read`), `method: "PUT"` para subir (requiere `objectstorage:write`). La URL es una credencial de portador: funciona para cualquiera que la tenga y la única forma de cortarla antes de que venza es revocar la llave S3 que la firmó. Pedí el TTL más corto que te sirva. Hereda además el alcance de esa llave: si está acotada a un bucket o es de solo lectura, la URL no puede más que ella. Scope: `objectstorage:read` Parametros: - `bucket` (requerido): string Body: ```json { "key": { "type": "string", "minLength": 1, "description": "Key relativa al bucket.", "example": "fotos/logo.png" }, "method": { "type": "string", "enum": [ "GET", "PUT" ], "description": "Qué habilita la URL: `GET` descarga, `PUT` sube. Default `GET`. Firmar un `PUT` requiere `objectstorage:write`." }, "expires_in": { "type": "integer", "minimum": 1, "maximum": 604800, "description": "Segundos de validez, 1–604800 (7 días). Default 900." } } ``` ## objectstorage.tenant.get GET /v1/object-storage → 200 Obtener el Object Storage de la cuenta Uso, endpoint y estado. Es singleton por cuenta: no hay listado ni id que pasar. El almacenamiento y el conteo de objetos salen del último snapshot diario, no de un escaneo en vivo, así que un objeto recién subido puede tardar en reflejarse en los totales. Scope: `objectstorage:read` ## operations.get GET /v1/operations/{id} → 200 Estado de una operación Se consulta el backend real al leer, con caché de 2 s. Si el backend no responde se devuelve el último estado conocido con `stale: true` y **nunca un 500**: un cliente haciendo polling no debe perder su operación por un hipo del backend, ni ser empujado a reintentar la mutación. Scope: `operations:read` Parametros: - `id` (requerido): string ## operations.list GET /v1/operations → 200 Listar operaciones recientes de la cuenta Scope: `operations:read` Parametros: - `limit`: string - `cursor`: string ## services.get GET /v1/services/{id} → 200 Obtener un servicio Un 404 acá significa tanto "no existe" como "existe pero esta credencial no puede verlo". Es deliberado: un 403 confirmaría la existencia del servicio y convertiría la API en un oráculo de enumeración. Scope: `services:read` Parametros: - `id` (requerido): string ## services.list GET /v1/services → 200 Listar los servicios de la cuenta Devuelve solo los servicios que esta credencial puede ver: se aplican la allowlist de la key y los permisos por servicio del usuario dueño. Es el punto de entrada para obtener los `service_id` que usan los demás recursos. Scope: `services:read` Parametros: - `limit`: string - `cursor`: string - `family`: string — uno de `vps`, `dns`, `dbaas`, `caas`, `lb`, `objectstorage`, `mailgateway`, `other` ## vps.backups.create POST /v1/vps/{id}/backups → 202 Crear un backup Encola un `vzdump`. Con `mode: snapshot` (default) la máquina sigue andando. La operación refleja que la tarea quedó encolada, no que el archivo esté listo: el tamaño final aparece en `GET /v1/vps/{id}/backups` cuando el hipervisor termina. Scope: `vps:write` Parametros: - `id` (requerido): string Body: ```json { "compress": { "type": "string", "enum": [ "zstd", "gzip", "lzo", "none" ] }, "mode": { "type": "string", "enum": [ "snapshot", "suspend", "stop" ], "description": "`snapshot` no interrumpe el servicio. `stop` apaga la VM durante el backup." }, "storage": { "type": "string", "maxLength": 64 } } ``` ## vps.backups.delete DELETE /v1/vps/{id}/backups/{backup_id} → 204 Borrar un backup Scope: `vps:write` Parametros: - `id` (requerido): string - `backup_id` (requerido): string ## vps.backups.list GET /v1/vps/{id}/backups → 200 Backups del VPS Scope: `vps:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## vps.backups.restore POST /v1/vps/{id}/backups/{backup_id}/restore → 202 Restaurar un backup **Destructivo.** Apaga la máquina y sobreescribe el disco entero: todo lo escrito después de ese backup se pierde. El backend verifica que el backup pertenezca a este VPS antes de tocar nada. Scope: `vps:write` Parametros: - `id` (requerido): string - `backup_id` (requerido): string ## vps.config.get GET /v1/vps/{id}/config → 200 Configuración de la máquina Scope: `vps:read` Parametros: - `id` (requerido): string ## vps.console.create POST /v1/vps/{id}/console → 200 Abrir una consola Emite un ticket de un solo uso. **Da acceso total al sistema operativo**, sin pasar por la red ni por SSH, y por eso vive en su propio scope (`vps:console`) en vez de caer bajo `vps:write`. No lo loguees: el `file` de SPICE lleva la contraseña adentro. Scope: `vps:console` Parametros: - `id` (requerido): string Body: ```json { "type": { "type": "string", "enum": [ "vnc", "spice" ], "default": "vnc" } } ``` ## vps.get GET /v1/vps/{id} → 200 Obtener un VPS con su estado real Consulta el hipervisor. Si no responde, los campos de estado vuelven en `null` en vez de fallar: que el hipervisor tenga un hipo no debería impedirte leer el resto del recurso ni sus `capabilities`. Scope: `vps:read` Parametros: - `id` (requerido): string ## vps.ips.list GET /v1/vps/{id}/ips → 200 IPs asignadas al VPS Scope: `vps:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## vps.list GET /v1/vps → 200 Listar los VPS de la cuenta Sale de la base, sin consultar el hipervisor: `state`, `cpu`, `memory` y `disk` vienen en `null`. Traerlos costaría una llamada al backend por cada elemento de la página. Para el estado vivo de uno, `GET /v1/vps/{id}`. Scope: `vps:read` Parametros: - `limit`: string - `cursor`: string ## vps.metrics.list GET /v1/vps/{id}/metrics → 200 Serie de uso de CPU, memoria, disco y red Serie RRD del hipervisor. La resolución la fija el `timeframe` y no es configurable: `hour` da minutos, `year` da semanas. `cpu_percent` es porcentaje del total asignado. Scope: `vps:read` Parametros: - `id` (requerido): string - `timeframe`: string — uno de `hour`, `day`, `week`, `month`, `year` ## vps.power POST /v1/vps/{id}/power → 202 Encender, apagar o reiniciar Devuelve `202` en cuanto la orden sale, no cuando la máquina llegó al estado pedido. La operación se resuelve contra el **estado real de la VM**, así que sobrevive a que el backend tarde más que el timeout HTTP — un `reboot` es apagar, esperar y encender, y eso no entra en una request. Esperá con `GET /v1/operations/{id}`. Scope: `vps:power` Parametros: - `id` (requerido): string Body: ```json { "action": { "type": "string", "enum": [ "start", "stop", "shutdown", "reboot" ], "description": "`shutdown` pide un apagado ordenado al sistema operativo y cae a corte duro si no responde. `stop` corta la energía de una: puede corromper el sistema de archivos." } } ``` ## vps.reinstall POST /v1/vps/{id}/reinstall → 202 Reinstalar el sistema operativo **Destructivo e irreversible: borra el disco entero.** Encola un job que corre la misma máquina de estados que un alta nueva (aprovisionar → esperar el boot → chequeo de salud), así que la operación reporta progreso real y puede tardar varios minutos. La IP se conserva. Scope: `vps:write` Parametros: - `id` (requerido): string Body: ```json { "template": { "type": "string", "minLength": 1, "description": "Un `id` de `GET /v1/vps/{id}/templates`." }, "root_password": { "type": "string", "minLength": 8, "maxLength": 128, "description": "Password de root del sistema nuevo. No se guarda ni se devuelve nunca: si se pierde, la única salida es otra reinstalación." } } ``` ## vps.templates.list GET /v1/vps/{id}/templates → 200 Sistemas operativos disponibles para reinstalar Depende del tipo de máquina y del nodo donde vive, así que se pide por VPS y no global. Scope: `vps:read` Parametros: - `id` (requerido): string - `limit`: string - `cursor`: string ## vps.update PATCH /v1/vps/{id} → 200 Renombrar un VPS Cambia el hostname del sistema operativo. En LXC toma efecto al vuelo; en KVM cambia el nombre de la VM y el sistema operativo lo adopta al reiniciar. Scope: `vps:write` Parametros: - `id` (requerido): string Body: ```json { "hostname": { "type": "string", "minLength": 1, "maxLength": 253, "description": "Etiqueta simple o FQDN. El backend valida el formato." } } ```