# Configuration schema

The schema endpoint returns everything a storefront needs to render a configurator UI for one product: dimensions with their per-choice range constraints, the product's price matrices (for hinting which matrix applies), and the option groups with their choices, price deltas, cross-choice rules, images, and `requires_quote` flags. See the chapter introduction for how these concepts fit together.

## GET /v1/api/commerce/configurator/products/{id}/schema

Returns the complete configuration schema for one active configurable product. Everything in the response is pruned to currently visible choices: only active, non-deleted choices appear, and dimension constraints, matrix conditions, and `requires` and `excludes` lists reference only those visible choice IDs. Returns 404 for any product that is not both `status = active` and `type = configurable`.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | string (uuid) | Yes | Product ID. Must be an active product of type `configurable`. |

### Response fields

The `data` object has three keys.

**data.dimensions** (array, in position order; the order defines the mapping to price matrix axes `dim1` and `dim2`):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| key | string | always | Request key to use for this dimension in quote calls. |
| label | string | always | Display label. |
| unit | string | always | Display unit string, for example `"cm"`. |
| min | integer | always | Minimum allowed value before constraint tightening. |
| max | integer | always | Maximum allowed value before constraint tightening. |
| constraints | array | always | Per-choice range tightenings; empty array when none. Each item: `choice_id` (uuid), `min` (integer or null), `max` (integer or null). When that choice is selected, a non-null `min` raises the effective minimum and a non-null `max` lowers the effective maximum. |

**data.price_matrices** (array, in creation order):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | string (uuid) | always | Matrix ID. Echoed back by the quote endpoint as `matrix.id` when this matrix supplied the base price. |
| name | string | always | Matrix name. |
| conditions | array of string (uuid) | always | Choice IDs that must all be selected for the matrix to be eligible. Empty array means always eligible. Among eligible matrices, the one with the most conditions wins. |

**data.option_groups** (array, in position order):

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | string (uuid) | always | Group ID. |
| name | string | always | Group name. Also used as the prefix of quote breakdown labels (`"Group: Choice"`). |
| type | string | always | `single` (at most one choice selectable) or `multi` (any number). |
| required | boolean | always | Whether at least one choice from this group must be selected. |
| choices | array | always | Choices in position order, see below. |

**data.option_groups[].choices[]**:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | string (uuid) | always | Choice ID. Send this in the quote request's `choices` array. |
| name | string | always | Choice name. |
| delta_type | string | always | `fixed`, `per_area`, `percent`, or `percent_of_total`. |
| delta_value | integer | always | Minor units for `fixed`; minor units per square meter for `per_area`; percent times 100 for `percent` and `percent_of_total` (1050 = 10.50 percent). |
| is_default | boolean | always | UI hint for the initial selection. |
| requires | array of string (uuid) | always | Choice IDs this choice requires. OR within one target group, AND across target groups. |
| excludes | array of string (uuid) | always | Choice IDs this choice cannot be combined with. |
| images | array | always | `[{url, alt}]`; empty array when the choice has no media. |
| requires_quote | boolean | always | When `true`, the storefront should route the buyer to a manual quote request instead of checkout. Informational: the quote endpoint still prices the configuration. |

```bash title="Request"
curl "https://api.borkol.com/v1/api/commerce/configurator/products/9c2f1e6a-8b4d-4f2a-9e11-3d5a7b9c0e21/schema" \
  -H "Authorization: Bearer {api_key}" \
  -H "Accept: application/json"
```

```javascript title="Request (JS)"
const res = await fetch(
  `https://api.borkol.com/v1/api/commerce/configurator/products/${productId}/schema`,
  {
    headers: {
      Authorization: `Bearer ${apiKey}`,
      Accept: "application/json",
    },
  }
);
const { data } = await res.json();
```

```json title="Response"
{
  "data": {
    "dimensions": [
      {
        "key": "width",
        "label": "Width",
        "unit": "cm",
        "min": 200,
        "max": 700,
        "constraints": [
          {
            "choice_id": "c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
            "min": null,
            "max": 600
          }
        ]
      },
      {
        "key": "depth",
        "label": "Depth",
        "unit": "cm",
        "min": 150,
        "max": 400,
        "constraints": []
      }
    ],
    "price_matrices": [
      {
        "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a",
        "name": "Glass roof",
        "conditions": ["c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c"]
      },
      {
        "id": "e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f7a8b9",
        "name": "Base",
        "conditions": []
      }
    ],
    "option_groups": [
      {
        "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "name": "Roof type",
        "type": "single",
        "required": true,
        "choices": [
          {
            "id": "c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c",
            "name": "Glass",
            "delta_type": "fixed",
            "delta_value": 0,
            "is_default": true,
            "requires": [],
            "excludes": ["f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0"],
            "images": [
              {
                "url": "https://cdn.borkol.com/t/9c2f/glass-roof.jpg",
                "alt": "Glass roof"
              }
            ],
            "requires_quote": false
          }
        ]
      },
      {
        "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
        "name": "LED lighting",
        "type": "multi",
        "required": false,
        "choices": [
          {
            "id": "f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0",
            "name": "LED set (6)",
            "delta_type": "per_area",
            "delta_value": 2500,
            "is_default": false,
            "requires": ["c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c"],
            "excludes": [],
            "images": [],
            "requires_quote": false
          }
        ]
      }
    ]
  }
}
```

### Errors

| Status | Body | Cause |
| --- | --- | --- |
| 401 | `{"message":"Unauthenticated."}` | Missing or invalid API key. |
| 403 | `{"message":"Invalid ability provided."}` | Key lacks the `commerce:read` scope. |
| 403 | `{"error":"Module not enabled: commerce"}` | Tenant does not have the commerce module enabled. Note the `error` key instead of `message`. |
| 403 | `{"message":"Tenant suspended.","code":"tenant_suspended"}` | Tenant account is suspended. |
| 404 | `{"message":"..."}` | Product does not exist, is not active, or its type is not `configurable`. |
| 429 | `{"message":"Too Many Attempts."}` | Rate limit of 120 requests per minute exceeded. |
