---
title: "Save Budget"
method: POST
path: "/api/order/save"
tags: ["copilot_frontend", "copilot_order"]
---

# Save Budget

`POST /api/order/save`

Saves a complete order (pedido) to database and creates/updates contact in HubSpot.

This endpoint:
1. Gets province name from database
2. Updates commercial phone if provided and different from current
3. Gets "pedido" status from database to use as estado_id
4. Creates/updates contact in HubSpot with client data
5. Updates contact with all order properties (payment, installation, subsidies, etc.)
6. Updates deal property "pedido_de_venta_backend" = "Si"
7. Saves order in presup_presupuestos table with estado="pedido" and all order data

**🔒 Protected Endpoint:** Requires JWT authentication and 'ui_comercial' or 'admin' role

Args:
    request: SaveOrderRequest with client data, calculation data (already calculated from Vue),
             selected option, and complete order data (payment, installation, subsidies, etc.)
    current_user: Current authenticated user

Returns:
    SaveBudgetResponse with success status, order ID, and HubSpot contact ID

Raises:
    ErrValidation: If user does not have comercial_id
    ErrNotFound: If province or "pedido" status not found
    Exception: If HubSpot or database save fails

## Request body

- SaveOrderRequest — Extended request to save a complete order (pedido) with all form fields. This extends SaveBudgetRequest with additional order-specific fields that need to be saved as HubSpot contact properties and deal property.
  - `client_data` ClientDataForOrder, required — Client contact data for orders (pedido) - all fields REQUIRED except telefono and telefono_comercial. This is a stricter version of ClientDataForBudget used specifically for orders. Accepts both 'numero_documento_identidad' (new) and 'dni' (legacy) for backwards compatibility.
    - `nombre` string, nullable — Client full name (REQUIRED for final)
    - `apellido` string, nullable — Client last name(s) (REQUIRED for final)
    - `numero_documento_identidad` string, nullable — DNI/NIE/CIF del cliente
    - `dni` string, nullable — LEGACY: DNI del cliente (usar numero_documento_identidad)
    - `direccion` string, nullable — Full address (REQUIRED for final)
    - `localidad` string, nullable — City/Town (REQUIRED for final)
    - `cp` string, nullable — Postal code (REQUIRED for final)
    - `provincia_id` integer, nullable — Province ID from presup_provincias_prod table (REQUIRED for final)
    - `email` string, email, nullable — Client email (REQUIRED for final)
    - `telefono` string, nullable — Client phone number (OPTIONAL)
    - `telefono_comercial` string, nullable — Commercial phone number (OPTIONAL)
  - `calculation_data` BudgetCalculationRequest, required — Request from frontend to calculate budget in real-time. User inputs from the form that trigger recalculation.
    - `solo_bateria` boolean — Battery-only budget (no panels, no consumption required)
    - `consumo_mes_kwh` integer — Monthly consumption in kWh
    - `provincia` integer, required — Province ID for production calculations
    - `presupuesto_id` integer, nullable — Budget ID (optional, used to apply client-specific promotions)
    - `panel_id` integer, nullable — Selected panel ID (None for solo_bateria)
    - `micro_inversor_id` integer, nullable — Selected microinverter ID
    - `baterias` BatterySelection — Battery selection with quantity.
      - `bateria_id` integer, required — Battery ID
      - `cantidad` integer — Battery quantity
    - `extras_ids` integer[] — List of selected extra IDs
    - `metros_cableado` integer — Extra cable meters
    - `estructura_id` integer, nullable — Structure/pergola ID (None = no pergola)
    - `num_paneles_sin_pergola` integer — Number of panels without pergola
    - `ground_type_id` integer, nullable — Ground type ID
    - `forma_pago` integer, nullable — Payment method ID (None = not selected)
    - `opcion_contado_id` integer, nullable — Cash payment option ID from presup_forma_pago_contado (required if forma_pago='contado')
    - `meses_financiacion` integer, nullable — Financing months if forma_pago='financiado'
    - `entidad_financiera` string, nullable — Financing entity if forma_pago='financiado'
    - `contado_financiacion` number, nullable — Down payment amount if forma_pago='contado financiado'
    - `con_backup` boolean — Include backup system cost (adds Backup constant price)
  - `selected_option` integer, nullable — ID/name of the selected budget option
  - `order_data` OrderDataForHubSpot — Complete order (pedido) data to save in HubSpot contact properties. This includes all the fields from the order form that need to be saved as contact properties in HubSpot. REQUIRED FIELDS: All fields are required except: - promocion_cuota_financiacion_id (optional) - foto_referencia_catastral (optional - file upload, not in this schema) Note: Some fields are conditional based on payment method or subsidy selection, but they are still marked as Optional in the schema for flexibility.
    - `dni_foto_por_delante` union — DNI front photo(s) - string or list of {name, base64}
      - string
      - object[]
    - `dni_detras` union — DNI back photo(s) - string or list of {name, base64}
      - string
      - object[]
    - `recibo_bancario` union — Bank receipt document(s) - string or list of {name, base64}
      - string
      - object[]
    - `foto_referencia_catastral` union — Cadastral reference photo(s) - string or list of {name, base64}
      - string
      - object[]
    - `ultima_factura_de_la_luz` union — Last electricity bill(s) - string or list of {name, base64} (required on creation)
      - string
      - object[]
    - `varios_archivos` object[], nullable — Multiple additional files, each with 'name' and 'base64'
    - `importe_total` number, nullable — Total amount of the order (REQUIRED for final)
    - `forma_de_pago_id` integer, nullable — Payment method ID from presup_forma_pago table (REQUIRED for final)
    - `cantidad_al_contado` number, nullable — Cash amount (required if cash payment)
    - `cantidad_financiada` number, nullable — Financed amount (required if financing)
    - `no_de_cuotas` integer, nullable — Number of financing installments (required if financing)
    - `entidad_financiera_firmada` string, nullable — Financial institution name from signed financing (required if financing)
    - `puesto_de_trabajo` string, nullable — Job position (required if financing)
    - `sueldo` number, nullable — Salary (required if financing)
    - `financiacion_firmada_id` integer, nullable — Financing signed status ID (REQUIRED if financing)
    - `promocion_cuota_financiacion_id` integer, nullable — Financing quota promotion ID (OPTIONAL)
    - `opciones_pago_al_contado_id` integer, nullable — Cash payment option ID (required if cash payment)
    - `opciones_pago_al_contado_tras_previsualizar_id` integer, nullable — Cash payment after preview option ID (conditional)
    - `observaciones` string, nullable — General observations (multiline text) (REQUIRED for final)
    - `fecha_preferente_de_montaje` string, date, nullable — Preferred installation date (REQUIRED for final)
    - `estructura_adicional_si_no` boolean, nullable — Additional structure (pergola) - YES/NO (REQUIRED for final unless solo_bateria)
    - `protecciones_electricas_id` integer, nullable — Electrical protections ID (REQUIRED for final unless solo_bateria)
    - `marca_estructura` string, nullable — Structure brand name: 'fischer' or 'standar' (REQUIRED for final unless solo_bateria)
    - `no_de_paneles` integer, nullable — Number of panels (1-50) (REQUIRED for final unless solo_bateria)
    - `marca_de_paneles_id` integer, nullable — Panel brand ID (REQUIRED for final unless solo_bateria)
    - `marca_panel_id` integer, nullable — Panel model ID (REQUIRED for final unless solo_bateria)
    - `marca_inversor_id` integer, nullable — Inverter brand ID (optional for basic installations)
    - `marca_bateria_id` integer, nullable — Battery brand ID (REQUIRED for final)
    - `modelo_bateria_id` integer, nullable — Battery model ID (REQUIRED for final)
    - `notas_instalacion` string, nullable — Installation notes (multiline text) (REQUIRED for final)
    - `backup` boolean, nullable — Backup system - YES/NO (REQUIRED for final)
    - `no_baterias` integer, nullable — Number of batteries (REQUIRED for final)
    - `tipo_subvencion` integer, nullable — Subsidy type ID (REQUIRED for final unless solo_bateria)
    - `auditoria_energetica` string, nullable — Energy audit - 'Si' or 'No' (required if subsidy)
    - `certificado_energetico` string, nullable — Energy certificate - 'Si' or 'No' (required if subsidy)
    - `tipo_de_suelo_id` integer, nullable — Ground type ID (required if subsidy)
    - `licencia_de_obra_a_cargo_de` string, nullable — Construction license responsibility (required if urban ground + subsidy)
    - `asesoramiento_tarifas_autoconsumo` string — Self-consumption rates advice (always 'Si')
  - `budget_id` integer, required — ID del presupuesto existente a convertir en pedido
  - `comercial_id` integer, nullable — Override comercial ID. Admin use only: assigns order to the specified comercial instead of the current user
  - `guardar_borrador` boolean — Si True, guarda como borrador (sin HubSpot y sin cambiar estado a pedido)

## Response `200`

Successful Response

- SaveBudgetResponse — Response after saving budget.
  - `success` boolean, required — Whether the save was successful
  - `message` string, required — Success/error message
  - `budget_id` integer, nullable — ID of the created budget in database
  - `hubspot_contact_id` string, nullable — HubSpot contact ID (created or updated)
  - `hubspot_warning` string, nullable — Warning message if HubSpot operation partially failed (e.g., invalid property)

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/hogarsolar/apis/hogarsolar-backend-api.md) · [All operations](https://skmtc.dev/hogarsolar/apis/hogarsolar-backend-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/hogarsolar/hogarsolar-backend-api/revisions/d9861e46bef8/schema)
