> For the complete documentation index, see [llms.txt](https://docs.novelsystems.ca/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.novelsystems.ca/cpq-engine-api.md).

# CPQ Engine API

The CPQ engine turns a scope of work into a priced quote with the margin exposed. It is the same engine that runs behind the estimator's screen — there is no second pricing implementation and no "web pricing" that drifts from what the field sees.

Send it labour hours, a burden rate, a materials list and a margin target. It returns line-level cost, the assembled cost base, the applied margin, and the sell price — plus a flag telling you whether your margin target survived the tenant's floor.

## Calculate a quote

```http
POST /v1/cpq/calculate-quote
```

### Request

```http
POST /v1/cpq/calculate-quote HTTP/1.1
Host: api.novelsystems.ca
Authorization: Bearer <API_TOKEN>
Content-Type: application/json
Idempotency-Key: 4c1f9b2e-8a7d-4e51-9c3a-2b6f0d7e15a4
```

```json
{
  "project_type": "Commercial Installation",
  "labor_hours": 42.5,
  "labor_burden_rate": 65.00,
  "materials": [
    { "item_sku": "MOT-301", "qty": 12, "unit_cost": 210.00 }
  ],
  "margin_target": 0.28
}
```

### Request fields

| Field                   | Type   | Required | Notes                                                                                                                                                                 |
| ----------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_type`          | string | yes      | `Commercial Installation`, `Service Call`, `Retrofit`, or `Tenant Improvement`. Selects the overhead recovery band applied to the cost base.                          |
| `labor_hours`           | number | yes      | Crew hours for the scope. Quarter-hour precision.                                                                                                                     |
| `labor_burden_rate`     | number | yes      | Fully burdened **cost** per hour — wage plus statutory burden plus vehicle. Not a billing rate. The engine applies margin; sending a sell rate here double-counts it. |
| `materials`             | array  | yes      | At least one line. Each item needs `item_sku`, `qty`, `unit_cost`.                                                                                                    |
| `materials[].unit_cost` | number | yes      | Landed cost per unit. Omit it to price against the tenant's active price book instead.                                                                                |
| `margin_target`         | number | no       | Decimal fraction. Defaults to the tenant's configured floor.                                                                                                          |
| `pass_through`          | array  | no       | Permits and statutory fees. Carried at cost and excluded from the marked-up base, so `margin_pct` stays meaningful.                                                   |
| `tax_region`            | string | no       | Province code. Defaults to the tenant's region. `ON` produces 13% HST.                                                                                                |

### Response — `200 OK`

```json
{
  "quote_id": "qte_01J9ZC4M7K2XQ8VB",
  "project_type": "Commercial Installation",
  "currency": "CAD",
  "line_items": [
    {
      "group": "labour",
      "label": "Field crew — 42.5 h @ $65.00",
      "basis": "42.5 h × $65.00",
      "cost": 2762.50
    },
    {
      "group": "labour",
      "label": "Foreman supervision (12%)",
      "basis": "12% of crew hours",
      "cost": 331.50
    },
    {
      "group": "material",
      "label": "MOT-301 — 12 @ $210.00",
      "basis": "12 × $210.00",
      "cost": 2520.00
    }
  ],
  "cost_summary": {
    "material_cost": 2520.00,
    "labour_cost": 3094.00,
    "equipment_cost": 0.00,
    "pass_through_cost": 0.00,
    "direct_cost": 5614.00,
    "overhead_rate": 0.11,
    "overhead_cost": 617.54,
    "cost_base": 6231.54
  },
  "margin": {
    "requested": 0.28,
    "applied": 0.28,
    "floor": 0.28,
    "floor_engaged": false,
    "gross_margin_amount": 2423.49
  },
  "totals": {
    "subtotal": 8655.03,
    "tax_region": "ON",
    "tax_rate": 0.13,
    "tax": 1125.15,
    "grand_total": 9780.18
  },
  "valid_until": "2026-08-29T23:59:59Z"
}
```

### Reading the response

`cost_base` is direct cost plus overhead recovery. Margin is applied once, to that base — never to a line individually, which is why the line `cost` values sum to `direct_cost` and not to `subtotal`.

`margin.floor_engaged` is the field to branch on. When it is `true`, your requested margin fell below the tenant's floor and the engine held the quote at the floor instead of silently honouring the request. `margin.requested` still reports what you asked for, so an approval workflow can carry the exception with the original number attached.

Every line is rounded to two decimal places before it is summed, so the totals a client sees reconcile to the lines exactly.

### Errors

| Status | `type`                 | Cause                                                                                                         |
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid-request`      | Missing required field, or `margin_target` outside `0`–`0.95`.                                                |
| `401`  | `unauthorized`         | Missing, malformed, or revoked key.                                                                           |
| `404`  | `sku-not-found`        | An `item_sku` is absent from the tenant's catalogue and no `unit_cost` was supplied.                          |
| `409`  | `idempotency-conflict` | The `Idempotency-Key` was reused with a different body.                                                       |
| `422`  | `margin-below-floor`   | Returned only for tenants with hard floor enforcement enabled, where the engine refuses rather than clamping. |

```json
{
  "type": "https://novelsystems.ca/problems/sku-not-found",
  "title": "SKU not found",
  "status": 404,
  "detail": "MOT-301 is not in the active price book and no unit_cost was supplied.",
  "instance": "/v1/cpq/calculate-quote",
  "sku": "MOT-301"
}
```

## Persist a calculated quote

`calculate-quote` is a pure calculation and writes nothing. To keep the result, post the returned `quote_id` to `POST /v1/cpq/quotes` — that creates the record, assigns a proposal reference, and fires the `quote.created` webhook.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.novelsystems.ca/cpq-engine-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
