C
Cotizera Docs

Cotizaciones

API de Cotizaciones

Endpoints para crear, listar y administrar cotizaciones profesionales con partidas, seguimiento de estado, generación de PDF e historial de versiones.


Listar Cotizaciones

GET /api/quotes

Permiso: quotes:read | Los colaboradores solo ven sus propias cotizaciones; los propietarios y administradores ven todas.

Obtén una lista paginada de cotizaciones del tenant actual.

Query Parameters:

Parameter Type Required Description
search string No Busca por número de cotización, nombre del contacto o nombre de la empresa (sin distinguir mayúsculas)
status string No Filtra por estado: GENERATED, SENT, WON, LOST, ACCEPTED, REJECTED
page number No Número de página (por defecto: 1)

Response 200 OK:

{
  "data": [
    {
      "id": "clxyz...",
      "quoteNumber": 42,
      "status": "SENT",
      "subtotal": "15000.00",
      "tax": "2400.00",
      "shippingCost": "500.00",
      "total": "17900.00",
      "notes": "Entrega en 5 días hábiles",
      "expiresAt": "2026-04-15T00:00:00.000Z",
      "createdAt": "2026-03-31T10:00:00.000Z",
      "client": {
        "contactName": "María López",
        "companyName": "Distribuidora del Norte S.A."
      },
      "user": {
        "name": "Carlos Méndez"
      },
      "_count": {
        "items": 3
      }
    }
  ],
  "total": 87,
  "page": 1,
  "totalPages": 5
}

Errores:

Status Error Cuándo
401 "No autorizado" Sesión ausente o inválida
403 "Sin permisos" El rol no tiene quotes:read

Ejemplo con cURL:

curl -X GET "https://cotizera.com/api/quotes?page=1&status=SENT&search=María" \
  -H "Cookie: next-auth.session-token=YOUR_TOKEN"

Crear Cotización

POST /api/quotes

Permiso: quotes:create

Crea una nueva cotización con partidas. Calcula automáticamente subtotales, impuesto (según la configuración del tenant o el 16% por defecto) y total. Asigna un número de cotización secuencial por tenant.

Request Body:

{
  "clientId": "clxyz...",
  "items": [
    {
      "productId": "clxyz...",
      "productName": "Laptop HP ProBook 450",
      "model": "450-G10",
      "unitPrice": 18500,
      "quantity": 2,
      "discountPct": 5
    }
  ],
  "notes": "Incluye instalación y configuración",
  "shippingCost": 350
}
Field Type Required Description
clientId string Yes ID del cliente (debe pertenecer al mismo tenant y estar activo)
items array Yes Al menos una partida
items[].productId string No Referencia al catálogo de productos
items[].productName string Yes Nombre del producto a mostrar
items[].model string No Modelo o SKU
items[].unitPrice number Yes Precio unitario (>= 0)
items[].quantity integer Yes Cantidad (>= 1)
items[].discountPct number No Porcentaje de descuento por partida (0–100, por defecto: 0)
notes string No Notas de texto libre (máximo 300 caracteres)
shippingCost number No Costo de envío (>= 0, por defecto: 0)

Response 201 Created:

{
  "id": "clxyz...",
  "quoteNumber": 43,
  "status": "GENERATED",
  "subtotal": 35150,
  "tax": 5624,
  "shippingCost": 350,
  "total": 41124,
  "notes": "Incluye instalación y configuración",
  "expiresAt": "2026-04-15T00:00:00.000Z",
  "publicToken": "abc123...",
  "createdAt": "2026-03-31T10:00:00.000Z",
  "items": [...],
  "client": { "id": "...", "contactName": "María López", ... }
}

Errores:

Status Error Cuándo
400 Mensaje de validación Zod Entrada inválida (por ejemplo, array de items vacío)
401 "No autorizado" Sesión ausente o inválida
403 "Sin permisos" El rol no tiene quotes:create
404 "Cliente no encontrado" El cliente no existe, pertenece a otro tenant o está inactivo

Efectos secundarios:

  • Dispara el evento webhook quote.created
  • Activa el paso de onboarding first_quote
  • Registra una entrada de auditoría con acción create

Obtener una Cotización

GET /api/quotes/:id

Permiso: quotes:read | Los colaboradores solo pueden acceder a sus propias cotizaciones.

Obtén una cotización individual con todas sus partidas, datos del cliente e información del creador.

Response 200 OK:

{
  "id": "clxyz...",
  "quoteNumber": 42,
  "status": "SENT",
  "subtotal": "15000.00",
  "tax": "2400.00",
  "total": "17900.00",
  "items": [
    {
      "id": "...",
      "productName": "Monitor Dell 27\"",
      "model": "U2723QE",
      "unitPrice": "7500.00",
      "quantity": 2,
      "discountPct": "0.00",
      "subtotal": "15000.00",
      "product": { "id": "...", "name": "Monitor Dell 27\"", "modelSku": "U2723QE" }
    }
  ],
  "client": { "id": "...", "contactName": "María López", "companyName": "...", "email": "...", "phone": "..." },
  "user": { "name": "Carlos Méndez", "email": "carlos@example.com" }
}

