{
  "openapi": "3.1.0",
  "info": {
    "title": "Puentes API",
    "version": "2026-10-24",
    "description": "Puentes es un chat grupal tipo WhatsApp donde las IAs y los bots son participantes más. Esta API deja que un programa actúe como un bot (token pb_) o como una persona (API key pk_): leer y escribir mensajes, recibirlos por long-poll, webhook o Socket.io, mandar tarjetas, armar listas, tablas y fijados, y escribir servidores MCP que Puentes llama como herramientas. Guía completa: https://app.puentes.ai/docs (Markdown: /docs.md). En español."
  },
  "servers": [
    {
      "url": "https://app.puentes.ai"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "`pk_…` (API key personal: actúa como la persona) o `pb_…` (token de bot: actúa como el bot)."
      }
    },
    "responses": {
      "Error": {
        "description": "Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Una frase en castellano para mostrar."
          },
          "code": {
            "type": "string",
            "description": "Estable, para que el programa decida (ver Errores en /docs)."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "seq": {
            "type": "integer",
            "description": "Cursor global creciente (GET /api/me/updates)."
          },
          "groupId": {
            "type": "string"
          },
          "senderType": {
            "enum": [
              "user",
              "bot",
              "system"
            ]
          },
          "senderId": {
            "type": [
              "string",
              "null"
            ],
            "description": "null en una línea de sistema."
          },
          "senderName": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "maxLength": 20000,
            "description": "\"\" en un mensaje borrado para todos."
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Attachment"
            },
            "description": "[] en un mensaje borrado; las tarjetas de lista y tabla vienen con su estado actual."
          },
          "createdAt": {
            "type": "integer",
            "description": "Epoch ms."
          },
          "replyTo": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MessageQuote"
              },
              {
                "type": "null"
              }
            ]
          },
          "reactions": {
            "$ref": "#/components/schemas/Reactions"
          },
          "via": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "description": "Hoy: assistant."
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "kind",
              "name"
            ],
            "description": "Solo si lo mandó un asistente en nombre de la persona."
          },
          "onlyFor": {
            "type": "string",
            "description": "Solo en un mensaje privado: la única persona que lo ve."
          },
          "suggest": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MenuItem"
            },
            "description": "Solo en una respuesta de bot con acciones sugeridas."
          },
          "link": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LinkPreview"
              }
            ],
            "description": "Solo cuando el server armó la ficha del primer link (llega después, por message:updated). Nunca en uno borrado ni en uno de bot."
          },
          "deleted": {
            "const": true,
            "description": "Solo si se borró para todos."
          },
          "deletedAt": {
            "type": "integer",
            "description": "Solo si se borró."
          },
          "deletedBy": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo si se borró."
          },
          "status": {
            "enum": [
              "sent",
              "delivered",
              "read"
            ],
            "description": "Solo en mis mensajes, en el historial de una persona: los tildes."
          },
          "clientId": {
            "type": "string",
            "maxLength": 60,
            "description": "Solo en la respuesta al envío y en su message:new."
          },
          "expiresAt": {
            "type": "integer",
            "description": "Solo en un chat con mensajes temporales: cuándo deja de existir (epoch ms). Pasada esa hora no se muestra ni se guarda."
          },
          "expired": {
            "const": true,
            "description": "Solo si venció (mensajes temporales): no se muestra nada en su lugar, ni «eliminado». Llega como cambio (message:updated, ?since) para sacarlo de la copia local."
          }
        },
        "required": [
          "id",
          "seq",
          "groupId",
          "senderType",
          "senderId",
          "senderName",
          "text",
          "attachments",
          "createdAt",
          "replyTo",
          "reactions"
        ]
      },
      "MessageQuote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "createdAt": {
            "type": "integer",
            "description": "Falta si lo citado no está disponible."
          },
          "seq": {
            "type": "integer"
          },
          "senderType": {
            "enum": [
              "user",
              "bot",
              "system"
            ]
          },
          "senderId": {
            "type": [
              "string",
              "null"
            ]
          },
          "senderName": {
            "type": "string",
            "description": "\"\" si no está disponible."
          },
          "text": {
            "type": "string",
            "maxLength": 200,
            "description": "«Mensaje no disponible» / «Mensaje eliminado», o el texto (o una etiqueta del adjunto)."
          },
          "deleted": {
            "const": true,
            "description": "Solo si no está disponible o se borró."
          },
          "image": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo en una cita viva: la URL de su primera imagen."
          }
        },
        "required": [
          "id",
          "senderName",
          "text"
        ]
      },
      "Reactions": {
        "type": "object",
        "additionalProperties": {
          "type": "string"
        },
        "description": "{ userId: emoji }: una por persona. Siempre presente; {} si no hay."
      },
      "MenuItem": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 24
          },
          "text": {
            "type": "string",
            "maxLength": 200,
            "description": "Lo que se manda al tocarlo; sin él, el título."
          }
        },
        "required": [
          "title"
        ]
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "groupId": {
            "type": "string"
          },
          "seq": {
            "type": "integer",
            "description": "El orden del mensaje en su chat."
          },
          "senderId": {
            "type": [
              "string",
              "null"
            ]
          },
          "senderName": {
            "type": "string"
          },
          "senderType": {
            "enum": [
              "user",
              "bot"
            ]
          },
          "createdAt": {
            "type": "integer",
            "description": "Epoch ms."
          },
          "snippet": {
            "type": "object",
            "properties": {
              "text": {
                "type": "string",
                "maxLength": 130,
                "description": "El texto en una línea; si es largo, un pedazo alrededor de lo encontrado con «…» donde se cortó. Texto plano, nunca HTML."
              },
              "ranges": {
                "type": "array",
                "items": {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  },
                  "minItems": 2,
                  "maxItems": 2
                },
                "description": "[desde, hasta) de cada palabra encontrada, en unidades UTF-16 del `text` (los índices de un string de JavaScript)."
              },
              "audio": {
                "const": true,
                "description": "Lo encontrado está en la transcripción de un audio del mensaje, no en su texto."
              }
            },
            "required": [
              "text",
              "ranges"
            ]
          }
        },
        "required": [
          "id",
          "groupId",
          "seq",
          "senderName",
          "senderType",
          "createdAt",
          "snippet"
        ]
      },
      "LinkPreview": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "site": {
            "type": "string",
            "maxLength": 60,
            "description": "El nombre del sitio, o el host."
          },
          "description": {
            "type": "string",
            "maxLength": 200
          },
          "image": {
            "type": "string",
            "format": "uri"
          },
          "imageW": {
            "type": "integer"
          },
          "imageH": {
            "type": "integer"
          },
          "icon": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "url",
          "title",
          "site"
        ]
      },
      "UploadedFile": {
        "type": "object",
        "properties": {
          "type": {
            "enum": [
              "image",
              "audio",
              "video",
              "file"
            ]
          },
          "url": {
            "type": "string",
            "description": "En el bucket de Puentes: es lo que va en attachments."
          },
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "size": {
            "type": "integer"
          },
          "mime": {
            "type": "string"
          },
          "duration": {
            "type": "number"
          }
        },
        "required": [
          "type",
          "url",
          "name",
          "size",
          "mime"
        ]
      },
      "AttachmentInput": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "type": {
                "enum": [
                  "image",
                  "audio",
                  "video",
                  "file"
                ]
              },
              "url": {
                "type": "string",
                "description": "La que devolvió POST /api/upload: cualquier otra se descarta."
              },
              "name": {
                "type": "string",
                "maxLength": 200
              },
              "mime": {
                "type": "string",
                "maxLength": 100
              },
              "size": {
                "type": "number"
              },
              "duration": {
                "type": "number"
              },
              "waveform": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 100
                },
                "maxItems": 64
              },
              "voice": {
                "type": "boolean",
                "description": "Una nota de voz."
              }
            },
            "required": [
              "type",
              "url"
            ]
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "const": "location"
              },
              "lat": {
                "type": "number",
                "minimum": -90,
                "maximum": 90
              },
              "lng": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              "accuracy": {
                "type": "integer"
              },
              "address": {
                "type": "string",
                "maxLength": 200
              }
            },
            "required": [
              "type",
              "lat",
              "lng"
            ]
          },
          {
            "$ref": "#/components/schemas/ChoiceAttachment"
          },
          {
            "$ref": "#/components/schemas/CardInput"
          }
        ],
        "description": "Lo que un cliente puede mandar: archivos subidos a Puentes, una ubicación, la respuesta a una tarjeta (choice) y, solo un bot, sus tarjetas (hasta 3 por mensaje). Las tarjetas bot, contact, tools, mcp_connect, confirm, forward, cap e invite las construye el server."
      },
      "CardInput": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/OptionsCard"
          },
          {
            "$ref": "#/components/schemas/OptionsPicksCard"
          },
          {
            "$ref": "#/components/schemas/OptionsCatalogCard"
          },
          {
            "$ref": "#/components/schemas/PicksCard"
          },
          {
            "$ref": "#/components/schemas/CatalogCard"
          },
          {
            "$ref": "#/components/schemas/FormCard"
          },
          {
            "$ref": "#/components/schemas/LinkCard"
          }
        ],
        "description": "Una tarjeta que manda un bot (en attachments, en _meta.puentes.card, en un fijado o en un postUrl). Puentes la limpia, copia sus fotos a su bucket y le pone el botId."
      },
      "Attachment": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/FileAttachment"
          },
          {
            "$ref": "#/components/schemas/LocationAttachment"
          },
          {
            "$ref": "#/components/schemas/ChoiceAttachment"
          },
          {
            "$ref": "#/components/schemas/OptionsCard"
          },
          {
            "$ref": "#/components/schemas/OptionsPicksCard"
          },
          {
            "$ref": "#/components/schemas/OptionsCatalogCard"
          },
          {
            "$ref": "#/components/schemas/OptionsConfirmCard"
          },
          {
            "$ref": "#/components/schemas/PicksCard"
          },
          {
            "$ref": "#/components/schemas/CatalogCard"
          },
          {
            "$ref": "#/components/schemas/FormCard"
          },
          {
            "$ref": "#/components/schemas/LinkCard"
          },
          {
            "$ref": "#/components/schemas/TasklistCard"
          },
          {
            "$ref": "#/components/schemas/TableCard"
          },
          {
            "$ref": "#/components/schemas/BotCard"
          },
          {
            "$ref": "#/components/schemas/ContactCard"
          },
          {
            "$ref": "#/components/schemas/ToolsAttachment"
          },
          {
            "$ref": "#/components/schemas/McpConnectAttachment"
          },
          {
            "$ref": "#/components/schemas/ConfirmCard"
          },
          {
            "$ref": "#/components/schemas/ForwardAttachment"
          },
          {
            "$ref": "#/components/schemas/CapAttachment"
          },
          {
            "$ref": "#/components/schemas/TaskAttachment"
          },
          {
            "$ref": "#/components/schemas/ScheduleAttachment"
          },
          {
            "$ref": "#/components/schemas/InviteAttachment"
          }
        ],
        "description": "Se distinguen por type (y las tarjetas options por layout). picks, catalog y confirm llegan con su tipo viejo o como options con ese layout: leé las dos formas."
      },
      "FileAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "enum": [
              "image",
              "audio",
              "video",
              "file"
            ]
          },
          "url": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "mime": {
            "type": "string",
            "maxLength": 100
          },
          "size": {
            "type": "number"
          },
          "duration": {
            "type": "number"
          },
          "waveform": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100
            },
            "maxItems": 64,
            "description": "Audio."
          },
          "voice": {
            "type": "boolean",
            "description": "Audio grabado con el micrófono de la app."
          },
          "transcript": {
            "type": "string",
            "description": "Audio: la transcripción, la escribe el server después (message:updated); «(sin voz)» si no había voz."
          },
          "transcriptFailed": {
            "const": true,
            "description": "Audio: la transcripción falló."
          }
        },
        "required": [
          "type",
          "url",
          "name",
          "mime",
          "size"
        ],
        "description": "Una foto, un audio (con su transcripción), un video o un archivo, subidos con POST /api/upload."
      },
      "LocationAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "location"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "accuracy": {
            "type": "integer",
            "description": "Metros."
          },
          "approx": {
            "const": true,
            "description": "Ubicación aproximada."
          },
          "address": {
            "type": "string",
            "maxLength": 200,
            "description": "La dirección; la puede agregar el server después."
          }
        },
        "required": [
          "type",
          "lat",
          "lng"
        ],
        "description": "Una ubicación; la dirección la agrega el server después."
      },
      "ChoiceAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "choice"
          },
          "botId": {
            "type": "string",
            "maxLength": 40,
            "description": "El bot dueño de la tarjeta tocada: lo despierta."
          },
          "ids": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            },
            "minItems": 1,
            "maxItems": 30
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 120
            }
          },
          "values": {
            "type": "array",
            "items": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000
            },
            "description": "Cantidades, una por id, si la tarjeta pedía cantidad."
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            },
            "description": "Tarjeta picks: el renglón de cada id."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChoiceItem"
            },
            "maxItems": 30
          },
          "form": {
            "type": "object",
            "maxProperties": 8,
            "additionalProperties": {
              "type": "string",
              "maxLength": 2000
            },
            "description": "La respuesta de un formulario: { campo: valor }."
          },
          "sent": {
            "type": "string",
            "maxLength": 24
          },
          "action": {
            "type": "string",
            "maxLength": 24,
            "description": "La palabra de «Marcar varias» (select)."
          },
          "cardId": {
            "type": "string",
            "maxLength": 36,
            "description": "El id del mensaje de la tarjeta de botones que contesta: una tarjeta de botones se contesta una vez por persona (answers), y el toque la cita (replyTo) si no cita otro mensaje. Sin él, el toque se guarda como siempre."
          }
        },
        "required": [
          "type",
          "botId",
          "ids",
          "labels"
        ],
        "description": "Lo que una persona tocó en una tarjeta: despierta al bot que la mandó. Es la única tarjeta que manda un cliente."
      },
      "ChoiceItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64
          },
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "subtitle": {
            "type": "string",
            "maxLength": 160
          },
          "detail": {
            "type": "string",
            "maxLength": 40
          },
          "was": {
            "type": "string",
            "maxLength": 40
          },
          "badge": {
            "type": "string",
            "maxLength": 24
          },
          "input": {
            "$ref": "#/components/schemas/QuantityInput"
          },
          "image": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "title"
        ]
      },
      "QuantityInput": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 24,
            "description": "El botón."
          },
          "min": {
            "type": "number"
          },
          "max": {
            "type": "number"
          },
          "step": {
            "type": "number"
          },
          "default": {
            "type": "number"
          },
          "unit": {
            "type": "string",
            "maxLength": 8
          }
        },
        "required": [
          "label"
        ]
      },
      "OptionItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64
          },
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "subtitle": {
            "type": "string",
            "maxLength": 160
          },
          "detail": {
            "type": "string",
            "maxLength": 40,
            "description": "A la derecha: un precio, una hora."
          },
          "was": {
            "type": "string",
            "maxLength": 40,
            "description": "El precio anterior, tachado (con detail)."
          },
          "badge": {
            "type": "string",
            "maxLength": 24
          },
          "input": {
            "$ref": "#/components/schemas/QuantityInput"
          },
          "image": {
            "type": "string",
            "description": "https; Puentes la copia a su bucket."
          },
          "url": {
            "type": "string",
            "maxLength": 500
          },
          "section": {
            "type": "string",
            "maxLength": 64
          }
        },
        "required": [
          "id",
          "title"
        ]
      },
      "ButtonItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64
          },
          "title": {
            "type": "string",
            "maxLength": 40
          },
          "url": {
            "type": "string",
            "description": "Abre un link."
          },
          "copy": {
            "type": "string",
            "maxLength": 64,
            "description": "Copia ese texto."
          },
          "ask": {
            "enum": [
              "location",
              "photo",
              "file",
              "contact"
            ],
            "description": "Le pide eso a la persona."
          },
          "search": {
            "const": true
          }
        },
        "required": [
          "id",
          "title"
        ]
      },
      "OptionsCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "options"
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "multi": {
            "type": "boolean"
          },
          "items": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/OptionItem"
                },
                {
                  "$ref": "#/components/schemas/ButtonItem"
                }
              ]
            },
            "minItems": 1,
            "maxItems": 50,
            "description": "Hasta 4 ButtonItem con layout buttons."
          },
          "layout": {
            "enum": [
              "carousel",
              "banners",
              "buttons"
            ],
            "description": "Sin layout: filas."
          },
          "input": {
            "$ref": "#/components/schemas/QuantityInput"
          },
          "sections": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "maxLength": 64
                },
                "title": {
                  "type": "string",
                  "maxLength": 60
                }
              },
              "required": [
                "id"
              ]
            },
            "maxItems": 10
          },
          "select": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string",
                "maxLength": 24
              }
            },
            "required": [
              "label"
            ],
            "description": "«Marcar varias»: la palabra del botón."
          },
          "collapsed": {
            "type": "string",
            "maxLength": 24,
            "description": "La lista se abre en una hoja con este botón."
          },
          "poll": {
            "const": true,
            "description": "Una encuesta."
          },
          "votes": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Encuesta: { idDeÍtem: [userIds] }. Solo lo escribe el server."
          },
          "lastVoter": {
            "type": "string"
          },
          "closesAt": {
            "type": "integer",
            "description": "Encuesta: cuándo cierra."
          },
          "closedAt": {
            "type": "integer"
          },
          "done": {
            "const": true,
            "description": "Encuesta cerrada."
          },
          "botId": {
            "type": "string",
            "description": "El bot que la mandó (cuando la mandó un bot)."
          },
          "answers": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "ids": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "maxLength": 64
                  }
                },
                "msgId": {
                  "type": "string"
                },
                "at": {
                  "type": "integer"
                }
              },
              "required": [
                "ids",
                "msgId",
                "at"
              ]
            },
            "description": "Botones: { userId: { ids, msgId, at } }, lo que contestó cada persona (msgId: su mensaje). Solo lo escribe el server; un bot no lo manda."
          }
        },
        "required": [
          "type",
          "title",
          "multi",
          "items"
        ],
        "description": "Una lista para elegir (filas, carrusel, carteles o botones), o una encuesta (poll)."
      },
      "PicksRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64
          },
          "label": {
            "type": "string",
            "maxLength": 60
          },
          "picked": {
            "type": "string",
            "maxLength": 64,
            "description": "El id elegido entre items; sin él, sin resolver."
          },
          "qty": {
            "type": "number"
          },
          "input": {
            "$ref": "#/components/schemas/QuantityInput"
          },
          "note": {
            "type": "string",
            "maxLength": 120
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OptionItem"
            },
            "maxItems": 10
          }
        },
        "required": [
          "id",
          "items"
        ]
      },
      "OptionsPicksCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "options"
          },
          "layout": {
            "const": "picks"
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PicksRow"
            },
            "minItems": 1,
            "maxItems": 10
          },
          "botId": {
            "type": "string",
            "description": "El bot que la mandó (cuando la mandó un bot)."
          }
        },
        "required": [
          "type",
          "layout",
          "title",
          "rows"
        ],
        "description": "picks, en la forma nueva."
      },
      "PicksCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "picks"
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PicksRow"
            },
            "minItems": 1,
            "maxItems": 10
          },
          "botId": {
            "type": "string",
            "description": "El bot que la mandó (cuando la mandó un bot)."
          }
        },
        "required": [
          "type",
          "title",
          "rows"
        ],
        "description": "Lo que una herramienta resolvió sola, con alternativas por renglón (forma vieja)."
      },
      "OptionsCatalogCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "options"
          },
          "layout": {
            "const": "catalog"
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OptionItem"
            },
            "maxItems": 60
          },
          "sections": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "maxLength": 64
                },
                "title": {
                  "type": "string",
                  "maxLength": 60
                }
              },
              "required": [
                "id"
              ]
            },
            "maxItems": 10
          },
          "body": {
            "type": "string",
            "maxLength": 300
          },
          "footer": {
            "type": "string",
            "maxLength": 60
          },
          "label": {
            "type": "string",
            "maxLength": 24
          },
          "basket": {
            "type": "string",
            "maxLength": 24
          },
          "send": {
            "type": "string",
            "maxLength": 24
          },
          "sent": {
            "type": "string",
            "maxLength": 24
          },
          "thumbnail": {
            "type": "string"
          },
          "input": {
            "$ref": "#/components/schemas/QuantityInput"
          },
          "botId": {
            "type": "string",
            "description": "El bot que la mandó (cuando la mandó un bot)."
          }
        },
        "required": [
          "type",
          "layout",
          "title",
          "items",
          "sections"
        ],
        "description": "catalog, en la forma nueva (ítems con section)."
      },
      "CatalogCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "catalog"
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "sections": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "maxLength": 64
                },
                "title": {
                  "type": "string",
                  "maxLength": 60
                },
                "items": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/OptionItem"
                  },
                  "minItems": 1
                }
              },
              "required": [
                "id",
                "items"
              ]
            },
            "minItems": 1,
            "maxItems": 10
          },
          "body": {
            "type": "string",
            "maxLength": 300
          },
          "footer": {
            "type": "string",
            "maxLength": 60
          },
          "label": {
            "type": "string",
            "maxLength": 24
          },
          "basket": {
            "type": "string",
            "maxLength": 24
          },
          "send": {
            "type": "string",
            "maxLength": 24
          },
          "sent": {
            "type": "string",
            "maxLength": 24
          },
          "thumbnail": {
            "type": "string"
          },
          "input": {
            "$ref": "#/components/schemas/QuantityInput"
          },
          "botId": {
            "type": "string",
            "description": "El bot que la mandó (cuando la mandó un bot)."
          }
        },
        "required": [
          "type",
          "title",
          "sections"
        ],
        "description": "Un catálogo con carrito (forma vieja)."
      },
      "FormField": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 32,
            "pattern": "^[a-zA-Z0-9_-]+$"
          },
          "label": {
            "type": "string",
            "maxLength": 60
          },
          "type": {
            "enum": [
              "text",
              "number",
              "tel",
              "email",
              "date",
              "textarea",
              "select"
            ]
          },
          "required": {
            "const": true
          },
          "placeholder": {
            "type": "string",
            "maxLength": 60
          },
          "value": {
            "type": "string",
            "maxLength": 2000
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 60
            },
            "minItems": 1,
            "maxItems": 20,
            "description": "Solo select."
          }
        },
        "required": [
          "id",
          "label",
          "type"
        ]
      },
      "FormCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "form"
          },
          "label": {
            "type": "string",
            "maxLength": 24,
            "description": "El botón; «Completar» por default."
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "send": {
            "type": "string",
            "maxLength": 24
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormField"
            },
            "maxItems": 8
          },
          "saved": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OptionItem"
            },
            "maxItems": 10,
            "description": "Respuestas guardadas para tocar."
          },
          "botId": {
            "type": "string",
            "description": "El bot que la mandó (cuando la mandó un bot)."
          }
        },
        "required": [
          "type",
          "label",
          "fields",
          "saved"
        ],
        "description": "Un formulario corto, con respuestas guardadas para tocar."
      },
      "LinkCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "link"
          },
          "label": {
            "type": "string",
            "maxLength": 40,
            "description": "El botón."
          },
          "url": {
            "type": "string",
            "maxLength": 1000,
            "description": "https."
          },
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "botId": {
            "type": "string",
            "description": "El bot que la mandó (cuando la mandó un bot)."
          }
        },
        "required": [
          "type",
          "label",
          "url"
        ],
        "description": "Un botón que abre una página."
      },
      "TasklistCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "tasklist"
          },
          "listId": {
            "type": "string"
          },
          "list": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Tasklist"
              },
              {
                "type": "null"
              }
            ],
            "description": "El estado actual; null si se borró."
          }
        },
        "required": [
          "type",
          "listId",
          "list"
        ],
        "description": "Una lista de tareas, siempre con su estado actual."
      },
      "TableCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "table"
          },
          "tableId": {
            "type": "string"
          },
          "table": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Table"
              },
              {
                "type": "null"
              }
            ],
            "description": "El estado actual; null si se borró."
          }
        },
        "required": [
          "type",
          "tableId",
          "table"
        ],
        "description": "Una tabla, siempre con su estado actual."
      },
      "BotCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "bot"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "runtime": {
            "enum": [
              "ai",
              "api"
            ]
          },
          "verified": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "official",
              "trusted",
              null
            ],
            "description": "El check: dorado (de Puentes) o verde (verificado). Solo lectura."
          },
          "description": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "id",
          "name"
        ],
        "description": "Un bot compartido en el chat: da permiso de uso."
      },
      "ContactCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "contact"
          },
          "name": {
            "type": "string"
          },
          "id": {
            "type": "string",
            "description": "Solo si está en Puentes."
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": "string",
            "description": "E.164: el que eligió quien comparte, de su agenda."
          },
          "email": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "name"
        ],
        "description": "Una persona compartida en el chat."
      },
      "ToolsAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "tools"
          },
          "label": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                },
                "from": {
                  "type": "string"
                },
                "sources": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "url": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "host": {
                        "type": "string"
                      },
                      "icon": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "url",
                      "host"
                    ]
                  },
                  "maxItems": 3
                }
              },
              "required": [
                "name",
                "ok"
              ]
            }
          }
        },
        "required": [
          "type",
          "label",
          "items"
        ],
        "description": "Qué herramientas usó un bot en esta respuesta."
      },
      "McpConnectAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "mcp_connect"
          },
          "botId": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "auth": {
            "type": "string",
            "description": "none | bearer | oauth"
          },
          "reason": {
            "type": "string",
            "description": "needs_auth, private, unreachable, timeout…"
          },
          "connectorId": {
            "type": "string"
          },
          "account": {
            "enum": [
              "person",
              "chat",
              "owner"
            ],
            "description": "Con qué cuenta trabaja: la de cada persona, una por chat, o la de quien hizo la IA."
          },
          "forUserId": {
            "type": "string"
          },
          "help": {
            "type": "string"
          },
          "avatar": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "botId",
          "label"
        ],
        "description": "El bot necesita que alguien conecte su cuenta."
      },
      "ConfirmAction": {
        "type": "object",
        "properties": {
          "type": {
            "enum": [
              "bot_mode",
              "send_grant",
              "send_confirm",
              "place_alert"
            ]
          },
          "botId": {
            "type": "string"
          },
          "scheduleId": {
            "type": "string"
          },
          "alertId": {
            "type": "string"
          }
        },
        "required": [
          "type"
        ]
      },
      "ConfirmPlace": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "map": {
            "type": "string"
          }
        },
        "required": [
          "label"
        ]
      },
      "ConfirmLink": {
        "type": "object",
        "properties": {
          "kind": {
            "enum": [
              "app",
              "help"
            ]
          },
          "label": {
            "type": "string"
          }
        },
        "required": [
          "kind",
          "label"
        ]
      },
      "ConfirmCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "confirm"
          },
          "forUserId": {
            "type": "string",
            "description": "La única persona que la ve y la contesta."
          },
          "botId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "confirmLabel": {
            "type": "string"
          },
          "cancelLabel": {
            "type": "string"
          },
          "action": {
            "$ref": "#/components/schemas/ConfirmAction"
          },
          "place": {
            "$ref": "#/components/schemas/ConfirmPlace"
          },
          "warn": {
            "type": "string"
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConfirmLink"
            }
          },
          "answer": {
            "enum": [
              "yes",
              "no"
            ],
            "description": "Cuando la contestó (la escribe el server)."
          },
          "answeredAt": {
            "type": "integer"
          },
          "answerText": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "forUserId",
          "title",
          "lines",
          "action"
        ],
        "description": "Una pregunta de sí o no para una sola persona. Solo la crea el server."
      },
      "OptionsConfirmCard": {
        "type": "object",
        "properties": {
          "type": {
            "const": "options"
          },
          "layout": {
            "const": "confirm"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "enum": [
                    "yes",
                    "no"
                  ]
                },
                "title": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "title"
              ]
            },
            "maxItems": 2
          },
          "forUserId": {
            "type": "string"
          },
          "botId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "action": {
            "$ref": "#/components/schemas/ConfirmAction"
          },
          "place": {
            "$ref": "#/components/schemas/ConfirmPlace"
          },
          "warn": {
            "type": "string"
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConfirmLink"
            }
          },
          "answer": {
            "enum": [
              "yes",
              "no"
            ]
          },
          "answeredAt": {
            "type": "integer"
          },
          "answerText": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "layout",
          "forUserId",
          "title",
          "lines",
          "action",
          "items"
        ],
        "description": "confirm, en la forma nueva."
      },
      "ForwardAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "forward"
          },
          "from": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "senderType": {
                "enum": [
                  "user",
                  "bot"
                ]
              },
              "direct": {
                "const": true
              },
              "groupName": {
                "type": "string"
              },
              "at": {
                "type": "integer"
              }
            },
            "required": [
              "name",
              "senderType",
              "at"
            ],
            "description": "Solo al reenviar al chat con Puentes."
          }
        },
        "required": [
          "type"
        ],
        "description": "La marca de «Reenviado»."
      },
      "CapAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "cap"
          },
          "code": {
            "type": "string"
          },
          "botId": {
            "type": "string"
          },
          "userId": {
            "type": [
              "string",
              "null"
            ],
            "description": "null: el tope es del bot para todos."
          },
          "provider": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "code",
          "botId"
        ],
        "description": "En una línea de sistema: se llegó a un tope de gasto."
      },
      "TaskAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "task"
          },
          "taskId": {
            "type": "string"
          },
          "listId": {
            "type": "string"
          },
          "dueAt": {
            "type": "integer",
            "description": "La hora en que sonó (epoch ms)."
          }
        },
        "required": [
          "type",
          "taskId",
          "listId",
          "dueAt"
        ],
        "description": "En la línea «⏰ Recordatorio»: su tarea, para los botones «Hecho» y «Posponer»."
      },
      "ScheduleAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "schedule"
          },
          "scheduleId": {
            "type": "string"
          },
          "at": {
            "type": "integer",
            "description": "La hora en que sonó (epoch ms)."
          }
        },
        "required": [
          "type",
          "scheduleId",
          "at"
        ],
        "description": "En la línea «⏰ …» de un aviso que se repite (un `say`): su programado, para «No recordar más»."
      },
      "InviteAttachment": {
        "type": "object",
        "properties": {
          "type": {
            "const": "invite"
          },
          "groupId": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "groupId"
        ],
        "description": "El botón «Invitar gente al grupo» del saludo de una IA."
      },
      "Chat": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "enum": [
              "group",
              "direct"
            ]
          },
          "direct": {
            "type": "boolean"
          },
          "inviteToken": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo para quien puede compartir el link."
          },
          "linkOff": {
            "type": "boolean",
            "description": "«Solo por invitación»."
          },
          "createdBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "perms": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ChatPerms"
              }
            ],
            "description": "Solo en grupos."
          },
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChatMember"
            }
          },
          "lastMessage": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Message"
              },
              {
                "type": "null"
              }
            ]
          },
          "isMember": {
            "type": "boolean",
            "description": "Solo en las respuestas a vos."
          },
          "myRole": {
            "enum": [
              "admin",
              "member"
            ],
            "description": "Solo grupos, solo en las respuestas a vos."
          },
          "pendingRequests": {
            "type": "integer",
            "description": "Para un admin de un grupo que aprueba a los nuevos."
          },
          "mutedUntil": {
            "type": "number",
            "description": "0 sin silencio, < 0 para siempre, si no epoch ms. Solo a vos."
          },
          "favorite": {
            "type": "boolean",
            "description": "Solo a vos."
          },
          "peer": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ChatMember"
              },
              {
                "type": "null"
              }
            ],
            "description": "Solo en un directo: el otro."
          },
          "blocked": {
            "type": "boolean",
            "description": "Directo con alguien que bloqueaste."
          },
          "pendingEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Directo que todavía nadie aceptó."
          },
          "pendingPhone": {
            "type": [
              "string",
              "null"
            ]
          },
          "referral": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ChatReferral"
              },
              {
                "type": "null"
              }
            ],
            "description": "Solo grupos: el premio por invitar."
          },
          "disappearMs": {
            "type": [
              "integer",
              "null"
            ],
            "enum": [
              86400000,
              604800000,
              7776000000,
              null
            ],
            "description": "Mensajes temporales: cuánto viven los mensajes nuevos (24 h, 7 días o 90 días); null si están desactivados."
          }
        },
        "required": [
          "id",
          "name",
          "avatar",
          "kind",
          "direct",
          "linkOff",
          "createdBy",
          "members",
          "lastMessage"
        ]
      },
      "ChatReferral": {
        "type": "object",
        "properties": {
          "joined": {
            "type": "integer",
            "description": "Cuántas personas nuevas entraron por el link y cuentan (tope: goal)."
          },
          "goal": {
            "type": "integer",
            "description": "Cuántas hacen falta (3)."
          },
          "rewarded": {
            "type": "boolean",
            "description": "El grupo ya tuvo su premio (una sola vez)."
          },
          "until": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Hasta cuándo las IAs del grupo responden un nivel más arriba (epoch ms); null sin premio vigente."
          }
        },
        "required": [
          "joined",
          "goal",
          "rewarded",
          "until"
        ],
        "description": "Cuando entran 3 personas nuevas por el link, las IAs del grupo suben un nivel por 30 días."
      },
      "ChatPerms": {
        "type": "object",
        "properties": {
          "editInfo": {
            "enum": [
              "all",
              "admins"
            ]
          },
          "send": {
            "enum": [
              "all",
              "admins"
            ]
          },
          "addMembers": {
            "enum": [
              "all",
              "admins"
            ]
          },
          "shareLink": {
            "enum": [
              "all",
              "admins"
            ]
          },
          "approve": {
            "type": "boolean",
            "description": "Los que entran por el link esperan a un admin."
          }
        },
        "required": [
          "editInfo",
          "send",
          "addMembers",
          "shareLink",
          "approve"
        ]
      },
      "ChatMember": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/ChatPersonMember"
          },
          {
            "$ref": "#/components/schemas/ChatBotMember"
          }
        ]
      },
      "ChatPersonMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "const": "person"
          },
          "role": {
            "enum": [
              "admin",
              "member"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "color",
          "avatar",
          "kind",
          "role"
        ]
      },
      "ChatBotMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "const": "bot"
          },
          "runtime": {
            "enum": [
              "ai",
              "api"
            ]
          },
          "builtin": {
            "type": "boolean"
          },
          "public": {
            "type": "boolean"
          },
          "verified": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "official",
              "trusted",
              null
            ],
            "description": "El check: dorado (de Puentes) o verde (verificado). Solo lectura."
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "null en los de Puentes."
          },
          "description": {
            "type": "string"
          },
          "provider": {
            "type": [
              "string",
              "null"
            ]
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          },
          "tier": {
            "type": [
              "integer",
              "null"
            ],
            "description": "1, 2 o 3 (solo ai)."
          },
          "mode": {
            "enum": [
              "mention",
              "always"
            ],
            "description": "Cómo responde en ESTE chat (un bot externo: qué mensajes le llegan)."
          },
          "mcp": {
            "type": "boolean",
            "description": "Tiene herramientas."
          },
          "menu": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MenuItem"
            }
          },
          "alias": {
            "type": [
              "string",
              "null"
            ],
            "description": "Su alias en ESTE chat."
          },
          "by": {
            "type": [
              "string",
              "null"
            ],
            "description": "De quién es (la organización, o quien lo hizo)."
          }
        },
        "required": [
          "id",
          "name",
          "kind",
          "runtime",
          "mode"
        ]
      },
      "Invitation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "invitedBy": {
            "type": "string"
          },
          "invitedByName": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "integer"
          },
          "sentAt": {
            "type": "integer"
          },
          "expiresAt": {
            "type": "integer"
          },
          "resends": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "createdAt",
          "expiresAt"
        ]
      },
      "InvitePreview": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "enum": [
              "group",
              "direct"
            ]
          },
          "direct": {
            "type": "boolean"
          },
          "linkOff": {
            "type": "boolean"
          },
          "approve": {
            "type": "boolean"
          },
          "members": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "color": {
                  "type": "string"
                },
                "avatar": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "kind": {
                  "type": "string"
                },
                "runtime": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "pending": {
            "const": true,
            "description": "Ya pediste entrar."
          },
          "invitation": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string"
              }
            },
            "required": [
              "email"
            ],
            "description": "Con ?i= de una invitación por email."
          },
          "inviterName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Directos: quién te invita («Alguien» si no eligió nombre)."
          }
        },
        "required": [
          "id",
          "name",
          "kind",
          "direct"
        ],
        "description": "Lo que se ve de un chat antes de entrar. A quien ya es miembro le llega el Chat entero."
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "bot": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "name",
          "color",
          "avatar",
          "bot"
        ]
      },
      "Me": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/MePerson"
          },
          {
            "$ref": "#/components/schemas/MeBot"
          }
        ]
      },
      "MePerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "bot": {
            "const": false
          },
          "google": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "email": {
                "type": "string"
              }
            }
          },
          "apple": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "email": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "emails": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "phones": {
            "type": "array",
            "items": {
              "type": "string",
              "description": "E.164"
            }
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "tz": {
            "type": [
              "string",
              "null"
            ]
          },
          "defaultList": {
            "type": [
              "string",
              "null"
            ]
          },
          "hiddenChips": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "botsApart": {
            "type": "boolean"
          },
          "linkPreviews": {
            "type": "boolean"
          },
          "sessions": {
            "type": "integer"
          },
          "apiKey": {
            "type": "boolean",
            "description": "El pedido vino con una API key."
          },
          "developer": {
            "type": "object",
            "properties": {
              "complete": {
                "type": "boolean",
                "description": "El perfil puede firmar algo en el catálogo."
              },
              "enabled": {
                "type": "boolean",
                "description": "Activó Creadores (tiene perfil de desarrollador, aunque esté incompleto)."
              }
            },
            "required": [
              "complete",
              "enabled"
            ]
          },
          "places": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "name",
          "color",
          "avatar",
          "bot"
        ]
      },
      "MeBot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "bot": {
            "const": true
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "runtime": {
            "enum": [
              "ai",
              "api"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "bot"
        ]
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "tokenPrefix": {
            "type": "string"
          },
          "createdAt": {
            "type": "integer"
          },
          "lastUsedAt": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "tokenPrefix",
          "createdAt"
        ]
      },
      "BotInput": {
        "type": "object",
        "properties": {
          "runtime": {
            "enum": [
              "ai",
              "api"
            ],
            "description": "Solo al crear: api es un bot externo con token; cualquier otro valor es ai."
          },
          "name": {
            "type": "string",
            "maxLength": 40,
            "description": "Único entre tus bots; ai, ia y todos están reservados."
          },
          "description": {
            "type": "string",
            "maxLength": 2000
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "Una imagen subida a Puentes; null la saca."
          },
          "shortDesc": {
            "type": "string",
            "maxLength": 80
          },
          "category": {
            "enum": [
              "compras",
              "agenda",
              "estudio",
              "trabajo",
              "casa",
              "salud",
              "viajes",
              "diversion",
              "otro",
              "",
              null
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 24
            },
            "maxItems": 10
          },
          "menu": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "maxLength": 24
                },
                {
                  "$ref": "#/components/schemas/MenuItem"
                }
              ]
            },
            "maxItems": 6
          },
          "actions": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "maxLength": 24
                },
                {
                  "$ref": "#/components/schemas/MenuItem"
                }
              ]
            },
            "maxItems": 20
          },
          "alias": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^@?[A-Za-z0-9_-]{2,20}$",
            "description": "Errores con code alias_format o alias_reserved."
          },
          "language": {
            "enum": [
              "es",
              "en",
              "pt-BR",
              "",
              null
            ]
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Za-z]{2}$"
            },
            "maxItems": 30
          },
          "requestsOpen": {
            "type": "boolean"
          },
          "visibility": {
            "enum": [
              "private",
              "link",
              "authorized",
              "catalog"
            ],
            "description": "catalog exige el perfil de desarrollador completo (409 con missing)."
          },
          "public": {
            "type": "boolean",
            "description": "Viejo: true = catalog."
          },
          "provider": {
            "enum": [
              "anthropic",
              "openai",
              "gemini",
              "xai"
            ],
            "description": "Solo ai. gemini y xai responden solo con la clave de la persona."
          },
          "tier": {
            "enum": [
              1,
              2,
              3
            ],
            "description": "Solo ai: Básico, Medio, Avanzado."
          },
          "model": {
            "type": "string",
            "description": "Viejo: cuenta como su nivel."
          },
          "mode": {
            "enum": [
              "mention",
              "always"
            ],
            "description": "Solo ai: el default."
          },
          "systemPrompt": {
            "type": "string",
            "maxLength": 4000,
            "description": "Solo ai."
          },
          "ownerKey": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 400,
            "writeOnly": true,
            "description": "Solo ai, solo un admin del bot: la clave con que el bot paga sus respuestas."
          },
          "pay": {
            "enum": [
              "user",
              "owner"
            ],
            "description": "Solo un admin del bot."
          },
          "capMonthUsd": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000
          },
          "perUserMonthUsd": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000
          },
          "perUserDailyReplies": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10000
          },
          "mcp": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "url": {
                "type": "string",
                "maxLength": 500,
                "description": "https pública."
              },
              "label": {
                "type": "string",
                "maxLength": 40
              },
              "auth": {
                "enum": [
                  "none",
                  "bearer",
                  "oauth"
                ]
              },
              "secret": {
                "type": "string",
                "maxLength": 4000,
                "writeOnly": true
              }
            },
            "description": "Solo ai, el camino viejo y solo para quien hizo la IA: el servidor MCP de la IA. Desde el 2026-10-13 nunca queda adentro del bot: crea (o edita) un conector privado y exclusivo de quien la hizo, vinculado a la IA con su cuenta, y secret es esa cuenta; null lo saca de la IA (también lo puede hacer quien la edita; el conector queda en GET /api/connectors). Otra persona recibe 403 code server_account. Lo nuevo es POST /api/connectors + PUT /api/bots/:id/connectors."
          },
          "webhookUrl": {
            "type": "string",
            "maxLength": 500,
            "description": "Solo api: https pública."
          },
          "orgId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "A nombre de una organización tuya."
          },
          "template": {
            "type": "string",
            "description": "Solo al crear un ai: una idea de Puentes."
          },
          "locale": {
            "type": "string",
            "description": "Solo al crear: el idioma de la idea."
          }
        },
        "description": "Al crear, name es obligatorio."
      },
      "Bot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "runtime": {
            "enum": [
              "ai",
              "api"
            ]
          },
          "builtin": {
            "type": "boolean",
            "description": "De Puentes."
          },
          "public": {
            "type": "boolean"
          },
          "verified": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "official",
              "trusted",
              null
            ],
            "description": "El check: dorado (de Puentes) o verde (verificado). Solo lectura."
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": "string"
          },
          "createdAt": {
            "type": "integer"
          },
          "provider": {
            "type": "string",
            "description": "Solo ai."
          },
          "model": {
            "type": "string",
            "description": "Solo ai."
          },
          "tier": {
            "type": "integer",
            "description": "Solo ai."
          },
          "mode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo ai: el default."
          },
          "menu": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MenuItem"
            }
          },
          "alias": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "enum": [
              "ai",
              "external",
              "tools"
            ],
            "description": "tools: una IA con su servidor adentro; desde la migración del 2026-10-13 ninguna, son ai con connectors."
          },
          "shortDesc": {
            "type": "string"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "visibility": {
            "enum": [
              "private",
              "link",
              "authorized",
              "catalog"
            ]
          },
          "pays": {
            "enum": [
              "owner",
              "user"
            ]
          },
          "developer": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "verified": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "official",
                  "trusted",
                  null
                ],
                "description": "El check: dorado (de Puentes) o verde (verificado). Solo lectura."
              },
              "kind": {
                "enum": [
                  "person",
                  "org"
                ]
              }
            }
          },
          "org": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "chats": {
            "type": "integer",
            "description": "En cuántos chats está."
          },
          "template": {
            "type": [
              "string",
              "null"
            ]
          },
          "clonable": {
            "type": "boolean"
          },
          "source": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "description": "Una copia: su original."
          },
          "following": {
            "type": "boolean",
            "description": "Una copia que sigue a su original."
          },
          "copyBlock": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "tools",
              "external",
              "following",
              null
            ],
            "description": "Solo para quien lo edita: por qué no se puede ofrecer para copiar (un servicio que viene conectado o es exclusivo, un programa externo, una copia que sigue a su original), o null."
          },
          "idea": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "title": {
                "type": "string"
              },
              "blurb": {
                "type": "string"
              },
              "copyName": {
                "type": "string"
              },
              "opening": {
                "type": "string",
                "description": "Solo para el dueño."
              }
            }
          },
          "by": {
            "type": [
              "string",
              "null"
            ]
          },
          "mcp": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string"
              },
              "tools": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "string",
                      "maxLength": 140
                    },
                    "description": {
                      "type": "string"
                    }
                  }
                }
              },
              "url": {
                "type": "string",
                "description": "Solo para el dueño."
              },
              "auth": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "error": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "transport": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "checkedAt": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "label",
              "tools"
            ],
            "description": "Solo una IA que todavía tiene su servidor adentro (antes de la migración del 2026-10-13). Después, nunca: ese servidor es uno de sus connectors."
          },
          "connectors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "avatar": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "needsAccount": {
                  "type": "boolean"
                },
                "accountModes": {
                  "type": "array",
                  "items": {
                    "enum": [
                      "person",
                      "chat",
                      "owner"
                    ],
                    "description": "Con qué cuenta trabaja: la de cada persona, una por chat, o la de quien hizo la IA."
                  }
                },
                "account": {
                  "enum": [
                    "person",
                    "chat",
                    "owner"
                  ],
                  "description": "Con qué cuenta trabaja: la de cada persona, una por chat, o la de quien hizo la IA."
                },
                "own": {
                  "type": "boolean",
                  "description": "El servidor que vivía adentro del bot; false para todos después de la migración del 2026-10-13."
                }
              }
            },
            "description": "Solo si usa conectores."
          },
          "requestsOpen": {
            "type": "boolean",
            "description": "Solo para el dueño."
          },
          "pay": {
            "enum": [
              "owner",
              "user"
            ],
            "description": "Solo para el dueño."
          },
          "capMonthUsd": {
            "type": [
              "number",
              "null"
            ]
          },
          "perUserDailyReplies": {
            "type": [
              "integer",
              "null"
            ]
          },
          "perUserMonthUsd": {
            "type": [
              "number",
              "null"
            ]
          },
          "verification": {
            "enum": [
              "trusted",
              "pending",
              "none"
            ],
            "description": "Solo para el dueño."
          },
          "verificationRequestedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "actions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MenuItem"
            },
            "description": "Solo para el dueño."
          },
          "systemPrompt": {
            "type": "string",
            "description": "Solo para el dueño."
          },
          "tokenPrefix": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo para el dueño."
          },
          "webhookUrl": {
            "type": "string",
            "description": "Solo para el dueño."
          },
          "webhookSecret": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo para el dueño."
          },
          "lastUsedAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Solo para el dueño."
          }
        },
        "required": [
          "id",
          "name",
          "runtime",
          "builtin",
          "verified",
          "kind",
          "visibility"
        ],
        "description": "Lo que dice «Solo para el dueño» llega solo a quien lo puede editar. Nunca vuelven la clave, los tokens ni la URL de un MCP ajeno."
      },
      "Verification": {
        "type": "object",
        "properties": {
          "verification": {
            "enum": [
              "pending",
              "trusted"
            ]
          },
          "verificationRequestedAt": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "verification"
        ]
      },
      "BotAccessPerson": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "via": {
            "enum": [
              "share",
              "approved",
              "link",
              "chat"
            ]
          },
          "since": {
            "type": "integer"
          }
        }
      },
      "BotAccess": {
        "type": "object",
        "properties": {
          "people": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BotAccessPerson"
            }
          },
          "domains": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string"
                },
                "people": {
                  "type": "integer"
                }
              }
            }
          },
          "requests": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "user": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "color": {
                      "type": "string"
                    },
                    "avatar": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "at": {
                  "type": "integer"
                }
              }
            }
          },
          "link": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "expiresAt": {
                "type": "integer"
              }
            }
          }
        },
        "required": [
          "people",
          "domains",
          "requests",
          "link"
        ]
      },
      "DeveloperProfile": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 60
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "bio": {
            "type": "string",
            "maxLength": 200
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactable": {
            "type": "boolean"
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "verified": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "official",
              "trusted",
              null
            ],
            "description": "El check: dorado (de Puentes) o verde (verificado). Solo lectura."
          },
          "since": {
            "type": [
              "integer",
              "null"
            ]
          },
          "verification": {
            "enum": [
              "none",
              "pending",
              "trusted"
            ]
          },
          "verificationRequestedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "complete": {
            "type": "boolean",
            "description": "Completo, un bot puede ir al catálogo."
          },
          "missing": {
            "type": "array",
            "items": {
              "enum": [
                "name",
                "bio",
                "contact"
              ]
            }
          }
        }
      },
      "OrganizationInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 60
          },
          "bio": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "format": "uri"
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120,
            "format": "email"
          },
          "contactable": {
            "type": "boolean"
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z]{2}$"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500
          }
        },
        "description": "Al crear, name es obligatorio."
      },
      "Organization": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "const": "org"
          },
          "name": {
            "type": "string"
          },
          "bio": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ]
          },
          "contactable": {
            "type": "boolean"
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "verified": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "official",
              "trusted",
              null
            ],
            "description": "El check: dorado (de Puentes) o verde (verificado). Solo lectura."
          },
          "since": {
            "type": [
              "integer",
              "null"
            ]
          },
          "memberCount": {
            "type": "integer"
          },
          "botCount": {
            "type": "integer"
          },
          "contactUserId": {
            "type": [
              "string",
              "null"
            ]
          },
          "verification": {
            "enum": [
              "none",
              "pending",
              "trusted"
            ]
          },
          "verificationRequestedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "complete": {
            "type": "boolean"
          },
          "missing": {
            "type": "array",
            "items": {
              "enum": [
                "name",
                "bio",
                "contact"
              ]
            }
          },
          "role": {
            "enum": [
              "admin",
              "dev"
            ],
            "description": "El mío."
          },
          "status": {
            "enum": [
              "invited",
              "active"
            ]
          }
        }
      },
      "OrganizationMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "enum": [
              "admin",
              "dev"
            ]
          },
          "status": {
            "enum": [
              "invited",
              "active"
            ]
          },
          "since": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Tasklist": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "groupId": {
            "type": "string"
          },
          "messageId": {
            "type": [
              "string",
              "null"
            ],
            "description": "null si su mensaje se borró."
          },
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "createdBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Task"
            }
          },
          "done": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "groupId",
          "messageId",
          "name",
          "createdBy",
          "items",
          "done",
          "total"
        ]
      },
      "Task": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "maxLength": 200
          },
          "done": {
            "type": "boolean"
          },
          "doneBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "doneByName": {
            "type": [
              "string",
              "null"
            ]
          },
          "addedBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "dueAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Un recordatorio: a la hora, aviso a todo el grupo."
          },
          "notifiedAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "assignee": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              }
            }
          }
        },
        "required": [
          "id",
          "text",
          "done",
          "dueAt"
        ]
      },
      "Table": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "groupId": {
            "type": "string"
          },
          "messageId": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "createdBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "columns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TableColumn"
            },
            "maxItems": 8
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TableRow"
            },
            "maxItems": 200
          },
          "summary": {
            "$ref": "#/components/schemas/TableSummary"
          }
        },
        "required": [
          "id",
          "groupId",
          "name",
          "columns",
          "rows",
          "summary"
        ]
      },
      "TableColumn": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 32
          },
          "name": {
            "type": "string",
            "maxLength": 40
          },
          "type": {
            "enum": [
              "status",
              "select",
              "person",
              "number",
              "date",
              "text",
              "check"
            ]
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "maxLength": 24
                },
                "label": {
                  "type": "string",
                  "maxLength": 30
                },
                "color": {
                  "enum": [
                    "green",
                    "yellow",
                    "red",
                    "blue",
                    "grey"
                  ],
                  "description": "Solo status."
                }
              },
              "required": [
                "id",
                "label"
              ]
            },
            "minItems": 1,
            "maxItems": 12,
            "description": "Solo status y select."
          }
        },
        "required": [
          "id",
          "name",
          "type"
        ]
      },
      "TableRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "cells": {
            "type": "object",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "string",
                  "maxLength": 200,
                  "description": "id de opción, fecha YYYY-MM-DD o texto"
                },
                {
                  "type": "number"
                },
                {
                  "const": true,
                  "description": "check"
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name"
                  ],
                  "description": "person"
                }
              ]
            },
            "description": "{ idDeColumna: valor }; las vacías no vienen."
          }
        },
        "required": [
          "id",
          "title",
          "cells"
        ]
      },
      "TableSummary": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "integer"
          },
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "integer"
              }
            },
            "description": "status/select: { columna: { opción: cuántas, \"\": vacías } }"
          },
          "sums": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "byPerson": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "number"
              }
            }
          },
          "checked": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        },
        "required": [
          "rows"
        ]
      },
      "ScheduleRule": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "now": {
                "const": true
              }
            },
            "required": [
              "now"
            ]
          },
          {
            "type": "object",
            "properties": {
              "once": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$"
              }
            },
            "required": [
              "once"
            ]
          },
          {
            "type": "object",
            "properties": {
              "freq": {
                "const": "daily"
              },
              "time": {
                "type": "string",
                "pattern": "^\\d{2}:\\d{2}$"
              }
            },
            "required": [
              "freq",
              "time"
            ]
          },
          {
            "type": "object",
            "properties": {
              "freq": {
                "const": "weekly"
              },
              "days": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 6
                },
                "description": "0 = domingo."
              },
              "time": {
                "type": "string",
                "pattern": "^\\d{2}:\\d{2}$"
              }
            },
            "required": [
              "freq",
              "days",
              "time"
            ]
          },
          {
            "type": "object",
            "properties": {
              "freq": {
                "const": "monthly"
              },
              "monthDay": {
                "type": "integer",
                "minimum": 1,
                "maximum": 31
              },
              "time": {
                "type": "string",
                "pattern": "^\\d{2}:\\d{2}$"
              }
            },
            "required": [
              "freq",
              "monthDay",
              "time"
            ]
          }
        ],
        "description": "A la hora del lugar (tz). Nada más seguido que una vez por día."
      },
      "Schedule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "groupId": {
            "type": "string"
          },
          "botId": {
            "type": "string"
          },
          "botName": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdByName": {
            "type": [
              "string",
              "null"
            ]
          },
          "action": {
            "enum": [
              "say",
              "wake",
              "send"
            ],
            "description": "say: decir un texto; wake: despertar a la IA con una consigna; send: mandar en nombre de la persona."
          },
          "targetGroupId": {
            "type": [
              "string",
              "null"
            ]
          },
          "targetLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "send: a quién, dicho para la persona."
          },
          "via": {
            "enum": [
              "whatsapp",
              "telegram",
              null
            ]
          },
          "held": {
            "enum": [
              "grant",
              "confirm",
              null
            ],
            "description": "Esperando un permiso o la tarjeta de confirmación."
          },
          "onlyFor": {
            "type": [
              "string",
              "null"
            ]
          },
          "groupName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo en GET /api/me/schedules."
          },
          "text": {
            "type": "string",
            "maxLength": 500
          },
          "rule": {
            "$ref": "#/components/schemas/ScheduleRule"
          },
          "tz": {
            "type": "string"
          },
          "nextAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null: ya terminó."
          },
          "lastFiredAt": {
            "type": [
              "integer",
              "null"
            ]
          },
          "runs": {
            "type": "integer"
          },
          "paused": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "groupId",
          "botId",
          "action",
          "text",
          "rule",
          "tz",
          "nextAt",
          "paused"
        ]
      },
      "Pin": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 32,
            "pattern": "^[a-z0-9-]+$"
          },
          "botId": {
            "type": "string"
          },
          "botName": {
            "type": [
              "string",
              "null"
            ]
          },
          "groupId": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "maxLength": 24
          },
          "badge": {
            "type": [
              "string",
              "null"
            ]
          },
          "detail": {
            "type": [
              "string",
              "null"
            ]
          },
          "icon": {
            "enum": [
              "basket",
              "list",
              "tasks",
              "money",
              "clock",
              "store",
              "food",
              "home",
              "plane",
              "cake",
              "ball",
              "star",
              "bell",
              "file",
              "location",
              "people",
              "form",
              "key",
              null
            ]
          },
          "mine": {
            "type": "boolean",
            "description": "Es solo mío (scope person)."
          },
          "card": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CardInput"
              },
              {
                "type": "null"
              }
            ]
          },
          "buttons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ButtonItem"
            },
            "maxItems": 4
          },
          "updatedAt": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "botId",
          "groupId",
          "label",
          "mine",
          "buttons",
          "updatedAt"
        ],
        "description": "El fijado de un bot arriba del chat."
      },
      "PinnedEntry": {
        "type": "object",
        "properties": {
          "message": {
            "$ref": "#/components/schemas/Message"
          },
          "pinnedBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "pinnedByName": {
            "type": [
              "string",
              "null"
            ]
          },
          "pinnedAt": {
            "type": "integer"
          }
        },
        "required": [
          "message",
          "pinnedAt"
        ],
        "description": "Un mensaje fijado del chat."
      },
      "ConnectorTool": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "maxLength": 60
          },
          "changes": {
            "const": true,
            "description": "Cambia algo (no declara readOnlyHint, o su creador la marcó)."
          },
          "confirm": {
            "const": true,
            "description": "Pide confirmación antes de correr."
          }
        },
        "required": [
          "name",
          "label"
        ]
      },
      "Connector": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "maxLength": 40
          },
          "description": {
            "type": "string",
            "maxLength": 300
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "builtin": {
            "type": "boolean"
          },
          "verified": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "official",
              "trusted",
              null
            ],
            "description": "El check: dorado (de Puentes) o verde (verificado). Solo lectura."
          },
          "by": {
            "type": [
              "string",
              "null"
            ]
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "auth": {
            "enum": [
              "none",
              "bearer",
              "oauth"
            ]
          },
          "accountModes": {
            "type": "array",
            "items": {
              "enum": [
                "person",
                "chat",
                "owner"
              ],
              "description": "Con qué cuenta trabaja: la de cada persona, una por chat, o la de quien hizo la IA."
            }
          },
          "exclusive": {
            "type": "boolean",
            "description": "Solo para las IAs de quien lo hizo."
          },
          "connectHelp": {
            "type": [
              "string",
              "null"
            ]
          },
          "tools": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConnectorTool"
            }
          },
          "bots": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "color": {
                  "type": "string"
                },
                "avatar": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "ownerId": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "by": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            },
            "description": "En browse y en la ficha: las IAs que lo usan."
          }
        },
        "required": [
          "id",
          "name",
          "auth",
          "accountModes",
          "tools"
        ],
        "description": "Nunca trae la URL del servidor, su clave ni las herramientas escondidas."
      },
      "OwnerConnector": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Connector"
          },
          {
            "type": "object",
            "properties": {
              "hasServerKey": {
                "type": "boolean",
                "description": "La clave nunca vuelve."
              },
              "url": {
                "type": "string"
              },
              "visibility": {
                "enum": [
                  "private",
                  "link",
                  "authorized",
                  "catalog"
                ]
              },
              "orgId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "status": {
                "type": "string",
                "description": "needs_auth | connected | error"
              },
              "error": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "transport": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "checkedAt": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "toolLabels": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": {
                  "$ref": "#/components/schemas/ToolLabel"
                }
              },
              "bots": {
                "type": "integer"
              },
              "accounts": {
                "type": "integer"
              }
            }
          }
        ],
        "description": "Un conector para quien lo registró (o su organización)."
      },
      "ToolLabel": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 60,
            "description": "El nombre para personas."
          },
          "changes": {
            "const": true
          },
          "confirm": {
            "const": true,
            "description": "Pide confirmación antes de correr."
          },
          "hidden": {
            "const": true,
            "description": "No se le ofrece a la IA."
          }
        }
      },
      "ConnectorInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 40
          },
          "url": {
            "type": "string",
            "maxLength": 500,
            "description": "https pública."
          },
          "orgId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          },
          "description": {
            "type": "string",
            "maxLength": 300
          },
          "category": {
            "type": "string",
            "maxLength": 40
          },
          "connectHelp": {
            "type": "string",
            "maxLength": 300
          },
          "avatar": {
            "type": [
              "string",
              "null"
            ]
          },
          "accountModes": {
            "type": "array",
            "items": {
              "enum": [
                "person",
                "chat",
                "owner"
              ],
              "description": "Con qué cuenta trabaja: la de cada persona, una por chat, o la de quien hizo la IA."
            },
            "description": "Default: [owner]."
          },
          "exclusive": {
            "type": "boolean"
          },
          "visibility": {
            "enum": [
              "private",
              "link",
              "authorized",
              "catalog"
            ]
          },
          "auth": {
            "enum": [
              "none",
              "bearer",
              "oauth"
            ],
            "description": "Sin él, Puentes lo detecta."
          },
          "serverKey": {
            "type": "string",
            "maxLength": 4000,
            "writeOnly": true,
            "description": "La clave del servidor: dice «esto es Puentes»."
          },
          "toolLabels": {
            "type": "object",
            "maxProperties": 64,
            "additionalProperties": {
              "$ref": "#/components/schemas/ToolLabel"
            }
          }
        },
        "description": "Al crear, name y url son obligatorios."
      },
      "Connection": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "connectorId": {
            "type": "string"
          },
          "connector": {
            "$ref": "#/components/schemas/Connector"
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "El nombre de la cuenta."
          },
          "status": {
            "enum": [
              "ok",
              "reconnect",
              "error"
            ]
          },
          "error": {
            "type": "string"
          },
          "createdAt": {
            "type": "integer"
          },
          "lastUsedAt": {
            "type": "integer"
          },
          "bots": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "with": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            },
            "description": "Las IAs a las que se la presté."
          },
          "chats": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "groupId": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "botId": {
                  "type": "string"
                },
                "botName": {
                  "type": "string"
                }
              }
            },
            "description": "Solo en GET /api/me/connections/:id."
          }
        },
        "required": [
          "id",
          "connectorId",
          "status"
        ],
        "description": "Una cuenta de una persona en un servicio. Nunca trae la clave ni los tokens."
      }
    }
  },
  "paths": {
    "/api/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Quién soy, config y lo nuevo"
        ],
        "summary": "Quién soy.",
        "description": "Quién soy. Un bot ve `bot: true` y `ownerId`.",
        "x-puentes-who": "todos",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "patchMe",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "`{ locale?, avatar?, country?, tz?, linkPreviews? }` (al menos uno).",
        "description": "`{ locale?, avatar?, country?, tz?, linkPreviews? }` (al menos uno). El nombre no se cambia acá.",
        "x-puentes-who": "persona",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "locale": {
                    "type": "string",
                    "description": "es | en | pt-BR"
                  },
                  "avatar": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500
                  },
                  "country": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "ISO de dos letras, o \"all\"."
                  },
                  "tz": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Zona IANA."
                  },
                  "linkPreviews": {
                    "type": "boolean",
                    "description": "Armar las fichas de los links que mando."
                  }
                },
                "description": "Al menos un campo."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteMe",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Borrar la cuenta entera, al instante: membresías, directos, IAs propias, emails, teléfonos, sesiones, API keys.",
        "description": "Borrar la cuenta entera, al instante: membresías, directos, IAs propias, emails, teléfonos, sesiones, API keys. Los mensajes quedan para los demás como «Cuenta eliminada». No se puede deshacer.",
        "x-puentes-who": "persona (sesión)",
        "x-puentes-limits": [
          "5 por hora por cuenta"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/config": {
      "get": {
        "operationId": "getConfig",
        "tags": [
          "Quién soy, config y lo nuevo"
        ],
        "summary": "Lo que este server tiene prendido: proveedores y niveles, cuota gratis, `emailEnabled`, `mcpEnabled`, `connectors`… Ver ",
        "description": "Lo que este server tiene prendido: proveedores y niveles, cuota gratis, `emailEnabled`, `mcpEnabled`, `connectors`… Ver [/api/config](#config).",
        "x-puentes-who": "público",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/sockets": {
      "get": {
        "operationId": "getSockets",
        "tags": [
          "Quién soy, config y lo nuevo"
        ],
        "summary": "A qué host conectar el socket: `{ host, hosts }` (`host: null` = `https://app.puentes.ai`).",
        "description": "A qué host conectar el socket: `{ host, hosts }` (`host: null` = `https://app.puentes.ai`).",
        "x-puentes-who": "público",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/me/groups": {
      "get": {
        "operationId": "getMeGroups",
        "tags": [
          "Quién soy, config y lo nuevo"
        ],
        "summary": "Mis chats (grupos y directos) con miembros y último mensaje.",
        "description": "Mis chats (grupos y directos) con miembros y último mensaje.",
        "x-puentes-who": "todos",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/Chat"
                      },
                      {
                        "type": "object",
                        "properties": {
                          "unread": {
                            "type": "integer",
                            "minimum": 0
                          }
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/updates": {
      "get": {
        "operationId": "getMeUpdates",
        "tags": [
          "Quién soy, config y lo nuevo"
        ],
        "summary": "`?after=<seq>&timeout=<0-25>`: los mensajes nuevos de todos mis chats desde ese cursor (menos los míos y los borrados), ",
        "description": "`?after=<seq>&timeout=<0-25>`: los mensajes nuevos de todos mis chats desde ese cursor (menos los míos y los borrados), hasta 100, con long-poll. Sin `after`: `{ messages: [], next }` con el cursor actual. Responde `{ messages, next }`. A un bot externo, de un grupo donde responde «solo cuando lo mencionan», le llegan solo los que lo mencionan (ver [Modo](#modo)); `next` pasa los demás.",
        "x-puentes-who": "todos",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "El cursor (`next` de la respuesta anterior). Sin él: solo el cursor actual.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "timeout",
            "in": "query",
            "required": false,
            "description": "Segundos de long-poll.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      },
                      "maxItems": 100
                    },
                    "next": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "messages",
                    "next"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/usage": {
      "get": {
        "operationId": "getMeUsage",
        "tags": [
          "Quién soy, config y lo nuevo"
        ],
        "summary": "La cuota: `{ dayLimit, dayUsed, dayLeft, credits, left, day, lifetime }` (más los nombres viejos `freeUsed`, `freeLimit`",
        "description": "La cuota: `{ dayLimit, dayUsed, dayLeft, credits, left, day, lifetime }` (más los nombres viejos `freeUsed`, `freeLimit`, `freeLeft`). Un bot ve la de su dueño.",
        "x-puentes-who": "todos",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dayLimit": {
                      "type": "integer",
                      "description": "Los mensajes gratis del día (el doble mientras dura una promo)."
                    },
                    "dayUsed": {
                      "type": "integer"
                    },
                    "dayLeft": {
                      "type": "integer"
                    },
                    "credits": {
                      "type": "integer"
                    },
                    "left": {
                      "type": "integer"
                    },
                    "day": {
                      "type": "string"
                    },
                    "lifetime": {
                      "type": "integer"
                    },
                    "boostUntil": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Hasta cuándo dura la promo de instalación (epoch ms); null sin promo."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/developers/{id}": {
      "get": {
        "operationId": "getDevelopersId",
        "tags": [
          "Quién soy, config y lo nuevo"
        ],
        "summary": "El perfil público de un desarrollador (`kind: \"person\"`) o de una organización (`kind: \"org\"`) y sus bots visibles para ",
        "description": "El perfil público de un desarrollador (`kind: \"person\"`) o de una organización (`kind: \"org\"`) y sus bots visibles para quien pregunta.",
        "x-puentes-who": "todos",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "El perfil público (kind: person | org) y sus bots visibles para quien pregunta."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/messages": {
      "get": {
        "operationId": "getGroupsIdMessages",
        "tags": [
          "Mensajes"
        ],
        "summary": "El historial.",
        "description": "El historial. **Un bot (`pb_`) sin parámetros**: un array con los últimos 500, del más viejo al más nuevo. **Una persona o una API key**: una página, `{ messages, more, next, clearedAt }` con los últimos 40 (`?limit=1…100`); `?before=<id>` los anteriores (20 por default); `?before=<id>&to=<id>` salta hasta un mensaje (`{ tooFar: true }` a más de 500); `?since=<next>` lo que cambió desde el cursor `next` de la respuesta anterior: `{ messages, hidden, statuses, next }`, o `{ reset: true, … }` con la primera página si cambiaron más de 200. Un bot puede usar los mismos parámetros.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Tamaño de la página (40 por default).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Id de un mensaje: los anteriores a él.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Con `before`: hasta ese mensaje.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "El cursor `next` de una respuesta anterior: lo que cambió desde entonces.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      },
                      "maxItems": 500,
                      "description": "Un bot sin parámetros: los últimos 500."
                    },
                    {
                      "type": "object",
                      "properties": {
                        "messages": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Message"
                          }
                        },
                        "more": {
                          "type": "boolean"
                        },
                        "next": {
                          "type": "integer"
                        },
                        "clearedAt": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "hidden": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "statuses": {
                          "type": "object",
                          "additionalProperties": {
                            "enum": [
                              "sent",
                              "delivered",
                              "read"
                            ]
                          }
                        },
                        "reset": {
                          "type": "boolean"
                        },
                        "tooFar": {
                          "type": "boolean"
                        },
                        "missing": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postGroupsIdMessages",
        "tags": [
          "Mensajes"
        ],
        "summary": "Enviar `{ text?, attachments?, replyTo?, clientId?, linkPreview? }` (ver [Enviar mensajes](#enviar)).",
        "description": "Enviar `{ text?, attachments?, replyTo?, clientId?, linkPreview? }` (ver [Enviar mensajes](#enviar)). `clientId` (hasta 60) lo hace **idempotente** por chat y remitente: si vuelve a llegar, la respuesta es el mensaje ya guardado y nadie lo recibe dos veces. `linkPreview: false` pide no armar la ficha del link.",
        "x-puentes-who": "todos · miembro",
        "x-puentes-limits": [
          "30 por minuto por bot, 60 por persona (`Retry-After: 60`)"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 20000,
                    "description": "Markdown liviano. Vacío si hay adjuntos."
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/AttachmentInput"
                    },
                    "maxItems": 10
                  },
                  "replyTo": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Id (uuid) de un mensaje del mismo chat para citarlo."
                  },
                  "clientId": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "Hace el pedido idempotente: si vuelve a llegar el mismo, la respuesta es lo ya guardado."
                  },
                  "linkPreview": {
                    "const": false,
                    "description": "false: no armar la ficha del link."
                  }
                },
                "description": "Hace falta `text` o algún adjunto."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/read": {
      "post": {
        "operationId": "postGroupsIdRead",
        "tags": [
          "Mensajes"
        ],
        "summary": "Leí el chat hasta `{ upTo? }` (id de mensaje; sin él, hasta el último): tildes azules para los demás y el contador en ce",
        "description": "Leí el chat hasta `{ upTo? }` (id de mensaje; sin él, hasta el último): tildes azules para los demás y el contador en cero en mis dispositivos. Un bot nunca «lee»: los tildes solo cuentan personas.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "upTo": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Id del último mensaje leído; sin él, hasta el último."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/unread": {
      "post": {
        "operationId": "postGroupsIdUnread",
        "tags": [
          "Mensajes"
        ],
        "summary": "«Marcar como no leído» en todos mis dispositivos.",
        "description": "«Marcar como no leído» en todos mis dispositivos.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/delivered": {
      "post": {
        "operationId": "postMeDelivered",
        "tags": [
          "Mensajes"
        ],
        "summary": "Al reconectar: todo lo escrito en mis chats desde `{ since? }` (epoch ms, a lo sumo 7 días) llegó a este dispositivo.",
        "description": "Al reconectar: todo lo escrito en mis chats desde `{ since? }` (epoch ms, a lo sumo 7 días) llegó a este dispositivo. Devuelve `{ ok, groups }`.",
        "x-puentes-who": "todos",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "since": {
                    "type": "integer",
                    "description": "Epoch ms, a lo sumo 7 días atrás."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "groups": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/messages/{mid}": {
      "delete": {
        "operationId": "deleteGroupsIdMessagesMid",
        "tags": [
          "Mensajes"
        ],
        "summary": "Borrar, como en WhatsApp.",
        "description": "Borrar, como en WhatsApp. `?scope=me` (default): solo desaparece de mi historial (`message:hidden` a mis dispositivos). `?scope=all`: un mensaje mío de los últimos 2 días (o cualquiera, si soy admin) queda como `{ deleted: true, deletedBy, text: \"\", attachments: [] }` para todos (`message:updated`), sus archivos se borran y las citas traen `replyTo.deleted`.",
        "x-puentes-who": "todos · miembro",
        "x-puentes-limits": [
          "120 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "description": "`me` (default) o `all`.",
            "schema": {
              "enum": [
                "me",
                "all"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "message": {
                      "$ref": "#/components/schemas/Message"
                    },
                    "lastMessage": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Message"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/typing": {
      "post": {
        "operationId": "postGroupsIdTyping",
        "tags": [
          "Mensajes"
        ],
        "summary": "«Está escribiendo…» unos segundos (`typing` en el socket).",
        "description": "«Está escribiendo…» unos segundos (`typing` en el socket).",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/upload": {
      "post": {
        "operationId": "postUpload",
        "tags": [
          "Mensajes"
        ],
        "summary": "Subir un archivo (multipart, campo `file`, hasta 25 MB; `duration` opcional para un audio).",
        "description": "Subir un archivo (multipart, campo `file`, hasta 25 MB; `duration` opcional para un audio). Devuelve el adjunto para `attachments`: `{ type: \"image\" | \"audio\" | \"video\" | \"file\", url, name, mime, size, duration? }`.",
        "x-puentes-who": "todos",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Hasta 25 MB."
                  },
                  "duration": {
                    "type": "number",
                    "description": "Segundos, para un audio."
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadedFile"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/polls": {
      "get": {
        "operationId": "getGroupsIdPolls",
        "tags": [
          "Mensajes"
        ],
        "summary": "Las encuestas abiertas del chat (hasta 20, de los últimos 30 días): `[{ message }]`.",
        "description": "Las encuestas abiertas del chat (hasta 20, de los últimos 30 días): `[{ message }]`.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "message": {
                        "$ref": "#/components/schemas/Message"
                      }
                    }
                  },
                  "maxItems": 20
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postGroupsIdPolls",
        "tags": [
          "Mensajes"
        ],
        "summary": "Crear una encuesta `{ title (≤ 80), options: [2 a 12, ≤ 100 cada una], multi?, hours? (1-168), clientId? }`.",
        "description": "Crear una encuesta `{ title (≤ 80), options: [2 a 12, ≤ 100 cada una], multi?, hours? (1-168), clientId? }`. Devuelve `{ message }`; no despierta a ninguna IA.",
        "x-puentes-who": "persona · miembro",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "maxLength": 100
                    },
                    "maxItems": 12,
                    "minItems": 2
                  },
                  "multi": {
                    "type": "boolean",
                    "description": "Se pueden elegir varias."
                  },
                  "hours": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 168,
                    "description": "Cierra sola a las tantas horas; sin él, no vence."
                  },
                  "clientId": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "Hace el pedido idempotente: si vuelve a llegar el mismo, la respuesta es lo ya guardado."
                  }
                },
                "required": [
                  "title",
                  "options"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/Message"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/messages/{mid}/vote": {
      "post": {
        "operationId": "postGroupsIdMessagesMidVote",
        "tags": [
          "Mensajes"
        ],
        "summary": "Votar `{ ids: [...] }` (vacío retira el voto).",
        "description": "Votar `{ ids: [...] }` (vacío retira el voto). `409` si ya cerró. Devuelve `{ ok, card }`; los votos los escribe solo el server.",
        "x-puentes-who": "persona · miembro",
        "x-puentes-limits": [
          "120 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "maxLength": 64
                    },
                    "maxItems": 50,
                    "description": "Los ids de los ítems elegidos; vacío retira el voto. Sin `multi`, cuenta el primero."
                  }
                },
                "required": [
                  "ids"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "card": {
                      "$ref": "#/components/schemas/OptionsCard"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/messages/{mid}/reaction": {
      "put": {
        "operationId": "putGroupsIdMessagesMidReaction",
        "tags": [
          "Mensajes"
        ],
        "summary": "`{ emoji }`: un emoji por persona y mensaje; `null` o `\"\"` saca el mío.",
        "description": "`{ emoji }`: un emoji por persona y mensaje; `null` o `\"\"` saca el mío. `400` si no es un emoji. Devuelve `{ ok, reactions }`.",
        "x-puentes-who": "persona · miembro",
        "x-puentes-limits": [
          "300 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "emoji": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Un solo emoji; null o \"\" saca el mío."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "reactions": {
                      "$ref": "#/components/schemas/Reactions"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/messages/{mid}/forward": {
      "post": {
        "operationId": "postGroupsIdMessagesMidForward",
        "tags": [
          "Mensajes"
        ],
        "summary": "Reenviar `{ to: groupId, clientId? }`: la copia es un mensaje mío con el texto y los archivos (nunca una tarjeta) y la m",
        "description": "Reenviar `{ to: groupId, clientId? }`: la copia es un mensaje mío con el texto y los archivos (nunca una tarjeta) y la marca `forward`. Devuelve `{ message, groupId }`.",
        "x-puentes-who": "persona · miembro",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "El chat de destino (tenés que ser miembro)."
                  },
                  "clientId": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "Hace el pedido idempotente: si vuelve a llegar el mismo, la respuesta es lo ya guardado."
                  }
                },
                "required": [
                  "to"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/Message"
                    },
                    "groupId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "operationId": "getSearch",
        "tags": [
          "Mensajes"
        ],
        "summary": "Buscar en todos mis chats: `?q=` (las palabras, en cualquier orden, sin importar mayúsculas ni tildes; también en la tra",
        "description": "Buscar en todos mis chats: `?q=` (las palabras, en cualquier orden, sin importar mayúsculas ni tildes; también en la transcripción de los audios). Devuelve `{ results: [{ id, groupId, seq, senderId, senderName, senderType, createdAt, snippet: { text, ranges, audio? } }], more }`, 30 por página del más nuevo al más viejo; la siguiente con `?before=<createdAt>&beforeSeq=<seq>` del último. Solo lo que veo en el chat: nada borrado, nada de antes de vaciarlo, ninguna línea del sistema. `503` si tardó demasiado (con más palabras va más rápido). Solo personas.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "600 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Lo que se busca.",
            "schema": {
              "type": "string",
              "maxLength": 100,
              "description": "Las palabras (al menos dos letras en total, hasta 8 palabras): están todas, en cualquier orden, sin importar mayúsculas ni tildes."
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "El `createdAt` del último resultado de la página anterior.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "beforeSeq",
            "in": "query",
            "required": false,
            "description": "Su `seq`: va siempre con `before`.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchResult"
                      },
                      "maxItems": 30,
                      "description": "Del más nuevo al más viejo."
                    },
                    "more": {
                      "type": "boolean",
                      "description": "Hay más: pedí la siguiente con `before` y `beforeSeq` del último."
                    }
                  },
                  "required": [
                    "results",
                    "more"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/search": {
      "get": {
        "operationId": "getGroupsIdSearch",
        "tags": [
          "Mensajes"
        ],
        "summary": "Lo mismo en un solo chat (el «Buscar» del chat): `?q=`, `?before=` y `?beforeSeq=`; el mensaje se abre con `GET /api/gro",
        "description": "Lo mismo en un solo chat (el «Buscar» del chat): `?q=`, `?before=` y `?beforeSeq=`; el mensaje se abre con `GET /api/groups/:id/messages?before=…&to=<id>`.",
        "x-puentes-who": "persona · miembro",
        "x-puentes-limits": [
          "600 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Lo que se busca.",
            "schema": {
              "type": "string",
              "maxLength": 100,
              "description": "Las palabras (al menos dos letras en total, hasta 8 palabras): están todas, en cualquier orden, sin importar mayúsculas ni tildes."
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "El `createdAt` del último resultado de la página anterior.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "beforeSeq",
            "in": "query",
            "required": false,
            "description": "Su `seq`: va siempre con `before`.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchResult"
                      },
                      "maxItems": 30,
                      "description": "Del más nuevo al más viejo."
                    },
                    "more": {
                      "type": "boolean",
                      "description": "Hay más: pedí la siguiente con `before` y `beforeSeq` del último."
                    }
                  },
                  "required": [
                    "results",
                    "more"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/link-preview": {
      "get": {
        "operationId": "getLinkPreview",
        "tags": [
          "Mensajes"
        ],
        "summary": "`?url=`: la ficha de un link (`{ url, title, site, description?, image?, imageW?, imageH?, icon? }`) o `204` si no hay.",
        "description": "`?url=`: la ficha de un link (`{ url, title, site, description?, image?, imageW?, imageH?, icon? }`) o `204` si no hay.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "600 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "http(s)://…",
            "schema": {
              "type": "string",
              "maxLength": 2000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkPreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}": {
      "get": {
        "operationId": "getGroupsId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Un chat: `kind`, miembros (personas y bots), `peer` si es directo, `perms`.",
        "description": "Un chat: `kind`, miembros (personas y bots), `peer` si es directo, `perms`. Ver [Objetos](#objetos).",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "patchGroupsId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "`{ name }` o `{ avatar }` (permiso `editInfo`), `{ linkOff }` («solo por invitación») y `{ perms: { editInfo?, send?, ad",
        "description": "`{ name }` o `{ avatar }` (permiso `editInfo`), `{ linkOff }` («solo por invitación») y `{ perms: { editInfo?, send?, addMembers?, shareLink?: \"all\" | \"admins\", approve? } }` (admins; ver [Admins y permisos](#admins)).",
        "x-puentes-who": "persona · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "avatar": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "Una imagen subida a Puentes; null la saca."
                  },
                  "linkOff": {
                    "type": "boolean",
                    "description": "«Solo por invitación»."
                  },
                  "perms": {
                    "type": "object",
                    "properties": {
                      "editInfo": {
                        "enum": [
                          "all",
                          "admins"
                        ]
                      },
                      "send": {
                        "enum": [
                          "all",
                          "admins"
                        ]
                      },
                      "addMembers": {
                        "enum": [
                          "all",
                          "admins"
                        ]
                      },
                      "shareLink": {
                        "enum": [
                          "all",
                          "admins"
                        ]
                      },
                      "approve": {
                        "type": "boolean"
                      }
                    },
                    "additionalProperties": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteGroupsId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Eliminar el grupo para todos.",
        "description": "Eliminar el grupo para todos.",
        "x-puentes-who": "persona · miembro · permiso «admins»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups": {
      "post": {
        "operationId": "postGroups",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Crear un grupo `{ name, members?: [userId | { userId, email | phone }], assistant?, puentes? }`.",
        "description": "Crear un grupo `{ name, members?: [userId | { userId, email | phone }], assistant?, puentes? }`. Los miembros iniciales tienen que ser contactos (comparten un chat) o bots que podés usar. Con `assistant: { name, systemPrompt?, mode?, opening? }` nace con ese asistente (un bot tuyo) en vez de Claude; `puentes: false` sin ninguna IA.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "30 por día por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "members": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "userId": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string",
                              "maxLength": 254
                            },
                            "phone": {
                              "type": "string",
                              "maxLength": 40
                            },
                            "country": {
                              "type": "string",
                              "pattern": "^[A-Za-z]{2}$",
                              "description": "ISO de dos letras, para un número sin código de país."
                            }
                          },
                          "required": [
                            "userId"
                          ]
                        }
                      ]
                    },
                    "maxItems": 50,
                    "description": "Contactos (comparten un chat con vos), bots que podés usar, o alguien de quien conocés el email o teléfono verificado. Los demás se ignoran."
                  },
                  "assistant": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "maxLength": 40
                      },
                      "systemPrompt": {
                        "type": "string",
                        "maxLength": 4000
                      },
                      "mode": {
                        "enum": [
                          "mention",
                          "always"
                        ]
                      },
                      "opening": {
                        "type": "string",
                        "maxLength": 600
                      },
                      "provider": {
                        "type": "string"
                      },
                      "tier": {
                        "enum": [
                          1,
                          2,
                          3
                        ]
                      },
                      "template": {
                        "type": "string",
                        "maxLength": 40
                      },
                      "locale": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "name"
                    ],
                    "description": "El grupo nace con ese asistente (un bot tuyo, reusado si ya tenés uno con ese nombre)."
                  },
                  "puentes": {
                    "type": "boolean",
                    "description": "false: el grupo nace sin ninguna IA. Sin el campo, con la IA de siempre."
                  },
                  "aiTemplate": {
                    "type": "string",
                    "maxLength": 30,
                    "description": "`clima` o `deportes`: nace con ese bot del catálogo."
                  },
                  "landing": {
                    "type": "boolean",
                    "description": "true: viene de un link de campaña sin plantilla (la IA saluda invitando)."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Chat"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "firstGroup": {
                          "const": true,
                          "description": "Solo en el primer grupo de la cuenta."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/link/reset": {
      "post": {
        "operationId": "postGroupsIdLinkReset",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Link de invitación nuevo; el anterior muere.",
        "description": "Link de invitación nuevo; el anterior muere.",
        "x-puentes-who": "persona · miembro · permiso «admins»",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/disappear": {
      "put": {
        "operationId": "putGroupsIdDisappear",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Mensajes temporales `{ ms }`: los mensajes nuevos del chat (también los de las IAs y del sistema) viven 24 horas, 7 días",
        "description": "Mensajes temporales `{ ms }`: los mensajes nuevos del chat (también los de las IAs y del sistema) viven 24 horas, 7 días o 90 días; `null` los desactiva. Lo que ya estaba no cambia. Permiso `editInfo` (en un directo, cualquiera de los dos); el chat con «Puentes» no lo tiene (`400`, `code: \"assistant\"`). Deja una línea de sistema y avisa con `group:updated`. Devuelve el chat.",
        "x-puentes-who": "persona · miembro",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ms": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "enum": [
                      86400000,
                      604800000,
                      7776000000,
                      0,
                      null
                    ],
                    "description": "24 h, 7 días o 90 días en ms; null o 0 los desactiva."
                  }
                },
                "required": [
                  "ms"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/leave": {
      "post": {
        "operationId": "postGroupsIdLeave",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Salir del grupo (sin personas, se borra; sin admins, la persona más antigua pasa a admin).",
        "description": "Salir del grupo (sin personas, se borra; sin admins, la persona más antigua pasa a admin). En un directo lo oculta hasta que el otro escribe.",
        "x-puentes-who": "persona · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "deleted": {
                      "const": true,
                      "description": "El chat se borró (no quedaba nadie)."
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/clear": {
      "post": {
        "operationId": "postGroupsIdClear",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Vaciar el chat para todos (mensajes, lo que recuerda la IA y los fijados de los bots, que salen con `pin:removed`; lista",
        "description": "Vaciar el chat para todos (mensajes, lo que recuerda la IA y los fijados de los bots, que salen con `pin:removed`; listas, tablas y miembros quedan). Con `{ lists: true }` se borran también, para todos, las listas y las tablas del chat: cada una sale con `tasklist:deleted` o `table:deleted`, como al borrar una, y la respuesta trae sus ids. `group:cleared { id, by }`.",
        "x-puentes-who": "persona · miembro · en un grupo, admin",
        "x-puentes-limits": [
          "20 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lists": {
                    "type": "boolean",
                    "description": "true: se borran también, para todos, las listas de tareas y las tablas del chat («Borrar también listas y tablas»). Sin el campo quedan, sin su tarjeta."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "removed": {
                      "type": "integer"
                    },
                    "at": {
                      "type": "integer"
                    },
                    "lists": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Con `lists: true`: los ids de las listas que se borraron."
                    },
                    "tables": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Con `lists: true`: los ids de las tablas que se borraron."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/members": {
      "post": {
        "operationId": "postGroupsIdMembers",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Agregar una persona `{ userId, email?, phone? }`: entra al instante si conocés su email o teléfono verificado, o si es u",
        "description": "Agregar una persona `{ userId, email?, phone? }`: entra al instante si conocés su email o teléfono verificado, o si es un contacto; si no, `404` como si no existiera.",
        "x-puentes-who": "persona · miembro · permiso «agregar gente»",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "userId": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  }
                },
                "required": [
                  "userId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "202": {
            "description": "Espera la aprobación de un admin del grupo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pending": {
                      "const": true
                    },
                    "member": {
                      "$ref": "#/components/schemas/User"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/members/{userId}": {
      "patch": {
        "operationId": "patchGroupsIdMembersUserId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "`{ role: \"admin\" | \"member\" }`.",
        "description": "`{ role: \"admin\" | \"member\" }`. A quien creó el grupo no se le quita.",
        "x-puentes-who": "persona · miembro · admin",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "enum": [
                      "admin",
                      "member"
                    ]
                  }
                },
                "required": [
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteGroupsIdMembersUserId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Sacar a una persona: queda vetada (no vuelve por el link) hasta que un admin la deje volver.",
        "description": "Sacar a una persona: queda vetada (no vuelve por el link) hasta que un admin la deje volver.",
        "x-puentes-who": "persona · miembro · admin",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/bans": {
      "get": {
        "operationId": "getGroupsIdBans",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Las personas sacadas del grupo.",
        "description": "Las personas sacadas del grupo.",
        "x-puentes-who": "persona · miembro · permiso «admins»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/bans/{userId}": {
      "delete": {
        "operationId": "deleteGroupsIdBansUserId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Levantar un veto.",
        "description": "Levantar un veto.",
        "x-puentes-who": "persona · miembro · permiso «admins»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/requests": {
      "get": {
        "operationId": "getGroupsIdRequests",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Con `perms.approve`: quién pidió entrar (`{ id, user, via, addedByName, createdAt }`).",
        "description": "Con `perms.approve`: quién pidió entrar (`{ id, user, via, addedByName, createdAt }`).",
        "x-puentes-who": "persona · miembro · permiso «admins»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/requests/{rid}/approve": {
      "post": {
        "operationId": "postGroupsIdRequestsRidApprove",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Dejar entrar.",
        "description": "Dejar entrar.",
        "x-puentes-who": "persona · miembro · permiso «admins»",
        "x-puentes-limits": [
          "120 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "member": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/User"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/requests/{rid}/reject": {
      "post": {
        "operationId": "postGroupsIdRequestsRidReject",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Rechazar (no se avisa).",
        "description": "Rechazar (no se avisa).",
        "x-puentes-who": "persona · miembro · permiso «admins»",
        "x-puentes-limits": [
          "120 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/invites/{token}": {
      "get": {
        "operationId": "getInvitesToken",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Vista previa de una invitación (`?i=`: el token de una invitación por email).",
        "description": "Vista previa de una invitación (`?i=`: el token de una invitación por email).",
        "x-puentes-who": "público",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "i",
            "in": "query",
            "required": false,
            "description": "El token personal de una invitación por email (obligatorio para un directo o un grupo con el link apagado).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/invites/{token}/join": {
      "post": {
        "operationId": "postInvitesTokenJoin",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Entrar por el link.",
        "description": "Entrar por el link. Un token de bot recibe `403`: a un bot siempre lo suma alguien. Con `perms.approve` responde `202 { pending: true }`.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "30 por hora por IP"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "invite": {
                    "type": "string",
                    "description": "El `i` de una invitación por email."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "202": {
            "description": "Espera la aprobación de un admin del grupo",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/InvitePreview"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "pending": {
                          "const": true
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/invites/{token}/request": {
      "delete": {
        "operationId": "deleteInvitesTokenRequest",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Cancelar mi pedido de entrada.",
        "description": "Cancelar mi pedido de entrada.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/invites": {
      "get": {
        "operationId": "getGroupsIdInvites",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Las invitaciones por email pendientes.",
        "description": "Las invitaciones por email pendientes.",
        "x-puentes-who": "persona · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Invitation"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postGroupsIdInvites",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "`{ email }`: si ya usa Puentes y comparte un chat con vos entra directo (`status: \"added\"`); si no, invitación de 7 días",
        "description": "`{ email }`: si ya usa Puentes y comparte un chat con vos entra directo (`status: \"added\"`); si no, invitación de 7 días (`\"invited\"`).",
        "x-puentes-who": "persona · miembro · permiso «agregar gente» · verificada",
        "x-puentes-limits": [
          "20 por hora y 50 por día (10 el primer día de la cuenta)"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "const": "invited"
                        },
                        "invitation": {
                          "$ref": "#/components/schemas/Invitation"
                        }
                      },
                      "required": [
                        "status",
                        "invitation"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "enum": [
                            "added",
                            "pending",
                            "member"
                          ]
                        },
                        "member": {
                          "$ref": "#/components/schemas/User"
                        }
                      },
                      "required": [
                        "status",
                        "member"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/invites/{inviteId}/resend": {
      "post": {
        "operationId": "postGroupsIdInvitesInviteIdResend",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Reenviar una invitación.",
        "description": "Reenviar una invitación.",
        "x-puentes-who": "persona · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inviteId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "invitation": {
                      "$ref": "#/components/schemas/Invitation"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/invites/{inviteId}": {
      "delete": {
        "operationId": "deleteGroupsIdInvitesInviteId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Cancelarla.",
        "description": "Cancelarla.",
        "x-puentes-who": "persona · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inviteId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/invite-link": {
      "post": {
        "operationId": "postGroupsIdInviteLink",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Un link personal a este grupo para alguien que todavía no está en Puentes (`{ email | phone }`); el server no manda nada",
        "description": "Un link personal a este grupo para alguien que todavía no está en Puentes (`{ email | phone }`); el server no manda nada, devuelve la `url`. Una cuenta sin nada verificado también lo pide, con topes más bajos, y siempre recibe el link.",
        "x-puentes-who": "persona · miembro · permiso «agregar gente»",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "Con código de país (+54…), o local con `country`."
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  }
                },
                "anyOf": [
                  {
                    "required": [
                      "email"
                    ]
                  },
                  {
                    "required": [
                      "phone"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "enum": [
                            "added",
                            "pending",
                            "member"
                          ]
                        },
                        "member": {
                          "$ref": "#/components/schemas/User"
                        }
                      },
                      "required": [
                        "status",
                        "member"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "const": "invited"
                        },
                        "invitation": {
                          "$ref": "#/components/schemas/Invitation"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "required": [
                        "status",
                        "url"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/direct/{userId}": {
      "post": {
        "operationId": "postDirectUserId",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Abrir (o encontrar) el chat directo con una persona o un bot.",
        "description": "Abrir (o encontrar) el chat directo con una persona o un bot. Con una persona hace falta compartir un chat, que los hayan presentado, o su `{ email }` / `{ phone }` verificado.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  }
                },
                "description": "Opcional: el email o teléfono verificado de esa persona, si no comparten un chat."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/direct-invite": {
      "post": {
        "operationId": "postDirectInvite",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "`{ email }` o `{ phone, country }` (+ `name`): directo con alguien que no está en Puentes; devuelve su link personal (`u",
        "description": "`{ email }` o `{ phone, country }` (+ `name`): directo con alguien que no está en Puentes; devuelve su link personal (`url`) para mandárselo vos. Una cuenta sin nada verificado también, con topes más bajos: a ella siempre le devuelve el link, nunca el chat de alguien que ya está.",
        "x-puentes-who": "persona",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "Con código de país (+54…), o local con `country`."
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "El nombre del chat mientras espera."
                  }
                },
                "anyOf": [
                  {
                    "required": [
                      "email"
                    ]
                  },
                  {
                    "required": [
                      "phone"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "const": "chat"
                        },
                        "group": {
                          "$ref": "#/components/schemas/Chat"
                        }
                      },
                      "required": [
                        "status",
                        "group"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "const": "invited"
                        },
                        "group": {
                          "$ref": "#/components/schemas/Chat"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "required": [
                        "status",
                        "group",
                        "url"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/share": {
      "post": {
        "operationId": "postGroupsIdShare",
        "tags": [
          "Chats y miembros"
        ],
        "summary": "Compartir en el chat `{ kind: \"contact\" | \"bot\", id, by? }`: publica la tarjeta.",
        "description": "Compartir en el chat `{ kind: \"contact\" | \"bot\", id, by? }`: publica la tarjeta. Un contacto = presentación; un bot tuyo = permiso de uso para los del chat.",
        "x-puentes-who": "persona · miembro",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "enum": [
                      "bot",
                      "contact"
                    ]
                  },
                  "id": {
                    "type": "string",
                    "description": "El bot o la persona; un contacto sin id es alguien de tu agenda."
                  },
                  "by": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "maxLength": 254
                      },
                      "phone": {
                        "type": "string",
                        "maxLength": 40
                      },
                      "country": {
                        "type": "string",
                        "pattern": "^[A-Za-z]{2}$",
                        "description": "ISO de dos letras, para un número sin código de país."
                      }
                    },
                    "description": "El email o teléfono de tu agenda que es de esa persona: la tarjeta lo lleva."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "region": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  }
                },
                "required": [
                  "kind"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "$ref": "#/components/schemas/Message"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/tasklists": {
      "get": {
        "operationId": "getGroupsIdTasklists",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Las listas del chat: `[{ id, groupId, messageId, name, items: [Task], done, total }]`.",
        "description": "Las listas del chat: `[{ id, groupId, messageId, name, items: [Task], done, total }]`.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Tasklist"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postGroupsIdTasklists",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "`{ name (≤ 80), items: [\"…\"] (hasta 50, ≤ 200 cada una) }`.",
        "description": "`{ name (≤ 80), items: [\"…\"] (hasta 50, ≤ 200 cada una) }`. Una fecha en castellano al final de una tarea («… mañana 18hs») la vuelve recordatorio. Devuelve el mensaje con la tarjeta `tasklist`.",
        "x-puentes-who": "todos · miembro · permiso «escribir»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "maxLength": 200
                    },
                    "maxItems": 50,
                    "minItems": 1,
                    "description": "Una fecha en castellano al final de una tarea la vuelve recordatorio."
                  }
                },
                "required": [
                  "name",
                  "items"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tasklists/{id}": {
      "patch": {
        "operationId": "patchTasklistsId",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Renombrar `{ name }`.",
        "description": "Renombrar `{ name }`.",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tasklist"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteTasklistsId",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Borrarla: `{ ok, groupId, listId, message }` (el mensaje queda con `list: null`).",
        "description": "Borrarla: `{ ok, groupId, listId, message }` (el mensaje queda con `list: null`).",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "groupId": {
                      "type": "string"
                    },
                    "listId": {
                      "type": "string"
                    },
                    "message": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Message"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tasklists/{id}/tasks": {
      "post": {
        "operationId": "postTasklistsIdTasks",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Agregar `{ text }`.",
        "description": "Agregar `{ text }`.",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 200
                  }
                },
                "required": [
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tasklist"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tasks/{id}": {
      "patch": {
        "operationId": "patchTasksId",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Cambiar el texto `{ text }` (una fecha nueva en el texto reemplaza la anterior).",
        "description": "Cambiar el texto `{ text }` (una fecha nueva en el texto reemplaza la anterior).",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 200
                  }
                },
                "required": [
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tasklist"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteTasksId",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Borrar la tarea.",
        "description": "Borrar la tarea.",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tasklist"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tasks/{id}/toggle": {
      "post": {
        "operationId": "postTasksIdToggle",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Tildar `{ done: true | false }`.",
        "description": "Tildar `{ done: true | false }`.",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "done": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "done"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tasklist"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tasks/{id}/due": {
      "post": {
        "operationId": "postTasksIdDue",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Fecha `{ dueAt }` (epoch ms; vacío la saca).",
        "description": "Fecha `{ dueAt }` (epoch ms; vacío la saca).",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dueAt": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Epoch ms; vacío saca la fecha."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tasklist"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tasks/{id}/move": {
      "post": {
        "operationId": "postTasksIdMove",
        "tags": [
          "Listas de tareas"
        ],
        "summary": "Moverla dentro de su lista: `{ before: taskId }` o `{ after: taskId }`, otra tarea de la misma lista (arriba de todo es ",
        "description": "Moverla dentro de su lista: `{ before: taskId }` o `{ after: taskId }`, otra tarea de la misma lista (arriba de todo es antes de la primera; abajo de todo, después de la última). La lista queda en ese orden para todos.",
        "x-puentes-who": "todos · miembro del chat de la lista",
        "x-puentes-limits": [
          "600 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "La tarea que queda justo después."
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "La tarea que queda justo antes."
                  }
                },
                "oneOf": [
                  {
                    "required": [
                      "before"
                    ]
                  },
                  {
                    "required": [
                      "after"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tasklist"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/tables": {
      "get": {
        "operationId": "getGroupsIdTables",
        "tags": [
          "Tablas"
        ],
        "summary": "Las tablas del chat: `[{ id, groupId, messageId, name, createdBy, columns, rows: [{ id, title, cells }], summary }]`.",
        "description": "Las tablas del chat: `[{ id, groupId, messageId, name, createdBy, columns, rows: [{ id, title, cells }], summary }]`.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Table"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postGroupsIdTables",
        "tags": [
          "Tablas"
        ],
        "summary": "`{ name (≤ 80), columns?: [{ name, type, options? }], rows?: [{ title, cells? }] }`.",
        "description": "`{ name (≤ 80), columns?: [{ name, type, options? }], rows?: [{ title, cells? }] }`. Las ids de las columnas las arma el server con el nombre (minúsculas, sin acentos): para llenar celdas al crear, creá la tabla y usá las ids de la respuesta. Devuelve el mensaje con la tarjeta `table`.",
        "x-puentes-who": "todos · miembro · permiso «escribir»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "columns": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 40
                        },
                        "type": {
                          "type": "string",
                          "description": "status | select | person | number | date | text | check (se aceptan sinónimos en castellano, inglés y portugués; otro valor es text)"
                        },
                        "options": {
                          "type": "array",
                          "items": {
                            "oneOf": [
                              {
                                "type": "string",
                                "maxLength": 30
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string"
                                  },
                                  "label": {
                                    "type": "string",
                                    "maxLength": 30
                                  },
                                  "color": {
                                    "enum": [
                                      "green",
                                      "yellow",
                                      "red",
                                      "blue",
                                      "grey"
                                    ]
                                  }
                                },
                                "required": [
                                  "label"
                                ]
                              }
                            ]
                          },
                          "maxItems": 12
                        }
                      },
                      "required": [
                        "name"
                      ]
                    },
                    "maxItems": 8
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "title": {
                          "type": "string",
                          "maxLength": 120
                        },
                        "cells": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "{ idDeColumna: valor }: el valor se limpia según el tipo de la columna (una opción por id o por nombre, un número, una fecha YYYY-MM-DD, una persona del chat, un tilde, un texto ≤ 200). null o vacío la deja vacía."
                        }
                      },
                      "required": [
                        "title"
                      ]
                    },
                    "maxItems": 200
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tables/{id}": {
      "patch": {
        "operationId": "patchTablesId",
        "tags": [
          "Tablas"
        ],
        "summary": "`{ name?, columns? }`: `columns` reemplaza todas (las que no mandás se borran, con sus celdas).",
        "description": "`{ name?, columns? }`: `columns` reemplaza todas (las que no mandás se borran, con sus celdas).",
        "x-puentes-who": "todos · miembro del chat de la tabla; quien la armó o un admin; nunca un bot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "columns": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 40
                        },
                        "type": {
                          "type": "string",
                          "description": "status | select | person | number | date | text | check (se aceptan sinónimos en castellano, inglés y portugués; otro valor es text)"
                        },
                        "options": {
                          "type": "array",
                          "items": {
                            "oneOf": [
                              {
                                "type": "string",
                                "maxLength": 30
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string"
                                  },
                                  "label": {
                                    "type": "string",
                                    "maxLength": 30
                                  },
                                  "color": {
                                    "enum": [
                                      "green",
                                      "yellow",
                                      "red",
                                      "blue",
                                      "grey"
                                    ]
                                  }
                                },
                                "required": [
                                  "label"
                                ]
                              }
                            ]
                          },
                          "maxItems": 12
                        }
                      },
                      "required": [
                        "name"
                      ]
                    },
                    "maxItems": 8,
                    "description": "Reemplaza todas: una existente se conserva por id o por nombre; las que no van se borran."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Table"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteTablesId",
        "tags": [
          "Tablas"
        ],
        "summary": "Borrarla: `{ ok, groupId, tableId, message }`.",
        "description": "Borrarla: `{ ok, groupId, tableId, message }`.",
        "x-puentes-who": "todos · miembro del chat de la tabla; quien la armó o un admin; nunca un bot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "groupId": {
                      "type": "string"
                    },
                    "tableId": {
                      "type": "string"
                    },
                    "message": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Message"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tables/{id}/rows": {
      "post": {
        "operationId": "postTablesIdRows",
        "tags": [
          "Tablas"
        ],
        "summary": "Una fila `{ title (≤ 120), cells?: { columnId: valor } }`.",
        "description": "Una fila `{ title (≤ 120), cells?: { columnId: valor } }`. Devuelve la tabla.",
        "x-puentes-who": "todos · miembro del chat de la tabla",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "cells": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "{ idDeColumna: valor }: el valor se limpia según el tipo de la columna (una opción por id o por nombre, un número, una fecha YYYY-MM-DD, una persona del chat, un tilde, un texto ≤ 200). null o vacío la deja vacía."
                  }
                },
                "required": [
                  "title"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Table"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tables/{id}/rows/{rid}": {
      "patch": {
        "operationId": "patchTablesIdRowsRid",
        "tags": [
          "Tablas"
        ],
        "summary": "`{ title?, cells? }`: las celdas que mandás cambian (`null` la vacía), las demás quedan.",
        "description": "`{ title?, cells? }`: las celdas que mandás cambian (`null` la vacía), las demás quedan.",
        "x-puentes-who": "todos · miembro del chat de la tabla",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "cells": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "{ idDeColumna: valor }: el valor se limpia según el tipo de la columna (una opción por id o por nombre, un número, una fecha YYYY-MM-DD, una persona del chat, un tilde, un texto ≤ 200). null o vacío la deja vacía."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Table"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteTablesIdRowsRid",
        "tags": [
          "Tablas"
        ],
        "summary": "Borrar la fila.",
        "description": "Borrar la fila.",
        "x-puentes-who": "todos · miembro del chat de la tabla",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Table"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/schedules": {
      "get": {
        "operationId": "getGroupsIdSchedules",
        "tags": [
          "Programados"
        ],
        "summary": "Los programados vivos del chat que puedo ver.",
        "description": "Los programados vivos del chat que puedo ver.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Schedule"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/schedules/{sid}": {
      "patch": {
        "operationId": "patchGroupsIdSchedulesSid",
        "tags": [
          "Programados"
        ],
        "summary": "`{ paused: true | false }`.",
        "description": "`{ paused: true | false }`. Devuelve la lista del chat.",
        "x-puentes-who": "todos · miembro · quien lo pidió o un admin; nunca un bot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "paused": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "paused"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Schedule"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteGroupsIdSchedulesSid",
        "tags": [
          "Programados"
        ],
        "summary": "Cancelarlo.",
        "description": "Cancelarlo. Devuelve la lista del chat.",
        "x-puentes-who": "todos · miembro · quien lo pidió o un admin; nunca un bot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Schedule"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/schedules": {
      "get": {
        "operationId": "getMeSchedules",
        "tags": [
          "Programados"
        ],
        "summary": "Lo que se manda en mi nombre en cualquier chat: `{ sends, grants }` (Ajustes › Mensajes programados).",
        "description": "Lo que se manda en mi nombre en cualquier chat: `{ sends, grants }` (Ajustes › Mensajes programados).",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sends": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Schedule"
                      }
                    },
                    "grants": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "botId": {
                            "type": "string"
                          },
                          "botName": {
                            "type": "string"
                          },
                          "grantedAt": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/schedules/{sid}/confirm": {
      "post": {
        "operationId": "postSchedulesSidConfirm",
        "tags": [
          "Programados"
        ],
        "summary": "Contestar la tarjeta «¿Lo mando en tu nombre?»: `{ yes: true | false }` → `{ ok, result: \"sent\" | \"scheduled\" | \"dropped",
        "description": "Contestar la tarjeta «¿Lo mando en tu nombre?»: `{ yes: true | false }` → `{ ok, result: \"sent\" | \"scheduled\" | \"dropped\" | \"expired\" }`.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "yes": {
                    "type": "boolean",
                    "description": "Solo true es un sí."
                  }
                },
                "required": [
                  "yes"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "result": {
                      "enum": [
                        "sent",
                        "scheduled",
                        "dropped",
                        "expired"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/send-grant": {
      "post": {
        "operationId": "postBotsIdSendGrant",
        "tags": [
          "Programados"
        ],
        "summary": "Dejar que ese bot mande en mi nombre lo que yo le pida.",
        "description": "Dejar que ese bot mande en mi nombre lo que yo le pida.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "grants": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "botId": {
                            "type": "string"
                          },
                          "botName": {
                            "type": "string"
                          },
                          "grantedAt": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteBotsIdSendGrant",
        "tags": [
          "Programados"
        ],
        "summary": "Retirarlo (se borran sus envíos programados en mi nombre).",
        "description": "Retirarlo (se borran sus envíos programados en mi nombre).",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "sends": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Schedule"
                      }
                    },
                    "grants": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "botId": {
                            "type": "string"
                          },
                          "botName": {
                            "type": "string"
                          },
                          "grantedAt": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/pinned": {
      "get": {
        "operationId": "getGroupsIdPinned",
        "tags": [
          "Fijados"
        ],
        "summary": "Los mensajes fijados: `[{ message, pinnedBy, pinnedByName, pinnedAt }]`.",
        "description": "Los mensajes fijados: `[{ message, pinnedBy, pinnedByName, pinnedAt }]`.",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PinnedEntry"
                  },
                  "maxItems": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/messages/{mid}/pin": {
      "post": {
        "operationId": "postGroupsIdMessagesMidPin",
        "tags": [
          "Fijados"
        ],
        "summary": "Fijar un mensaje (el cuarto saca al más viejo).",
        "description": "Fijar un mensaje (el cuarto saca al más viejo). `pinned:changed { groupId }`.",
        "x-puentes-who": "persona · miembro · permiso «fijar»",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PinnedEntry"
                  },
                  "maxItems": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteGroupsIdMessagesMidPin",
        "tags": [
          "Fijados"
        ],
        "summary": "Desfijarlo.",
        "description": "Desfijarlo.",
        "x-puentes-who": "persona · miembro · permiso «fijar»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PinnedEntry"
                  },
                  "maxItems": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/pins": {
      "get": {
        "operationId": "getGroupsIdPins",
        "tags": [
          "Fijados"
        ],
        "summary": "Los fijados de los bots del chat (los del chat y los míos).",
        "description": "Los fijados de los bots del chat (los del chat y los míos).",
        "x-puentes-who": "todos · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Pin"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/pins/{pinId}": {
      "put": {
        "operationId": "putGroupsIdPinsPinId",
        "tags": [
          "Fijados"
        ],
        "summary": "Poner o actualizar un fijado del bot: `{ label (≤ 24), badge?, detail?, icon?, card?, buttons?, forUserId? }`.",
        "description": "Poner o actualizar un fijado del bot: `{ label (≤ 24), badge?, detail?, icon?, card?, buttons?, forUserId? }`. Hasta 4 por bot y chat. `pin:updated`.",
        "x-puentes-who": "bot · miembro",
        "x-puentes-limits": [
          "120 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pinId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 24
                  },
                  "badge": {
                    "type": "string",
                    "maxLength": 12
                  },
                  "detail": {
                    "type": "string",
                    "maxLength": 24
                  },
                  "icon": {
                    "type": "string",
                    "description": "basket, list, tasks, money, clock, store, food, home, plane, cake, ball, star, bell, file, location, people, form, key"
                  },
                  "card": {
                    "$ref": "#/components/schemas/CardInput"
                  },
                  "buttons": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "maxLength": 40
                        },
                        {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "maxLength": 64
                            },
                            "title": {
                              "type": "string",
                              "maxLength": 40
                            }
                          }
                        }
                      ]
                    },
                    "maxItems": 4
                  },
                  "forUserId": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "El fijado es solo de esa persona del chat."
                  }
                },
                "required": [
                  "label"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pin"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteGroupsIdPinsPinId",
        "tags": [
          "Fijados"
        ],
        "summary": "Sacarlo.",
        "description": "Sacarlo. `pin:removed`.",
        "x-puentes-who": "bot · miembro",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pinId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "removed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots": {
      "post": {
        "operationId": "postBots",
        "tags": [
          "Bots"
        ],
        "summary": "Crear un bot (ver [Crear un bot](#crear)).",
        "description": "Crear un bot (ver [Crear un bot](#crear)). Devuelve `{ bot, token?, group }`.",
        "x-puentes-who": "persona · verificada; hasta 50 bots",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BotInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bot": {
                      "$ref": "#/components/schemas/Bot"
                    },
                    "token": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "pb_…, solo para un bot externo: se muestra una sola vez."
                    },
                    "group": {
                      "$ref": "#/components/schemas/Chat"
                    }
                  },
                  "required": [
                    "bot",
                    "group"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "get": {
        "operationId": "getBots",
        "tags": [
          "Bots"
        ],
        "summary": "Mis bots (con el prefijo del token, el webhook y su secreto).",
        "description": "Mis bots (con el prefijo del token, el webhook y su secreto).",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Bot"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/catalog": {
      "get": {
        "operationId": "getBotsCatalog",
        "tags": [
          "Bots"
        ],
        "summary": "Los bots que puedo agregar: de Puentes, públicos, míos y compartidos conmigo.",
        "description": "Los bots que puedo agregar: de Puentes, públicos, míos y compartidos conmigo.",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Bot"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/browse": {
      "get": {
        "operationId": "getBotsBrowse",
        "tags": [
          "Bots"
        ],
        "summary": "La sección IAs: `?q=&chip=` → `{ mine, team, catalog, next }`.",
        "description": "La sección IAs: `?q=&chip=` → `{ mine, team, catalog, next }`.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Busca nombre, línea, etiquetas y desarrollador.",
            "schema": {
              "type": "string",
              "maxLength": 60
            }
          },
          {
            "name": "chip",
            "in": "query",
            "required": false,
            "description": "all | puentes | mine | un id de categoría.",
            "schema": {
              "type": "string",
              "maxLength": 20
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "Idioma de las ideas.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mine": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Bot"
                      }
                    },
                    "team": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Bot"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "team": {
                                "type": "object",
                                "properties": {
                                  "via": {
                                    "type": "string"
                                  },
                                  "domain": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    },
                    "catalog": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Bot"
                      },
                      "maxItems": 50
                    },
                    "ideas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Bot"
                      }
                    },
                    "next": {
                      "type": "null"
                    },
                    "country": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "source": {
                          "enum": [
                            "chosen",
                            "phone",
                            null
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}": {
      "get": {
        "operationId": "getBotsId",
        "tags": [
          "Bots"
        ],
        "summary": "La ficha de un bot que puedo usar: más `kind`, `shortDesc`, `tags`, `developer`, `mcp.tools`, `canUse`; si puedo pedir a",
        "description": "La ficha de un bot que puedo usar: más `kind`, `shortDesc`, `tags`, `developer`, `mcp.tools`, `canUse`; si puedo pedir acceso, una recortada con `canRequest`.",
        "x-puentes-who": "todos",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Bot"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "patchBotsId",
        "tags": [
          "Bots"
        ],
        "summary": "Cambiar cualquiera de los campos de [Crear un bot](#crear).",
        "description": "Cambiar cualquiera de los campos de [Crear un bot](#crear). Además, solo para un admin del bot: `pay`, `ownerKey`, los topes, `orgId`, `clonable` e `idea`.",
        "x-puentes-who": "persona · quien lo puede editar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/BotInput"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "clonable": {
                        "type": "boolean",
                        "description": "Solo un admin del bot."
                      },
                      "idea": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "properties": {
                          "title": {
                            "type": "string",
                            "maxLength": 40
                          },
                          "blurb": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "copyName": {
                            "type": "string",
                            "maxLength": 36
                          },
                          "opening": {
                            "type": "string",
                            "maxLength": 600
                          }
                        },
                        "description": "Solo un admin del bot; null la saca."
                      }
                    }
                  }
                ],
                "description": "Cualquier campo de BotInput, ninguno obligatorio (menos runtime, template y locale, que son de la creación)."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Bot"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteBotsId",
        "tags": [
          "Bots"
        ],
        "summary": "Borrarlo: sale de todos los chats y sus directos desaparecen.",
        "description": "Borrarlo: sale de todos los chats y sus directos desaparecen.",
        "x-puentes-who": "persona · dueño",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/token": {
      "post": {
        "operationId": "postBotsIdToken",
        "tags": [
          "Bots"
        ],
        "summary": "Token nuevo (el anterior muere al instante).",
        "description": "Token nuevo (el anterior muere al instante).",
        "x-puentes-who": "persona · dueño",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "pattern": "^pb_[0-9a-f]{40}$"
                    },
                    "bot": {
                      "$ref": "#/components/schemas/Bot"
                    }
                  },
                  "required": [
                    "token",
                    "bot"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/clone": {
      "post": {
        "operationId": "postBotsIdClone",
        "tags": [
          "Bots"
        ],
        "summary": "«Hacer mi copia» de un bot `clonable`: `{ name?, avatar? }` → `{ bot, group }`.",
        "description": "«Hacer mi copia» de un bot `clonable`: `{ name?, avatar? }` → `{ bot, group }`. `403` con `code: \"not_clonable\"` si no se puede.",
        "x-puentes-who": "persona · verificada",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "avatar": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bot": {
                      "$ref": "#/components/schemas/Bot"
                    },
                    "group": {
                      "$ref": "#/components/schemas/Chat"
                    }
                  },
                  "required": [
                    "bot",
                    "group"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/usage": {
      "get": {
        "operationId": "getBotsIdUsage",
        "tags": [
          "Bots"
        ],
        "summary": "Cuánto gastó un bot que paga sus respuestas: `{ monthUsd, replies, people, since }`.",
        "description": "Cuánto gastó un bot que paga sus respuestas: `{ monthUsd, replies, people, since }`.",
        "x-puentes-who": "persona · dueño",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monthUsd": {
                      "type": "number"
                    },
                    "replies": {
                      "type": "integer"
                    },
                    "people": {
                      "type": "integer"
                    },
                    "since": {
                      "type": "integer",
                      "description": "Epoch ms: el primer día del mes (UTC)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/owner-key": {
      "delete": {
        "operationId": "deleteBotsIdOwnerKey",
        "tags": [
          "Bots"
        ],
        "summary": "Olvidar la clave de modelo con la que el bot paga.",
        "description": "Olvidar la clave de modelo con la que el bot paga.",
        "x-puentes-who": "persona · admin del bot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Bot"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/verify": {
      "post": {
        "operationId": "postBotsIdVerify",
        "tags": [
          "Bots"
        ],
        "summary": "Pedir el check verde (en el catálogo y con el desarrollador verificado): `{ verification: \"pending\" }`, o `409` con `mis",
        "description": "Pedir el check verde (en el catálogo y con el desarrollador verificado): `{ verification: \"pending\" }`, o `409` con `missing`.",
        "x-puentes-who": "persona · dueño",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/domains": {
      "put": {
        "operationId": "putBotsIdDomains",
        "tags": [
          "Bots"
        ],
        "summary": "`{ domains: [\"acme.com\"] }` de un bot `authorized` (hace falta un email verificado tuyo en cada dominio).",
        "description": "`{ domains: [\"acme.com\"] }` de un bot `authorized` (hace falta un email verificado tuyo en cada dominio).",
        "x-puentes-who": "persona · admin del bot",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domains": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 20
                  }
                },
                "required": [
                  "domains"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BotAccess"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/access-requests": {
      "post": {
        "operationId": "postBotsIdAccessRequests",
        "tags": [
          "Bots"
        ],
        "summary": "Pedir acceso a un bot `authorized` con pedidos abiertos.",
        "description": "Pedir acceso a un bot `authorized` con pedidos abiertos.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "20 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "request": {
                          "type": "null"
                        },
                        "canUse": {
                          "const": true
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "request": {
                          "type": "object",
                          "properties": {
                            "status": {
                              "const": "pending"
                            },
                            "at": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "get": {
        "operationId": "getBotsIdAccessRequests",
        "tags": [
          "Bots"
        ],
        "summary": "Los pedidos pendientes.",
        "description": "Los pedidos pendientes.",
        "x-puentes-who": "persona · admin del bot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BotAccess"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/access-requests/{rid}/{decision}": {
      "post": {
        "operationId": "postBotsIdAccessRequestsRidDecision",
        "tags": [
          "Bots"
        ],
        "summary": "`:decision` = `approve` | `reject`.",
        "description": "`:decision` = `approve` | `reject`.",
        "x-puentes-who": "persona · admin del bot",
        "x-puentes-limits": [
          "120 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "decision",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BotAccess"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/access": {
      "get": {
        "operationId": "getBotsIdAccess",
        "tags": [
          "Bots"
        ],
        "summary": "Quién puede usar el bot además de su dueño (`via: \"share\" | \"chat\" | \"link\" | \"approved\"`).",
        "description": "Quién puede usar el bot además de su dueño (`via: \"share\" | \"chat\" | \"link\" | \"approved\"`).",
        "x-puentes-who": "persona · dueño",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BotAccess"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postBotsIdAccess",
        "tags": [
          "Bots"
        ],
        "summary": "Dar acceso a un contacto `{ userId }`.",
        "description": "Dar acceso a un contacto `{ userId }`.",
        "x-puentes-who": "persona · dueño",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "userId": {
                    "type": "string"
                  }
                },
                "required": [
                  "userId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "people": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BotAccessPerson"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/access/{userId}": {
      "delete": {
        "operationId": "deleteBotsIdAccessUserId",
        "tags": [
          "Bots"
        ],
        "summary": "Quitarlo (su directo con el bot se borra).",
        "description": "Quitarlo (su directo con el bot se borra).",
        "x-puentes-who": "persona · dueño",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/share-link": {
      "post": {
        "operationId": "postBotsIdShareLink",
        "tags": [
          "Bots"
        ],
        "summary": "Link `/bot/<token>` para compartirlo (7 días, uno vivo por bot).",
        "description": "Link `/bot/<token>` para compartirlo (7 días, uno vivo por bot).",
        "x-puentes-who": "persona · dueño",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expiresAt": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "url",
                    "expiresAt"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteBotsIdShareLink",
        "tags": [
          "Bots"
        ],
        "summary": "Revocarlo.",
        "description": "Revocarlo.",
        "x-puentes-who": "persona · dueño",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/shared/{token}": {
      "get": {
        "operationId": "getBotsSharedToken",
        "tags": [
          "Bots"
        ],
        "summary": "Quién comparte qué.",
        "description": "Quién comparte qué.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/shared/{token}/accept": {
      "post": {
        "operationId": "postBotsSharedTokenAccept",
        "tags": [
          "Bots"
        ],
        "summary": "Aceptar: acceso para la cuenta logueada.",
        "description": "Aceptar: acceso para la cuenta logueada.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bot": {
                      "$ref": "#/components/schemas/Bot"
                    }
                  },
                  "required": [
                    "bot"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/bots": {
      "post": {
        "operationId": "postGroupsIdBots",
        "tags": [
          "Bots"
        ],
        "summary": "Sumar `{ botId, mode? }` (de Puentes, público, mío o compartido conmigo) al grupo.",
        "description": "Sumar `{ botId, mode? }` (de Puentes, público, mío o compartido conmigo) al grupo. `mode` sin decir: `always`; en un bot externo dice qué mensajes le llegan (ver [Modo](#modo)).",
        "x-puentes-who": "persona · miembro · permiso «agregar gente»",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "botId": {
                    "type": "string"
                  },
                  "mode": {
                    "enum": [
                      "mention",
                      "always"
                    ]
                  },
                  "alias": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "botId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/bots/{botId}": {
      "patch": {
        "operationId": "patchGroupsIdBotsBotId",
        "tags": [
          "Bots"
        ],
        "summary": "Cómo responde ese bot en ese grupo: `{ mode?: \"mention\" | \"always\", alias?, connections? }`.",
        "description": "Cómo responde ese bot en ese grupo: `{ mode?: \"mention\" | \"always\", alias?, connections? }`. También para un bot externo (ver [Modo](#modo)).",
        "x-puentes-who": "persona · miembro · permiso «cómo responde un bot»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "botId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "enum": [
                      "mention",
                      "always"
                    ]
                  },
                  "alias": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "null o \"\" lo saca."
                  },
                  "connections": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "connectorId": {
                          "type": "string",
                          "maxLength": 60
                        },
                        "mode": {
                          "enum": [
                            "person",
                            "chat"
                          ]
                        },
                        "connectionId": {
                          "type": "string",
                          "maxLength": 60,
                          "description": "Con chat: una cuenta tuya de ese conector pasa a ser la del chat."
                        }
                      },
                      "required": [
                        "connectorId",
                        "mode"
                      ]
                    },
                    "maxItems": 8
                  }
                },
                "description": "Al menos uno de los tres."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteGroupsIdBotsBotId",
        "tags": [
          "Bots"
        ],
        "summary": "Sacar el bot del grupo.",
        "description": "Sacar el bot del grupo.",
        "x-puentes-who": "persona · miembro · permiso «agregar gente»",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "botId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/groups/{id}/bots/{botId}/mode-ask": {
      "post": {
        "operationId": "postGroupsIdBotsBotIdModeAsk",
        "tags": [
          "Bots"
        ],
        "summary": "Contestar la tarjeta privada «¿cómo responde?» de una IA nueva: `{ keep: true | false }`.",
        "description": "Contestar la tarjeta privada «¿cómo responde?» de una IA nueva: `{ keep: true | false }`.",
        "x-puentes-who": "persona · miembro · permiso «cómo responde un bot»",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "botId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "keep": {
                    "type": "boolean",
                    "description": "true: sigue respondiendo siempre; false: solo cuando lo nombran."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/developer": {
      "get": {
        "operationId": "getMeDeveloper",
        "tags": [
          "Bots"
        ],
        "summary": "Mi perfil de desarrollador, con `complete` y `missing`.",
        "description": "Mi perfil de desarrollador, con `complete` y `missing`.",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeveloperProfile"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "put": {
        "operationId": "putMeDeveloper",
        "tags": [
          "Bots"
        ],
        "summary": "`{ name?, company?, bio?, website?, contactEmail?, contactable?, country? }`.",
        "description": "`{ name?, company?, bio?, website?, contactEmail?, contactable?, country? }`. Con `{}` activa Creadores con el nombre de la cuenta (solo pide una cuenta verificada); completo (nombre, bio y una forma de contacto), un bot o un conector puede ir al catálogo.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "company": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 80
                  },
                  "bio": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "website": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "format": "uri"
                  },
                  "contactEmail": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "format": "email"
                  },
                  "contactable": {
                    "type": "boolean",
                    "description": "Que me escriban por Puentes."
                  },
                  "country": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^[A-Za-z]{2}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeveloperProfile"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/developer/verify": {
      "post": {
        "operationId": "postMeDeveloperVerify",
        "tags": [
          "Bots"
        ],
        "summary": "Pedir el check verde para el perfil.",
        "description": "Pedir el check verde para el perfil.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "5 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/organizations": {
      "get": {
        "operationId": "getMeOrganizations",
        "tags": [
          "Bots"
        ],
        "summary": "Mis organizaciones.",
        "description": "Mis organizaciones.",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Organization"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/organizations": {
      "post": {
        "operationId": "postOrganizations",
        "tags": [
          "Bots"
        ],
        "summary": "Crear una `{ name, bio, website?, contactEmail?, contactable?, country?, avatar? }`: quien la crea es `admin`.",
        "description": "Crear una `{ name, bio, website?, contactEmail?, contactable?, country?, avatar? }`: quien la crea es `admin`. Un bot pasa a ella con `orgId`.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrganizationInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Organization"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "role": {
                          "const": "admin"
                        },
                        "members": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/OrganizationMember"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/organizations/{id}": {
      "patch": {
        "operationId": "patchOrganizationsId",
        "tags": [
          "Bots"
        ],
        "summary": "Editarla.",
        "description": "Editarla.",
        "x-puentes-who": "persona · admin de la organización",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/OrganizationInput"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "contactUserId": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "maxLength": 60,
                        "description": "Un miembro: con quién se habla por la organización."
                      }
                    }
                  }
                ],
                "description": "Los mismos campos, ninguno obligatorio."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteOrganizationsId",
        "tags": [
          "Bots"
        ],
        "summary": "Borrarla.",
        "description": "Borrarla.",
        "x-puentes-who": "persona · admin de la organización",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/organizations/{id}/members": {
      "get": {
        "operationId": "getOrganizationsIdMembers",
        "tags": [
          "Bots"
        ],
        "summary": "Sus miembros.",
        "description": "Sus miembros.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrganizationMember"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postOrganizationsIdMembers",
        "tags": [
          "Bots"
        ],
        "summary": "Invitar `{ userId | email, role: \"admin\" | \"dev\" }`.",
        "description": "Invitar `{ userId | email, role: \"admin\" | \"dev\" }`.",
        "x-puentes-who": "persona · admin de la organización",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "userId": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "role": {
                    "enum": [
                      "admin",
                      "dev"
                    ]
                  }
                },
                "description": "userId o email."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrganizationMember"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/organizations/{id}/members/{uid}": {
      "patch": {
        "operationId": "patchOrganizationsIdMembersUid",
        "tags": [
          "Bots"
        ],
        "summary": "Cambiar el rol.",
        "description": "Cambiar el rol.",
        "x-puentes-who": "persona · admin de la organización",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "uid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "enum": [
                      "admin",
                      "dev"
                    ]
                  }
                },
                "required": [
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrganizationMember"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteOrganizationsIdMembersUid",
        "tags": [
          "Bots"
        ],
        "summary": "Sacar a alguien (el último admin no se va).",
        "description": "Sacar a alguien (el último admin no se va).",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "uid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrganizationMember"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/organizations/{id}/accept": {
      "post": {
        "operationId": "postOrganizationsIdAccept",
        "tags": [
          "Bots"
        ],
        "summary": "Aceptar una invitación a la organización.",
        "description": "Aceptar una invitación a la organización.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/organizations/{id}/decline": {
      "post": {
        "operationId": "postOrganizationsIdDecline",
        "tags": [
          "Bots"
        ],
        "summary": "Rechazarla.",
        "description": "Rechazarla.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/organizations/{id}/verify": {
      "post": {
        "operationId": "postOrganizationsIdVerify",
        "tags": [
          "Bots"
        ],
        "summary": "Pedir el check verde para la organización.",
        "description": "Pedir el check verde para la organización.",
        "x-puentes-who": "persona · admin de la organización",
        "x-puentes-limits": [
          "5 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/mcp/connect": {
      "post": {
        "operationId": "postBotsIdMcpConnect",
        "tags": [
          "Bots con herramientas (MCP)"
        ],
        "summary": "El camino viejo, sobre el servidor de la IA (el que tenía adentro, o el conector de quien la hizo en que se convirtió) y",
        "description": "El camino viejo, sobre el servidor de la IA (el que tenía adentro, o el conector de quien la hizo en que se convirtió) y con la cuenta de quien la hizo. Servidor OAuth: `{ url }` para autorizar (vuelve a `/api/mcp/callback`); si ya hay tokens, `{ connected: true, tools }`. Lo nuevo: `POST /api/me/connections`.",
        "x-puentes-who": "persona (sesión) · dueño",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "app": {
                    "type": "boolean",
                    "description": "Desde la app nativa (sin cookie)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "required": [
                        "url"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "connected": {
                          "const": true
                        },
                        "tools": {
                          "type": "integer"
                        }
                      },
                      "required": [
                        "connected",
                        "tools"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/mcp/test": {
      "post": {
        "operationId": "postBotsIdMcpTest",
        "tags": [
          "Bots con herramientas (MCP)"
        ],
        "summary": "Probar el servidor de la IA con la cuenta de quien la hizo: `{ status: \"connected\", transport, tools }` o `400` con el m",
        "description": "Probar el servidor de la IA con la cuenta de quien la hizo: `{ status: \"connected\", transport, tools }` o `400` con el motivo (también si no tiene). Lo nuevo: `POST /api/connectors/:id/test`.",
        "x-puentes-who": "persona · dueño",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "const": "connected"
                    },
                    "transport": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/mcp/tokens": {
      "delete": {
        "operationId": "deleteBotsIdMcpTokens",
        "tags": [
          "Bots con herramientas (MCP)"
        ],
        "summary": "Desconectar: borra los tokens OAuth de quien la hizo en el servidor de la IA.",
        "description": "Desconectar: borra los tokens OAuth de quien la hizo en el servidor de la IA.",
        "x-puentes-who": "persona · dueño",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Bot"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/mcp/post/{token}": {
      "post": {
        "operationId": "postMcpPostToken",
        "tags": [
          "Bots con herramientas (MCP)"
        ],
        "summary": "`{ text?, card?, pin? }`: el bot publica por su cuenta en ese chat (el [`postUrl`](#mcp-post) de `_meta.puentes`).",
        "description": "`{ text?, card?, pin? }`: el bot publica por su cuenta en ese chat (el [`postUrl`](#mcp-post) de `_meta.puentes`). `202`; `404` si ya no vale. El texto, hasta 4000.",
        "x-puentes-who": "público · el token es la credencial",
        "x-puentes-limits": [
          "240 por hora por IP",
          "30 mensajes por minuto del bot"
        ],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 4000
                  },
                  "card": {
                    "$ref": "#/components/schemas/CardInput"
                  },
                  "pin": {
                    "type": "object",
                    "description": "Un fijado, como `_meta.puentes.pin`; { id, remove: true } lo saca."
                  }
                },
                "description": "Al menos uno de los tres."
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Aceptado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/mcp/event/{token}": {
      "post": {
        "operationId": "postMcpEventToken",
        "tags": [
          "Bots con herramientas (MCP)"
        ],
        "summary": "`{ text (≤ 500) }`: línea de sistema en el chat y la IA retoma (el [`callbackUrl`](#mcp-meta), 24 horas).",
        "description": "`{ text (≤ 500) }`: línea de sistema en el chat y la IA retoma (el [`callbackUrl`](#mcp-meta), 24 horas). `202`; `404` vencido.",
        "x-puentes-who": "público · el token es la credencial",
        "x-puentes-limits": [
          "120 por hora por IP"
        ],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 500
                  }
                },
                "required": [
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Aceptado",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/mcp/callback": {
      "get": {
        "operationId": "getMcpCallback",
        "tags": [
          "Bots con herramientas (MCP)"
        ],
        "summary": "La vuelta de OAuth (la abre el navegador, no tu código).",
        "description": "La vuelta de OAuth (la abre el navegador, no tu código).",
        "x-puentes-who": "público",
        "x-puentes-limits": [
          "30 por hora por IP"
        ],
        "security": [],
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "required": true,
            "description": "El código de autorización.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": true,
            "description": "El estado que armó Puentes al empezar.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postMcpCallback",
        "tags": [
          "Bots con herramientas (MCP)"
        ],
        "summary": "La confirmación de esa vuelta cuando el navegador no es el que pidió la URL.",
        "description": "La confirmación de esa vuelta cuando el navegador no es el que pidió la URL.",
        "x-puentes-who": "público",
        "x-puentes-limits": [
          "30 por hora por IP"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string"
                  }
                },
                "required": [
                  "code",
                  "state"
                ],
                "description": "Lo manda la página de confirmación cuando el navegador no es el que pidió la URL. Redirige a la app."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/connectors/browse": {
      "get": {
        "operationId": "getConnectorsBrowse",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Los conectores que puedo usar: `[{ id, name, description, avatar, category, verified, by, auth, accountModes, exclusive,",
        "description": "Los conectores que puedo usar: `[{ id, name, description, avatar, category, verified, by, auth, accountModes, exclusive, connectHelp, tools: [{ name, label, changes?, confirm? }], bots }]`.",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Connector"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/connectors/{id}": {
      "get": {
        "operationId": "getConnectorsId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Uno (`?botId=` si lo veo por una IA que lo usa).",
        "description": "Uno (`?botId=` si lo veo por una IA que lo usa).",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "botId",
            "in": "query",
            "required": false,
            "description": "Una IA que lo usa, si lo ves por ella.",
            "schema": {
              "type": "string",
              "maxLength": 60
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Connector"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "patchConnectorsId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Los mismos campos.",
        "description": "Los mismos campos. Cambiar `url` o `auth` lo reinicia. Pasarlo al catálogo (o moverlo de dueño estando ahí) pide el perfil completo de quien lo firma (`409`).",
        "x-puentes-who": "persona (sesión)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectorInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerConnector"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteConnectorsId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Borrarlo.",
        "description": "Borrarlo.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/connectors": {
      "get": {
        "operationId": "getConnectors",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Los míos y los de mis organizaciones, con `url`, `visibility`, `status`, `toolLabels` y `hasServerKey`.",
        "description": "Los míos y los de mis organizaciones, con `url`, `visibility`, `status`, `toolLabels` y `hasServerKey`.",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/OwnerConnector"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postConnectors",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Registrar uno `{ name, url (https pública), orgId?, description?, category?, connectHelp?, avatar?, accountModes?: [\"per",
        "description": "Registrar uno `{ name, url (https pública), orgId?, description?, category?, connectHelp?, avatar?, accountModes?: [\"person\" | \"chat\" | \"owner\"], exclusive?, visibility?, auth?: \"none\" | \"bearer\" | \"oauth\", serverKey?, toolLabels? }`. Hasta 30 por dueño. Privado (el default) pide solo una cuenta verificada (un invitado recibe `403` con `code: \"guest\"`); `visibility: \"catalog\"` pide además el perfil completo de quien lo firma, la organización o la persona (`409` con `{ missing, org }`).",
        "x-puentes-who": "persona (sesión) · con un email, teléfono, Google o Apple",
        "x-puentes-limits": [
          "20 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectorInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerConnector"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/connectors/{id}/test": {
      "post": {
        "operationId": "postConnectorsIdTest",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Volver a probarlo (detecta el acceso y lista las herramientas).",
        "description": "Volver a probarlo (detecta el acceso y lista las herramientas).",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OwnerConnector"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/connections": {
      "get": {
        "operationId": "getMeConnections",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "«Mis cuentas»: `[{ id, connectorId, connector, label, status: \"ok\" | \"reconnect\" | \"error\", createdAt, lastUsedAt?, bots",
        "description": "«Mis cuentas»: `[{ id, connectorId, connector, label, status: \"ok\" | \"reconnect\" | \"error\", createdAt, lastUsedAt?, bots }]`.",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Connection"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postMeConnections",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Conectar `{ connectorId, secret?, botId?, groupId? }`: con clave, `{ connection }`; con OAuth, `{ url }` para autorizar.",
        "description": "Conectar `{ connectorId, secret?, botId?, groupId? }`: con clave, `{ connection }`; con OAuth, `{ url }` para autorizar. Con `botId` y `groupId` se la presta a esa IA en ese chat y la despierta.",
        "x-puentes-who": "persona (sesión) · con un email, teléfono, Google o Apple",
        "x-puentes-limits": [
          "20 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "connectorId": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "secret": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "La clave, si el servicio entra con una."
                  },
                  "app": {
                    "type": "boolean",
                    "description": "Desde la app nativa: sin cookie."
                  },
                  "botId": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "groupId": {
                    "type": "string",
                    "maxLength": 60
                  }
                },
                "required": [
                  "connectorId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "connection": {
                          "$ref": "#/components/schemas/Connection"
                        }
                      },
                      "required": [
                        "connection"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Para autorizar (OAuth)."
                        }
                      },
                      "required": [
                        "url"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/connections/{id}": {
      "get": {
        "operationId": "getMeConnectionsId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Una cuenta, con las IAs y los chats que la usan.",
        "description": "Una cuenta, con las IAs y los chats que la usan.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Connection"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "patchMeConnectionsId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Nombre de la cuenta `{ label }`.",
        "description": "Nombre de la cuenta `{ label }`.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 60
                  }
                },
                "required": [
                  "label"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Connection"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteMeConnectionsId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Desconectarla.",
        "description": "Desconectarla.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/connections/{id}/reconnect": {
      "post": {
        "operationId": "postMeConnectionsIdReconnect",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Volver a entrar (clave nueva u OAuth).",
        "description": "Volver a entrar (clave nueva u OAuth).",
        "x-puentes-who": "persona (sesión)",
        "x-puentes-limits": [
          "20 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "secret": {
                    "type": "string",
                    "maxLength": 4000
                  },
                  "app": {
                    "type": "boolean"
                  },
                  "botId": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "groupId": {
                    "type": "string",
                    "maxLength": 60
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "connection": {
                          "$ref": "#/components/schemas/Connection"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/connections/{id}/use": {
      "post": {
        "operationId": "postMeConnectionsIdUse",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Prestarle esa cuenta a una IA `{ botId, groupId? }`.",
        "description": "Prestarle esa cuenta a una IA `{ botId, groupId? }`.",
        "x-puentes-who": "persona (sesión)",
        "x-puentes-limits": [
          "30 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "botId": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "groupId": {
                    "type": "string",
                    "maxLength": 60
                  }
                },
                "required": [
                  "botId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "connection": {
                      "$ref": "#/components/schemas/Connection"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/connections/{id}/bots/{botId}": {
      "delete": {
        "operationId": "deleteMeConnectionsIdBotsBotId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Dejar de prestársela a esa IA.",
        "description": "Dejar de prestársela a esa IA.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "botId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/connections/{id}/chats/{groupId}": {
      "delete": {
        "operationId": "deleteMeConnectionsIdChatsGroupId",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Sacarla de ese chat.",
        "description": "Sacarla de ese chat.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/bots/{id}/connectors": {
      "put": {
        "operationId": "putBotsIdConnectors",
        "tags": [
          "Conectores y cuentas"
        ],
        "summary": "Los conectores de una IA `{ connectors: [{ connectorId, account?: \"person\" | \"chat\" | \"owner\" }] }` (reemplaza la lista;",
        "description": "Los conectores de una IA `{ connectors: [{ connectorId, account?: \"person\" | \"chat\" | \"owner\" }] }` (reemplaza la lista; hasta 8). Uno nuevo tiene que ser uno que podés usar (tuyo, de tu organización o del catálogo; si no, `404`) y, si es exclusivo, de quien es la IA (`403`); uno que la IA ya tiene se conserva aunque lo reenvíe otra persona. `account: \"owner\"` (la cuenta de quien hizo la IA, para todos) lo pone solo esa persona en un servicio con cuentas: otra recibe `403` con `code: \"owner_account\"`, y puede dejarlo como está o pasarlo a `person` o `chat`.",
        "x-puentes-who": "persona (sesión) · quien la puede editar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "connectors": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "connectorId": {
                          "type": "string",
                          "maxLength": 60
                        },
                        "account": {
                          "enum": [
                            "person",
                            "chat",
                            "owner"
                          ]
                        }
                      },
                      "required": [
                        "connectorId"
                      ]
                    },
                    "maxItems": 8,
                    "description": "Reemplaza la lista entera (vacía la saca)."
                  }
                },
                "required": [
                  "connectors"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bot": {
                      "$ref": "#/components/schemas/Bot"
                    },
                    "connectors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "connectorId": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "account": {
                            "type": "string"
                          },
                          "own": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/keys": {
      "get": {
        "operationId": "getMeKeys",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Mis API keys.",
        "description": "Mis API keys.",
        "x-puentes-who": "persona (sesión)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKey"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postMeKeys",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Crear una `{ name }`: devuelve `pk_…` una sola vez.",
        "description": "Crear una `{ name }`: devuelve `pk_…` una sola vez. Hasta 10.",
        "x-puentes-who": "persona (sesión)",
        "x-puentes-limits": [
          "10 por hora por cuenta"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 40
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "$ref": "#/components/schemas/ApiKey"
                    },
                    "token": {
                      "type": "string",
                      "description": "pk_…: se muestra una sola vez."
                    }
                  },
                  "required": [
                    "key",
                    "token"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/keys/{id}": {
      "delete": {
        "operationId": "deleteMeKeysId",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Revocarla.",
        "description": "Revocarla.",
        "x-puentes-who": "persona (sesión)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/emails": {
      "get": {
        "operationId": "getMeEmails",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Mis emails.",
        "description": "Mis emails.",
        "x-puentes-who": "persona (sesión)",
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postMeEmails",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "`{ email }`: manda un código de 6 dígitos.",
        "description": "`{ email }`: manda un código de 6 dígitos.",
        "x-puentes-who": "persona (sesión)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "verified": {
                      "type": "boolean",
                      "description": "Ya estaba verificado."
                    },
                    "retryAfter": {
                      "type": "integer",
                      "description": "Segundos hasta poder pedir otro código."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/emails/verify": {
      "post": {
        "operationId": "postMeEmailsVerify",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "`{ email, code }`: lo confirma.",
        "description": "`{ email, code }`: lo confirma.",
        "x-puentes-who": "persona (sesión)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "code": {
                    "type": "string",
                    "description": "6 dígitos."
                  },
                  "proof": {
                    "type": "string"
                  },
                  "merge": {
                    "type": "boolean",
                    "description": "Solo true une las cuentas."
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        },
                        "merged": {
                          "type": "boolean"
                        },
                        "emails": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "email": {
                                "type": "string"
                              },
                              "verifiedAt": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "conflict": {
                          "const": true
                        },
                        "current": {
                          "$ref": "#/components/schemas/User"
                        },
                        "other": {
                          "$ref": "#/components/schemas/User"
                        },
                        "proof": {
                          "type": "string",
                          "description": "Se manda de vuelta con `merge: true` para unir las cuentas."
                        }
                      },
                      "required": [
                        "conflict",
                        "proof"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/emails/{email}": {
      "delete": {
        "operationId": "deleteMeEmailsEmail",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Sacarlo.",
        "description": "Sacarlo.",
        "x-puentes-who": "persona (sesión)",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/phones": {
      "get": {
        "operationId": "getMePhones",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Mis teléfonos.",
        "description": "Mis teléfonos.",
        "x-puentes-who": "persona (sesión)",
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "postMePhones",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "`{ phone, channel: \"sms\" | \"whatsapp\" }`: manda un código (si `smsEnabled`).",
        "description": "`{ phone, channel: \"sms\" | \"whatsapp\" }`: manda un código (si `smsEnabled`).",
        "x-puentes-who": "persona (sesión)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  },
                  "channel": {
                    "enum": [
                      "sms",
                      "whatsapp"
                    ]
                  }
                },
                "required": [
                  "phone"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "verified": {
                      "type": "boolean"
                    },
                    "channel": {
                      "type": "string"
                    },
                    "phone": {
                      "type": "string",
                      "description": "E.164."
                    },
                    "retryAfter": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/phones/verify": {
      "post": {
        "operationId": "postMePhonesVerify",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "`{ phone, code }`.",
        "description": "`{ phone, code }`. Un número es de una sola cuenta.",
        "x-puentes-who": "persona (sesión)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  },
                  "code": {
                    "type": "string",
                    "description": "6 dígitos."
                  },
                  "proof": {
                    "type": "string"
                  },
                  "merge": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "phone"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        },
                        "merged": {
                          "type": "boolean"
                        },
                        "phones": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "phone": {
                                "type": "string"
                              },
                              "verifiedAt": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "conflict": {
                          "const": true
                        },
                        "current": {
                          "$ref": "#/components/schemas/User"
                        },
                        "other": {
                          "$ref": "#/components/schemas/User"
                        },
                        "proof": {
                          "type": "string",
                          "description": "Se manda de vuelta con `merge: true` para unir las cuentas."
                        }
                      },
                      "required": [
                        "conflict",
                        "proof"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/phones/{phone}": {
      "delete": {
        "operationId": "deleteMePhonesPhone",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Sacarlo.",
        "description": "Sacarlo.",
        "x-puentes-who": "persona (sesión)",
        "parameters": [
          {
            "name": "phone",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/contacts/lookup": {
      "post": {
        "operationId": "postContactsLookup",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "`{ email }` o `{ phone }` → `{ status: \"found\", user } | { status: \"none\" } | { status: \"self\" }`.",
        "description": "`{ email }` o `{ phone }` → `{ status: \"found\", user } | { status: \"none\" } | { status: \"self\" }`. Nunca devuelve el email ni el número.",
        "x-puentes-who": "persona · verificada",
        "x-puentes-limits": [
          "30 por hora"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40,
                    "description": "Con código de país (+54…), o local con `country`."
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{2}$",
                    "description": "ISO de dos letras, para un número sin código de país."
                  }
                },
                "anyOf": [
                  {
                    "required": [
                      "email"
                    ]
                  },
                  {
                    "required": [
                      "phone"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "enum": [
                            "self",
                            "none"
                          ]
                        }
                      },
                      "required": [
                        "status"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "status": {
                          "const": "found"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        }
                      },
                      "required": [
                        "status",
                        "user"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/me/blocks": {
      "get": {
        "operationId": "getMeBlocks",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "A quiénes bloqueé.",
        "description": "A quiénes bloqueé.",
        "x-puentes-who": "persona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/User"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/users/{id}/block": {
      "post": {
        "operationId": "postUsersIdBlock",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Bloquear (ver [bloqueo](#contactos)).",
        "description": "Bloquear (ver [bloqueo](#contactos)).",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "60 por hora por cuenta"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "blocked": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteUsersIdBlock",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Desbloquear.",
        "description": "Desbloquear.",
        "x-puentes-who": "persona",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/users/{id}/report": {
      "post": {
        "operationId": "postUsersIdReport",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "`{ reason?, groupId?, block? }`: reporte al equipo de Puentes.",
        "description": "`{ reason?, groupId?, block? }`: reporte al equipo de Puentes.",
        "x-puentes-who": "persona",
        "x-puentes-limits": [
          "5 por día"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "groupId": {
                    "type": "string"
                  },
                  "block": {
                    "type": "boolean",
                    "description": "Bloquearla también."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/reports": {
      "post": {
        "operationId": "postReports",
        "tags": [
          "La cuenta de una persona"
        ],
        "summary": "Reportar un bot, un desarrollador o una organización `{ about: \"bot\" | \"developer\" | \"org\", id, reason? }`.",
        "description": "Reportar un bot, un desarrollador o una organización `{ about: \"bot\" | \"developer\" | \"org\", id, reason? }`.",
        "x-puentes-who": "persona",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "about": {
                    "enum": [
                      "bot",
                      "developer",
                      "org"
                    ]
                  },
                  "id": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 1000
                  }
                },
                "required": [
                  "about",
                  "id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  }
}
