Skip to main content
Quotation management is split across two layers:
  • Web routes (/admin/cotizaciones/*) — session-authenticated, return HTML views or redirects.
  • API routes (/api/v1/quotes/*) — Sanctum Bearer token, return JSON.
All web routes require the auth + verified middleware. All API routes require a valid Sanctum Bearer token and are throttled at 30 requests per minute.

Web routes

List quotes

GET /admin/cotizaciones Returns a paginated HTML view of all quotes, 15 per page, ordered newest first.
status
string
Filter by quote status. Accepted values: draft, sent, accepted, rejected, expired.
q
string
Full-text search across client_name, client_email, and reference (case-insensitive LIKE match).
cURL
curl --request GET \
  --url 'https://your-app.test/admin/cotizaciones?status=sent&q=acme' \
  --header 'Accept: text/html' \
  --cookie 'laravel_session=<session_cookie>'
This route returns an HTML view, not JSON. Use it as a browser navigation target, not a data API.

Show quote

GET /admin/cotizaciones/{quote} Loads a single quote with its line items (and their associated blocks) and replies (sorted newest first).
quote
integer
required
The quote’s primary key.
cURL
curl --request GET \
  --url https://your-app.test/admin/cotizaciones/42 \
  --cookie 'laravel_session=<session_cookie>'

Update quote status

PATCH /admin/cotizaciones/{quote}/status Updates the status field of the quote.
quote
integer
required
The quote’s primary key.
status
string
required
New status value. Accepted: draft, sent, accepted, rejected, expired.
cURL
curl --request PATCH \
  --url https://your-app.test/admin/cotizaciones/42/status \
  --header 'Content-Type: application/json' \
  --header 'X-CSRF-TOKEN: <token>' \
  --cookie 'laravel_session=<session_cookie>' \
  --data '{"status": "reviewed"}'
On success the response redirects back to the quote detail page with a success flash message.

Reply to quote (schedule meeting + send email)

POST /admin/cotizaciones/{quote}/reply Schedules a virtual meeting, saves a QuoteReply record, generates a PDF of the quote, and emails the client with the PDF attached.
quote
integer
required
The quote’s primary key.
meeting_date
string
required
The meeting date and time in any format parseable by PHP’s DateTime. Recommend ISO 8601: YYYY-MM-DD HH:MM. This value is stored verbatim in quote_replies.sent_at.
The meeting message saved on the QuoteReply and included in the email is formatted as: Cita Virtual en GoogleMeet: DD/MM/YYYY HH:MM
The handler:
  1. Validates meeting_date as a required date.
  2. Creates a QuoteReply row with quote_id, the formatted message, and sent_at = meeting_date.
  3. Generates a fresh PDF from the quote’s items.
  4. Saves the PDF to storage/app/public/quotes/{reference}.pdf.
  5. Dispatches QuoteReplyMail to quote.client_email with the PDF attached.
  6. Redirects to admin.quotes.show with a success flash.
cURL
curl --request POST \
  --url https://your-app.test/admin/cotizaciones/42/reply \
  --header 'Content-Type: application/json' \
  --header 'X-CSRF-TOKEN: <token>' \
  --cookie 'laravel_session=<session_cookie>' \
  --data '{"meeting_date": "2026-04-15 10:00"}'
This action sends a live email to the client. Confirm the meeting date before submitting.

Download quote PDF

GET /admin/cotizaciones/{quote}/pdf Generates and streams the PDF for an existing quote as a file download.
quote
integer
required
The quote’s primary key.
The response sets:
  • Content-Type: application/pdf
  • Content-Disposition: attachment; filename="cotizacion-{reference}.pdf"
cURL
curl --request GET \
  --url https://your-app.test/admin/cotizaciones/42/pdf \
  --output cotizacion.pdf \
  --cookie 'laravel_session=<session_cookie>'

API routes (Bearer token)

All routes below are prefixed /api/v1/ and require Authorization: Bearer <token>.

Get statistics

GET /api/v1/quotes/statistics Returns aggregate counts and monetary totals across all quotes. Results are cached for one hour per calendar month.
cURL
curl --request GET \
  --url https://your-app.test/api/v1/quotes/statistics \
  --header 'Authorization: Bearer <your-token>' \
  --header 'Accept: application/json'
total
integer
Total number of quotes in the database across all time.
sent
integer
Number of quotes with status sent.
accepted
integer
Number of quotes with status accepted.
today
integer
Number of quotes created today (server’s local date).
this_month
integer
Number of quotes created since the start of the current calendar month.
this_year
integer
Number of quotes created since the start of the current year.
total_value
number
Sum of the total column across all quotes.
average_value
number
Average of the total column across all quotes. 0 when there are no quotes.
top_clients
object[]
Top 5 clients by quote count, each with client_email, client_name, count, and total.
Response example
{
  "total": 148,
  "sent": 120,
  "accepted": 42,
  "today": 3,
  "this_month": 18,
  "this_year": 96,
  "total_value": 284750.00,
  "average_value": 1923.31,
  "top_clients": [
    {
      "client_email": "[email protected]",
      "client_name": "Acme Corp",
      "count": 8,
      "total": 24800.00
    }
  ]
}

Get recent quotes

GET /api/v1/quotes/recent Returns the most recently created quotes, each including their line items.
limit
integer
default:"10"
Maximum number of quotes to return. No upper bound is enforced by the server.
cURL
curl --request GET \
  --url 'https://your-app.test/api/v1/quotes/recent?limit=5' \
  --header 'Authorization: Bearer <your-token>' \
  --header 'Accept: application/json'
success
boolean
Always true on a successful response.
quotes
object[]
Array of quote objects, ordered newest first, with related items eager-loaded.
Response example
{
  "success": true,
  "quotes": [
    {
      "id": 42,
      "reference": "COT-65F3A1B2C4D5E",
      "client_name": "Acme Corp",
      "client_email": "[email protected]",
      "client_company": "Acme Corp",
      "client_phone": "+52 55 1234 5678",
      "subtotal": "1800.00",
      "tax": "288.00",
      "total": "2088.00",
      "total_hours": 40,
      "status": "sent",
      "sent_at": "2026-03-19T14:30:00.000000Z",
      "pdf_path": "quotes/COT-65F3A1B2C4D5E.pdf",
      "created_at": "2026-03-19T14:25:00.000000Z",
      "updated_at": "2026-03-19T14:30:00.000000Z",
      "items": []
    }
  ]
}

Duplicate quote

POST /api/v1/quotes/{id}/duplicate Creates a copy of a quote. The duplicate gets a fresh auto-generated reference, its status is reset to draft, and sent_at and pdf_path are cleared. No line items are copied in the current implementation — the top-level quote fields are replicated.
id
integer
required
The primary key of the quote to duplicate.
cURL
curl --request POST \
  --url https://your-app.test/api/v1/quotes/42/duplicate \
  --header 'Authorization: Bearer <your-token>' \
  --header 'Accept: application/json'
success
boolean
Always true on a successful duplication.
reference
string
Auto-generated reference for the duplicated quote (e.g. COT-67A1B2C3D4E5F).
message
string
Confirmation message: "Cotización duplicada exitosamente".
Response example
{
  "success": true,
  "reference": "COT-67A1B2C3D4E5F",
  "message": "Cotización duplicada exitosamente"
}

Export quotes to CSV

GET /api/v1/quotes/export Exports quotes to a CSV file. Supports optional date range and status filters. Results are ordered newest first.
start_date
string
Filter to quotes created on or after this date. Format: YYYY-MM-DD.
end_date
string
Filter to quotes created on or before this date. Must be >= start_date. Format: YYYY-MM-DD.
status
string
Filter by quote status. Accepted: draft, sent, accepted, rejected, expired.
The response is a CSV file download with the following columns: Referencia, Cliente, Email, Empresa, Total, IVA, Subtotal, Horas, Estado, Fecha
cURL
curl --request GET \
  --url 'https://your-app.test/api/v1/quotes/export?status=sent&start_date=2026-01-01' \
  --header 'Authorization: Bearer <your-token>' \
  --output quotes_export.csv
The response sets:
  • Content-Type: text/csv
  • Content-Disposition: attachment; filename="quotes_export_{YYYY-MM-DD_HH-MM-SS}.csv"
On validation failure returns HTTP 422:
{
  "success": false,
  "errors": {
    "end_date": ["The end date field must be a date after or equal to start date."],
    "status": ["The selected status is invalid."]
  }
}

Quote model fields reference

FieldTypeNotes
idintegerAuto-increment primary key
referencestringAuto-generated on creation: COT- + uppercased uniqid()
client_namestringRequired
client_emailstringRequired
client_companystring|nullOptional
client_phonestring|nullOptional
additional_requirementsstring|nullFree-text notes
dataarray (JSON)Full raw payload from the quote builder
citaarray (JSON)Appointment data
subtotaldecimal:2Pre-tax total
taxdecimal:2Tax amount
totaldecimal:2Grand total
total_hoursintegerAggregate estimated hours
statusstringdraft / sent / accepted / rejected / expired
sent_atdatetime|nullPopulated on submission
pdf_pathstring|nullRelative path in storage/app/public/

Build docs developers (and LLMs) love