Errores:

Status Error Cuándo
401 "No autorizado" Sesión ausente o inválida
403 "Sin permisos" El rol no tiene quotes:read
404 "Cotización no encontrada" La cotización no existe o el acceso fue denegado

Actualizar Estado de Cotización

PATCH /api/quotes/:id/status

Permiso: quotes:read_all (solo propietario/administrador)

Actualiza el estado de una cotización. Sigue una máquina de estados estricta:

Desde Transiciones permitidas
GENERATED -> SENT
SENT -> WON, LOST
WON (terminal)
LOST (terminal)
ACCEPTED (terminal)
REJECTED (terminal)

Request Body:

{
  "status": "SENT"
}
Field Type Required Description
status QuoteStatus Yes GENERATED, SENT, WON, LOST, ACCEPTED, REJECTED

Response 200 OK: Devuelve el objeto de cotización actualizado.

Errores:

Status Error Cuándo
400 "No se puede cambiar de GENERATED a WON" Transición de estado inválida
404 "Cotización no encontrada" Cotización no encontrada en el tenant

Efectos secundarios:

  • Dispara el evento webhook quote.status_changed
  • Registra una entrada de auditoría con acción status_change

Duplicar Cotización

POST /api/quotes/:id/duplicate

Permiso: quotes:create | Plan: PRO

Crea una copia de una cotización existente con un nuevo número, nueva fecha de expiración e impuesto recalculado.

Request Body: No se requiere.

Response 201 Created: Devuelve el nuevo objeto de cotización con partidas y cliente.

Errores:

Status Error Cuándo
403 "Función disponible en el plan Pro" El tenant está en el plan FREE
404 "Cotización no encontrada" La cotización original no fue encontrada

Compartir Cotización por Correo

POST /api/quotes/:id/share

Permiso: quotes:read

Genera un PDF (si no está en caché), lo sube a S3 y lo envía por correo al cliente. Cambia automáticamente el estado de GENERATED a SENT.

Request Body: No se requiere.

Response 200 OK:

{
  "success": true
}

Errores:

Status Error Cuándo
404 "Cotización no encontrada" Cotización no encontrada o acceso denegado
500 "Error al enviar el correo" Falló el envío del correo

Generar / Transmitir PDF

GET /api/quotes/:id/pdf

Permiso: quotes:read

Transmite el PDF directamente como respuesta binaria. Lo genera al momento con los datos actuales, incluyendo firma digital si existe. Los tenants PRO obtienen marca personalizada (colores, logo, sin marca de agua, datos bancarios).

Response 200 OK:

  • Content-Type: application/pdf
  • Content-Disposition: inline; filename="COT-0042.pdf"

POST /api/quotes/:id/pdf

Permiso: quotes:read

Genera el PDF, lo sube a S3 y devuelve una URL de descarga prefirmada. Si ya fue subido, devuelve una URL prefirmada nueva sin regenerar el archivo.

Response 200 OK:

{
  "pdfUrl": "https://s3.amazonaws.com/..."
}

Revisar Cotización (Nueva Versión)

POST /api/quotes/:id/revise

Permiso: quotes:create | Plan: PRO (requiere la función quote_versioning)

Crea una nueva versión de la cotización. Guarda una instantánea del estado actual en QuoteVersion, luego reemplaza las partidas y totales con los nuevos datos. Incrementa el campo version.

Request Body: Mismo esquema que Crear Cotización (createQuoteSchema).

{
  "clientId": "clxyz...",
  "items": [...],
  "notes": "Versión actualizada con descuento",
  "shippingCost": 0
}

Response 200 OK: Devuelve la cotización actualizada con las nuevas partidas y cliente.

Errores:

Status Error Cuándo
403 "Función no disponible en el plan actual" El plan no soporta versionamiento
404 "Cotización no encontrada" Cotización no encontrada en el tenant

Efectos secundarios:

  • Crea una instantánea QuoteVersion del estado anterior
  • Dispara el evento webhook quote.revised
  • Registra una entrada de auditoría con acción quote.revised

Obtener Historial de Versiones

GET /api/quotes/:id/versions

Permiso: quotes:read

Obtén todas las versiones anteriores de una cotización, ordenadas por número de versión de forma descendente.

Response 200 OK:

[
  {
    "id": "clxyz...",
    "version": 1,
    "snapshot": {
      "clientId": "...",
      "items": [...],
      "notes": "...",
      "subtotal": 15000,
      "tax": 2400,
      "total": 17400
    },
    "createdBy": "Carlos Méndez",
    "createdAt": "2026-03-30T10:00:00.000Z"
  }
]

Obtener Datos de Firma

GET /api/quotes/:id/signature

Permiso: quotes:read

Obtén los datos de la firma digital de una cotización, si existe.

Response 200 OK:

{
  "signature": {
    "type": "draw",
    "data": "data:image/png;base64,...",
    "signerName": "María López",
    "signerEmail": "maria@distribuidora.mx",
    "signedAt": "2026-03-31T14:30:00.000Z"
  }
}

Si no existe firma, signature será null.

© 2026 Cotizera. All rights reserved.