# 10 - Agent API Guide: Pricing Servers from a TZ This guide is written for an AI agent that needs to price a server configuration (техническое задание, ТЗ) using the QuoteForge HTTP API. ## Runtime assumptions - QuoteForge runs locally, binds to `127.0.0.1:8080` by default. - No authentication is required — the app is single-user, loopback-only. - All responses are JSON. All request bodies are JSON unless stated otherwise. - The port can be overridden with the `QF_SERVER_PORT` environment variable. Base URL for all examples: `http://127.0.0.1:8080` --- ## Configuration composition rules These rules are mandatory and must be respected before saving any configuration. ### 1. Every configuration must belong to a project Configurations cannot be created in isolation. The correct sequence is: 1. Create a project (`POST /api/projects`) and save the returned `uuid`. 2. Create the configuration inside that project by passing `project_uuid` in the config body, or by using `POST /api/projects/:uuid/configs`. If the project for a given TZ already exists, retrieve its `uuid` first: ``` GET /api/projects?page=1&per_page=100 ``` then pass the matching `uuid` in `project_uuid`. ### 2. Every server configuration must contain all four required component groups A configuration is not valid for pricing unless items from all four of the following category groups are present: | Category code | Meaning | Notes | |---------------|------------------|---------------------------------------------------| | `MB` | Motherboard | exactly one MB per configuration | | `CPU` | Processor | one or more CPUs | | `MEM` | Memory / RAM | one or more memory modules | | `PS` / `PSU` | Power supply | `PSU` is the current code; `PS` is legacy — both are accepted | Before saving, verify the assembled BOM with `POST /api/quote/validate`: the response `errors` array will contain `"Component not found: …"` entries for unknown lot names, and `warnings` will list lots without a price. Reject the configuration and report back to the user if any of the four required categories is missing. ### 3. Category codes to use when searching Use `category=` in `GET /api/components` to narrow results: ``` GET /api/components?category=MB&search=X13&has_price=true GET /api/components?category=CPU&search=Xeon+Gold&has_price=true GET /api/components?category=MEM&search=32GB+DDR5&has_price=true GET /api/components?category=PSU&search=800W&has_price=true ``` Retrieve the full list of active categories at any time: ``` GET /api/categories ``` --- ## Typical workflow for pricing a server ``` 1. Check the app is up GET /api/ping 2. Find or create a project GET /api/projects → POST /api/projects 3. Find the latest pricelist GET /api/pricelists/latest?source=estimate 4. Look up lot names for MB GET /api/components?category=MB&search=… 5. Look up lot names for CPU GET /api/components?category=CPU&search=… 6. Look up lot names for MEM GET /api/components?category=MEM&search=… 7. Look up lot names for PSU GET /api/components?category=PSU&search=… 8. (Repeat for other components) GET /api/components?category=…&search=… 9. Validate and calculate the quote POST /api/quote/validate 10. (Optional) Compare price tiers POST /api/quote/price-levels 11. Save configuration in the project POST /api/projects/:uuid/configs ``` --- ## Step 1 — Verify the app is running ``` GET /api/ping ``` Response `200 OK`: ```json {"status": "ok"} ``` --- ## Step 2 — Find or create a project Each TZ maps to one project. Use the TZ identifier as the `code` field. ### Find an existing project ``` GET /api/projects?page=1&per_page=100 ``` Response `200 OK`: ```json { "projects": [ { "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "code": "TZ-123", "variant": "", "name": "Проект по ТЗ №123", "tracker_url": "", "is_active": true, "created_at": "2026-06-01T00:00:00Z" } ], "total": 1, "page": 1, "per_page": 100 } ``` ### Create a new project ``` POST /api/projects Content-Type: application/json ``` Request body: ```json { "code": "TZ-123", "name": "Проект по ТЗ №123", "tracker_url": "" } ``` Fields: | field | type | required | description | |---------------|--------|----------|--------------------------------------------------------------------| | `code` | string | yes | short identifier, unique per variant; use the TZ number or ticket | | `variant` | string | no | variant label within the same `code`; default is empty string | | `name` | string | no | human-readable title | | `tracker_url` | string | no | link to a ticket or issue tracker | Response `201 Created`: ```json { "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "code": "TZ-123", "variant": "", "name": "Проект по ТЗ №123", "is_active": true, "created_at": "2026-06-11T10:00:00Z" } ``` Save the `uuid` — it is required to create configurations inside this project. --- ## Step 3 — Find the latest pricelist QuoteForge maintains three pricing tiers. The `source` values are: | source | meaning | |--------------|-----------------------------| | `estimate` | list / catalogue price | | `warehouse` | stock price (purchase cost) | | `competitor` | competitor reference price | ``` GET /api/pricelists/latest?source=estimate ``` Response `200 OK`: ```json { "id": 42, "source": "estimate", "version": "2026-05-28", "item_count": 12500, "is_active": true, "created_at": "2026-05-28T06:00:00Z" } ``` The `id` field is a numeric pricelist identifier. Pass it as `pricelist_id` when calculating a quote to pin pricing to a specific pricelist. To list all available pricelists: ``` GET /api/pricelists?source=estimate&active_only=true ``` --- ## Steps 4–8 — Look up component lot names Each component is identified by a `lot_name` (internal SKU). The TZ typically contains model names or descriptions; use the search endpoint to resolve them. ``` GET /api/components?search=Xeon+Gold+6342&category=CPU&has_price=true&page=1&per_page=20 ``` Query parameters: | parameter | default | description | |------------------|---------|---------------------------------------------------| | `search` | — | free-text search in lot name and description | | `category` | — | filter by category code (`MB`, `CPU`, `MEM`, `PSU`, …) | | `has_price` | false | return only components that have a price | | `include_hidden` | false | include hidden/retired components | | `page` | 1 | page number | | `per_page` | 20 | page size | Response `200 OK`: ```json { "components": [ { "lot_name": "CPU-XEON-6342", "description": "Intel Xeon Gold 6342, 24C/48T, 2.8 GHz, LGA4189", "category": "CPU", "category_name": "CPU", "model": "Xeon Gold 6342" } ], "total": 1, "page": 1, "per_page": 20 } ``` To look up a single component by exact lot name: ``` GET /api/components/CPU-XEON-6342 ``` To list all known categories: ``` GET /api/categories ``` --- ## Step 9 — Validate and calculate the quote Before saving, validate the assembled BOM. This catches unknown lot names and missing prices, and also confirms that all required categories are covered. ``` POST /api/quote/validate Content-Type: application/json ``` Request body: ```json { "items": [ {"lot_name": "MB-X13DAI-N", "quantity": 1}, {"lot_name": "CPU-XEON-6342", "quantity": 2}, {"lot_name": "RAM-32GB-DDR4-3200", "quantity": 8}, {"lot_name": "SSD-480GB-SATA", "quantity": 2}, {"lot_name": "PSU-800W-TITANIUM", "quantity": 2} ], "pricelist_id": 42 } ``` Response `200 OK`: ```json { "valid": true, "items": [ { "lot_name": "MB-X13DAI-N", "quantity": 1, "unit_price": 95000.00, "total_price": 95000.00, "description": "Supermicro X13DAi-N dual-socket server board", "category": "MB", "has_price": true }, { "lot_name": "CPU-XEON-6342", "quantity": 2, "unit_price": 87500.00, "total_price": 175000.00, "description": "Intel Xeon Gold 6342, 24C/48T, 2.8 GHz", "category": "CPU", "has_price": true }, { "lot_name": "RAM-32GB-DDR4-3200", "quantity": 8, "unit_price": 12000.00, "total_price": 96000.00, "description": "32 GB DDR4-3200 ECC RDIMM", "category": "MEM", "has_price": true }, { "lot_name": "PSU-800W-TITANIUM", "quantity": 2, "unit_price": 18500.00, "total_price": 37000.00, "description": "800W 80+ Titanium redundant PSU", "category": "PSU", "has_price": true } ], "errors": [], "warnings": [], "total": 403000.00 } ``` **Agent check after validation:** 1. `valid` must be `true` — all lot names resolved. 2. `errors` must be empty — no unknown components. 3. The returned `items` array must contain at least one entry from each required category: `MB`, `CPU`, `MEM`, and `PS` or `PSU`. 4. Items with `has_price: false` are allowed but should be flagged to the user. If any check fails, do not save the configuration. Report the issue and ask the user to clarify or replace the problematic component. For simple price totals without validation metadata use `POST /api/quote/calculate` — identical request body, response contains only `items` and `total`. --- ## Step 10 (optional) — Compare price tiers To see estimate, warehouse, and competitor prices side-by-side for a BOM: ``` POST /api/quote/price-levels Content-Type: application/json ``` Request body: ```json { "items": [ {"lot_name": "CPU-XEON-6342", "quantity": 2}, {"lot_name": "RAM-32GB-DDR4-3200", "quantity": 8} ], "pricelist_ids": { "estimate": 42, "warehouse": 31, "competitor": 15 }, "no_cache": false } ``` `pricelist_ids` is optional. When omitted the latest pricelist for each source is used automatically. Response `200 OK`: ```json { "items": [ { "lot_name": "CPU-XEON-6342", "quantity": 2, "estimate_price": 87500.00, "warehouse_price": 71000.00, "competitor_price": 85000.00, "delta_wh_estimate_abs": -16500.00, "delta_wh_estimate_pct": -18.86, "delta_comp_estimate_abs": -2500.00, "delta_comp_estimate_pct": -2.86, "delta_comp_wh_abs": 14000.00, "delta_comp_wh_pct": 19.72, "price_missing": [] } ], "resolved_pricelist_ids": { "estimate": 42, "warehouse": 31, "competitor": 15 } } ``` `price_missing` lists the source names for which no price was found for that lot. Delta fields are `null` when either operand price is missing. --- ## Step 11 — Save a configuration inside the project Use the project-scoped endpoint so the configuration is immediately linked to the correct project without a separate move operation. ``` POST /api/projects/:project_uuid/configs Content-Type: application/json ``` The request body is identical to `POST /api/configs` — the `project_uuid` field in the body is ignored when using the project-scoped route; the URL parameter takes precedence. Request body: ```json { "name": "Сервер по ТЗ №123 — вариант А", "items": [ {"lot_name": "MB-X13DAI-N", "quantity": 1, "unit_price": 95000.00}, {"lot_name": "CPU-XEON-6342", "quantity": 2, "unit_price": 87500.00}, {"lot_name": "RAM-32GB-DDR4-3200", "quantity": 8, "unit_price": 12000.00}, {"lot_name": "SSD-480GB-SATA", "quantity": 2, "unit_price": 8500.00}, {"lot_name": "PSU-800W-TITANIUM", "quantity": 2, "unit_price": 18500.00} ], "server_model": "2U", "support_code": "NBD", "server_count": 1, "pricelist_id": 42, "warehouse_pricelist_id": 31, "competitor_pricelist_id": 15, "config_type": "server", "notes": "Автоматически создано агентом на основании ТЗ №123" } ``` Key fields: | field | type | required | description | |--------------------------|--------|----------|-----------------------------------------------------| | `name` | string | yes | human-readable name | | `items` | array | yes | `{lot_name, quantity, unit_price}` from validate | | `server_model` | string | no | chassis/form-factor code; used for article generation | | `support_code` | string | no | support tier code; used for article generation | | `server_count` | int | no | number of identical servers; total is multiplied | | `pricelist_id` | uint | no | estimate pricelist to attach | | `warehouse_pricelist_id` | uint | no | warehouse pricelist to attach | | `competitor_pricelist_id`| uint | no | competitor pricelist to attach | | `config_type` | string | no | `"server"` (default) or `"storage"` | | `notes` | string | no | free text | | `custom_price` | float | no | override total price | | `disable_price_refresh` | bool | no | prevent automatic price refresh on open | | `only_in_stock` | bool | no | filter to in-stock components only | Response `201 Created`: ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "Сервер по ТЗ №123 — вариант А", "items": [...], "total_price": 403000.00, "server_count": 1, "config_type": "server", "article": "2U-6342x2-32GBx8-NBD", "project_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "created_at": "2026-06-11T10:00:00Z" } ``` The `uuid` can be used for all subsequent operations on this configuration. --- ## Working with saved configurations ``` GET /api/configs/:uuid — retrieve a saved configuration PUT /api/configs/:uuid — full update (same body as create) POST /api/configs/:uuid/refresh-prices — re-price from latest pricelist POST /api/configs/:uuid/clone — duplicate: body {"name": "clone name"} GET /api/configs/:uuid/versions — revision history GET /api/configs?page=1&per_page=20 — list all configurations ``` --- ## Error responses All error responses follow the same shape: ```json {"error": "human-readable message"} ``` Common status codes: | code | meaning | |------|-------------------------------------------------------| | 400 | invalid request body or validation failure | | 404 | entity (component, pricelist, config) not found | | 423 | sync readiness is blocked; retry after sync completes | | 500 | internal server error | --- ## Minimal end-to-end example ```bash BASE=http://127.0.0.1:8080 # 1. Verify the app is up curl -s $BASE/api/ping # 2. Create a project for this TZ PROJECT_UUID=$(curl -s -X POST $BASE/api/projects \ -H "Content-Type: application/json" \ -d '{"code": "TZ-123", "name": "Проект по ТЗ №123"}' | jq -r .uuid) # 3. Get latest estimate pricelist PRICELIST_ID=$(curl -s "$BASE/api/pricelists/latest?source=estimate" | jq .id) # 4. Find lot names for required categories curl -s "$BASE/api/components?category=MB&search=X13&has_price=true" | jq '.components[].lot_name' curl -s "$BASE/api/components?category=CPU&search=Xeon&has_price=true" | jq '.components[].lot_name' curl -s "$BASE/api/components?category=MEM&search=32GB&has_price=true" | jq '.components[].lot_name' curl -s "$BASE/api/components?category=PSU&search=800W&has_price=true" | jq '.components[].lot_name' # 5. Validate the BOM (must contain MB, CPU, MEM, PSU/PS) curl -s -X POST $BASE/api/quote/validate \ -H "Content-Type: application/json" \ -d "{ \"pricelist_id\": $PRICELIST_ID, \"items\": [ {\"lot_name\": \"MB-X13DAI-N\", \"quantity\": 1}, {\"lot_name\": \"CPU-XEON-6342\", \"quantity\": 2}, {\"lot_name\": \"RAM-32GB-DDR4-3200\", \"quantity\": 8}, {\"lot_name\": \"PSU-800W-TITANIUM\", \"quantity\": 2} ] }" | jq '{valid, errors, warnings, total}' # 6. Save the configuration inside the project curl -s -X POST "$BASE/api/projects/$PROJECT_UUID/configs" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"Сервер по ТЗ №123\", \"pricelist_id\": $PRICELIST_ID, \"server_model\": \"2U\", \"server_count\": 1, \"config_type\": \"server\", \"items\": [ {\"lot_name\": \"MB-X13DAI-N\", \"quantity\": 1, \"unit_price\": 95000}, {\"lot_name\": \"CPU-XEON-6342\", \"quantity\": 2, \"unit_price\": 87500}, {\"lot_name\": \"RAM-32GB-DDR4-3200\", \"quantity\": 8, \"unit_price\": 12000}, {\"lot_name\": \"PSU-800W-TITANIUM\", \"quantity\": 2, \"unit_price\": 18500} ] }" | jq '{uuid, total_price, article}' ```