{
  "openapi": "3.1.0",
  "info": {
    "title": "Vertichat Public API",
    "version": "1.0.0",
    "description": "Contrato público para el widget web y los webhooks salientes de Vertichat. Las APIs internas de la bandeja y el CRM requieren una sesión de aplicación y no forman parte de este contrato.",
    "contact": {
      "name": "Vertichat",
      "url": "https://vertichat.verticali.mx/developers"
    }
  },
  "servers": [
    {
      "url": "https://vertichat.verticali.mx",
      "description": "Producción"
    }
  ],
  "tags": [
    { "name": "Widget", "description": "Chat web embebible" },
    { "name": "Attachments", "description": "Descargas temporales firmadas" }
  ],
  "paths": {
    "/api/widget/{publicKey}/bootstrap": {
      "get": {
        "tags": ["Widget"],
        "summary": "Obtener la configuración pública del widget",
        "operationId": "getWidgetBootstrap",
        "parameters": [
          { "$ref": "#/components/parameters/PublicKey" }
        ],
        "responses": {
          "200": {
            "description": "Configuración del canal",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WidgetBootstrapResponse" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/widget/{publicKey}/session": {
      "post": {
        "tags": ["Widget"],
        "summary": "Crear una sesión de visitante",
        "description": "El encabezado Origin debe pertenecer a la lista de dominios permitidos del canal.",
        "operationId": "createWidgetSession",
        "parameters": [
          { "$ref": "#/components/parameters/PublicKey" },
          { "$ref": "#/components/parameters/Origin" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateWidgetSessionRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sesión creada",
            "headers": {
              "Access-Control-Allow-Origin": {
                "schema": { "type": "string" },
                "description": "Origen autorizado que inició la sesión"
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WidgetSessionResponse" }
              }
            }
          },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/widget/{publicKey}/messages": {
      "get": {
        "tags": ["Widget"],
        "summary": "Listar mensajes de la sesión",
        "operationId": "listWidgetMessages",
        "security": [{ "widgetBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/PublicKey" },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Cursor entero devuelto por una consulta anterior",
            "schema": { "type": "integer", "minimum": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Mensajes de la conversación",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WidgetMessagesResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Widget"],
        "summary": "Enviar un mensaje de texto",
        "operationId": "postWidgetMessage",
        "security": [{ "widgetBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/PublicKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["content"],
                "properties": {
                  "content": { "type": "string", "minLength": 1, "maxLength": 4000 }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Mensaje persistido",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DataObjectResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/widget/{publicKey}/uploads": {
      "post": {
        "tags": ["Widget"],
        "summary": "Enviar un archivo",
        "description": "Requiere Content-Length. El límite de la solicitud es 27 MiB y se aplican límites adicionales por sesión, visitante y canal.",
        "operationId": "postWidgetUpload",
        "security": [{ "widgetBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/PublicKey" },
          {
            "name": "Content-Length",
            "in": "header",
            "required": true,
            "schema": { "type": "integer", "minimum": 1, "maximum": 28311552 }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": { "type": "string", "format": "binary" },
                  "caption": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Archivo persistido y asociado a un mensaje",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DataObjectResponse" }
              }
            }
          },
          "411": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/widget/{publicKey}/read": {
      "post": {
        "tags": ["Widget"],
        "summary": "Marcar la conversación como leída",
        "operationId": "markWidgetRead",
        "security": [{ "widgetBearer": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/PublicKey" }
        ],
        "responses": {
          "200": {
            "description": "Lectura registrada",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DataObjectResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/public/webhook-attachments/{attachmentId}": {
      "get": {
        "tags": ["Attachments"],
        "summary": "Descargar un adjunto mediante una URL firmada",
        "description": "Vertichat genera la URL completa dentro de los eventos. No construyas ni reutilices manualmente el token.",
        "operationId": "downloadWebhookAttachment",
        "parameters": [
          {
            "name": "attachmentId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Contenido binario del adjunto",
            "content": {
              "application/octet-stream": {
                "schema": { "type": "string", "format": "binary" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "webhooks": {
    "sipochatEvent": {
      "post": {
        "summary": "Evento saliente de Vertichat",
        "description": "Entrega al menos una vez. Verifica X-SipoChat-Signature sobre timestamp + '.' + raw_body y deduplica por el campo id.",
        "parameters": [
          { "$ref": "#/components/parameters/SipoChatEvent" },
          { "$ref": "#/components/parameters/SipoChatDelivery" },
          { "$ref": "#/components/parameters/SipoChatTimestamp" },
          { "$ref": "#/components/parameters/SipoChatAttempt" },
          { "$ref": "#/components/parameters/SipoChatSignature" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEnvelope" }
            }
          }
        },
        "responses": {
          "200": { "description": "Evento aceptado" },
          "202": { "description": "Evento encolado" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "widgetBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Opaque widget session token",
        "description": "Token devuelto al crear la sesión del visitante"
      }
    },
    "parameters": {
      "PublicKey": {
        "name": "publicKey",
        "in": "path",
        "required": true,
        "description": "Clave pública del canal web",
        "schema": { "type": "string", "example": "web_72e570bdfd5d40968ed4af92e25e310b" }
      },
      "Origin": {
        "name": "Origin",
        "in": "header",
        "required": true,
        "schema": { "type": "string", "format": "uri", "example": "https://tuempresa.com" }
      },
      "SipoChatEvent": {
        "name": "X-SipoChat-Event",
        "in": "header",
        "required": true,
        "schema": { "type": "string", "example": "deal.stage_changed" }
      },
      "SipoChatDelivery": {
        "name": "X-SipoChat-Delivery",
        "in": "header",
        "required": true,
        "schema": { "type": "string" }
      },
      "SipoChatTimestamp": {
        "name": "X-SipoChat-Timestamp",
        "in": "header",
        "required": true,
        "schema": { "type": "string", "description": "Unix timestamp en segundos" }
      },
      "SipoChatAttempt": {
        "name": "X-SipoChat-Attempt",
        "in": "header",
        "required": true,
        "schema": { "type": "integer", "minimum": 1 }
      },
      "SipoChatSignature": {
        "name": "X-SipoChat-Signature",
        "in": "header",
        "required": true,
        "schema": { "type": "string", "pattern": "^v1=[a-f0-9]{64}$" }
      }
    },
    "schemas": {
      "ErrorBody": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "details": {}
            }
          }
        }
      },
      "DataObjectResponse": {
        "type": "object",
        "required": ["data"],
        "properties": { "data": { "type": "object", "additionalProperties": true } }
      },
      "WidgetChannel": {
        "type": "object",
        "required": ["name", "greeting", "color"],
        "properties": {
          "publicKey": { "type": "string" },
          "name": { "type": "string" },
          "greeting": { "type": "string" },
          "color": { "type": "string", "pattern": "^#[A-Fa-f0-9]{6}$" },
          "position": { "type": "string" }
        }
      },
      "WidgetBootstrapResponse": {
        "type": "object",
        "required": ["data"],
        "properties": {
          "data": {
            "type": "object",
            "required": ["channel", "pollingIntervalMs"],
            "properties": {
              "channel": { "$ref": "#/components/schemas/WidgetChannel" },
              "pollingIntervalMs": { "type": "integer", "example": 5000 }
            }
          }
        }
      },
      "CreateWidgetSessionRequest": {
        "type": "object",
        "properties": {
          "anonymousId": { "type": "string", "maxLength": 128 },
          "pageUrl": { "type": "string", "format": "uri", "maxLength": 2000 }
        },
        "additionalProperties": false
      },
      "WidgetSessionResponse": {
        "type": "object",
        "required": ["data"],
        "properties": {
          "data": {
            "type": "object",
            "required": ["token", "anonymousId", "expiresAt", "channel"],
            "properties": {
              "token": { "type": "string", "writeOnly": true },
              "anonymousId": { "type": "string" },
              "expiresAt": { "type": "integer", "description": "Epoch en milisegundos" },
              "channel": { "$ref": "#/components/schemas/WidgetChannel" }
            }
          }
        }
      },
      "WidgetMessagesResponse": {
        "type": "object",
        "required": ["data"],
        "properties": {
          "data": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "messages": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
              "cursor": { "type": ["integer", "null"] }
            }
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "required": ["specversion", "id", "type", "version", "source", "workspace_id", "time", "context", "data", "private"],
        "properties": {
          "specversion": { "const": "1.0" },
          "id": { "type": "string" },
          "type": {
            "type": "string",
            "enum": [
              "message.created", "conversation.created", "conversation.updated", "conversation.status_changed",
              "note.created", "attachment.created", "contact.created", "contact.updated", "company.created", "company.updated",
              "deal.created", "deal.updated", "deal.stage_changed", "deal.won", "deal.lost",
              "task.created", "task.updated", "task.completed", "task.cancelled", "task.reopened", "activity.created",
              "quote.created", "quote.sent", "quote.accepted", "quote.rejected", "webhook.test"
            ]
          },
          "version": { "const": "2026-07-01" },
          "source": { "type": "string" },
          "workspace_id": { "type": "string" },
          "subject": { "type": "string" },
          "time": { "type": "string", "format": "date-time" },
          "actor": {
            "type": "object",
            "properties": {
              "type": { "type": "string" },
              "id": { "type": "string" },
              "name": { "type": "string" }
            }
          },
          "context": { "type": "object", "additionalProperties": true },
          "data": { "type": "object", "additionalProperties": true },
          "changes": { "type": "object", "additionalProperties": true },
          "private": { "type": "boolean" }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Error",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "Unauthorized": {
        "description": "Token ausente, vencido o inválido",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "Forbidden": {
        "description": "Origen no permitido",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "NotFound": {
        "description": "Recurso no encontrado",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "RateLimited": {
        "description": "Límite temporal alcanzado",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      }
    }
  }
}
