C
Cotizera Docs

Quotes

Quotes API

Endpoints for creating, listing, and managing professional quotes with line items, status tracking, PDF generation, and version history.


List Quotes

GET /api/quotes

Permission: quotes:read | Collaborators see only their own quotes; Owners/Admins see all.

Retrieve a paginated list of quotes for the current tenant.

Query Parameters:

Parameter Type Required Description
search string No Search by quote number, client contact name, or company name (case-insensitive)
status string No Filter by status: GENERATED, SENT, WON, LOST, ACCEPTED, REJECTED
page number No Page number (default: 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
}

Errors:

Status Error When
401 "No autorizado" Missing or invalid session
403 "Sin permisos" Role lacks quotes:read

cURL Example:

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

Create Quote

POST /api/quotes

Permission: quotes:create

Create a new quote with line items. Automatically calculates subtotals, tax (from tenant settings or default 16%), and total. Assigns a sequential quote number per 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 Client ID (must belong to the same tenant and be active)
items array Yes At least one line item
items[].productId string No Reference to product catalog
items[].productName string Yes Product display name
items[].model string No Model or SKU
items[].unitPrice number Yes Unit price (≥ 0)
items[].quantity integer Yes Quantity (≥ 1)
items[].discountPct number No Line discount percentage (0–100, default: 0)
notes string No Free-text notes (max 300 characters)
shippingCost number No Shipping cost (≥ 0, default: 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", ... }
}

Errors:

Status Error When
400 Zod validation message Invalid input (e.g., empty items array)
401 "No autorizado" Missing or invalid session
403 "Sin permisos" Role lacks quotes:create
404 "Cliente no encontrado" Client doesn't exist, belongs to another tenant, or is inactive

Side Effects:

  • Fires quote.created webhook event
  • Triggers onboarding step first_quote completion
  • Logs audit entry with action create

Get Single Quote

GET /api/quotes/:id

Permission: quotes:read | Collaborators can only access their own quotes.

Retrieve a single quote with full line items, client details, and creator info.

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" }
}

Errors:

Status Error When
401 "No autorizado" Missing or invalid session
403 "Sin permisos" Role lacks quotes:read
404 "Cotización no encontrada" Quote doesn't exist or access denied

Update Quote Status

PATCH /api/quotes/:id/status

Permission: quotes:read_all (Owner/Admin only)

Update the status of a quote. Follows a strict state machine:

From Allowed Transitions
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: Returns the updated quote object.

Errors:

Status Error When
400 "No se puede cambiar de GENERATED a WON" Invalid state transition
404 "Cotización no encontrada" Quote not found in tenant

Side Effects:

  • Fires quote.status_changed webhook event
  • Logs audit entry with action status_change

Duplicate Quote

POST /api/quotes/:id/duplicate

Permission: quotes:create | Plan: PRO

Create a copy of an existing quote with a new quote number, fresh expiration date, and recalculated tax.

Request Body: None required.

Response 201 Created: Returns the new quote object with items and client.

Errors:

Status Error When
403 "Función disponible en el plan Pro" Tenant is on FREE plan
404 "Cotización no encontrada" Original quote not found

Share Quote via Email

POST /api/quotes/:id/share

Permission: quotes:read

Generates a PDF (if not already cached), uploads it to S3, and sends it via email to the client. Automatically transitions status from GENERATEDSENT.

Request Body: None required.

Response 200 OK:

{
  "success": true
}

Errors:

Status Error When
404 "Cotización no encontrada" Quote not found or access denied
500 "Error al enviar el correo" Email delivery failed

Generate / Stream PDF

GET /api/quotes/:id/pdf

Permission: quotes:read

Streams the PDF directly as a binary response. Generates it on-the-fly with current data, including digital signature if present. PRO tenants get custom branding (colors, logo, no watermark, bank info).

Response 200 OK:

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

POST /api/quotes/:id/pdf

Permission: quotes:read

Generates the PDF, uploads it to S3, and returns a presigned download URL. If already uploaded, returns a fresh presigned URL without regenerating.

Response 200 OK:

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

Revise Quote (New Version)

POST /api/quotes/:id/revise

Permission: quotes:create | Plan: PRO (requires quote_versioning feature)

Creates a new version of the quote. Snapshots the current state into QuoteVersion, then replaces items and totals with the new data. Increments the version field.

Request Body: Same schema as Create Quote (createQuoteSchema).

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

Response 200 OK: Returns the updated quote with new items and client.

Errors:

Status Error When
403 "Función no disponible en el plan actual" Plan doesn't support versioning
404 "Cotización no encontrada" Quote not found in tenant

Side Effects:

  • Creates a QuoteVersion snapshot of the previous state
  • Fires quote.revised webhook event
  • Logs audit entry with action quote.revised

Get Version History

GET /api/quotes/:id/versions

Permission: quotes:read

Retrieve all previous versions of a quote, ordered by version number descending.

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"
  }
]

Get Signature Data

GET /api/quotes/:id/signature

Permission: quotes:read

Retrieve the digital signature data for a quote, if one exists.

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"
  }
}

If no signature exists, signature will be null.

© 2026 Cotizera. All rights reserved.