{"openapi":"3.1.0","info":{"title":"TruoCloud API","version":"1.0.0","description":"API pública de TruoCloud. Todos los recursos de infraestructura de una cuenta —VPS, DNS, bases de datos, contenedores, balanceadores, Object Storage y Mail Gateway— gestionables por HTTP, por el CLI `truo` y por MCP.\n\n## Autenticación\n\nBearer token: `Authorization: Bearer tc_live_...`. Las keys se crean desde el panel o con `POST /v1/api-keys` desde una sesión.\n\nUna API key **nunca** puede crear, modificar ni revocar API keys, ni gestionar usuarios — ni siquiera con scope `*`. Eso es lo que hace que revocar una key sea definitivo.\n\nLa autoridad efectiva de una key es la intersección de cuatro cosas: sus scopes, los permisos del usuario que la creó, los permisos por servicio de ese usuario, y la allowlist de servicios de la key. Si al usuario que creó la key le revocan un permiso, la key lo pierde también.\n\n## Convenciones\n\n- Los ids se devuelven con prefijo tipado (`svc_10432`); se aceptan con o sin él.\n- Paginación por cursor opaco (`?limit=&cursor=`). No hay total count.\n- Toda mutación acepta `Idempotency-Key`. Solo se guardan las respuestas 2xx.\n- Toda respuesta trae `X-Request-Id`; citalo al reportar un problema.\n- Lo que tarda devuelve `202` + `Location: /v1/operations/{id}`.","contact":{"name":"Soporte TruoCloud","url":"https://www.truocloud.com"}},"servers":[{"url":"https://api.truo.cloud","description":"Producción"}],"tags":[{"name":"Meta","description":"Qué soporta esta instancia."},{"name":"Account","description":"La cuenta y la credencial en uso."},{"name":"API Keys","description":"Credenciales. Requiere sesión."},{"name":"Services","description":"Servicios contratados. Punto de entrada al resto."},{"name":"Operations","description":"Trabajo asíncrono."},{"name":"Audit","description":"Actividad de API de la cuenta."}],"externalDocs":{"description":"Documentación","url":"https://docs.truo.cloud"},"components":{"schemas":{"Capabilities":{"type":"object","properties":{"object":{"type":"string","enum":["capabilities"]},"api_version":{"type":"string","example":"v1"},"brand":{"type":"string","example":"truocloud"},"resources":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}},"scopes_never_grantable_to_keys":{"type":"array","items":{"type":"string"},"description":"Estos scopes existen solo para sesiones. Ninguna API key puede tenerlos, ni bajo `*`."},"limits":{"type":"object","properties":{"max_page_size":{"type":"integer"},"default_page_size":{"type":"integer"},"rate_limits":{"type":"object","additionalProperties":{"type":"object","properties":{"per_key":{"type":"integer"},"per_account":{"type":"integer"}},"required":["per_key","per_account"]}}},"required":["max_page_size","default_page_size","rate_limits"]},"features":{"type":"object","additionalProperties":{"type":"boolean"}}},"required":["object","api_version","brand","resources","scopes","scopes_never_grantable_to_keys","limits","features"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["authentication_error","authorization_error","invalid_request_error","rate_limit_error","api_error"],"description":"Clase gruesa del error."},"code":{"type":"string","description":"Identidad específica y estable del error. Nunca se renombra.","example":"insufficient_scope"},"message":{"type":"string","description":"Explicación en prosa, apta para mostrar."},"param":{"type":["string","null"],"description":"Campo que falló, en notación de punto.","example":"records.0.ttl"},"request_id":{"type":"string","example":"req_01JQ8XKM4N7P2R9TAB12CD34EF"},"required_scope":{"type":"string"},"retry_after":{"type":"integer"},"errors":{"type":"array","items":{"type":"object","properties":{"param":{"type":"string"},"message":{"type":"string"}},"required":["param","message"]},"description":"Detalle por campo cuando `code` es `validation_failed`."}},"required":["type","code","message","param","request_id"]}},"required":["error"]},"Account":{"type":"object","properties":{"object":{"type":"string","enum":["account"]},"id":{"type":"string","example":"cus_1043"},"name":{"type":["string","null"]},"company":{"type":["string","null"]},"email":{"type":["string","null"]},"country":{"type":["string","null"]},"status":{"type":["string","null"]},"credit":{"type":["number","null"],"description":"Saldo a favor, en la moneda de la cuenta."},"credential":{"type":"object","properties":{"object":{"type":"string","enum":["credential"]},"id":{"type":["string","null"],"example":"key_42"},"type":{"type":"string","enum":["api_key","session"]},"scopes":{"type":"array","items":{"type":"string"}},"service_allowlist":{"type":"array","items":{"type":"string"},"description":"Vacío = la credencial alcanza todos los servicios de la cuenta."},"is_owner":{"type":"boolean","description":"true si el usuario dueño de la credencial es el owner de la cuenta."},"rate_limit_tier":{"type":["string","null"]}},"required":["object","id","type","scopes","service_allowlist","is_owner","rate_limit_tier"]}},"required":["object","id","name","company","email","country","status","credit","credential"]},"ApiKey":{"type":"object","properties":{"object":{"type":"string","enum":["api_key"]},"id":{"type":"string","example":"key_42"},"name":{"type":["string","null"]},"token_prefix":{"type":["string","null"],"example":"tc_live_aB3d"},"token_last4":{"type":["string","null"],"example":"Xy7Q"},"scopes":{"type":"array","items":{"type":"string"}},"service_allowlist":{"type":"array","items":{"type":"string"},"description":"Vacío = la key alcanza todos los servicios de la cuenta."},"rate_limit_tier":{"type":["string","null"]},"last_used_at":{"type":["string","null"]},"expires_at":{"type":["string","null"]},"created_at":{"type":["string","null"]},"revoked_at":{"type":["string","null"]}},"required":["object","id","name","token_prefix","token_last4","scopes","service_allowlist","rate_limit_tier","last_used_at","expires_at","created_at","revoked_at"]},"ApiKeyList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"ApiKeyCreated":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","properties":{"token":{"type":"string","description":"El token en claro. **Se muestra una sola vez**: no se almacena, solo su hash SHA-256. Si se pierde, hay que crear otra key.","example":"tc_live_aB3dEf..."}},"required":["token"]}]},"ApiKeyCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255,"example":"terraform-prod"},"scopes":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Scopes concretos. `*` no se acepta: enumerá lo que la key necesita. `apikeys:*` y `users:*` no son otorgables.","example":["services:read","vps:read","vps:power"]},"service_allowlist":{"type":"array","items":{"type":"string"},"description":"Restringe la key a estos servicios. Omitido o vacío = toda la cuenta.","example":["svc_10432"]},"expires_at":{"type":"string","format":"date-time","description":"Vencimiento en ISO 8601. Sin esto la key no vence."}},"required":["name","scopes"]},"ApiKeyUpdate":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"scopes":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Solo se puede **estrechar**. Ampliar devuelve 403."},"service_allowlist":{"type":"array","items":{"type":"string"},"description":"Solo se puede estrechar: una key sin restricción puede ganarla, nunca perderla."}}},"Service":{"type":"object","properties":{"object":{"type":"string","enum":["service"]},"id":{"type":"string","example":"svc_10432"},"family":{"type":"string","enum":["vps","dns","dbaas","caas","lb","objectstorage","mailgateway","other"],"description":"Familia del servicio. Determina bajo qué recurso se gestiona. `other` es un producto real de la cuenta que todavía no tiene recurso dedicado en v1."},"status":{"type":"string","enum":["active","pending","suspended","terminated","cancelled","fraud","unknown"]},"name":{"type":["string","null"],"description":"Nombre del producto en el catálogo."},"label":{"type":["string","null"],"description":"Etiqueta del servicio: dominio, hostname o identificador que puso el cliente."},"primary_ip":{"type":["string","null"]},"billing_cycle":{"type":["string","null"]},"next_due_date":{"type":["string","null"]},"created_at":{"type":["string","null"]},"capabilities":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Capacidades reales de este servicio. Pedir una operación con capacidad `false` devuelve 400 `unsupported_for_product`."}},"required":["object","id","family","status","name","label","primary_ip","billing_cycle","next_due_date","created_at","capabilities"]},"ServiceList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"Operation":{"type":"object","properties":{"object":{"type":"string","enum":["operation"]},"id":{"type":"string","example":"op_01JQ8XKM4N7P2R9TAB12CD34EF"},"type":{"type":"string","example":"vps.reinstall"},"status":{"type":"string","enum":["pending","running","succeeded","failed"]},"progress":{"type":["integer","null"],"minimum":0,"maximum":100},"resource":{"type":["object","null"],"properties":{"object":{"type":"string"},"id":{"type":"string"}},"required":["object","id"],"description":"Recurso sobre el que corre la operación."},"result":{"description":"Payload del resultado. La forma depende de `type`."},"error":{"description":"Detalle del fallo cuando `status` es `failed`."},"stale":{"type":"boolean","description":"true cuando no se pudo consultar el backend y esto es el último estado conocido. Reintentá: la operación no se perdió."},"created_at":{"type":["string","null"]},"completed_at":{"type":["string","null"]}},"required":["object","id","type","status","progress","resource","created_at","completed_at"]},"OperationList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Operation"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"AuditLog":{"type":"object","properties":{"object":{"type":"string","enum":["audit_log"]},"id":{"type":"string"},"request_id":{"type":"string"},"method":{"type":"string"},"route":{"type":"string","description":"Template de la ruta, no el path concreto. Agregable por definición.","example":"/v1/vps/:id/power"},"path":{"type":"string"},"status":{"type":"integer"},"error_code":{"type":["string","null"]},"scope_required":{"type":["string","null"]},"credential_id":{"type":["string","null"]},"via":{"type":["string","null"],"description":"`api`, `cli` o `mcp`."},"ip":{"type":["string","null"]},"duration_ms":{"type":["integer","null"]},"created_at":{"type":["string","null"]}},"required":["object","id","request_id","method","route","path","status","error_code","scope_required","credential_id","via","ip","duration_ms","created_at"]},"AuditLogList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLog"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"Vps":{"type":"object","properties":{"object":{"type":"string","enum":["vps"]},"id":{"type":"string","example":"svc_10432"},"status":{"type":"string","enum":["active","pending","suspended","terminated","cancelled","fraud","unknown"]},"hostname":{"type":["string","null"]},"type":{"type":["string","null"],"enum":["qemu","lxc"],"description":"`qemu` = virtualización completa (KVM). `lxc` = contenedor."},"node":{"type":["string","null"],"description":"Nodo físico. Informativo: cambia si migramos la VM y no es direccionable."},"vmid":{"type":["integer","null"]},"primary_ip":{"type":["string","null"]},"state":{"type":["string","null"],"enum":["running","stopped","paused","unknown"],"description":"Estado real de la máquina. Distinto de `status`, que es el estado del **contrato** en facturación: un VPS `active` puede estar `stopped` porque el cliente lo apagó."},"uptime_seconds":{"type":["number","null"]},"cpu":{"type":["object","null"],"properties":{"cores":{"type":["number","null"]},"usage_percent":{"type":["number","null"]}},"required":["cores","usage_percent"]},"memory":{"type":["object","null"],"properties":{"used_bytes":{"type":["number","null"]},"total_bytes":{"type":["number","null"]}},"required":["used_bytes","total_bytes"]},"disk":{"type":["object","null"],"properties":{"used_bytes":{"type":["number","null"]},"total_bytes":{"type":["number","null"]}},"required":["used_bytes","total_bytes"]},"reinstalling":{"type":"boolean","description":"Hay una reinstalación en curso. Las operaciones de power fallan mientras dure."},"capabilities":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Qué endpoints de `/v1` responden para ESTE VPS. Una capacidad en `false` devuelve `400 unsupported_for_product`; no es un error transitorio y reintentar no ayuda."}},"required":["object","id","status","hostname","type","node","vmid","primary_ip","state","uptime_seconds","cpu","memory","disk","reinstalling","capabilities"]},"VpsList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Vps"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"VpsConfig":{"type":"object","properties":{"object":{"type":"string","enum":["vps_config"]},"vmid":{"type":["integer","null"]},"node":{"type":["string","null"]},"type":{"type":"string","enum":["qemu","lxc"],"description":"`qemu` = virtualización completa (KVM). `lxc` = contenedor."},"cores":{"type":["number","null"]},"memory_mb":{"type":["number","null"]},"boot_disk":{"type":["string","null"],"description":"Descriptor de almacenamiento tal como lo reporta el hipervisor."},"os_type":{"type":["string","null"]},"hostname":{"type":["string","null"]}},"required":["object","vmid","node","type","cores","memory_mb","boot_disk","os_type","hostname"]},"VpsMetricPoint":{"type":"object","properties":{"time":{"type":"string"},"cpu_percent":{"type":["number","null"]},"memory_bytes":{"type":["number","null"]},"disk_read_bytes":{"type":["number","null"]},"disk_write_bytes":{"type":["number","null"]},"net_in_bytes":{"type":["number","null"]},"net_out_bytes":{"type":["number","null"]}},"required":["time","cpu_percent","memory_bytes","disk_read_bytes","disk_write_bytes","net_in_bytes","net_out_bytes"]},"VpsMetrics":{"type":"object","properties":{"object":{"type":"string","enum":["vps_metrics"]},"timeframe":{"type":"string","enum":["hour","day","week","month","year"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/VpsMetricPoint"}}},"required":["object","timeframe","data"]},"VpsIp":{"type":"object","properties":{"object":{"type":"string","enum":["vps_ip"]},"version":{"anyOf":[{"type":"number","enum":[4]},{"type":"number","enum":[6]}]},"address":{"type":["string","null"]},"netmask":{"type":["string","null"]},"gateway":{"type":["string","null"]},"prefix":{"type":["string","null"]}},"required":["object","version","address","netmask","gateway","prefix"]},"VpsIpList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/VpsIp"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"VpsTemplate":{"type":"object","properties":{"object":{"type":"string","enum":["vps_template"]},"id":{"type":"string","description":"Pasalo como `template` en la reinstalación."},"name":{"type":"string"},"type":{"type":"string","enum":["qemu","lxc"],"description":"`qemu` = virtualización completa (KVM). `lxc` = contenedor."},"storage":{"type":["string","null"]}},"required":["object","id","name","type","storage"]},"VpsTemplateList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/VpsTemplate"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"ConsoleTicket":{"type":"object","properties":{"object":{"type":"string","enum":["console_ticket"]},"type":{"type":"string","enum":["vnc","spice"]},"url":{"type":["string","null"],"description":"noVNC: abrir en el navegador."},"file":{"type":["string","null"],"description":"SPICE: contenido del `.vv` para `remote-viewer`. **Es una credencial**: da acceso total al sistema operativo. No lo loguees ni lo guardes."},"filename":{"type":["string","null"]}},"required":["object","type","url","file","filename"]},"VpsBackup":{"type":"object","properties":{"object":{"type":"string","enum":["vps_backup"]},"id":{"type":"string","example":"local:backup/vzdump-qemu-101-2026_07_27-03_00_01.vma.zst"},"size_bytes":{"type":["number","null"]},"format":{"type":["string","null"]},"storage":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["object","id","size_bytes","format","storage","created_at"]},"VpsBackupList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/VpsBackup"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"DnsZone":{"type":"object","properties":{"object":{"type":"string","enum":["dns_zone"]},"id":{"type":"string","example":"ejemplo.com"},"service":{"type":"string","description":"Servicio que respalda la zona. Solo para facturación y soporte.","example":"svc_10432"},"status":{"type":"string","enum":["active","pending","suspended","terminated","cancelled","fraud","unknown"]},"record_count":{"type":["integer","null"]},"serial":{"type":["string","null"]}},"required":["object","id","service","status","record_count","serial"]},"DnsZoneList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/DnsZone"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"DnsZoneExport":{"type":"object","properties":{"object":{"type":"string","enum":["dns_zone_export"]},"zone":{"type":"string"},"format":{"type":"string","enum":["bind"]},"content":{"type":"string"}},"required":["object","zone","format","content"]},"DnsRecord":{"type":"object","properties":{"object":{"type":"string","enum":["dns_record"]},"id":{"type":"string","example":"www.ejemplo.com/A"},"name":{"type":"string","description":"FQDN sin punto final.","example":"www.ejemplo.com"},"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"},"values":{"type":"array","items":{"type":"string"},"description":"Todos los valores del RRset. Para `MX` incluyen la prioridad (`10 mail.ejemplo.com`); para `SRV`, prioridad, peso y puerto.","example":["1.2.3.4"]}},"required":["object","id","name","type","ttl","values"]},"DnsRecordList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/DnsRecord"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"Dbaas":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas"]},"id":{"type":"string","example":"svc_10432"},"status":{"type":"string","enum":["active","pending","suspended","terminated","cancelled","fraud","unknown"]},"state":{"type":["string","null"],"enum":["running","stopped","provisioning","error","unknown"],"description":"Estado real del motor. Distinto de `status`, que es el estado del **contrato** en facturación: un servicio `active` puede estar `provisioning` los primeros minutos."},"engine":{"type":["string","null"],"description":"Motor: `mysql`, `postgresql`, `mongodb`, `valkey` (o `redis`, histórico).","example":"postgresql"},"engine_version":{"type":["string","null"]},"host":{"type":["string","null"]},"port":{"type":["integer","null"]},"node":{"type":["string","null"],"description":"Nodo físico. Informativo y no direccionable; `null` en el tier compartido."},"region":{"type":["string","null"]},"plan":{"type":["object","null"],"properties":{"cpu_cores":{"type":["number","null"]},"ram_mb":{"type":["number","null"]},"storage_gb":{"type":["number","null"]}},"required":["cpu_cores","ram_mb","storage_gb"]},"created_at":{"type":["string","null"]},"capabilities":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Qué endpoints de `/v1` responden para ESTE servicio. Una capacidad en `false` devuelve `400 unsupported_for_product`: no es transitorio y reintentar no ayuda. Una clave **ausente** es distinta de `false` — significa que no se consultó (ver el listado)."}},"required":["object","id","status","state","engine","engine_version","host","port","node","region","plan","created_at","capabilities"]},"DbaasList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Dbaas"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"DbaasConnection":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas_connection"]},"engine":{"type":["string","null"],"description":"Motor: `mysql`, `postgresql`, `mongodb`, `valkey` (o `redis`, histórico).","example":"postgresql"},"host":{"type":["string","null"]},"port":{"type":["integer","null"]},"database":{"type":["string","null"],"description":"Base por defecto. `null` en los servicios donde el administrador elige al entrar."},"username":{"type":["string","null"],"description":"Usuario administrador del motor."},"sslmode":{"type":["string","null"],"description":"Modo TLS en el vocabulario del motor (`verify-ca`, `VERIFY_CA`, `rediss`).","example":"verify-ca"},"ca_cert":{"type":["string","null"],"description":"PEM de la CA del servicio, para `sslrootcert`/`--ssl-ca`. Es **público**: sirve para verificar al servidor, no para entrar."}},"required":["object","engine","host","port","database","username","sslmode","ca_cert"]},"DbaasCredential":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas_credential"]},"username":{"type":["string","null"]},"password":{"type":["string","null"],"description":"Password del administrador, en claro. **Es acceso total a los datos**: no la loguees, no la guardes en un archivo de configuración compartido y no la pases por la línea de comandos. No se devuelve ninguna URI: una URI con la credencial adentro termina en el historial del shell y en los logs del proceso que la recibe."},"host":{"type":["string","null"]},"port":{"type":["integer","null"]},"database":{"type":["string","null"]}},"required":["object","username","password","host","port","database"]},"DbaasStats":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas_stats"]},"cpu_percent":{"type":["number","null"]},"memory_used_bytes":{"type":["number","null"]},"memory_total_bytes":{"type":["number","null"]},"disk_used_bytes":{"type":["number","null"]},"disk_total_bytes":{"type":["number","null"]},"connections":{"type":["number","null"]},"max_connections":{"type":["number","null"]},"uptime_seconds":{"type":["number","null"]},"database_count":{"type":["number","null"]},"total_size_bytes":{"type":["number","null"]}},"required":["object","cpu_percent","memory_used_bytes","memory_total_bytes","disk_used_bytes","disk_total_bytes","connections","max_connections","uptime_seconds","database_count","total_size_bytes"],"description":"Qué campos vienen llenos depende del backend del servicio: unos miden el **contenedor** (CPU, memoria, disco) y otros el **motor** (conexiones, tamaño, uptime). Un `null` es \"no se mide acá\", no cero."},"DbaasLogs":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas_logs"]},"lines":{"type":"array","items":{"type":"string"},"description":"De la más vieja a la más nueva, como las devuelve el motor."}},"required":["object","lines"]},"DbaasDatabase":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas_database"]},"id":{"type":"string","example":"appdb"},"name":{"type":"string"},"owner":{"type":["string","null"]},"size_bytes":{"type":["number","null"]},"table_count":{"type":["number","null"],"description":"Tablas (o colecciones en MongoDB). `null` cuando el motor no lo reporta."}},"required":["object","id","name","owner","size_bytes","table_count"]},"DbaasDatabaseList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/DbaasDatabase"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"DbaasUser":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas_user"]},"id":{"type":"string","example":"app@%"},"username":{"type":"string"},"host":{"type":["string","null"],"description":"Solo MySQL: desde dónde puede conectarse. `%` es cualquier origen."},"can_login":{"type":["boolean","null"]},"can_create_db":{"type":["boolean","null"]}},"required":["object","id","username","host","can_login","can_create_db"]},"DbaasUserList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/DbaasUser"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"DbaasBackup":{"type":"object","properties":{"object":{"type":"string","enum":["dbaas_backup"]},"id":{"type":"string","example":"20260727-030001F"},"type":{"type":["string","null"],"description":"`full`, `diff`, `incr`, `manual`…"},"size_bytes":{"type":["number","null"]},"location":{"type":["string","null"],"description":"`local`, `s3` o `both`."},"created_at":{"type":["string","null"]}},"required":["object","id","type","size_bytes","location","created_at"]},"DbaasBackupList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/DbaasBackup"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"Caas":{"type":"object","properties":{"object":{"type":"string","enum":["caas"]},"id":{"type":"string","example":"svc_10432"},"status":{"type":"string","enum":["active","pending","suspended","terminated","cancelled","fraud","unknown"]},"label":{"type":["string","null"],"description":"Identificador del servicio."},"primary_ip":{"type":["string","null"]},"provisioning_state":{"type":["string","null"],"enum":["draft","provisioning","active","suspended","terminated","error","unknown"],"description":"Avance del aprovisionamiento. Distinto de `status`, que es el estado del **contrato** en facturación: un servicio `active` en facturación puede seguir en `provisioning` acá. Mientras no esté `active`, los endpoints de apps devuelven `400 unsupported_for_product`."},"machine":{"type":["object","null"],"properties":{"state":{"type":"string","enum":["running","stopped","unknown"]},"uptime_seconds":{"type":["number","null"]}},"required":["state","uptime_seconds"],"description":"Máquina que corre el servicio. `null` en el listado y también cuando todavía no existe — un servicio recién comprado no tiene máquina hasta que el alta termina."},"capabilities":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Qué endpoints de `/v1` responden para ESTE servicio. Una capacidad en `false` devuelve `400 unsupported_for_product`; no es un error transitorio y reintentar no ayuda."}},"required":["object","id","status","label","primary_ip","provisioning_state","machine","capabilities"]},"CaasList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Caas"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"CaasAppSource":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["git","docker_image","other"],"description":"`other` = el origen se configuró por fuera de esta API y no es representable acá."},"ref":{"type":["string","null"],"description":"URL del repositorio, o referencia de la imagen (`nginx:1.27`)."},"branch":{"type":["string","null"]}},"required":["type","ref","branch"],"description":"Origen del código. Viene en `null` en el listado —el backend no lo trae ahí— y también cuando la app se creó sin origen configurado. Usá `GET /v1/caas/{id}/apps/{app_id}`."},"CaasApp":{"type":"object","properties":{"object":{"type":"string","enum":["caas_app"]},"id":{"type":"string","example":"app_9f2c1b7e"},"name":{"type":"string"},"status":{"type":"string","enum":["idle","deploying","deployed","error","unknown"],"description":"En qué quedó el **último despliegue**. No es un healthcheck: un `deployed` no garantiza que el proceso siga vivo ahora. `idle` = nunca se desplegó desde que se creó."},"source":{"$ref":"#/components/schemas/CaasAppSource"}},"required":["object","id","name","status","source"]},"CaasAppList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/CaasApp"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"CaasDeployment":{"type":"object","properties":{"object":{"type":"string","enum":["caas_deployment"]},"id":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","unknown"]},"created_at":{"type":["string","null"]}},"required":["object","id","status","created_at"]},"CaasDeploymentList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/CaasDeployment"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"CaasLogs":{"type":"object","properties":{"object":{"type":"string","enum":["caas_logs"]},"app":{"type":"string"},"content":{"type":"string","description":"Salida tal como la emite la app, con saltos de línea. Puede venir vacía."}},"required":["object","app","content"]},"CaasEnvVar":{"type":"object","properties":{"object":{"type":"string","enum":["caas_env_var"]},"key":{"type":"string","example":"DATABASE_URL"}},"required":["object","key"]},"CaasEnvVarList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/CaasEnvVar"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"CaasDomain":{"type":"object","properties":{"object":{"type":"string","enum":["caas_domain"]},"host":{"type":"string","example":"app.ejemplo.com"},"https":{"type":"boolean"},"certificate_type":{"type":["string","null"],"enum":["none","letsencrypt","custom","unknown"],"description":"Qué emisor está configurado, **no** si el certificado ya se emitió: la emisión es asíncrona y no hay ningún estado ni id que consultar. Viene en `null` en la respuesta del alta, porque en ese momento todavía no hay certificado. La única verificación real de que el dominio quedó sirviendo es una petición HTTPS al host."}},"required":["object","host","https","certificate_type"]},"CaasDomainList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/CaasDomain"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"CaasDatabase":{"type":"object","properties":{"object":{"type":"string","enum":["caas_database"]},"id":{"type":"string"},"engine":{"type":"string","example":"postgres"},"name":{"type":"string"},"status":{"type":"string","enum":["idle","deploying","deployed","error","unknown"],"description":"En qué quedó el **último despliegue**. No es un healthcheck: un `deployed` no garantiza que el proceso siga vivo ahora. `idle` = nunca se desplegó desde que se creó."}},"required":["object","id","engine","name","status"]},"CaasDatabaseList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/CaasDatabase"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"LoadBalancer":{"type":"object","properties":{"object":{"type":"string","enum":["load_balancer"]},"id":{"type":"string","example":"svc_10432"},"status":{"type":"string","enum":["active","pending","suspended","terminated","cancelled","fraud","unknown"]},"provisioning_state":{"type":["string","null"],"enum":["draft","provisioning","active","suspended","terminated","error","unknown"],"description":"Avance del aprovisionamiento. Distinto de `status`, que es el estado del **contrato** en facturación. Mientras no esté `active`, escribir listeners devuelve `400 unsupported_for_product`."},"hostname":{"type":["string","null"],"description":"Nombre estable del balanceador. Apuntá tus registros DNS acá, no a la IP."},"public_ip":{"type":["string","null"],"description":"IP por la que entra el tráfico. Puede moverse a otra máquina ante una falla: es parte de cómo el servicio se recupera, y por eso no es lo que conviene poner en un DNS."},"healthy":{"type":["boolean","null"],"description":"¿Responde el balanceador? `null` = no se pudo verificar, que es distinto de `false`: pasa mientras se aprovisiona. No dice nada sobre la salud de tus destinos — eso está en `GET /v1/load-balancers/{id}/stats`."},"listener_count":{"type":["integer","null"]},"capabilities":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Qué endpoints de `/v1` responden para ESTE servicio. Una capacidad en `false` devuelve `400 unsupported_for_product`."}},"required":["object","id","status","provisioning_state","hostname","public_ip","healthy","listener_count","capabilities"]},"LoadBalancerList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/LoadBalancer"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"LbListener":{"type":"object","properties":{"object":{"type":"string","enum":["lb_listener"]},"id":{"type":"string","example":"web"},"name":{"type":"string"},"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"},"tls":{"type":"string","enum":["none","passthrough","terminate"],"description":"`passthrough` entrega el TLS intacto a tus destinos. `terminate` lo descifra en el balanceador —requiere `protocol: http` y `domain`— y el tráfico hacia tus destinos viaja en claro por la red interna."},"domain":{"type":["string","null"]},"algorithm":{"type":"string","enum":["roundrobin","leastconn","source"],"description":"`source` manda cada IP de origen siempre al mismo destino: es lo que se usa cuando la aplicación guarda sesión en memoria."},"health_check":{"type":"object","properties":{"type":{"type":"string","enum":["tcp","http"]},"path":{"type":["string","null"]},"interval_ms":{"type":"integer"}},"required":["type","path","interval_ms"]},"backends":{"type":"array","items":{"type":"object","properties":{"ip":{"type":"string"},"port":{"type":"integer"},"weight":{"type":["integer","null"]}},"required":["ip","port","weight"]}}},"required":["object","id","name","protocol","port","tls","domain","algorithm","health_check","backends"]},"LbListenerList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/LbListener"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"LbBackend":{"type":"object","properties":{"object":{"type":"string","enum":["lb_backend"]},"id":{"type":"string","example":"web/10.0.0.5/8080"},"listener":{"type":"string"},"ip":{"type":"string","description":"IP o nombre de host del destino."},"port":{"type":"integer"},"weight":{"type":["integer","null"],"description":"Proporción de tráfico frente a los demás destinos del listener. Default 1."}},"required":["object","id","listener","ip","port","weight"]},"LbBackendList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/LbBackend"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"LbStats":{"type":"object","properties":{"object":{"type":"string","enum":["lb_stats"]},"listeners":{"type":"array","items":{"type":"object","properties":{"listener":{"type":"string"},"protocol":{"type":"string","enum":["http","tcp","unknown"]},"port":{"type":["integer","null"]},"state":{"type":"string","enum":["up","down","unknown"]},"sessions":{"type":"integer","description":"Conexiones en curso."},"bytes_in":{"type":"number"},"bytes_out":{"type":"number"},"backends":{"type":"array","items":{"type":"object","properties":{"target":{"type":"string","description":"El destino tal como está declarado, en la forma `host:puerto`.","example":"10.0.0.5:8080"},"health":{"type":"string","enum":["healthy","unhealthy","unknown"]}},"required":["target","health"]}}},"required":["listener","protocol","port","state","sessions","bytes_in","bytes_out","backends"]}}},"required":["object","listeners"]},"ObjectStorage":{"type":"object","properties":{"object":{"type":"string","enum":["object_storage"]},"service":{"type":["string","null"],"example":"svc_10432"},"status":{"type":"string","enum":["active","suspended","error"]},"endpoint":{"type":"string","description":"Endpoint S3. Configurá el cliente con `addressing_style=path`.","example":"s3.truo.cloud"},"storage_bytes":{"type":"integer"},"objects":{"type":"integer"},"buckets":{"type":"integer"},"egress_used_bytes":{"type":"integer","description":"Descargado en los últimos 30 días."},"egress_included_bytes":{"type":"integer","description":"Egress incluido del período, derivado del almacenamiento en uso."},"requests_30d":{"type":"integer"}},"required":["object","service","status","endpoint","storage_bytes","objects","buckets","egress_used_bytes","egress_included_bytes","requests_30d"]},"StorageBucket":{"type":"object","properties":{"object":{"type":"string","enum":["storage_bucket"]},"id":{"type":"string","example":"respaldos"},"name":{"type":"string","example":"respaldos"},"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."},"objects":{"type":"integer"},"size_bytes":{"type":"integer"},"public_url":{"type":["string","null"],"description":"Base de lectura anónima. `null` mientras el bucket sea privado.","example":"https://s3.truo.cloud/_public/k3m9xz4n/"},"created_at":{"type":["string","null"],"description":"Null en un bucket creado por un cliente S3 directo, que no tiene fila de registro."}},"required":["object","id","name","access","objects","size_bytes","public_url","created_at"]},"StorageBucketList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StorageBucket"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"StorageDeletion":{"type":"object","properties":{"object":{"type":"string","enum":["storage_deletion"]},"bucket":{"type":"string"},"deleted":{"type":"integer","description":"Objetos efectivamente borrados."}},"required":["object","bucket","deleted"]},"StorageBucketMetrics":{"type":"object","properties":{"object":{"type":"string","enum":["storage_bucket_metrics"]},"bucket":{"type":"string"},"range":{"type":"string","enum":["7d","30d","90d"]},"storage_bytes":{"type":"integer"},"storage_delta_bytes":{"type":"integer","description":"Diferencia contra el primer snapshot del rango. Negativo si el bucket bajó."},"objects":{"type":"integer"},"objects_delta":{"type":"integer"},"egress_used_bytes":{"type":"integer"},"egress_included_bytes":{"type":"integer"},"requests_30d":{"type":"integer","description":"Siempre 30 días, independiente de `range`."},"get_pct":{"type":"integer","description":"Porcentaje de lecturas sobre el total de requests del rango."},"storage_series":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"bytes":{"type":"integer"}},"required":["date","bytes"]},"description":"Un punto por día UTC. Vacío mientras no haya snapshots."},"requests_series":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"get":{"type":"integer"},"put":{"type":"integer"}},"required":["date","get","put"]},"description":"Un punto por día UTC. Vacío si no hubo tráfico en el rango."},"egress_by_origin":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"bytes":{"type":"integer"}},"required":["label","bytes"]},"description":"Cuánto salió por el cache del edge y cuánto por el origen."}},"required":["object","bucket","range","storage_bytes","storage_delta_bytes","objects","objects_delta","egress_used_bytes","egress_included_bytes","requests_30d","get_pct","storage_series","requests_series","egress_by_origin"]},"StorageObject":{"type":"object","properties":{"object":{"type":"string","enum":["storage_object"]},"id":{"type":"string","example":"fotos/logo.png"},"key":{"type":"string","description":"Relativa al bucket. Las carpetas terminan en `/`.","example":"fotos/logo.png"},"name":{"type":"string","description":"Último segmento de la key.","example":"logo.png"},"is_folder":{"type":"boolean","description":"Una carpeta no es un objeto: es un prefijo común. Se lista para poder navegar, y no tiene tamaño ni fecha."},"content_type":{"type":["string","null"],"description":"Inferido de la extensión, no leído del objeto.","example":"image/png"},"size_bytes":{"type":["integer","null"]},"last_modified":{"type":["string","null"]}},"required":["object","id","key","name","is_folder","content_type","size_bytes","last_modified"]},"StorageObjectList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StorageObject"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"StoragePresignedUrl":{"type":"object","properties":{"object":{"type":"string","enum":["storage_presigned_url"]},"url":{"type":"string"},"method":{"type":"string","enum":["GET","PUT"]},"expires_in":{"type":"integer"},"expires_at":{"type":["string","null"]}},"required":["object","url","method","expires_in","expires_at"]},"StorageAccessKey":{"type":"object","properties":{"object":{"type":"string","enum":["storage_access_key"]},"id":{"type":"string","description":"Hoy coincide con `access_key_id`; tratalo igual como identificador del recurso.","example":"AKIA7QF2K3M9XZ4NPRTV"},"name":{"type":"string"},"access_key_id":{"type":"string","description":"Lo que va en el cliente S3."},"scope":{"type":"string","description":"Bucket al que está acotada la llave, o `*` para todos.","example":"*"},"permission":{"type":"string","enum":["read","readwrite","full"],"description":"`read` solo descarga, `readwrite` sube y borra, `full` agrega la gestión de buckets desde el protocolo S3."},"created_at":{"type":["string","null"]},"last_used":{"type":["string","null"]}},"required":["object","id","name","access_key_id","scope","permission","created_at","last_used"]},"StorageAccessKeyList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/StorageAccessKey"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"StorageAccessKeyWithSecret":{"allOf":[{"$ref":"#/components/schemas/StorageAccessKey"},{"type":"object","properties":{"secret_access_key":{"type":"string","description":"Aparece **una sola vez**, en esta respuesta. No se guarda en claro de nuestro lado y no hay endpoint que lo recupere: si se pierde, la salida es emitir otra llave y rotar la aplicación."}},"required":["secret_access_key"]}]},"MailGatewayUsage":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway_usage"]},"period":{"type":"string","description":"Mes calendario UTC en curso, `YYYY-MM`. Es el período que factura.","example":"2026-07"},"sends":{"type":"integer","description":"Envíos aceptados en el período."},"rejected":{"type":"integer","description":"Envíos rechazados por el gateway antes de llegar a SES (dominio sin verificar, tenant suspendido, payload inválido). No se facturan."}},"required":["object","period","sends","rejected"]},"MailGateway":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway"]},"id":{"type":"string","example":"mg3f9a2b1c4d5e"},"status":{"type":"string","enum":["active","suspended","deprovisioned","unknown"]},"plan":{"type":["string","null"]},"api_endpoint":{"type":"string","description":"Dónde se envía. El envío NO pasa por esta API: se hace contra este endpoint con una API key de `POST /v1/mail-gateway/keys`.","example":"https://mg.truo.cloud/v1"},"config_set":{"type":["string","null"],"description":"Config set de SES del tenant. Es el que reporta los eventos de entrega que alimentan `/metrics` y `/messages`; en `null` significa que el tenant todavía no tiene métricas."},"domain_count":{"type":"integer"},"key_count":{"type":"integer","description":"API keys activas y revocadas."},"usage_month":{"$ref":"#/components/schemas/MailGatewayUsage"},"created_at":{"type":["string","null"]}},"required":["object","id","status","plan","api_endpoint","config_set","domain_count","key_count","usage_month","created_at"]},"MailGatewayMetrics":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway_metrics"]},"range":{"type":"string","enum":["7d","30d","90d"]},"sends":{"type":"integer"},"sends_delta_pct":{"type":["number","null"],"description":"Variación contra el período anterior, en fracción (`0.12` = +12 %). `null` cuando el período anterior no tuvo envíos: dividir por cero no es un 0 %."},"delivery_rate":{"type":"number","description":"Fracción 0–1, no porcentaje."},"open_rate":{"type":"number"},"bounce_rate":{"type":"number"},"complaint_rate":{"type":"number"},"bounce_rate_limit":{"type":"number","description":"Umbral de rebote de SES. Pasarlo pone en riesgo la reputación de envío de toda la plataforma, así que el gateway suspende antes de llegar.","example":0.05},"complaint_rate_limit":{"type":"number","example":0.001},"complaints":{"type":"integer"},"series":{"type":"array","items":{"type":"object","properties":{"date":{"type":["string","null"],"description":"Inicio del día UTC, ISO 8601."},"sends":{"type":"integer"},"opens":{"type":"integer"},"bounces":{"type":"integer"}},"required":["date","sends","opens","bounces"]},"description":"Serie diaria del rango."},"latency":{"type":"array","items":{"type":"object","properties":{"stage":{"type":"string","description":"Etapa medida. Hoy solo `send_to_delivery`.","example":"send_to_delivery"},"p50_seconds":{"type":"number"},"p95_seconds":{"type":"number"},"p99_seconds":{"type":"number"}},"required":["stage","p50_seconds","p95_seconds","p99_seconds"]},"description":"Vacío cuando no hubo entregas en el rango."},"by_domain":{"type":"array","items":{"type":"object","properties":{"domain":{"type":"string"},"sends":{"type":"integer"},"delivery_rate":{"type":"number"},"open_rate":{"type":"number"},"bounce_rate":{"type":"number"}},"required":["domain","sends","delivery_rate","open_rate","bounce_rate"]}}},"required":["object","range","sends","sends_delta_pct","delivery_rate","open_rate","bounce_rate","complaint_rate","bounce_rate_limit","complaint_rate_limit","complaints","series","latency","by_domain"]},"MailGatewayDnsRecord":{"type":"object","properties":{"purpose":{"type":"string","description":"Para qué sirve el registro. Valores actuales: `dkim`, `spf`, `mail_from_mx`, `mail_from_spf`, `dmarc`.","example":"dkim"},"type":{"type":"string","example":"TXT"},"host":{"type":"string","description":"Nombre completo del registro, sin punto final.","example":"mg._domainkey.ejemplo.com"},"value":{"type":"string"},"status":{"type":"string","enum":["pending","verified","info","unknown"],"description":"`info` = no lo verifica nadie, pero publicarlo mejora la entrega. `pending` = SES todavía no lo vio."}},"required":["purpose","type","host","value","status"]},"MailGatewayDomain":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway_domain"]},"id":{"type":"string","example":"ejemplo.com"},"domain":{"type":"string","example":"ejemplo.com"},"status":{"type":"string","enum":["pending","verified","failed","unknown"]},"verified_at":{"type":["string","null"]},"records":{"type":"array","items":{"$ref":"#/components/schemas/MailGatewayDnsRecord"},"description":"Los registros que hay que publicar en la zona del dominio."}},"required":["object","id","domain","status","verified_at","records"]},"MailGatewayDomainList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/MailGatewayDomain"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"MailGatewayMessage":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway_message"]},"id":{"type":"string","example":"0100019a2b3c4d5e-..."},"recipient":{"type":["string","null"]},"subject":{"type":["string","null"]},"status":{"type":"string","enum":["queued","delivered","opened","deferred","bounced","complained","unknown"]},"category":{"type":["string","null"],"description":"La etiqueta `mg-category` que se le puso al envío, si se le puso alguna."},"from_domain":{"type":["string","null"]},"occurred_at":{"type":["string","null"],"description":"Momento del último evento del mensaje, no el del envío."}},"required":["object","id","recipient","subject","status","category","from_domain","occurred_at"]},"MailGatewayMessageList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/MailGatewayMessage"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"MailGatewayKey":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway_key"]},"id":{"type":"string","example":"mgk3f9a2b1c4d5"},"hint":{"type":["string","null"],"description":"Prefijo de display, para reconocer la key sin verla entera.","example":"mg_live_AbCd..."},"status":{"type":"string","enum":["active","revoked","unknown"]},"secret":{"type":["string","null"],"description":"La key completa. **Solo viene en la respuesta del alta y una única vez**: de esta key guardamos su hash, así que no hay forma de recuperarla después —tampoco para nosotros—. En el listado siempre es `null`.","example":"mg_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},"api_endpoint":{"type":"string","description":"Dónde se usa esta key: `POST {api_endpoint}/emails`.","example":"https://mg.truo.cloud/v1"},"created_at":{"type":["string","null"]}},"required":["object","id","hint","status","secret","api_endpoint","created_at"]},"MailGatewayKeyList":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/MailGatewayKey"}},"has_more":{"type":"boolean"},"next_cursor":{"type":["string","null"],"description":"Pasalo como `cursor` para la página siguiente. Es **opaco**: no lo construyas, no lo parsees y no lo guardes entre versiones."}},"required":["object","data","has_more","next_cursor"]},"MailGatewaySmtp":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway_smtp"]},"host":{"type":"string","example":"smtp.mg.truo.cloud"},"port":{"type":"integer","description":"SMTPS, TLS implícito desde el saludo. No hay STARTTLS en 587.","example":465},"username":{"type":"string"},"status":{"type":"string","enum":["active","suspended","revoked","unknown"],"description":"`suspended` = el envío por SMTP está cortado, normalmente por reputación o mora."},"config_set":{"type":["string","null"]}},"required":["object","host","port","username","status","config_set"]},"MailGatewaySmtpCredentials":{"type":"object","properties":{"object":{"type":"string","enum":["mail_gateway_smtp"]},"host":{"type":"string","example":"smtp.mg.truo.cloud"},"port":{"type":"integer","description":"SMTPS, TLS implícito desde el saludo. No hay STARTTLS en 587.","example":465},"username":{"type":"string"},"status":{"type":"string","enum":["active","suspended","revoked","unknown"],"description":"`suspended` = el envío por SMTP está cortado, normalmente por reputación o mora."},"config_set":{"type":["string","null"]},"password":{"type":"string","description":"La password SMTP en claro. Tratala como un secreto: envía en tu nombre."}},"required":["object","host","port","username","status","config_set","password"]}},"parameters":{}},"paths":{"/v1/meta/capabilities":{"get":{"operationId":"meta.capabilities","summary":"Qué soporta esta instancia de la API","description":"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.","tags":["Meta"],"x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-mcp":{"toolset":"meta","action":"capabilities","readonly":true},"responses":{"200":{"description":"Manifiesto de la instancia.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Capabilities"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/account":{"get":{"operationId":"account.get","summary":"Obtener la cuenta y la credencial actual","tags":["Account"],"security":[{"bearerAuth":[]}],"x-truo-scope":"account:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"auth status"},"x-truo-mcp":{"toolset":"meta","action":"whoami","readonly":true},"responses":{"200":{"description":"La cuenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/api-keys":{"get":{"operationId":"apiKeys.list","summary":"Listar las API keys de la cuenta","description":"Solo con sesión. Nunca devuelve tokens: solo prefijo y últimos 4.","tags":["API Keys"],"security":[{"bearerAuth":[]}],"x-truo-scope":"apikeys:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"auth token list"},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Lista de keys activas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"apiKeys.create","summary":"Crear una API key","description":"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.","tags":["API Keys"],"security":[{"bearerAuth":[]}],"x-truo-scope":"apikeys:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"auth token create"},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreate"}}}},"responses":{"201":{"description":"La key creada, con su token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/api-keys/{id}":{"get":{"operationId":"apiKeys.get","summary":"Obtener una API key","tags":["API Keys"],"security":[{"bearerAuth":[]}],"x-truo-scope":"apikeys:read","x-truo-rate-bucket":"read","x-truo-danger":"none","parameters":[{"schema":{"type":"string","example":"key_42"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"La key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"apiKeys.update","summary":"Modificar una API key","description":"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.","tags":["API Keys"],"security":[{"bearerAuth":[]}],"x-truo-scope":"apikeys:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"parameters":[{"schema":{"type":"string","example":"key_42"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyUpdate"}}}},"responses":{"200":{"description":"La key actualizada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/api-keys/{id}/revoke":{"post":{"operationId":"apiKeys.revoke","summary":"Revocar una API key","description":"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).","tags":["API Keys"],"security":[{"bearerAuth":[]}],"x-truo-scope":"apikeys:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"auth token revoke","positional":["key_id"]},"parameters":[{"schema":{"type":"string","example":"key_42"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"La key revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKey"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/services":{"get":{"operationId":"services.list","summary":"Listar los servicios de la cuenta","description":"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.","tags":["Services"],"security":[{"bearerAuth":[]}],"x-truo-scope":"services:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"services list"},"x-truo-mcp":{"toolset":"services","action":"list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","enum":["vps","dns","dbaas","caas","lb","objectstorage","mailgateway","other"],"description":"Familia del servicio. Determina bajo qué recurso se gestiona. `other` es un producto real de la cuenta que todavía no tiene recurso dedicado en v1."},"required":false,"name":"family","in":"query"}],"responses":{"200":{"description":"Lista de servicios.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/services/{id}":{"get":{"operationId":"services.get","summary":"Obtener un servicio","description":"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.","tags":["Services"],"security":[{"bearerAuth":[]}],"x-truo-scope":"services:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"services get","positional":["service_id"]},"x-truo-mcp":{"toolset":"services","action":"get","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"El servicio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Service"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/operations/{id}":{"get":{"operationId":"operations.get","summary":"Estado de una operación","description":"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.","tags":["Operations"],"security":[{"bearerAuth":[]}],"x-truo-scope":"operations:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"operation get","positional":["operation_id"]},"x-truo-mcp":{"toolset":"meta","action":"operation_get","readonly":true},"parameters":[{"schema":{"type":"string","example":"op_01JQ8XKM4N7P2R9TAB12CD34EF"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"La operación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/operations":{"get":{"operationId":"operations.list","summary":"Listar operaciones recientes de la cuenta","tags":["Operations"],"security":[{"bearerAuth":[]}],"x-truo-scope":"operations:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"operation list"},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Operaciones, más recientes primero.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/audit-logs":{"get":{"operationId":"auditLogs.list","summary":"Listar la actividad de API de la cuenta","description":"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.","tags":["Audit"],"security":[{"bearerAuth":[]}],"x-truo-scope":"audit:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"audit list"},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","description":"Filtrar por status HTTP exacto."},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","description":"`true` deja solo los 4xx — los intentos denegados."},"required":false,"name":"denied_only","in":"query"}],"responses":{"200":{"description":"Entradas de auditoría, más recientes primero.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditLogList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps":{"get":{"operationId":"vps.list","summary":"Listar los VPS de la cuenta","description":"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}`.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"vps list"},"x-truo-mcp":{"toolset":"vps","action":"list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"VPS de la cuenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpsList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}":{"get":{"operationId":"vps.get","summary":"Obtener un VPS con su estado real","description":"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`.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"vps get","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"get","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"El VPS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vps"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"vps.update","summary":"Renombrar un VPS","description":"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.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"vps rename","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"rename","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hostname":{"type":"string","minLength":1,"maxLength":253,"description":"Etiqueta simple o FQDN. El backend valida el formato."}},"required":["hostname"]}}}},"responses":{"200":{"description":"El VPS actualizado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Vps"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/power":{"post":{"operationId":"vps.power","summary":"Encender, apagar o reiniciar","description":"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}`.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:power","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-long-running":true,"x-truo-idempotent":true,"x-truo-cli":{"command":"vps power","positional":["service_id","action"]},"x-truo-mcp":{"toolset":"vps","action":"power","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["action"]}}}},"responses":{"202":{"description":"Operación en curso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/config":{"get":{"operationId":"vps.config.get","summary":"Configuración de la máquina","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"vps config","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"config","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Configuración.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpsConfig"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/metrics":{"get":{"operationId":"vps.metrics.list","summary":"Serie de uso de CPU, memoria, disco y red","description":"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.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:read","x-truo-rate-bucket":"expensive","x-truo-danger":"none","x-truo-cli":{"command":"vps stats","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"metrics","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","enum":["hour","day","week","month","year"],"description":"Ventana de la serie. Default `day`."},"required":false,"name":"timeframe","in":"query"}],"responses":{"200":{"description":"Serie temporal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpsMetrics"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/ips":{"get":{"operationId":"vps.ips.list","summary":"IPs asignadas al VPS","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"vps ip list","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"ips","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"IPs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpsIpList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/templates":{"get":{"operationId":"vps.templates.list","summary":"Sistemas operativos disponibles para reinstalar","description":"Depende del tipo de máquina y del nodo donde vive, así que se pide por VPS y no global.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"vps template list","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"templates","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Templates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpsTemplateList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/console":{"post":{"operationId":"vps.console.create","summary":"Abrir una consola","description":"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.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:console","x-truo-rate-bucket":"expensive","x-truo-danger":"reversible","x-truo-cli":{"command":"vps console","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"console","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["vnc","spice"],"default":"vnc"}}}}}},"responses":{"200":{"description":"Ticket de consola.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsoleTicket"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/backups":{"get":{"operationId":"vps.backups.list","summary":"Backups del VPS","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"vps backup list","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"backup_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Backups, del más nuevo al más viejo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VpsBackupList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"vps.backups.create","summary":"Crear un backup","description":"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.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:write","x-truo-rate-bucket":"expensive","x-truo-danger":"reversible","x-truo-long-running":true,"x-truo-idempotent":true,"x-truo-cli":{"command":"vps backup create","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"backup_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"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}}}}}},"responses":{"202":{"description":"Backup encolado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/backups/{backup_id}/restore":{"post":{"operationId":"vps.backups.restore","summary":"Restaurar un backup","description":"**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.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:write","x-truo-rate-bucket":"expensive","x-truo-danger":"destructive","x-truo-long-running":true,"x-truo-idempotent":true,"x-truo-cli":{"command":"vps backup restore","positional":["service_id","backup_id"]},"x-truo-mcp":{"toolset":"vps","action":"backup_restore","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","example":"local:backup/vzdump-qemu-101-2026_07_27-03_00_01.vma.zst"},"required":true,"name":"backup_id","in":"path"}],"responses":{"202":{"description":"Restauración encolada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/backups/{backup_id}":{"delete":{"operationId":"vps.backups.delete","summary":"Borrar un backup","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"vps backup delete","positional":["service_id","backup_id"]},"x-truo-mcp":{"toolset":"vps","action":"backup_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","example":"local:backup/vzdump-qemu-101-2026_07_27-03_00_01.vma.zst"},"required":true,"name":"backup_id","in":"path"}],"responses":{"204":{"description":"Backup borrado."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/vps/{id}/reinstall":{"post":{"operationId":"vps.reinstall","summary":"Reinstalar el sistema operativo","description":"**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.","tags":["VPS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"vps:write","x-truo-rate-bucket":"expensive","x-truo-danger":"destructive","x-truo-long-running":true,"x-truo-idempotent":true,"x-truo-cli":{"command":"vps reinstall","positional":["service_id"]},"x-truo-mcp":{"toolset":"vps","action":"reinstall","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["template","root_password"]}}}},"responses":{"202":{"description":"Reinstalación encolada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dns/zones":{"get":{"operationId":"dns.zones.list","summary":"Listar las zonas DNS de la cuenta","description":"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.","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dns zone list"},"x-truo-mcp":{"toolset":"dns","action":"zone_list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Zonas de la cuenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsZoneList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dns/zones/{zone}":{"get":{"operationId":"dns.zones.get","summary":"Obtener una zona","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dns zone get","positional":["zone"]},"x-truo-mcp":{"toolset":"dns","action":"zone_get","readonly":true},"parameters":[{"schema":{"type":"string","example":"ejemplo.com"},"required":true,"name":"zone","in":"path"}],"responses":{"200":{"description":"La zona.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsZone"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dns/zones/{zone}/export":{"get":{"operationId":"dns.zones.export","summary":"Exportar la zona en formato BIND","description":"El archivo de zona tal como lo emite el backend. Útil para respaldo o migración.","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dns zone export","positional":["zone"]},"x-truo-mcp":{"toolset":"dns","action":"zone_export","readonly":true},"parameters":[{"schema":{"type":"string","example":"ejemplo.com"},"required":true,"name":"zone","in":"path"}],"responses":{"200":{"description":"Zona en formato BIND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsZoneExport"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dns/zones/{zone}/records":{"get":{"operationId":"dns.records.list","summary":"Listar los registros de una zona","description":"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.","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dns record list","positional":["zone"]},"x-truo-mcp":{"toolset":"dns","action":"record_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"ejemplo.com"},"required":true,"name":"zone","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Registros de la zona.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsRecordList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"dns.records.patch","summary":"Aplicar varios cambios a la vez","description":"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ó.","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"dns record apply","positional":["zone"]},"x-truo-mcp":{"toolset":"dns","action":"record_apply","readonly":false},"parameters":[{"schema":{"type":"string","example":"ejemplo.com"},"required":true,"name":"zone","in":"path"},{"schema":{"type":"string","description":"ETag de `GET /v1/dns/zones/{zone}/records`. Si la zona cambió desde entonces devuelve `412` y no aplica nada. Es lo único que evita que dos `apply` concurrentes se pisen: sin esto el último que escribe gana, en silencio."},"required":false,"name":"if-match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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}},"required":["records"]}}}},"responses":{"200":{"description":"La zona después del cambio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsRecordList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"Falló la precondición `If-Match`. Alguien más modificó el recurso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dns/zones/{zone}/records/{name}/{type}":{"get":{"operationId":"dns.records.get","summary":"Obtener un RRset","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dns record get","positional":["zone","name","type"]},"x-truo-mcp":{"toolset":"dns","action":"record_get","readonly":true},"parameters":[{"schema":{"type":"string","example":"ejemplo.com"},"required":true,"name":"zone","in":"path"},{"schema":{"type":"string","description":"Relativo (`www`), `@` para la raíz, o el FQDN completo.","example":"www"},"required":true,"name":"name","in":"path"},{"schema":{"type":"string","example":"A"},"required":true,"name":"type","in":"path"}],"responses":{"200":{"description":"El RRset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsRecord"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"dns.records.put","summary":"Crear o reemplazar un RRset","description":"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`.","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"dns record set","positional":["zone","name","type"]},"x-truo-mcp":{"toolset":"dns","action":"record_set","readonly":false},"parameters":[{"schema":{"type":"string","example":"ejemplo.com"},"required":true,"name":"zone","in":"path"},{"schema":{"type":"string","description":"Relativo (`www`), `@` para la raíz, o el FQDN completo.","example":"www"},"required":true,"name":"name","in":"path"},{"schema":{"type":"string","example":"A"},"required":true,"name":"type","in":"path"},{"schema":{"type":"string","description":"ETag de `GET /v1/dns/zones/{zone}/records`. Si la zona cambió desde entonces devuelve `412` y no aplica nada. Es lo único que evita que dos `apply` concurrentes se pisen: sin esto el último que escribe gana, en silencio."},"required":false,"name":"if-match","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["values"]}}}},"responses":{"200":{"description":"El RRset resultante.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DnsRecord"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"Falló la precondición `If-Match`. Alguien más modificó el recurso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"dns.records.delete","summary":"Borrar un RRset","description":"Borra todos los valores de ese nombre y tipo.","tags":["DNS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dns:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"dns record delete","positional":["zone","name","type"]},"x-truo-mcp":{"toolset":"dns","action":"record_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"ejemplo.com"},"required":true,"name":"zone","in":"path"},{"schema":{"type":"string","description":"Relativo (`www`), `@` para la raíz, o el FQDN completo.","example":"www"},"required":true,"name":"name","in":"path"},{"schema":{"type":"string","example":"A"},"required":true,"name":"type","in":"path"},{"schema":{"type":"string","description":"ETag de `GET /v1/dns/zones/{zone}/records`. Si la zona cambió desde entonces devuelve `412` y no aplica nada. Es lo único que evita que dos `apply` concurrentes se pisen: sin esto el último que escribe gana, en silencio."},"required":false,"name":"if-match","in":"header"}],"responses":{"204":{"description":"RRset borrado."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"Falló la precondición `If-Match`. Alguien más modificó el recurso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas":{"get":{"operationId":"dbaas.instances.list","summary":"Listar las bases de datos gestionadas de la cuenta","description":"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}`.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dbaas list"},"x-truo-mcp":{"toolset":"dbaas","action":"list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Bases de datos de la cuenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}":{"get":{"operationId":"dbaas.instances.get","summary":"Obtener una base de datos con su estado real","description":"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.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dbaas get","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"get","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"La base de datos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Dbaas"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/connection":{"get":{"operationId":"dbaas.connection.get","summary":"Datos de conexión, sin la credencial","description":"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`.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dbaas connection","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"connection","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Datos de conexión.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasConnection"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/credentials":{"post":{"operationId":"dbaas.credentials.create","summary":"Revelar la credencial de administración","description":"Devuelve la password del administrador **en claro**. No rota nada: es la credencial que ya está en uso.\n\nEs 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.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:credentials","x-truo-rate-bucket":"expensive","x-truo-danger":"none","x-truo-cli":{"command":"dbaas credentials","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"credentials","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"La credencial.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasCredential"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/stats":{"get":{"operationId":"dbaas.stats.get","summary":"Métricas de la instancia","description":"Instantánea, no serie temporal. Qué campos vienen llenos depende del backend del servicio: unos miden el contenedor y otros el motor.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"expensive","x-truo-danger":"none","x-truo-cli":{"command":"dbaas stats","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"stats","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Métricas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasStats"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/logs":{"get":{"operationId":"dbaas.logs.get","summary":"Últimas líneas del log del motor","description":"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.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"expensive","x-truo-danger":"none","x-truo-cli":{"command":"dbaas logs","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"logs","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–2000. Default 200.","example":"200"},"required":false,"name":"lines","in":"query"}],"responses":{"200":{"description":"Líneas del log.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasLogs"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/restart":{"post":{"operationId":"dbaas.instances.restart","summary":"Reiniciar el motor","description":"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.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"dbaas restart","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"restart","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"202":{"description":"Reinicio hecho.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/databases":{"get":{"operationId":"dbaas.databases.list","summary":"Listar las bases del servicio","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dbaas database list","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"database_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Bases del servicio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasDatabaseList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"dbaas.databases.create","summary":"Crear una base","description":"`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.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"dbaas database create","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"database_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["name"]}}}},"responses":{"201":{"description":"La base creada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasDatabase"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/databases/{name}":{"delete":{"operationId":"dbaas.databases.delete","summary":"Borrar una base","description":"**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`.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"dbaas database delete","positional":["service_id","name"]},"x-truo-mcp":{"toolset":"dbaas","action":"database_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","example":"appdb"},"required":true,"name":"name","in":"path"}],"responses":{"204":{"description":"Base borrada."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/users":{"get":{"operationId":"dbaas.users.list","summary":"Listar los usuarios del motor","description":"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.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dbaas user list","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"user_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Usuarios del motor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasUserList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"dbaas.users.create","summary":"Crear un usuario","description":"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`.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"dbaas user create","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"user_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["username","password"]}}}},"responses":{"201":{"description":"El usuario creado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasUser"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/users/{username}":{"delete":{"operationId":"dbaas.users.delete","summary":"Borrar un usuario","description":"**Corta el acceso de todo lo que estuviera conectado con ese usuario.** No borra datos: las bases que creó siguen ahí.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"dbaas user delete","positional":["service_id","username"]},"x-truo-mcp":{"toolset":"dbaas","action":"user_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","example":"app"},"required":true,"name":"username","in":"path"},{"schema":{"type":"string","maxLength":60,"pattern":"^[A-Za-z0-9_.:%/-]+$","description":"Solo MySQL, donde el usuario es el par `usuario@host`. Default `%`. En los demás motores se ignora: el nombre alcanza.","example":"%"},"required":false,"name":"host","in":"query"}],"responses":{"204":{"description":"Usuario borrado."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/users/{username}/password":{"post":{"operationId":"dbaas.users.set_password","summary":"Cambiar la password de un usuario","description":"Toma efecto de inmediato: las aplicaciones que sigan usando la anterior van a fallar al reconectar. Sirve también para el usuario administrador.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"dbaas user password","positional":["service_id","username"]},"x-truo-mcp":{"toolset":"dbaas","action":"user_set_password","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","example":"app"},"required":true,"name":"username","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","minLength":12,"maxLength":128}},"required":["password"]}}}},"responses":{"204":{"description":"Password cambiada."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/dbaas/{id}/backups":{"get":{"operationId":"dbaas.backups.list","summary":"Backups del servicio","description":"Del más nuevo al más viejo. Un servicio cuyo motor no tiene backups gestionados devuelve una lista vacía, no un error.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"dbaas backup list","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"backup_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Backups.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbaasBackupList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"dbaas.backups.create","summary":"Crear un backup","description":"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}`.","tags":["DBaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"dbaas:write","x-truo-rate-bucket":"expensive","x-truo-danger":"reversible","x-truo-long-running":true,"x-truo-idempotent":true,"x-truo-cli":{"command":"dbaas backup create","positional":["service_id"]},"x-truo-mcp":{"toolset":"dbaas","action":"backup_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"202":{"description":"Backup en curso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas":{"get":{"operationId":"caas.instances.list","summary":"Listar los servicios CaaS de la cuenta","description":"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.\n\nUna 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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas list"},"x-truo-mcp":{"toolset":"caas","action":"list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Servicios CaaS de la cuenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}":{"get":{"operationId":"caas.instances.get","summary":"Obtener un servicio CaaS con su estado real","description":"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`.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas get","positional":["service_id"]},"x-truo-mcp":{"toolset":"caas","action":"get","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"El servicio CaaS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Caas"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps":{"get":{"operationId":"caas.apps.list","summary":"Listar las apps del servicio","description":"`source` viene en `null`: el backend no lo trae en el listado.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas app list","positional":["service_id"]},"x-truo-mcp":{"toolset":"caas","action":"app_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Apps del servicio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasAppList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"caas.apps.create","summary":"Crear una app","description":"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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"caas app create","positional":["service_id"]},"x-truo-mcp":{"toolset":"caas","action":"app_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["name"]}}}},"responses":{"201":{"description":"La app creada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasApp"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}":{"get":{"operationId":"caas.apps.get","summary":"Obtener una app","description":"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`.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas app get","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"app_get","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"responses":{"200":{"description":"La app.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasApp"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"caas.apps.delete","summary":"Borrar una app","description":"**Destructivo.** Borra la app, sus variables y sus dominios. Los datos de las bases del servicio no se tocan: viven aparte.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"caas app delete","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"app_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"responses":{"204":{"description":"App borrada."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}/deploy":{"post":{"operationId":"caas.apps.deploy","summary":"Desplegar una app","description":"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`.\n\nVive 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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:deploy","x-truo-rate-bucket":"expensive","x-truo-danger":"reversible","x-truo-long-running":true,"x-truo-idempotent":true,"x-truo-cli":{"command":"caas deploy","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"deploy","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"responses":{"202":{"description":"Despliegue en curso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}/restart":{"post":{"operationId":"caas.apps.restart","summary":"Reiniciar una app","description":"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`.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"caas app restart","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"app_restart","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"responses":{"202":{"description":"Reinicio ordenado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}/deployments":{"get":{"operationId":"caas.deployments.list","summary":"Historial de despliegues de una app","description":"Del más reciente al más viejo, según lo devuelve el backend.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas deployment list","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"deployment_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Despliegues.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasDeploymentList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}/logs":{"get":{"operationId":"caas.apps.logs","summary":"Logs de una app","description":"**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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"expensive","x-truo-danger":"none","x-truo-cli":{"command":"caas logs","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"logs","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"responses":{"200":{"description":"Logs de la app.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasLogs"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}/env":{"get":{"operationId":"caas.env.list","summary":"Nombres de las variables de entorno","description":"**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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas env list","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"env_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"responses":{"200":{"description":"Nombres de las variables.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasEnvVarList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"caas.env.replace","summary":"Reemplazar las variables de entorno","description":"**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.\n\nComo `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.\n\nLos cambios toman efecto en el próximo `deploy` o `restart`.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"caas env set","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"env_replace","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["vars"]}}}},"responses":{"200":{"description":"Nombres que quedaron.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasEnvVarList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}/domains":{"get":{"operationId":"caas.domains.list","summary":"Dominios de una app","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas domain list","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"domain_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"responses":{"200":{"description":"Dominios.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasDomainList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"caas.domains.create","summary":"Agregar un dominio a una app","description":"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.\n\nDos cosas más que hay que saber:\n\n- **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.\n- **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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"caas domain add","positional":["service_id","app_id"]},"x-truo-mcp":{"toolset":"caas","action":"domain_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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"}},"required":["host"]}}}},"responses":{"201":{"description":"Dominio agregado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasDomain"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/apps/{app_id}/domains/{host}":{"delete":{"operationId":"caas.domains.delete","summary":"Quitar un dominio de una app","description":"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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"caas domain remove","positional":["service_id","app_id","host"]},"x-truo-mcp":{"toolset":"caas","action":"domain_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"El `id` de un elemento de `GET /v1/caas/{id}/apps`. Opaco.","example":"app_9f2c1b7e"},"required":true,"name":"app_id","in":"path"},{"schema":{"type":"string","example":"app.ejemplo.com"},"required":true,"name":"host","in":"path"}],"responses":{"204":{"description":"Dominio quitado."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/caas/{id}/databases":{"get":{"operationId":"caas.databases.list","summary":"Bases de datos del servicio","description":"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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"caas database list","positional":["service_id"]},"x-truo-mcp":{"toolset":"caas","action":"database_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Bases de datos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasDatabaseList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"caas.databases.create","summary":"Crear una base de datos","description":"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.\n\nBorrar 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.","tags":["CaaS"],"security":[{"bearerAuth":[]}],"x-truo-scope":"caas:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"caas database create","positional":["service_id"]},"x-truo-mcp":{"toolset":"caas","action":"database_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"engine":{"type":"string","enum":["postgres","mysql","mariadb","mongo","redis"]},"name":{"type":"string","minLength":1,"maxLength":63,"example":"principal"}},"required":["engine","name"]}}}},"responses":{"201":{"description":"La base creada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaasDatabase"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/load-balancers":{"get":{"operationId":"lb.instances.list","summary":"Listar los load balancers de la cuenta","description":"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.\n\nUna 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.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"lb list"},"x-truo-mcp":{"toolset":"lb","action":"list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Load balancers de la cuenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoadBalancerList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/load-balancers/{id}":{"get":{"operationId":"lb.instances.get","summary":"Obtener un load balancer con su estado real","description":"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.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"lb get","positional":["service_id"]},"x-truo-mcp":{"toolset":"lb","action":"get","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"El load balancer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoadBalancer"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/load-balancers/{id}/listeners":{"get":{"operationId":"lb.listeners.list","summary":"Listar los listeners","description":"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`.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"lb listener list","positional":["service_id"]},"x-truo-mcp":{"toolset":"lb","action":"listener_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Listeners del servicio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LbListenerList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"lb.listeners.replace","summary":"Reemplazar la configuración de listeners","description":"**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.\n\n**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.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"lb listener apply","positional":["service_id"]},"x-truo-mcp":{"toolset":"lb","action":"listener_replace","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["listeners"]}}}},"responses":{"200":{"description":"La configuración que quedó aplicada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LbListenerList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/load-balancers/{id}/backends":{"get":{"operationId":"lb.backends.list","summary":"Listar los destinos","description":"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`.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"lb backend list","positional":["service_id"]},"x-truo-mcp":{"toolset":"lb","action":"backend_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Destinos del servicio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LbBackendList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"lb.backends.create","summary":"Agregar un destino a un listener","description":"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.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"lb backend add","positional":["service_id"]},"x-truo-mcp":{"toolset":"lb","action":"backend_create","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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}},"required":["listener","ip","port"]}}}},"responses":{"201":{"description":"El destino agregado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LbBackend"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/load-balancers/{id}/backends/{listener}/{ip}/{port}":{"delete":{"operationId":"lb.backends.delete","summary":"Quitar un destino de un listener","description":"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.\n\n**Un listener no puede quedarse sin destinos.** Quitar el último devuelve `400 validation_failed`: para eliminar el listener entero usá `PUT /listeners` sin él.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"lb backend remove","positional":["service_id","listener","ip","port"]},"x-truo-mcp":{"toolset":"lb","action":"backend_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","example":"web"},"required":true,"name":"listener","in":"path"},{"schema":{"type":"string","example":"10.0.0.5"},"required":true,"name":"ip","in":"path"},{"schema":{"type":"string","example":"8080"},"required":true,"name":"port","in":"path"}],"responses":{"204":{"description":"Destino quitado."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/load-balancers/{id}/stats":{"get":{"operationId":"lb.stats.get","summary":"Estado y tráfico por listener","description":"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.\n\nSi 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`.","tags":["Load Balancer"],"security":[{"bearerAuth":[]}],"x-truo-scope":"lb:read","x-truo-rate-bucket":"expensive","x-truo-danger":"none","x-truo-cli":{"command":"lb stats","positional":["service_id"]},"x-truo-mcp":{"toolset":"lb","action":"stats","readonly":true},"parameters":[{"schema":{"type":"string","example":"svc_10432"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Estadísticas por listener.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LbStats"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage":{"get":{"operationId":"objectstorage.tenant.get","summary":"Obtener el Object Storage de la cuenta","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"object-storage get"},"x-truo-mcp":{"toolset":"objectstorage","action":"get","readonly":true},"responses":{"200":{"description":"El Object Storage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObjectStorage"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/buckets":{"get":{"operationId":"objectstorage.buckets.list","summary":"Listar los buckets","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"object-storage bucket list"},"x-truo-mcp":{"toolset":"objectstorage","action":"bucket_list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Buckets del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageBucketList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"objectstorage.buckets.create","summary":"Crear un bucket","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"object-storage bucket create","positional":["name"]},"x-truo-mcp":{"toolset":"objectstorage","action":"bucket_create","readonly":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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`."}},"required":["name"]}}}},"responses":{"201":{"description":"El bucket creado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageBucket"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/buckets/{bucket}":{"get":{"operationId":"objectstorage.buckets.get","summary":"Obtener un bucket","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"object-storage bucket get","positional":["bucket"]},"x-truo-mcp":{"toolset":"objectstorage","action":"bucket_get","readonly":true},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"}],"responses":{"200":{"description":"El bucket.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageBucket"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"objectstorage.buckets.update","summary":"Cambiar la visibilidad de un bucket","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"object-storage bucket update","positional":["bucket"]},"x-truo-mcp":{"toolset":"objectstorage","action":"bucket_update","readonly":false},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["access"]}}}},"responses":{"200":{"description":"El bucket actualizado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageBucket"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"objectstorage.buckets.delete","summary":"Borrar un bucket","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"object-storage bucket delete","positional":["bucket"]},"x-truo-mcp":{"toolset":"objectstorage","action":"bucket_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"},{"schema":{"type":"string","enum":["true","false"],"description":"Borra el bucket **con todo lo que tenga adentro**. Sin esto, un bucket con objetos no se borra y el request falla."},"required":false,"name":"purge","in":"query"}],"responses":{"204":{"description":"Bucket borrado."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/buckets/{bucket}/empty":{"post":{"operationId":"objectstorage.buckets.empty","summary":"Vaciar un bucket","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:write","x-truo-rate-bucket":"expensive","x-truo-danger":"destructive","x-truo-long-running":true,"x-truo-idempotent":true,"x-truo-cli":{"command":"object-storage bucket empty","positional":["bucket"]},"x-truo-mcp":{"toolset":"objectstorage","action":"bucket_empty","readonly":false},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"}],"responses":{"200":{"description":"Cuántos objetos se borraron.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageDeletion"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/buckets/{bucket}/metrics":{"get":{"operationId":"objectstorage.buckets.metrics","summary":"Métricas de un bucket","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"object-storage bucket metrics","positional":["bucket"]},"x-truo-mcp":{"toolset":"objectstorage","action":"bucket_metrics","readonly":true},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"},{"schema":{"type":"string","enum":["7d","30d","90d"],"description":"Ventana de las series. Default `30d`."},"required":false,"name":"range","in":"query"}],"responses":{"200":{"description":"Métricas del bucket.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageBucketMetrics"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/buckets/{bucket}/objects":{"get":{"operationId":"objectstorage.objects.list","summary":"Listar objetos de un bucket","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"object-storage object list","positional":["bucket"]},"x-truo-mcp":{"toolset":"objectstorage","action":"object_list","readonly":true},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"},{"schema":{"type":"string","description":"Prefijo dentro del bucket. Terminalo en `/` para listar una carpeta.","example":"fotos/"},"required":false,"name":"prefix","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Una página del bucket.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageObjectList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/buckets/{bucket}/objects/delete":{"post":{"operationId":"objectstorage.objects.delete","summary":"Borrar objetos","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"object-storage object delete","positional":["bucket"]},"x-truo-mcp":{"toolset":"objectstorage","action":"object_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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"]}},"required":["keys"]}}}},"responses":{"200":{"description":"Cuántos objetos se borraron.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageDeletion"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/buckets/{bucket}/presign":{"post":{"operationId":"objectstorage.objects.presign","summary":"Firmar una URL temporal","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:read","x-truo-rate-bucket":"expensive","x-truo-danger":"reversible","x-truo-cli":{"command":"object-storage object presign","positional":["bucket","key"]},"x-truo-mcp":{"toolset":"objectstorage","action":"object_presign","readonly":false},"parameters":[{"schema":{"type":"string","example":"respaldos"},"required":true,"name":"bucket","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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."}},"required":["key"]}}}},"responses":{"200":{"description":"La URL firmada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoragePresignedUrl"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/keys":{"get":{"operationId":"objectstorage.keys.list","summary":"Listar las llaves de acceso","description":"Solo las activas, y nunca el secreto.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"object-storage key list"},"x-truo-mcp":{"toolset":"objectstorage","action":"key_list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Llaves activas del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageAccessKeyList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"objectstorage.keys.create","summary":"Emitir una llave de acceso","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:keys","x-truo-rate-bucket":"expensive","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"object-storage key create","positional":["name"]},"x-truo-mcp":{"toolset":"objectstorage","action":"key_create","readonly":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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`."}},"required":["name"]}}}},"responses":{"201":{"description":"La llave, con el secreto. Es la única vez que aparece.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageAccessKeyWithSecret"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/object-storage/keys/{key_id}":{"delete":{"operationId":"objectstorage.keys.delete","summary":"Revocar una llave de acceso","description":"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.","tags":["Object Storage"],"security":[{"bearerAuth":[]}],"x-truo-scope":"objectstorage:keys","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"object-storage key delete","positional":["key_id"]},"x-truo-mcp":{"toolset":"objectstorage","action":"key_delete","readonly":false},"parameters":[{"schema":{"type":"string","example":"AKIA7QF2K3M9XZ4NPRTV"},"required":true,"name":"key_id","in":"path"}],"responses":{"204":{"description":"Llave revocada."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway":{"get":{"operationId":"mailgateway.tenant.get","summary":"Obtener el Mail Gateway de la cuenta","description":"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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway get"},"x-truo-mcp":{"toolset":"mailgateway","action":"get","readonly":true},"responses":{"200":{"description":"El Mail Gateway.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGateway"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/usage":{"get":{"operationId":"mailgateway.usage.get","summary":"Uso del mes en curso","description":"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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway usage"},"x-truo-mcp":{"toolset":"mailgateway","action":"usage","readonly":true},"responses":{"200":{"description":"Uso del mes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayUsage"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/metrics":{"get":{"operationId":"mailgateway.metrics.get","summary":"Métricas de entrega y reputación","description":"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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway metrics"},"x-truo-mcp":{"toolset":"mailgateway","action":"metrics","readonly":true},"parameters":[{"schema":{"type":"string","enum":["7d","30d","90d"],"description":"Ventana de las métricas. Default `30d`.","example":"30d"},"required":false,"name":"range","in":"query"}],"responses":{"200":{"description":"Las métricas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayMetrics"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/domains":{"get":{"operationId":"mailgateway.domains.list","summary":"Listar los dominios de envío","description":"Cada dominio viene con los registros DNS que le corresponden y el estado de cada uno. Solo se puede enviar desde un dominio `verified`.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway domain list"},"x-truo-mcp":{"toolset":"mailgateway","action":"domain_list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Dominios del Mail Gateway.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayDomainList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"mailgateway.domains.create","summary":"Agregar un dominio de envío","description":"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.\n\nLa 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`.\n\nEs 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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:write","x-truo-rate-bucket":"write","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"mail-gateway domain add","positional":["domain"]},"x-truo-mcp":{"toolset":"mailgateway","action":"domain_add","readonly":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"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"}},"required":["domain"]}}}},"responses":{"201":{"description":"El dominio, con sus registros DNS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayDomain"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/domains/{domain}/verify":{"post":{"operationId":"mailgateway.domains.verify","summary":"Consultar la verificación de un dominio","description":"**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.\n\nLos 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.\n\n`verified_at` viene en `null` en esta respuesta aunque el estado sea `verified`; el dato está en `GET /v1/mail-gateway/domains`.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:write","x-truo-rate-bucket":"write","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway domain verify","positional":["domain"]},"x-truo-mcp":{"toolset":"mailgateway","action":"domain_verify","readonly":false},"parameters":[{"schema":{"type":"string","description":"El dominio de envío tal como se dio de alta.","example":"ejemplo.com"},"required":true,"name":"domain","in":"path"}],"responses":{"200":{"description":"El dominio con su estado actualizado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayDomain"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/domains/{domain}":{"delete":{"operationId":"mailgateway.domains.delete","summary":"Quitar un dominio de envío","description":"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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"mail-gateway domain remove","positional":["domain"]},"x-truo-mcp":{"toolset":"mailgateway","action":"domain_remove","readonly":false},"parameters":[{"schema":{"type":"string","description":"El dominio de envío tal como se dio de alta.","example":"ejemplo.com"},"required":true,"name":"domain","in":"path"}],"responses":{"204":{"description":"Dominio quitado."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/messages":{"get":{"operationId":"mailgateway.messages.list","summary":"Listar los mensajes enviados","description":"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.\n\nSin `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`.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway message list"},"x-truo-mcp":{"toolset":"mailgateway","action":"message_list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"string","description":"Filtra por destinatario. Es igualdad exacta, no búsqueda: `ana@ejemplo.com` no encuentra `Ana+news@ejemplo.com`.","example":"ana@ejemplo.com"},"required":false,"name":"recipient","in":"query"},{"schema":{"type":"string","description":"Ventana hacia atrás, 1–90 días. Default 30. El log se retiene 90 días: sin este parámetro los mensajes más viejos que 30 días no aparecen, y eso se confunde con que no existan. La ventana se cuenta desde *ahora* en cada request, así que hay que repetirlo en cada página al paginar.","example":"90"},"required":false,"name":"days","in":"query"}],"responses":{"200":{"description":"Mensajes del período.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayMessageList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/keys":{"get":{"operationId":"mailgateway.keys.list","summary":"Listar las API keys de envío","description":"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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway key list"},"x-truo-mcp":{"toolset":"mailgateway","action":"key_list","readonly":true},"parameters":[{"schema":{"type":"string","description":"1–100. Default 25.","example":"25"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor opaco de `next_cursor`."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"Keys del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayKeyList"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"mailgateway.keys.create","summary":"Crear una API key de envío","description":"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.\n\nEsta 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.\n\nPide `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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:send","x-truo-rate-bucket":"expensive","x-truo-danger":"reversible","x-truo-idempotent":true,"x-truo-cli":{"command":"mail-gateway key create"},"x-truo-mcp":{"toolset":"mailgateway","action":"key_create","readonly":false},"responses":{"201":{"description":"La key nueva, con su `secret`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewayKey"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/keys/{key_id}":{"delete":{"operationId":"mailgateway.keys.delete","summary":"Revocar una API key de envío","description":"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.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:write","x-truo-rate-bucket":"write","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"mail-gateway key revoke","positional":["key_id"]},"x-truo-mcp":{"toolset":"mailgateway","action":"key_revoke","readonly":false},"parameters":[{"schema":{"type":"string","description":"El `id` que devuelve `GET /v1/mail-gateway/keys`.","example":"mgk3f9a2b1c4d5"},"required":true,"name":"key_id","in":"path"}],"responses":{"204":{"description":"Key revocada."},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/smtp":{"get":{"operationId":"mailgateway.smtp.get","summary":"Ver la configuración SMTP","description":"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`.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:read","x-truo-rate-bucket":"read","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway smtp get"},"x-truo-mcp":{"toolset":"mailgateway","action":"smtp_get","readonly":true},"responses":{"200":{"description":"Configuración SMTP, sin credenciales.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewaySmtp"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"mailgateway.smtp.reveal","summary":"Revelar la password SMTP","description":"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\".\n\nLa 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`.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:send","x-truo-rate-bucket":"expensive","x-truo-danger":"none","x-truo-cli":{"command":"mail-gateway smtp reveal"},"x-truo-mcp":{"toolset":"mailgateway","action":"smtp_reveal","readonly":false},"responses":{"200":{"description":"Credenciales SMTP completas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewaySmtpCredentials"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/mail-gateway/smtp/rotate":{"post":{"operationId":"mailgateway.smtp.rotate","summary":"Rotar la credencial SMTP","description":"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á.","tags":["Mail Gateway"],"security":[{"bearerAuth":[]}],"x-truo-scope":"mailgateway:send","x-truo-rate-bucket":"expensive","x-truo-danger":"destructive","x-truo-idempotent":true,"x-truo-cli":{"command":"mail-gateway smtp rotate"},"x-truo-mcp":{"toolset":"mailgateway","action":"smtp_rotate","readonly":false},"responses":{"200":{"description":"Las credenciales nuevas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MailGatewaySmtpCredentials"}}}},"400":{"description":"Request inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Credencial ausente, inválida, revocada o vencida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"La credencial no tiene el scope o el permiso necesario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"El recurso no existe o esta credencial no puede verlo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflicto: ya existe, o la `Idempotency-Key` se reusó con otro body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit superado. Mirá `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Un servicio del que dependemos no está disponible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}