Documentation/Integrations
Integrations and data exchange
Import and export of the price list as CSV or JSON, API keys, and the reference of the integration API.
What can be imported and exported
A shop's price list is stored as one pricing configuration. It can be exported and imported from the panel or through the API, so data already held in an ERP, a CRM or a spreadsheet does not have to be typed in again or kept in step by hand. The configuration contains:
- Materials: name, EN and DIN designation, density, price per kilogram, roughing and turning removal rates, finishing rate, stock allowance, cutting speed.
- Machines: name, technology, hourly rate, work envelope, axes, spindle power and maximum speed, tool stations, minimum batch size, maker, model, control, year, serial number and inventory number.
- Finishing operations: name, kind, cost per kilogram, cost per square decimetre, fixed batch fee, working days added to delivery, visibility to the customer.
- Pricing rules: margin, setup and handling minutes, minimum cycle, lead times, express multiplier, delivery, VAT rate, and the multiplier tables for tolerance class, geometric class and surface finish.
Two formats are available:
- CSV: one file per catalogue. Most ERP exports produce this format, and the files can be edited in a spreadsheet.
- JSON: the whole configuration in one document. Use it when another system keeps the MetronQ configuration up to date without manual edits.
Where to find it
- 1In the panel, open Quoting rules.
- 2In the section list on the left, select Import and export.
- 3Under Catalogues (CSV), use “Export CSV” or “Import CSV” in the row of a catalogue.
- 4Under Whole configuration (JSON), select the mode in “How to apply it”, then use “Export JSON” or “Import JSON”.
Export before the first import, even if you only intend to import. The exported file contains the column names and the shop's catalogue keys, so the ERP mapping can be written to fill in that file.
CSV format
One file per catalogue: materials, machines, finishing. The first row contains the column names. Column names are English identifiers in every interface language, and the import reads the same names.
- Rows are matched on the key column only. An existing key is updated; an unknown key is added.
- Only the columns present in the file are read. A missing column leaves that field unchanged in every row.
- An empty cell leaves the value unchanged; it does not clear it. Partial files, for example only the materials in stock or only this quarter's rates, are supported.
- An import never deletes entries. Delete a material or a machine in the panel.
- The decimal mark depends on the column separator: semicolon with a decimal comma (1234,56), comma with a decimal point (1234.56). The import reads both. The export uses the Separator setting of the account (Settings, Quote document, CSV export).
- Accepted encodings: UTF-8 with or without a byte-order mark, and Windows-1250, the Central European code page written by Excel on Windows.
- Limits: at most 20 000 rows and 2 MB per file.
The machines file does not contain the tool library, because a list does not fit in one row. A machine imported from CSV keeps its existing tool library, so a CSV import cannot change which parts the machine is allowed to cut.
JSON format
The JSON export is the complete pricing configuration in the form the panel stores it, and the import accepts it back unchanged. It contains the three catalogues, all pricing parameters, the safety-gate settings, the machine and blank selection strategies, and the currency. Use this format for a system that keeps its own copy of the price list.
- Merge (default): applies only the values the document contains. A file with three materials and one margin changes those three materials and that margin; everything else keeps its current value.
- Replace: applies the whole document, including deletions. Use it when the other system is the system of record for the price list.
- Catalogue entries are matched on the same key column as in CSV, so both formats identify a material the same way.
Import preview
Every import starts with a preview, which writes nothing. The preview (“Before you apply it”) shows:
- the number of entries that would be added and updated;
- the field-by-field difference between the current price list and the result;
- every row that could not be read;
- every column that was not recognised.
“Apply” writes the changes. The following rules apply:
- If any row is invalid, for example a missing density or a price that is not a number, the whole file is rejected and nothing is written. A price list is never partly imported, because a partial import produces wrong prices without any visible error.
- Unrecognised columns are listed by name. This detects shifted columns and misspelt headers, which would otherwise import as no change and look like a successful import.
- If the price list is saved while the preview is open, the import is refused and has to be run again, because the preview no longer describes the result.
Every applied import is recorded in the settings history, field by field, as an import. A changed price can be traced back to the file that changed it.
API keys
The same data can be exchanged without the panel: the shop creates an API key and configures it in its ERP, CRM or automation tool. API access is enabled for each shop individually. To request it, write to us and name the system you use, because an integration depends on the system at the other end. A key belongs to the shop, not to a user, so it keeps working when the user who created it is absent.
- 1In the panel, open Settings, then Integrations. This tab is visible to the account owner only.
- 2Click “New key”. In “What is it for”, enter the name of the system that will use the key. Under “What it may do”, select the permissions.
- 3Click “Create key” and copy the key immediately. It is shown once and MetronQ does not store it. If the key is lost, revoke it and create a new one.
A key lets your ERP, CRM or automation tool work with this account without anybody signing in: read and update the catalogue and pricing rules, read quotes and orders, send part files for quoting and download files - each only with the permissions you give it.
Allowance: 120 requests a minute and 20,000 a day, counted from 00:00 UTC. Used today: 212.
| Name | Permissions | Last used | Expires | Daily limit | |
|---|---|---|---|---|---|
| ERP synca1b2c3d4... | config:read, quotes:read, quotes:write | today 09:14 | 16/09/2027 | account allowance | Revoke |
| Price list importe5f6a7b8... | config:read, config:write | yesterday 06:00 | 02/10/2027 | account allowance | Revoke |
When something gained or lost the ability to read this account. Kept for two years.
- Catalogue importedPrice list import · e5f6a7b818/09/2026 06:00
- Key createdERP sync · a1b2c3d416/09/2026 10:12
- Key createdPrice list import · e5f6a7b802/10/2026 08:30
Quick start
The six calls below are in the order an integration usually implements them. Every call sends the key in the same header: Authorization: Bearer <key>. Base URL: https://app.metronq.com with the path prefix /api/v1/integration. Responses are JSON, except for CSV (paths ending in .csv), PDF (/pdf), the uploaded file (/file) and 3D previews (.glb).
1. Check the key
GET /whoami requires no permission. It returns the shop the key belongs to, the key's permissions and its request limits. If this call answers 200, the key is valid.
curl https://app.metronq.com/api/v1/integration/whoami \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"tenant": { "name": "Precision Works Ltd", "slug": "precision-works", "currency": "EUR" },
"key": {
"name": "ERP sync",
"prefix": "a1b2c3d4",
"scopes": ["config:read", "quotes:read"],
"expires_at": "2027-09-16T10:12:00+00:00"
},
"rate_limit": {
"requests_per_minute": 120,
"requests_per_day": 20000,
"day_resets": "00:00 UTC",
"requests_total": 1000,
"requests_total_used": 3,
"requests_total_remaining": 997
}
}2. Read a catalogue
Permission: config:read. GET /catalog/{name} returns one catalogue: materials, machines or treatments. GET /config returns the whole pricing configuration in one document.
curl https://app.metronq.com/api/v1/integration/catalog/materials \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"catalog": "materials",
"count": 12,
"items": [
{
"key": "alu_6061",
"name": "Aluminium 6061",
"kind": "metal",
"en_number": "EN AW-6061",
"din_number": "3.3211",
"density_g_cm3": 2.7,
"price_per_kg": 6.4,
"mrr_rough_cm3_min": 50.0,
"mrr_turning_cm3_min": 60.0,
"finishing_rate_cm2_min": 90.0,
"stock_allowance_mm": 3.0,
"cutting_speed_m_min": 250.0
}
]
}3. Poll for new enquiries
Permission: quotes:read. Request the quotes created at or after a given time with since=. The API keeps no cursor; the client stores the newest created_at it has read and sends it in the next request. limit is 1 to 200 (default 100); offset skips that many results.
curl "https://app.metronq.com/api/v1/integration/quotes?since=2026-09-16T00:00:00Z&limit=50" \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"items": [
{
"id": "6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33",
"number": 1042,
"ref": "WID-1042",
"status": "priced",
"created_at": "2026-09-16T08:41:12+00:00",
"filename": "bracket.step",
"material_key": "alu_6061",
"quantity": 25,
"unit_price": 41.5,
"total_price": 1037.5,
"discount_pct": null,
"customer_unit_price": 41.5,
"currency": "EUR",
"customer_email": "purchasing@example.com",
"customer_data_visible": true,
"external_ref": "",
"source": "widget"
}
],
"limit": 50,
"offset": 0
}4. Read the analysis results
Permission: quotes:read. GET /quotes/{id}/metrics returns what the analysis measured in the file of one quote. Use it when the system needs the part geometry, not only the price.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/metrics \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"quote_id": "6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33",
"analyzed_at": "2026-09-16T08:41:29+00:00",
"file_extension": ".step",
"status": "priced",
"gate_status": "instant",
"gate_reasons": [],
"complexity": 34.2,
"material_key": "alu_6061",
"quantity": 25,
"tolerance_class": "m",
"surface_finish": "ra_3_2",
"confirmed_threads": ["M8"],
"geometry_metrics": {
"bounding_box": { "x": 120.0, "y": 80.0, "z": 18.0 },
"volume": 74210.5,
"surface_area": 31890.2,
"face_count": 46,
"holes": [
{ "diameter": 8.2, "depth": 18.0, "through": true, "direction": [0, 0, 1] }
],
"pockets": [
{ "depth": 6.0, "floor_area": 1840.0, "corner_radius": 5.0, "open": false }
],
"min_wall_thickness": 3.1,
"derived": { "volume_ratio": 0.43, "area_ratio": 1.71 }
},
"parts": []
}5. Write back a reference and an order status
Permission: quotes:write. PATCH /orders/{id} sets external_ref, status and payment_status. Only the fields present in the request body are changed.
curl -X PATCH https://app.metronq.com/api/v1/integration/orders/ORDER_ID \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"external_ref": "WO-2026-0912", "status": "in_production"}'{
"id": "1b7a44c0-9d2e-4e51-8a10-64d1f0a2e777",
"quote_id": "6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33",
"status": "in_production",
"payment_status": "unpaid",
"external_ref": "WO-2026-0912",
"quantity": 25,
"unit_price": 41.5,
"total_price": 1037.5,
"currency": "EUR",
"customer": { "name": "Alex Morgan", "email": "purchasing@example.com", "city": "Riverton" },
"...": "..."
}6. Update the price list from your ERP
Permission: config:write. An import takes two calls: a preview (POST /config/import/preview or POST /config/import/preview-json) and the apply (POST /config/import). The preview writes nothing and returns a base_hash. The apply call must send that base_hash back.
Permissions (scopes)
- config:read - read the catalogue and the pricing rules.
- config:write - change the catalogue and the pricing rules, with the same preview-then-apply sequence as the panel.
- quotes:read - read quotes and orders: the part, the material, the quantity, the price and the analysis readout. Customer contact details additionally require customers:read. A key with this permission can read every quote and order the shop has, so grant it only to a system that needs this data.
- customers:read - read the customer's contact details on quotes and orders. It works only together with quotes:read. Without it, quotes and orders are still returned, with customer_email, the customer block and the customer's note set to null. Leave it off for a system that needs only the part, the quantity and the price; a key without it cannot expose the customer list.
- quotes:write - write the shop's own reference back, and change an order's status and payment status. It cannot price or approve anything.
- quotes:create - send part files for quoting, and change the parameters of any quote of the shop until a person has decided on it. The engine calculates the price; the key cannot set a price, approve, reject or give a discount. Every file is a quote counted against the shop's plan, so grant this permission only to a system that should create quotes. It does not include quotes:read: a system that only sends files does not need to read all quotes.
- files:read - download the files customers uploaded and the 3D previews, and read a quote's access code. It works only together with quotes:read. It is a separate permission because a readout contains only numbers, from which the part cannot be rebuilt, while a file is the customer's design.
No key can create a key, revoke a key or change the permissions of a key. Only the signed-in owner can, so a stolen key cannot extend its own validity. A key expires 365 days after it is created. A revoked key is refused from its next request.
What the API can read
A key reads the catalogue, quotes and orders of its own shop and no data of any other shop. Endpoints by data type:
- Materials: GET /catalog/materials. The whole material catalogue: EN number and Werkstoffnummer, density, price per kilogram, removal rates and cutting speed.
- Machines: GET /catalog/machines. Technology, hourly rate, work envelope, number of axes, spindle power and maximum speed, tool stations, maximum workpiece weight, and the datasheet block (maker, model, control, year, inventory number).
- Tools: GET /tools. All tool libraries as one flat list. Each row names its machine and the corner radius the engine derives from the tool.
- Cutting data: GET /cutting-data. Per material, the cutting speed and the removal rates used for pricing; per machine, the spindle data the removal rates are scaled by.
- Treatments: GET /catalog/treatments. Surface and heat treatments with their costs and lead times.
- Quotes and orders: GET /quotes and GET /orders, newest first, filterable with since=. A single quote contains the price breakdown; an order contains the customer's invoice and delivery details.
- Analysis readout: GET /quotes/{id}/metrics. What the analysis measured: overall size, weight, holes, pockets, threads, faces and tolerances, per part for an assembly.
- Every tool belongs to one machine, so every row of GET /tools names its machine. The catalogue CSV does not contain tools, because a row has no room for a list.
- GET /cutting-data is not a feeds-and-speeds table. MetronQ stores no feed per tooth and no depth of cut, because the time model uses removal rates per material. The endpoint returns all figures behind a price: the shop's own values and the values the engine derives from them. A connected system can therefore hold the same material data as the engine.
Field reference
Units are given in the field names: _mm, _kg, _cm3_min, _m_min. Values are plain JSON numbers or strings; amounts are not formatted. All prices are in the shop's currency, which GET /whoami returns.
Material (GET /catalog/materials)
- key - the material identifier. Quotes refer to it as material_key. Changing a key creates a new material; it does not rename the existing one.
- name - the name the shop's customer sees in the widget and on the quote. It is the shop's own text and is not translated.
- kind - metal or plastic. It determines which tolerances, fits and finishes are offered.
- en_number, din_number - standard designations (EN AW-6061, 1.0503). For information only; the engine does not calculate with them.
- density_g_cm3 - density in g/cm3. The part mass, and therefore the material cost, is calculated from it.
- price_per_kg - purchase price per kilogram in the shop's currency. This is the field an ERP writes most often.
- mrr_rough_cm3_min - roughing removal rate in cm3/min on the 15 kW reference spindle. It determines how much slower one material machines than another.
- mrr_turning_cm3_min - removal rate for turning, in cm3/min. null: the roughing rate is used.
- finishing_rate_cm2_min - surface area a finishing pass covers per minute, in cm2/min.
- stock_allowance_mm - allowance added on each side of the part when the blank is selected, in mm.
- cutting_speed_m_min - cutting speed vc in m/min. null: derived from the roughing rate. GET /cutting-data returns the value used.
Machine (GET /catalog/machines)
Fields that affect a price or a refusal:
- key, name - identifier and name of the machine.
- technology - milling, turning, bar_turning or mill_turn. It determines which parts the machine can take and which removal rate applies.
- hourly_rate - machine rate per hour in the shop's currency. Picking a model from the MetronQ machine catalogue never writes this field; the rate is set by the shop.
- envelope_mm - work envelope in mm. A mill has three values (x, y, z). On a lathe the first value is the turning diameter and the second the turning length.
- axes - number of axes: 3 to 5 on a mill. With more than 3 axes a part needs fewer setups.
- spindle_power_kw - spindle power in kW. Below the 15 kW reference, the removal rates are reduced in proportion. Above it, they are not increased, so a stated power can only make a quote more expensive.
- max_spindle_rpm - maximum spindle speed. It matters only for small tools, where the speed needed for the cutting speed cannot be reached.
- min_tool_radius_mm - the smallest inside corner radius the machine can produce, in mm. It is no longer used once the machine's tool library contains a tool that cuts a corner (an endmill on a mill, a turning insert on a lathe); the smallest such tool then applies. A library of drills and taps only does not change it.
- tool_stations, max_workpiece_kg, through_spindle_coolant - magazine size, maximum table load, coolant through the spindle.
- datasheet - maker, model, control, year, serial, inventory_no, notes. For identification only; the engine does not read this block.
Tool (GET /tools)
{
"count": 2,
"items": [
{
"key": "e12",
"kind": "endmill",
"name": "12 mm carbide, 4 flutes",
"diameter_mm": 12.0,
"nose_radius_mm": null,
"flute_length_mm": 45.0,
"designation": "",
"machine_key": "dmu50",
"machine_name": "DMU 50",
"cut_radius_mm": 6.0
}
]
}- machine_key, machine_name - the machine the tool belongs to. Every tool belongs to one machine.
- kind - drill, endmill, tap, thread_mill, reamer or turning_insert.
- diameter_mm - cutting diameter in mm: for a drill or a reamer the hole diameter, for an endmill the cutter diameter, for a tap the nominal thread diameter.
- nose_radius_mm - nose radius of a turning insert: the smallest fillet it can produce on a turned profile.
- flute_length_mm - usable cutting length in mm. null: no limit is stated, and the engine applies none.
- cut_radius_mm - the smallest inside radius the tool can produce, calculated by MetronQ: half the diameter, or the nose radius of an insert. The smallest cut_radius_mm among the corner-cutting tools (endmills on a mill, inserts on a lathe) becomes the corner limit of the machine. Drills and taps are not counted.
Cutting data (GET /cutting-data)
Besides the shop's own values, the response contains the values the engine derives:
{
"reference": { "spindle_power_kw": 15.0, "cutting_speed_m_min": 250.0 },
"materials": [
{
"key": "steel_c45",
"name": "Steel C45",
"kind": "metal",
"en_number": "1.0503",
"din_number": "",
"density_g_cm3": 7.85,
"cutting_speed_m_min": null,
"cutting_speed_effective_m_min": 156.2,
"machinability_factor": 1.6,
"mrr_rough_cm3_min": 19.5,
"mrr_turning_cm3_min": null,
"mrr_turning_effective_cm3_min": 19.5,
"finishing_rate_cm2_min": 40.0,
"stock_allowance_mm": 3.0
}
],
"machines": [
{
"key": "dmu50",
"name": "DMU 50",
"technology": "milling",
"spindle_power_kw": 13.0,
"max_spindle_rpm": 10000.0,
"spindle_power_factor": 0.867,
"min_tool_radius_mm": 1.0,
"tool_radius_effective_mm": 6.0,
"tool_count": 2
}
]
}- reference - the reference spindle power and cutting speed that all figures of the price list refer to: 15 kW and 250 m/min. They are returned so the derived values can be checked.
- cutting_speed_effective_m_min - the cutting speed used: the shop's own value if one is set, otherwise reference.cutting_speed_m_min / machinability_factor.
- machinability_factor - how much harder the material is to cut than aluminium: 1.0 for aluminium, about 1.6 for an ordinary steel, about 3 for a stainless steel. It is the square root of the ratio of the aluminium baseline removal rate (50 cm3/min) to the material's mrr_rough_cm3_min, limited to the range 1.0 to 4.0.
- mrr_turning_effective_cm3_min - the turning removal rate with the fallback applied: mrr_turning_cm3_min, or mrr_rough_cm3_min when that is null.
- spindle_power_factor - the factor applied to the removal rates on this machine: spindle_power_kw / 15, at most 1.0 and at least 0.25; 1.0 when spindle_power_kw is not set.
- tool_radius_effective_mm - the corner radius in force: the typed min_tool_radius_mm or, once the tool library contains a corner-cutting tool (endmills on a mill, inserts on a lathe), the smallest cut_radius_mm among those tools.
Enquiry and order (GET /quotes, GET /orders)
- id - the identifier all other calls take. number is the quote number. ref is the reference shown in the panel: WID-1042 for the widget or the hosted page, WYC-1042 for the panel and for quotes created before prefixes were introduced, API-1042 for this API.
- status - created, uploaded, analyzing, analyzed, priced, approved, rejected, ordered or analysis_failed. priced, approved and ordered carry a price.
- unit_price, total_price, currency - price per piece and for the batch, in the shop's currency. null for a quote that was never priced.
- discount_pct, customer_unit_price - the discount the shop gave this customer, in percent, and the resulting unit price. unit_price remains the approved price. An order already contains the discounted price. Without a discount, discount_pct is null.
- min_order_top_up, delivery_cost - orders only: the top-up to the shop's minimum order value, and shipping. Both are included in the order's total_price, as the customer saw them in the widget and on the quote PDF. Orders placed before these fields existed have 0 in both.
- material_key, quantity - the customer's choice. material_key refers to the material catalogue.
- customer_email - the only contact detail on a quote. All other customer data exist only on an order.
- customer_email, customer, note - null unless the key holds customers:read. customer_data_visible states which case applies: true means the key may read contact data, so null means none was given; false means the data is withheld from this key.
- external_ref - the shop's own reference. Empty until the shop's system writes one. Filter by it with ?external_ref=.
- source - where the quote came from: widget (the shop's own site), panel (a technologist uploaded the file in the panel) or api (the shop's system sent it through this API). Empty for quotes created before the source was recorded.
- Orders additionally carry: status (new, confirmed, in_production, shipped, cancelled), payment_status (unpaid, paid), and customer with the full invoice and delivery address.
- breakdown - GET /quotes/{id} only: the stored cost breakdown behind the price, for reconciliation in the shop's own system.
Readout of a file (GET /quotes/{id}/metrics)
Fields used for work preparation:
- geometry_metrics.bounding_box - overall size in mm. derived.bbox_sorted_dims contains the same three values sorted; feasibility is checked with this form.
- geometry_metrics.volume, surface_area - part volume in mm3 and surface area in mm2. derived.volume_ratio is the part volume divided by the bounding-box volume; a low value means a large share of the material is removed.
- holes[] - every counted bore: diameter, depth, through or not, and axis. A bore is counted when its wall forms a full circle; the diameter is not the criterion, so small bores are counted too.
- pockets[] - depth, floor area, corner radius, and whether the pocket is open or through.
- min_wall_thickness - the thinnest wall found, in mm; null when no wall thin enough to matter was measured.
- gate_status, gate_reasons - whether the quote could be priced automatically and, if not, why. Each reason has the format code:machine:field:actual:limit and carries the measurement behind the refusal.
Sending a part for quoting
The shop's system can send a file for quoting through the API. The file goes through the same analysis and pricing as a file uploaded in the widget, with the parameters given in the first call. Supported files: STEP and IGES models, DXF profiles, PDF drawings and photos of drawings. Required permission: quotes:create. Order of calls: open the quote, upload the file, confirm the upload, read the result.
1. Open the quote
Send POST /quotes with the file name, the file size and the quoting parameters.
- Required: filename, file_size_bytes.
- Optional: quantity, material_key, tolerance_class, geometric_class, surface_finish, treatments, lead_time, thickness_mm and units_mm (for a DXF), process, customer_email, locale, external_ref, label. process sets how a flat part is made in a shop that has a sheet-cutting machine: machining (a mill or a lathe), sheet_cutting, or auto. auto, and a request without process, is the engine's choice: every part a sheet-cutting machine of the shop can cut is priced as cut. In a shop without a sheet-cutting machine, auto prices exactly as before.
- A field this call does not know is refused, not ignored. A misspelt field name therefore cannot produce a quote in the default material.
- An unknown material is refused in this call, not later during the analysis.
The answer is 201 with quote, release and upload. upload contains the URL, the headers and expires_at needed in step 2.
curl -X POST https://app.metronq.com/api/v1/integration/quotes \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "bracket.step", "file_size_bytes": 482113, "quantity": 25,
"material_key": "alu_6061", "tolerance_class": "f", "external_ref": "RFQ-2026-0455"}'{
"quote": {
"id": "6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33",
"number": 1043,
"ref": "API-1043",
"status": "created",
"filename": "bracket.step",
"quantity": 25,
"unit_price": null,
"external_ref": "RFQ-2026-0455",
"source": "api",
"...": "..."
},
"release": { "state": "waiting", "customer_may_see_price": false },
"upload": {
"method": "PUT",
"url": "https://OBJECT_STORAGE/...",
"headers": {
"x-amz-server-side-encryption-customer-algorithm": "AES256",
"x-amz-server-side-encryption-customer-key": "...",
"x-amz-server-side-encryption-customer-key-MD5": "..."
},
"content_length": 482113,
"expires_at": "2026-09-25T10:15:00+00:00"
}
}2. Upload the file with a PUT to upload.url
Upload the file with PUT to upload.url. The URL points to the object store, not to the MetronQ API.
- Send every header from upload.headers unchanged, because the encryption headers are part of the signature.
- Content-Length must equal the size declared in step 1.
- The URL is valid until expires_at: 15 minutes after the quote was opened.
- The file does not pass through the MetronQ application server. It is encrypted as it is written, with a key used for this file only.
- If your network allows outbound traffic only to known hosts, allow the object store host from upload.url.
curl -X PUT "UPLOAD_URL" \ -H "x-amz-server-side-encryption-customer-algorithm: AES256" \ -H "x-amz-server-side-encryption-customer-key: ..." \ -H "x-amz-server-side-encryption-customer-key-MD5: ..." \ --data-binary @bracket.step
3. Confirm
Send POST /quotes/{id}/confirm-upload. The analysis is queued, and the part is priced right after it with the parameters from step 1. The plan's allowance and the limit of 5 files in analysis are checked again in this call, because this call starts the work.
- 409 file_not_uploaded - the PUT did not arrive. Upload the file again while the URL is valid.
- 409 upload_expired - more than 45 minutes have passed since the quote was opened (15 minutes of URL validity plus 30 minutes of grace). Open a new quote.
- 409 quote_not_awaiting_upload - the upload was already confirmed. A repeated confirmation changes nothing.
curl -X POST https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/confirm-upload \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"quote": { "id": "6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33", "ref": "API-1043", "status": "uploaded", "...": "..." },
"release": { "state": "waiting", "customer_may_see_price": false },
"poll_after_seconds": 10
}4. Wait for the price
Poll GET /quotes/{id}, which returns the short form, about every 10 seconds until status is neither uploaded nor analyzing. Then read the full quote once with GET /quotes/{id}/detail, described below. Both calls require quotes:read, which is a separate permission: a system that only sends files does not need to read every quote of the shop.
- Every file is a quote. It counts against the plan like a quote from the widget or the panel, and the shop's own daily quote limit applies. 403 quoting_blocked:<reason> names the limit that was reached. A quote is billed only after the engine has priced it.
- At most 5 files sent through the API can be in analysis at the same time. Quotes from the widget and the panel do not count toward this limit. The sixth file is refused with 429 analysis_backlog and Retry-After: 30. A quote whose upload URL expired unused no longer counts.
- Limits per call: a file of at most 50 MB; quantity 1 to 100 000; at most 10 treatments; thickness_mm at most 200 and units_mm at most 1000; external_ref at most 80 characters and label at most 200; filename at most 255 characters, without control characters. On drawing-inputs: scale_ratio 0.02 to 50 and thickness_mm 0.2 to 500. Values outside these limits are refused with 422.
- Files sent through the API are analysed after the files uploaded in a widget or in the panel. A single part usually takes seconds, a large assembly a few minutes.
- The reference of the quote starts with API (API-1043), and the panel shows the name of the key that sent it.
- If the quote has customer_email, approving it in the panel e-mails the customer the quote link, as for a quote created in the panel. Without customer_email, MetronQ sends nothing to anybody outside the shop.
- A POST /quotes that timed out may have created a quote. Send external_ref, and after a timeout search for it with GET /quotes?external_ref= before creating the quote again. A quote whose file never arrives is not billed, and any uploaded data is deleted after 6 hours. The empty quote stays in the list and counts toward the shop's own daily quote limit.
Re-pricing
POST /quotes/{id}/reprice (permission quotes:create) changes the parameters of a quote, and the engine prices it again. It works for any quote of the shop, whatever its source, with these exceptions:
- 409 quote_closed - a person has approved or rejected the quote, or the customer has ordered it.
- 409 quote_in_review - a technologist has entered a price for the quote or for one of its parts. A re-price would overwrite that price.
All fields are optional; a field left out keeps its value: quantity, material_key, tolerance_class, geometric_class, surface_finish, confirmed_threads, confirmed_external_threads, hole_fits, face_tolerances, treatments, lead_time, process, countersinks. process takes the same values as in POST /quotes. countersinks marks the through holes of a cut part to be countersunk: a hole key from choices.sheet_holes mapped to an angle from choices.countersink_angles ({"6.0": 90}); on a set, the part's own key (2:8.0). {} removes every mark. It is ignored for a machined part. The values accepted for threads and fits are listed in choices of the quote detail. On an assembly, confirmed_threads, confirmed_external_threads and hole_fits can name one part: "<index>:<designation>" for a thread (1:M16), "<index>:<diameter>" for a fit (2:80.0). GET /quotes/{id}/parts lists these keys for each part. A designation or a diameter without an index applies to every part that has it. confirmed_threads, confirmed_external_threads and face_tolerances take at most 200 entries each. face_tolerances is keyed by the face names of the 3D preview (face_3), so it needs a key that can read the preview.
No key can set a price, approve, reject or give a discount. The engine calculates the price and a person in the panel decides on it.
Drawings
If the engine cannot turn a drawing into a 3D model, the quote stops with status analyzed and release.state needs_input, and drawing.reconstruction_declined names the missing information. Two calls continue from there. Both work only for a drawing sent through the API; for a drawing from the widget they answer 409 not_an_api_quote, because its review belongs to the customer.
- POST /quotes/{id}/drawing-inputs sends the missing values: material_key, scale_ratio (1:2 on the sheet is 2.0) or thickness_mm. The analysis runs again and prices the part.
- POST /quotes/{id}/submit-drawing passes the drawing to a technologist, who prices it by hand.
Everything the quote screen shows
GET /quotes/{id}/detail (permission quotes:read) returns all data a technologist sees on the quote screen in the panel. It starts with all fields of GET /quotes/{id}, with the same names and meanings, and adds the fields below. Reading it changes nothing: the quote is not marked as opened in the panel.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/detail \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"id": "6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33",
"ref": "API-1043",
"status": "priced",
"unit_price": 41.5,
"source": "api",
"...": "...",
"breakdown": { "machine_name": "DMU 50", "...": "..." },
"release": { "state": "needs_review", "customer_may_see_price": false },
"pricing": {
"engine_unit_price": 41.5,
"adjusted_unit_price": null,
"customer_total": 1037.5,
"min_order_top_up": 0.0,
"delivery_cost": 25.0,
"order_total": 1062.5,
"vat_rate_pct": 19.0,
"order_vat": 201.88,
"order_gross": 1264.38,
"lead_days": 10
},
"material": { "key": "alu_6061", "name": "Aluminium 6061", "en_number": "EN AW-6061", "din_number": "3.3211", "density_g_cm3": 2.7 },
"part_mass_kg": 0.2004,
"specification": {
"tolerance_class": "f",
"geometric_class": "K",
"surface_finish": "ra_3_2",
"lead_time": "standard",
"treatments": [ { "key": "anodising", "name": "Anodising", "customer_visible": true } ],
"confirmed_threads": null,
"hole_fits": {},
"...": "..."
},
"choices": {
"thread_candidates": [ { "designation": "M8", "count": 4, "total_depth_mm": 72.0, "hole_key": "6.8" } ],
"external_thread_candidates": [],
"hole_fit_options": [ { "key": "8.0", "diameter": 8.0, "count": 2 } ],
"section_candidate": null
},
"outcome": {
"gate_status": "pending_review",
"gate_reasons": [
{
"raw": "part_exceeds_envelope:vf2:part_size_y:350:310",
"code": "part_exceeds_envelope",
"machine_key": "vf2",
"machine_name": "Haas VF-2",
"field": "part_size_y",
"actual": 350.0,
"limit": 310.0
}
],
"dfm_reasons": [],
"complexity": 34.2,
"thresholds": { "complexity": 60.0, "thin_wall_mm": 1.0 },
"analysis_error": null,
"upload_abandoned": false,
"review": { "note": null, "reviewed_at": null, "reviewed_by": null }
},
"drawing": { "is_drawing": false, "fallback": false, "reconstruction_declined": null },
"readout": { "...": "exactly what GET /quotes/{id}/metrics returns" },
"items": [],
"order": null,
"files": {
"deleted_at": null,
"original": { "available": true, "size_bytes": 482113, "path": "/quotes/6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33/file" },
"preview": { "available": true, "path": "/quotes/6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33/preview.glb" },
"readable": false
},
"customer_link": "https://quoting.metronq.com/precision-works/quote/1043?lang=en",
"customer_link_shared_at": null,
"access_code": null,
"created_by_key": { "name": "PDM", "prefix": "a1b2c3d4" },
"timestamps": { "analysis_queued_at": "...", "analyzed_at": "...", "billable_at": "...", "...": "..." }
}- release.state - the state of the price: waiting (no price yet), abandoned (the file never arrived), needs_input (a drawing waiting for its inputs), failed (the file could not be read), instant (priced, not held for review), needs_review (priced; a technologist must approve it before a customer sees it), manual (the engine could not price it; a person sets the price), approved, rejected, ordered.
- release.customer_may_see_price - whether the customer's quote page shows this price now, by the same rule that page uses. Check it before your system sends a price to anybody: unit_price is filled in for every priced quote, including a quote still waiting for a technologist.
- pricing.engine_unit_price - the price the engine calculated. unit_price is the price in force: the technologist's price when they adjusted it (pricing.adjusted_unit_price).
- pricing.order_total, order_vat, order_gross - the amounts an order would be placed at: the parts after the customer's discount, plus the top-up to the shop's minimum order value and shipping, then VAT at the shop's rate. The calculation is the same as in the widget and on the quote PDF.
- material, part_mass_kg - the material from the shop's catalogue, and the part mass: its volume times the material density.
- specification - the parameters the part is priced with. confirmed_threads null means the question was not answered; an empty list means the answer was none. A treatment with customer_visible false is charged but never named to the customer. price_overrides are a technologist's corrections for this quote; they can be read here and changed only in the panel. process is the process chosen for the quote, null for the engine's choice; technologies says how the priced part is made: milling, turning, laser, plasma or waterjet. A part cut from sheet also has edge_quality (its ISO 9013 designation and limit deviation) and countersinks. The ISO 2768 classes, surface_finish, hole_fits and face_tolerances do not apply to a cut part.
- choices - the values POST /quotes/{id}/reprice accepts for this part, taken from its analysis: thread designations, external threads, the holes that can take an ISO 286 fit, and the rolled section the part was recognised as, if any. part_kind is sheet for a part quoted as cut from sheet, and processes lists the values process accepts for the part (two where the shop can both cut and machine it). For a cut part: sheet_threads (the threads its sheet is thick enough to tap), sheet_countersinks (countersinks drawn in the model), sheet_holes and countersink_angles (what countersinks accepts). On an assembly, choices describe the whole model, with the holes of all parts counted together; the options of each part are returned by GET /quotes/{id}/parts.
- outcome.gate_reasons - why the gates stopped the quote. Each reason is given once as the raw string and once parsed: code, machine_key with its machine_name, field, actual and limit. Use the parsed fields and do not parse raw yourself, because a machine key can contain a colon. dfm_reasons lists the codes that mean the part itself is hard to make, as opposed to a rule of the shop. outcome.review contains the technologist's note, the time of the decision and who made it.
- readout - the same object GET /quotes/{id}/metrics returns, or null before the analysis.
- items - the parts of an assembly, each with its own price, status, reasons and cost breakdown. Their geometry is in readout.parts under the same index. label is null when the file does not name the part.
- files - the uploaded file and the 3D previews as API paths. Downloading them requires files:read; readable states whether this key holds it. The answer contains no download link, so no link that opens a customer's file ends up in your logs.
- customer_link, access_code - the customer's page for this quote and the access code it asks for. access_code is returned only with files:read, because that page shows the customer's drawing and 3D model.
- created_by_key - name and prefix of the key that sent the quote, for a quote sent through the API.
Four more calls return the rest of the quote screen:
- GET /quotes/{id}/similar - the shop's three reviewed parts most similar to this one, with the approved price and the deviation of the engine from it.
- GET /quotes/{id}/machine-options - the price of the quote on every machine of the park, the reasons a machine cannot take the part, and the machine the engine selects by itself. Pinning a machine is available only in the panel.
- GET /quotes/{id}/parts - the parts of an assembly, each with its own thread and fit options and the machine, material and blank strategy a technologist chose for it.
- GET /quotes/{id}/pdf - the quote document, in the shop's language or in another language the shop offers (?locale=). Without customers:read the document has no "prepared for" line.
Every call, with an example
Base URL: https://app.metronq.com with the path prefix /api/v1/integration. Authentication: the key in an Authorization: Bearer header. Request and response bodies are JSON, except where a path ends in .csv, /pdf, /file or .glb. Each entry starts with the permission the key must hold; GET /whoami requires none.
A quote in the panel's Trash is still returned by GET /quotes, GET /quotes/{id}, GET /quotes/{id}/metrics and PATCH /quotes/{id}. All other per-quote routes below answer 404 not_found for it.
GET / whoami
No permission required. Returns the shop the key belongs to, the key's permissions and its request limits. Call it first, and after a 403 to see which permissions the key holds.
curl https://app.metronq.com/api/v1/integration/whoami \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET / config
config:read. The whole pricing configuration: three catalogues, all pricing parameters, the safety gates, the selection strategies and the currency. POST /config/import/preview-json accepts this document as the request body without a wrapper. Replace mode, which also deletes what the document leaves out, must be requested with a wrapper: {"config": ..., "mode": "replace"}.
curl https://app.metronq.com/api/v1/integration/config \ -H "Authorization: Bearer mq_live_YOUR_KEY" > pricing.json
GET / catalog/ {name}
config:read. One catalogue as JSON; {name} is materials, machines or treatments. Answer: {catalog, count, items}.
curl https://app.metronq.com/api/v1/integration/catalog/materials \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET / catalog/ {name}.csv
config:read. The same catalogue as the CSV file the panel's “Export CSV” produces: with a byte-order mark (BOM) for Excel, the shop's separator, one row per entry. The machines file contains no tool lists, because a row has no room for one.
curl https://app.metronq.com/api/v1/integration/catalog/machines.csv \ -H "Authorization: Bearer mq_live_YOUR_KEY" > machines.csv
key;name;technology;hourly_rate;min_quantity;envelope_mm.1;envelope_mm.2;... dmu50;DMU 50;milling;280,00;1;500,0;450,0;400,0;...
GET / tools
config:read. All tools of the shop as a flat list; each row names its machine. ?machine= limits the list to one machine.
curl https://app.metronq.com/api/v1/integration/tools \ -H "Authorization: Bearer mq_live_YOUR_KEY" curl "https://app.metronq.com/api/v1/integration/tools?machine=dmu50" \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET / machines/ {key}/ tools
config:read. The tool library of one machine, with the corner radius that results from it.
curl https://app.metronq.com/api/v1/integration/machines/dmu50/tools \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"machine_key": "dmu50",
"machine_name": "DMU 50",
"count": 3,
"min_tool_radius_mm": 1.0,
"tool_radius_effective_mm": 6.0,
"items": [
{
"key": "e12",
"kind": "endmill",
"name": "12 mm carbide",
"diameter_mm": 12.0,
"nose_radius_mm": null,
"flute_length_mm": 45.0,
"designation": "",
"machine_key": "dmu50",
"machine_name": "DMU 50",
"cut_radius_mm": 6.0
},
{ "key": "d6.8", "kind": "drill", "diameter_mm": 6.8, "cut_radius_mm": 3.4, "...": "..." },
{ "key": "m8", "kind": "tap", "diameter_mm": 8.0, "designation": "M8", "...": "..." }
]
}PUT / machines/ {key}/ tools
config:write. Replaces the whole tool library of the machine. The call is idempotent, so a synchronisation can send the list it holds without knowing the previous state. Two tools with the same key are refused with 422 duplicate_tool_key.
curl -X PUT https://app.metronq.com/api/v1/integration/machines/dmu50/tools \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"tools": [
{"key": "e12", "kind": "endmill", "name": "12 mm carbide",
"diameter_mm": 12.0, "flute_length_mm": 45.0},
{"key": "d6.8", "kind": "drill", "diameter_mm": 6.8},
{"key": "m8", "kind": "tap", "diameter_mm": 8.0, "designation": "M8"}
]}'POST / machines/ {key}/ tools
config:write. Adds one tool, or replaces the tool with the same key. Use it to report a single change without sending the whole library again.
curl -X POST https://app.metronq.com/api/v1/integration/machines/dmu50/tools \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"key": "r8h7", "kind": "reamer", "diameter_mm": 8.0}'DELETE / machines/ {key}/ tools/ {tool}
config:write. Removes one tool. 404 if the key is not in the library, so a synchronisation can tell a removal from an error in its own mapping.
curl -X DELETE https://app.metronq.com/api/v1/integration/machines/dmu50/tools/r8h7 \ -H "Authorization: Bearer mq_live_YOUR_KEY"
A tool library changes which parts the machine can quote. The smallest endmill (on a lathe, the smallest turning insert) becomes the smallest inside corner the machine can produce, and the typed min_tool_radius_mm is no longer read. Drills and taps produce no corner and change nothing here. A machine described with 12 mm cutters only therefore stops quoting parts with smaller inside corners that it quoted before. For this reason every answer of these calls contains tool_radius_effective_mm. An empty list returns the machine to its typed value.
GET / cutting-data
config:read. Per material, the cutting speed and the removal rates a price is calculated from; per machine, the spindle data those rates are scaled by. It is not a feeds-and-speeds table (see What the API can read).
curl https://app.metronq.com/api/v1/integration/cutting-data \ -H "Authorization: Bearer mq_live_YOUR_KEY"
POST / config/ import/ preview
config:write. Previews the import of one CSV file. Body: catalog (materials, machines or treatments) and csv_base64 (the file, base64-encoded). Writes nothing. Answers with the resulting document, the added and updated keys, the rows that could not be read, the columns that were not recognised, and a base_hash.
curl -X POST https://app.metronq.com/api/v1/integration/config/import/preview \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d "{\"catalog\": \"materials\", \"csv_base64\": \"$(base64 -w0 materials.csv)\"}"POST / config/ import/ preview-json
config:write. The same for a whole configuration document. Body: config and mode (merge or replace; default merge). merge applies only what the document states, but the document itself must be complete: the material and machine catalogues are required, and every entry needs its required fields. The mode decides what is written, not what the request may leave out. replace applies the document in full, including deletions.
curl -X POST https://app.metronq.com/api/v1/integration/config/import/preview-json \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "merge", "config": {"materials": [ ... ], "machines": [ ... ]}}'POST / config/ import
config:write. Applies the document a preview returned. Body: config and the base_hash from the same preview. 409 config_changed if the price list was saved between the preview and this call. Answers with the stored document.
curl -X POST https://app.metronq.com/api/v1/integration/config/import \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @commit.json # {"config": ..., "base_hash": "..."} from the preview{
"materials": [ { "key": "alu-6061", "price_per_kg": 32.0, "...": "..." } ],
"machines": [ "..." ],
"treatments": [ "..." ],
"params": { "margin_pct": 30.0, "...": "..." },
"gates": { "mode": "review_all", "...": "..." },
"machine_selection": "cheapest",
"stock_selection": "cheapest",
"currency": "PLN"
}GET / quotes
quotes:read. Quotes, newest first. Parameters: since= (created at or after this time) for polling, status=, external_ref=, limit (1-200) and offset. Contact fields are null without customers:read.
curl "https://app.metronq.com/api/v1/integration/quotes?since=2026-09-16T00:00:00Z&status=priced&limit=50&offset=0" \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET / quotes/ {id}
quotes:read. One quote, with the stored cost breakdown behind its price.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET / quotes/ {id}/ metrics
quotes:read. What the analysis measured. 409 not_analysed while the file is still being analysed: retry shortly. It does not mean that the id is wrong.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/metrics \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET / orders
quotes:read. Orders, newest first, with the same parameters as GET /quotes. Orders in the shop's Trash are not returned. Contact fields are null without customers:read.
curl "https://app.metronq.com/api/v1/integration/orders?since=2026-09-16T00:00:00Z&external_ref=WO-2026-0912" \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"items": [
{
"id": "1b7a44c0-9d2e-4e51-8a10-64d1f0a2e777",
"quote_id": "6f1c0f5a-2a4d-4d7e-9f0b-2c9b8f0a1d33",
"status": "confirmed",
"payment_status": "unpaid",
"created_at": "2026-09-16T09:12:40+00:00",
"paid_at": null,
"quantity": 25,
"unit_price": 41.5,
"total_price": 1062.5,
"min_order_top_up": 0.0,
"delivery_cost": 25.0,
"currency": "EUR",
"external_ref": "WO-2026-0912",
"customer": {
"name": "Alex Morgan",
"email": "purchasing@example.com",
"company": "Example Engineering Ltd",
"phone": "+00 000 000 000",
"address": "12 Industrial Road",
"postcode": "AB1 2CD",
"city": "Riverton"
},
"note": "",
"customer_data_visible": true
}
],
"limit": 100,
"offset": 0
}GET / orders/ {id}
quotes:read. One order, with the customer's invoice and delivery details.
curl https://app.metronq.com/api/v1/integration/orders/ORDER_ID \ -H "Authorization: Bearer mq_live_YOUR_KEY"
PATCH / quotes/ {id}
quotes:write. Sets the shop's own reference (external_ref) on a quote. An empty string clears it. No other field can be changed: the price and the approval are set by the engine and a technologist.
curl -X PATCH https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"external_ref": "RFQ-2026-0455"}'PATCH / orders/ {id}
quotes:write. Sets the shop's reference, the status and the payment marker. Only the fields present in the body are changed. The statuses confirmed and shipped e-mail the shop's customer.
curl -X PATCH https://app.metronq.com/api/v1/integration/orders/ORDER_ID \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"external_ref": "WO-2026-0912", "status": "in_production",
"payment_status": "paid"}'POST / quotes
quotes:create. Opens a quote for a file and returns the upload URL. Answer: 201 with quote, release and upload. Errors: 403 quoting_blocked:<reason>, 429 analysis_backlog, 422 for an unknown field, file type, material, treatment or class.
curl -X POST https://app.metronq.com/api/v1/integration/quotes \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "bracket.step", "file_size_bytes": 482113, "quantity": 25,
"material_key": "alu_6061", "tolerance_class": "f", "external_ref": "RFQ-2026-0455"}'POST / quotes/ {id}/ confirm-upload
quotes:create. Confirms that the file is uploaded; queues the analysis, followed by pricing. Answer: 202. Only for a quote opened through the API (otherwise 409 not_an_api_quote). The allowance and the backlog are checked again (403 quoting_blocked, 429 analysis_backlog). 409 upload_expired if more than 45 minutes have passed since the quote was opened.
curl -X POST https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/confirm-upload \ -H "Authorization: Bearer mq_live_YOUR_KEY"
POST / quotes/ {id}/ reprice
quotes:create. Sets new parameters, and the engine prices the quote again. Works for any quote of the shop until a person decides on it. 409 analysis_pending while the file is still being analysed; 409 drawing_needs_input for a drawing waiting for its inputs; 409 quote_closed and 409 quote_in_review as described under Re-pricing. On an assembly, a thread or fit key can name one part (1:M16, 2:80.0); the keys of each part are listed by GET /quotes/{id}/parts.
curl -X POST https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/reprice \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"quantity": 50, "confirmed_threads": ["M8"], "hole_fits": {"8.0": "H7"}}'POST / quotes/ {id}/ drawing-inputs
quotes:create. Material, scale or thickness for a drawing that was sent through the API and that the engine could not rebuild. The analysis runs again and prices the part. Answer: 202. Counts toward the backlog like a new file.
curl -X POST https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/drawing-inputs \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"scale_ratio": 2.0, "material_key": "steel_c45"}'POST / quotes/ {id}/ submit-drawing
quotes:create. Passes such a drawing (sent through the API) to a technologist, who prices it by hand. Optional fields: quantity, material_key, customer_email.
curl -X POST https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/submit-drawing \
-H "Authorization: Bearer mq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"quantity": 10}'GET / quotes/ {id}/ detail
quotes:read. All data the quote screen shows; see Everything the quote screen shows.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/detail \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET / quotes/ {id}/ similar
quotes:read. The shop's three reviewed parts most similar to this one. Empty before the analysis.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/similar \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"count": 1,
"items": [
{
"id": "0d9e7c21-5b3a-4f10-8c6d-9a2e4b7f1c05",
"number": 1017,
"ref": "WID-1017",
"original_filename": "bracket-rev-a.step",
"approved_unit_price": 39.0,
"quantity": 20,
"currency": "EUR",
"auto_error_pct": 4.1,
"date": "2026-08-30T14:02:11+00:00"
}
]
}GET / quotes/ {id}/ machine-options
quotes:read. The price on every machine of the park, or the reasons a machine cannot take the part. Read-only. Empty before the analysis.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/machine-options \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"count": 2,
"items": [
{ "key": "dmu50", "name": "DMU 50", "technology": "milling", "unit_price": 41.5,
"feasible": true, "reasons": [], "pinned": false, "auto": true },
{ "key": "vf2", "name": "Haas VF-2", "technology": "milling", "unit_price": null,
"feasible": false, "pinned": false, "auto": false,
"reasons": [ { "raw": "part_exceeds_envelope:vf2:part_size_y:350:310", "code": "part_exceeds_envelope", "...": "..." } ] }
]
}GET / quotes/ {id}/ parts
quotes:read. The parts of an assembly, each with the threads and holes it can be confirmed for and the choices a technologist made for it in the panel. Read-only. items is empty for a single part, whose options are in choices of GET /quotes/{id}/detail, and before the analysis. 404 not_found for an unknown quote or a quote of another shop.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/parts \ -H "Authorization: Bearer mq_live_YOUR_KEY"
{
"count": 2,
"items": [
{
"index": 1,
"label": "housing",
"count": 1,
"gate_status": "instant",
"gate_reasons": [],
"unpriced_reason": null,
"price_overrides": { "machine_key": "dmu50", "stock_selection": "standard_stock" },
"choices": {
"thread_candidates": [ { "designation": "M16", "count": 4, "total_depth_mm": 96.0, "hole_key": "14.0", "key": "1:M16", "item_index": 1 } ],
"external_thread_candidates": [],
"hole_fit_options": [ { "key": "1:80.0", "diameter": 80.0, "count": 1, "hole_key": "80.0", "item_index": 1 } ]
}
},
{
"index": 2,
"label": null,
"count": 4,
"gate_status": "tbd",
"gate_reasons": [ { "raw": "probable_norm_part", "code": "probable_norm_part", "...": "..." } ],
"unpriced_reason": "norm_part",
"price_overrides": {},
"choices": { "thread_candidates": [], "external_thread_candidates": [], "hole_fit_options": [] }
}
]
}- index - the number of the part, the same as in items of GET /quotes/{id}/detail and in readout.parts. label is null when the file does not name the part. count - how many times the part occurs in the assembly.
- gate_status, gate_reasons - the gate result of this part, each reason raw and parsed as in the quote detail.
- unpriced_reason - why the engine gave the part no price: norm_part (probably a standard part to be bought), casting (probably a casting), implausible_time (the calculated machining time is not plausible), no_machine (no machine of the park takes the part; gate_reasons gives the details). null when the engine priced the part.
- price_overrides - what a technologist chose for this part in the panel: machine_key, material_key and stock_selection (cheapest, cut_to_size or standard_stock). A key that is absent means the part follows the quote; {} means nothing was chosen. They are changed only in the panel.
- choices - the options of this part only: thread_candidates, external_thread_candidates and hole_fit_options. key is the value POST /quotes/{id}/reprice accepts for this part: "<index>:<designation>" for a thread (1:M16) in confirmed_threads or confirmed_external_threads, "<index>:<diameter>" for a fit (1:80.0) as a key of hole_fits. hole_key is the diameter the 3D preview names the hole by.
GET / quotes/ {id}/ pdf
quotes:read. The quote document as a PDF. ?locale= selects one of the languages the shop offers; any other language is refused with 422 language_not_offered.
curl "https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/pdf?locale=en" \ -H "Authorization: Bearer mq_live_YOUR_KEY" -o quote.pdf
GET / quotes/ {id}/ file
quotes:read and files:read. The file the customer uploaded, with its original name. 409 file_not_uploaded while the file has not arrived; 404 files_deleted after the retention period removed it.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/file \ -H "Authorization: Bearer mq_live_YOUR_KEY" -o bracket.step
GET / quotes/ {id}/ preview.glb and GET / quotes/ {id}/ items/ {index}/ preview.glb
quotes:read and files:read. The 3D preview as binary glTF, of the whole part or of one part of an assembly. 404 no_preview for a quote without a preview.
curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/preview.glb \ -H "Authorization: Bearer mq_live_YOUR_KEY" -o bracket.glb curl https://app.metronq.com/api/v1/integration/quotes/QUOTE_ID/items/2/preview.glb \ -H "Authorization: Bearer mq_live_YOUR_KEY" -o part-2.glb
How much a key may use
Request limits are agreed with each shop together with API access; they are not a published tariff. Default limits:
- 120 requests per minute;
- 20 000 requests per day, counted from 00:00 UTC.
Limits agreed for an account can differ from the defaults. A catalogue synchronised every hour, with quotes polled every minute, stays well within the defaults. The limits of your key are returned in the headers of every response and by GET /whoami, and the panel shows them in Settings, Integrations. When creating a key, the owner can give it a lower daily limit than the rest of the account (“Daily limit for this key (optional)”).
An evaluation account has, in addition to these limits, a total for the whole evaluation. The total does not reset. Its size is agreed with each shop: enough to build an integration, not enough to run one in production, which requires an agreed integration. The panel shows the total and the number of requests used in Settings, Integrations. GET /whoami returns it as requests_total, and every response carries the counter in its headers. The numbers in the examples on this page are examples only. When the total is used up, the API answers 429 total_quota_exceeded without Retry-After, because waiting does not restore it.
Every response to a request with a valid key carries the following headers, so a client can slow down before it reaches a limit. When both a key counter and an account counter apply, the headers report the one with fewer requests remaining.
- X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset - the per-minute limit, the requests left in the current minute, and the seconds until the minute window resets.
- X-RateLimit-Quota, X-RateLimit-Quota-Remaining, X-RateLimit-Quota-Reset - the same for the day.
- X-RateLimit-Total, X-RateLimit-Total-Remaining - the evaluation total and the requests left of it. These two lines appear only while an evaluation total applies.
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 118 X-RateLimit-Reset: 41 X-RateLimit-Quota: 20000 X-RateLimit-Quota-Remaining: 19863 X-RateLimit-Quota-Reset: 51240 X-RateLimit-Total: 1000 X-RateLimit-Total-Remaining: 947
What a refusal means
An error response names the reason in its body, in most cases as a short code: {"detail": "code"}. Act on the code; the HTTP status alone does not say what to do next. The exception is a body that does not match the schema, described under 422 below.
- 429 rate_limited - the per-minute limit is used up. The same request works again shortly; Retry-After gives the number of seconds.
- 429 daily_quota_exceeded - the daily limit is used up. Retry-After counts down to 00:00 UTC.
- 429 total_quota_exceeded - the evaluation total is used up. There is no Retry-After, because waiting changes nothing.
- 401 invalid_api_key - the key is unknown, revoked or expired, or the account's API access is closed, or its trial, plan or licence has ended. All these cases give the same answer, so the answer tells nothing to somebody holding a stolen key.
- 403 missing_scope - the key is valid but does not hold the permission this endpoint requires. A retry does not help; a key with the required permission does.
- 404 not_found, unknown_catalog, unknown_machine, unknown_tool - no such quote, order, catalogue, machine or tool in this account.
- 409 config_changed - the price list was saved between the preview and the apply, so the document no longer describes the result. Read the configuration again, run the preview again, then apply.
- 409 not_analysed - the quote exists but has no readout yet. The file is still being analysed; retry shortly.
- 403 quoting_blocked:<reason> - the shop cannot open new quotes at the moment. <reason> is trial_limit (the trial's quotes are used up), plan_limit (the plan's monthly quotes are used up) or daily_limit (the shop's own daily quote limit), the same reason the panel shows.
- 429 analysis_backlog - 5 files sent through the API are already in analysis. Retry-After (30 seconds) says when to send the next one.
- 409 file_not_uploaded, quote_not_awaiting_upload - the file did not arrive in storage, or the upload was already confirmed.
- 409 upload_expired - more than 45 minutes have passed since the quote was opened; open a new quote.
- 409 quote_in_review - a technologist has entered a price for this quote or for one of its parts; re-price it in the panel.
- 409 analysis_pending, drawing_needs_input, quote_closed, not_an_api_quote - in that order: the file is still being analysed; the quote is a drawing waiting for its inputs; a person has decided on the quote or the customer has ordered it; the quote was not opened through the API, so confirm-upload, drawing-inputs and submit-drawing are not available for it.
- 409 not_a_drawing, quote_already_analyzed - drawing-inputs or submit-drawing was called for a quote that is not a drawing, or for a drawing that is no longer waiting for its inputs: it was rebuilt, passed to a technologist, or is still being analysed.
- 404 files_deleted, no_preview, file_not_found - the files were deleted after the retention period, the quote has no 3D preview, or the stored file could not be read.
- 400 invalid_base64 - csv_base64 in a CSV preview is not valid base64.
- 422 - the request was refused and nothing was written. A known problem comes back as a short code: unknown_material, unknown_treatment, unknown_lead_time, unknown_tolerance_class, unknown_geometric_class, unknown_surface_finish, unknown_thread, unknown_external_thread, unknown_hole, unknown_hole_fit, unknown_countersink_angle, unknown_region, file_too_large, unsupported_file_type, no_inputs_given, implausible_scale, implausible_thickness. For an import, the code is the reason the file or document was refused; the preview lists the rows and columns behind it. A body that does not match the schema (an unknown field, a value out of range, a control character in a file name) comes back as the FastAPI list instead: {"detail": [{"type": ..., "loc": ..., "msg": ...}]}. Unknown fields are refused by POST /quotes, POST /quotes/{id}/reprice, POST /quotes/{id}/drawing-inputs and POST /quotes/{id}/submit-drawing; the other routes ignore them.
Every key starts with mq_live_. Add this pattern to the secret scanner your team uses, so that a key committed by mistake is detected like any other credential.
Who can do this
In the panel, import and export require the Quoting rules permission: the owner and every employee with access to that tab can use them. Exports in the panel are downloads within a signed-in session. Outside the panel, the same data leaves the account only through an API key with the matching permission.
Scheduled synchronisation and sending new quotes to the shop's system as they are created are not available. To request them, write to contact@metronq.com and name the system you use.