Documentation/Integrations
Integrations and data exchange
The material catalogue, machine rates and pricing rules can come from the system that already holds them - and the whole price list can be taken back out.
The catalogue can come from another system
MetronQ keeps the whole of a shop's price list - the materials it buys, the machines it runs, the finishing operations it sells and the parameters the engine prices with - as one configuration that can be read and written from outside the panel. If those numbers already live in an ERP, a CRM or a spreadsheet, there is no need to type them a second time, nor to keep two copies in step by hand.
- 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 top speed, tool stations, minimum batch size, plus the maker, model, control, year, serial and inventory number.
- Finishing operations - name, kind, cost per kilogram, cost per square decimetre, fixed batch fee and the working days it adds to delivery.
- 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.
There are two formats, and which one applies depends on what is at the other end. A spreadsheet per catalogue is what most ERP exports produce and what a person can edit. One JSON document carrying everything is what to write against if another system is going to keep MetronQ up to date on its own.
Where to find it
- 1Quoting rules is opened in the panel.
- 2Import and export is chosen in the left-hand rail.
- 3Export produces a file in the format described below; Import accepts one back.
It is worth exporting first, even when only an import is intended. The exported file already carries the exact column names and the shop's own catalogue keys, so the simplest mapping to write on the ERP side is the one that fills in our own export.
The spreadsheet format
One file per catalogue - materials, machines, finishing. The first row is the column names, and those are English identifiers in every language, because they are what a mapping on the other system is written against and what MetronQ reads back on import.
- Rows are matched on the key column and nothing else. A key MetronQ already knows is updated; a key it does not know is added.
- Only the columns present in the file are read. A column left out means that field keeps its value in every row.
- An empty cell means leave this as it is, not clear it. A partial export - only the materials currently in stock, only this quarter's rates - is the ordinary case rather than the exception.
- An import never deletes. Removing a material or a machine is done in the panel, deliberately.
- The decimal mark travels with the column separator: semicolons go with comma decimals (1234,56), commas with dots (1234.56). Both are read, and the export follows the separator setting on the account.
- UTF-8 with or without a byte-order mark, and the Central European code page that Windows Excel writes, are all accepted.
- One file may contain up to 20 000 rows and be up to 2 MB in size.
A machine's tool library is deliberately not in the spreadsheet: a list of cutters does not fit in a row. A machine imported from a spreadsheet therefore keeps the tool library it already has, so a round trip through Excel cannot quietly change what the machine is allowed to cut.
The whole configuration as one document
The JSON export is the entire pricing configuration exactly as the panel stores it, and it is accepted back unchanged. It carries the three catalogues plus every pricing parameter, the safety-gate settings, the machine and blank selection strategies, and the currency. That makes it the format for a system that keeps its own copy of the price list.
- Merge, the default, applies only what the document actually states. A file carrying three materials and one margin changes three materials and one margin; everything it stays silent about keeps the value already set.
- Replace applies the document in full, deletions included - for when another system is meant to be the system of record rather than one more source.
- Catalogues merge on the same key column the spreadsheet uses, so the two formats agree about what counts as the same material.
Nothing is applied before the preview shows what it does
Every import is a dry run first. Before a single value is written, the panel shows how many entries would be added and how many updated, the field-by-field difference between the current price list and the one the file would leave, every row that could not be read, and every column MetronQ did not recognise.
- A row the price list refuses - a missing density, a price that is not a number - rejects the whole file. Nothing is ever half applied, because a partly imported price list produces wrong prices in a way nobody can see.
- A column MetronQ does not recognise is listed by name. This is what catches an export whose columns have shifted or a header with a typo in it, which would otherwise import as no change at all and look exactly like a file that worked.
- If somebody saves the price list while the preview is open, the import is refused and has to be run again: the difference on screen would no longer describe what would happen.
Every confirmed import is recorded in the settings history as an import, field by field, so a price that moved can always be traced back to the file that moved it.
Connecting a system directly
Everything above can also happen without anybody opening the panel: a shop can issue an API key and give it to its ERP, CRM or automation tool. Access is agreed with each shop individually - a note telling us which system is in use is enough - because an integration depends on what sits at the other end rather than being a box to tick. The key belongs to the shop rather than to a person, so it keeps working when the person who created it is away.
- 1Settings, then Integrations, is opened in the panel. Only the account owner sees this.
- 2New key is pressed, the key is named after the system that will hold it, and its permissions are ticked.
- 3The key is copied immediately. It is shown once and we do not store it; a lost key is revoked and replaced.
Your first integration, step by step
Six calls, in the order they are normally written. Every one of them uses the same header, so a client that manages the first manages all of them. The base address is https://app.metronq.com and every response is JSON unless the path ends in .csv.
1. Check the key. whoami needs no permission and answers with the shop the key belongs to, what it may do and what it may spend. If this call works, everything below is a matter of paths.
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": 400,
"requests_total_used": 3,
"requests_total_remaining": 397
}
}2. Read a catalogue. One call per catalogue - materials, machines or treatments - or GET /config for the whole pricing document at once.
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. Ask for everything created after the newest created_at you already hold; there is no cursor to keep on our side. limit is 1 to 200, offset pages through the rest.
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,
"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,
"currency": "EUR",
"customer_email": "purchasing@example.com",
"external_ref": "",
"source": "widget"
}
],
"limit": 50,
"offset": 0
}4. Read what the analysis measured, when you need the part rather than the price. One call per quote.
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": "standard",
"surface_finish": "as_machined",
"confirmed_threads": { "8.0": 4 },
"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 your own number back, and move the order as it moves on your shop floor. Only the fields you send are touched.
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 - two calls, preview and apply. The preview writes nothing and returns a base_hash the apply call must quote back.
What a key may do
- config:read - read the catalogue and the pricing rules.
- config:write - update them, through the same preview-then-apply sequence the panel uses.
- quotes:read - read quotes and orders: the part, the material, the quantity, the price and the readout. It no longer carries the contact details - that is customers:read below. A key holding it can read every enquiry the shop has ever taken, so grant it to a system that needs the jobs.
- customers:read - read the customer's contact details on quotes and orders. It is an add-on to quotes:read, not a scope of its own: without it the rows still come back, with customer_email, the whole customer block and the customer's own note as null. Leave it off for a system that only needs the part, the quantity and the price - a key that cannot read a name cannot leak a customer list.
- quotes:write - write your own reference back and move an order through its statuses. It cannot price or approve anything.
No key can create another key, revoke one, or change what any key may do. That stays with the signed-in owner, so a stolen key cannot extend its own validity. A key expires after a year unless another term is chosen, and revoking one takes effect on its very next request.
What the API can read
Everything the panel shows about this shop's own catalogue and its own enquiries, and nothing about anybody else's. Each question has one endpoint:
- Materials - the whole catalogue: the designations an engineer reads (EN number and Werkstoffnummer), density, price per kilo, removal rates and cutting speed. GET /catalog/materials
- Machines - the park: technology, hourly rate, work envelope, number of axes, spindle power and top speed, tool stations, maximum workpiece weight, and the datasheet block beside them (maker, model, control, year, inventory number). GET /catalog/machines
- Tools - the tool library as a flat list, each row naming the machine it sits in and the corner radius the engine derives from it. GET /tools
- Cutting data - per material the cutting speed and the removal rates a quote is computed from, per machine the spindle envelope those rates are scaled by. GET /cutting-data
- Treatments - surface and heat treatment, with their costs and lead times. GET /catalog/treatments
- Enquiries and orders - GET /quotes and GET /orders, newest first, pollable with since=. A single quote carries the price breakdown; an order carries the customer to invoice and ship to.
- The readout of a file - what the analysis measured: overall size, weight, holes, pockets, threads, faces and tolerances, per part in an assembly. GET /quotes/{id}/metrics
Two of those deserve a sentence each. A tool belongs to a MACHINE - it is what that spindle can reach for - so every row names its machine, and tools are the one thing the catalogue spreadsheet cannot carry, because a row has no room for a list. And cutting data is not a feeds-and-speeds table: there is no feed per tooth and no depth of cut anywhere in MetronQ, because the time model works from removal rates per material. What that endpoint publishes is the complete set of figures behind a price - the shop's own, plus the two the engine derives from them - so a connected system can hold the same view of a material as the engine that quotes with it.
What every field means
Units are in the field names wherever there is any doubt: _mm, _kg, _cm3_min, _m_min. Everything is a plain JSON number or string, nothing is a formatted amount, and every price is in the shop's own currency, which whoami returns.
Material (GET /catalog/materials):
- key - the identifier a quote refers to as material_key. It is how your system and ours agree on a material; changing it makes a new material rather than renaming one.
- name - what the shop's own customer reads in the widget and on the offer. It is the shop's wording, so it is not translated.
- kind - metal or plastic. It decides which tolerances, fits and finishes are offered at all.
- en_number, din_number - the standard designations (EN AW-6061, 1.0503). Shown to engineers; the engine never computes with them.
- density_g_cm3 - density in g/cm3. The part's weight, and therefore its material cost, comes from this.
- price_per_kg - purchase price per kilogram in the shop's currency. This is the single most common field an ERP writes.
- mrr_rough_cm3_min - roughing removal rate in cm3/min, on the reference spindle of 15 kW. It is what makes one material slower to machine than another.
- mrr_turning_cm3_min - the same for turning. null means: use the roughing rate.
- finishing_rate_cm2_min - how fast a finishing pass covers surface, in cm2/min.
- stock_allowance_mm - allowance added on each side of the part when the blank is chosen, in mm.
- cutting_speed_m_min - cutting speed vc in m/min. null means it is derived from the roughing rate; GET /cutting-data shows the value actually used.
Machine (GET /catalog/machines) - the fields that decide a price or a refusal:
- key, name - the identifier and the name on the shop floor.
- technology - milling, turning, bar_turning or mill_turn. It decides which parts the machine may take and which removal rate applies.
- hourly_rate - the machine rate per hour, in the shop's currency. Never written by a catalogue import from us: it is the number a shop can defend.
- envelope_mm - work envelope. Three numbers for a mill (x, y, z); for a lathe the first is the turning diameter and the second the turning length.
- axes - number of axes, 3 to 5 on a mill. Above 3 the part needs fewer set-ups.
- spindle_power_kw - spindle power. Below the reference 15 kW the removal rates are scaled down in proportion; above it nothing is scaled up, so stating power can only make a quote dearer.
- max_spindle_rpm - top spindle speed. It only matters for small tools, where the required rpm for the cutting speed cannot be reached.
- min_tool_radius_mm - the smallest inside corner this machine can leave. Retired the moment the machine has a tool library: the smallest tool then decides.
- tool_stations, max_workpiece_kg, through_spindle_coolant - magazine size, table load, coolant through the spindle.
- datasheet - maker, model, control, year, serial, inventory_no, notes. Identification only; nothing in this block reaches the engine.
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 this tool sits in. A tool always belongs to one.
- kind - drill, endmill, tap, thread_mill, reamer or turning_insert.
- diameter_mm - cutting diameter. For a drill or reamer the hole it makes; for an endmill the cutter; for a tap the nominal thread.
- nose_radius_mm - nose radius of a turning insert, the smallest fillet it can leave on a turned profile.
- flute_length_mm - usable cutting length in mm. null means no stated limit, and the engine then imposes none.
- cut_radius_mm - the smallest inside radius this tool can leave, computed by us: half the diameter, or the nose radius on an insert. The smallest of these on a machine becomes that machine's real corner limit.
Cutting data (GET /cutting-data) - the two columns the engine derives, beside the shop's own:
{
"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 spindle and the cutting speed every figure in the price list is written against: 15 kW and 250 m/min. They are returned so the derivation below can be checked rather than believed.
- cutting_speed_effective_m_min - the cutting speed actually used: the shop's own when it typed one, otherwise reference / machinability_factor.
- machinability_factor - how much harder than aluminium this material is to cut: 1.0 aluminium, about 1.6 an ordinary steel, about 3 a stainless. It is the square root of the removal-rate ratio against the aluminium baseline.
- mrr_turning_effective_cm3_min - the turning rate with the fallback already applied, so a reader never has to repeat that rule.
- spindle_power_factor - how the removal rates are scaled on this machine: spindle_power_kw / 15, capped at 1.0 and floored at 0.25.
- tool_radius_effective_mm - the corner radius actually in force: the typed min_tool_radius_mm, or the smallest tool in the library once there is one.
Enquiry and order (GET /quotes, GET /orders):
- id - the identifier every other call takes. number is the human one the shop sees as WYC-1042.
- status - where the enquiry is: created, analyzing, priced, approved, rejected, analysis_failed. Only priced and approved carry a price.
- unit_price, total_price, currency - the price per piece and for the batch, in the shop's currency. A quote that was never priced has null here.
- material_key, quantity - what the customer chose. material_key points into the catalogue above.
- customer_email - the only contact datum an enquiry carries. Everything else about the person appears once they order.
- customer_email, customer, note - null unless the key holds customers:read. customer_data_visible says which of the two it is, because "this key may not read it" and "nobody left an address" are different facts and a client that confuses them chases a customer who does not exist.
- external_ref - your own number. Empty until your system writes one; you can then find the job again with ?external_ref=.
- source - where the enquiry came from: widget (the shop's own site) or panel (a technologist uploaded the file here). Empty on enquiries taken before this was recorded.
- On an order additionally: status (new, confirmed, in_production, shipped, cancelled), payment_status (unpaid, paid), and customer with the full invoice and delivery address.
- breakdown - on GET /quotes/{id} only: the stored cost breakdown behind the price, which is what a system reconciling in its own books needs.
Readout of a file (GET /quotes/{id}/metrics) - work preparation:
- geometry_metrics.bounding_box - overall size in mm. derived.bbox_sorted_dims is the same three numbers sorted, which is what feasibility is decided on.
- geometry_metrics.volume, surface_area - part volume in mm3 and surface in mm2. derived.volume_ratio is volume over bounding box: a low ratio means a lot of metal comes off.
- holes[] - every counted bore: diameter, depth, whether it goes through, and its axis. A hole is counted because it is a full circle, not because it is large.
- pockets[] - depth, floor area, corner radius, and whether it is open or through.
- min_wall_thickness - the thinnest wall found, in mm, or null when nothing thin enough to matter was measured.
- gate_status, gate_reasons - whether the quote could be priced automatically, and if not, why: code:machine:field:actual:limit, with the measurement behind the refusal.
Every call, with an example
Base address https://app.metronq.com, the key in an Authorization: Bearer header, JSON in and out except where a path ends in .csv. The permission beside each line is the scope the key must hold; whoami needs none.
GET /whoami - no permission needed. Who the key belongs to, what it may do and what it may spend. The first call to write, and the one that turns a later 403 into a sentence.
curl https://app.metronq.com/api/v1/integration/whoami \ -H "Authorization: Bearer mq_live_YOUR_KEY"
GET /config - config:read. The whole pricing document: three catalogues, every pricing parameter, the safety gates, the selection strategies and the currency. This is exactly what the import takes back - you can post the document straight in, with no envelope around it. The replace mode, which also deletes what the document leaves out, is asked for separately: {"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, where {name} is materials, machines or treatments. Answers {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 spreadsheet the panel's Export button produces: a BOM for Excel, the shop's own separator, one row per entry. Machines lose their tool list here - 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. Every tool the shop owns, flat, each row naming its machine. ?machine= narrows it to one.
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. One machine's crib, and the corner radius it implies.
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": 3.4,
"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. Replace the whole crib. Idempotent, so a sync can send the list it holds without knowing what was here before. Two tools under one 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. Add one tool, or replace the one already under that key. For the system that reports a single change rather than re-sending two hundred.
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. Remove one tool. 404 when that key is not in the crib, so a sync can tell a removal from a typo 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"
Writing a tool library CHANGES what the machine can quote. The smallest ENDMILL in it - a turning insert on a lathe - becomes the smallest inside corner that machine can leave, and the typed min_tool_radius_mm stops being read; drills and taps leave no corner and change nothing here - so a park described with only 12 mm cutters stops quoting parts it used to take. Every answer here returns tool_radius_effective_mm for exactly that reason. An empty list puts the machine back on its typed figure.
GET /cutting-data - config:read. Per material the cutting speed and removal rates a price is computed from; per machine the spindle those rates are scaled by. Not a feeds-and-speeds table - see above.
curl https://app.metronq.com/api/v1/integration/cutting-data \ -H "Authorization: Bearer mq_live_YOUR_KEY"
POST /config/import/preview - config:write. What this spreadsheet would do. Writes nothing and answers with the resulting document, the added and updated keys, the rows it could not read, the columns it did not recognise, 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 document. merge applies only what the document states; replace lets it win outright, deletions included.
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. Apply what a preview returned, quoting the base_hash that came with it. 409 config_changed when somebody saved the price list in between. 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. Enquiries, newest first. since= is what a poll uses; status=, external_ref=, limit (1-200) and offset do the rest. 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 enquiry, 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 read - that is 'ask again shortly', not 'wrong id'.
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, same filters. Orders the shop moved to its trash are absent. 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": 1037.5,
"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": ""
}
],
"limit": 100,
"offset": 0
}GET /orders/{id} - quotes:read. One order, with the customer to invoice and ship to.
curl https://app.metronq.com/api/v1/integration/orders/ORDER_ID \ -H "Authorization: Bearer mq_live_YOUR_KEY"
PATCH /quotes/{id} - quotes:write. Your own reference on an enquiry, and nothing else: the price and the approval belong to the engine and to 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. Your reference, the status and the payment marker. Only the fields the body states are touched. 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"}'How much a key may use
API access is agreed with each shop individually, and so are its limits - they are part of the same arrangement, not a published tariff. An integration that has been agreed runs on 120 requests a minute and 20 000 a day, counted from 00:00 UTC. A catalogue synchronised hourly, with quotes polled every minute, sits well inside that; if your system needs more, say so and we will set a figure that fits it. A shop can also hold one of its own keys tighter than the rest when it creates it.
A key issued for an EVALUATION carries a third figure instead: 400 requests for the whole test, which do not come back the next day. That is the size of a free test: enough to build an integration against - reading the whole surface costs about thirty calls, the walkthrough above six - and not enough to operate one, which is what an agreed integration is for. The panel shows it under Settings, then Integrations, with how much of it is spent, and whoami returns it as requests_total. When it runs out the answer is 429 with total_quota_exceeded and no Retry-After: there is no hour at which it starts working again.
Every answer says where the caller stands, so a client can pace itself before it meets a limit rather than after. The last two lines appear only while an evaluation total is in force:
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: 400 X-RateLimit-Total-Remaining: 347
What a refusal means
Every error answers with a short code in the body, and the code is the part worth acting on - the HTTP status alone does not say what to do next.
- 429 rate_limited - the minute is full. The same request works shortly; Retry-After says in how many seconds.
- 429 daily_quota_exceeded - the day is spent. Retry-After counts down to 00:00 UTC.
- 429 total_quota_exceeded - an evaluation budget is spent. No Retry-After, because waiting changes nothing.
- 401 invalid_api_key - unknown, revoked or expired key, or an account whose API access is closed. Deliberately one answer for all of them: telling them apart would be worth something to whoever stole the key and nothing to its owner.
- 403 missing_scope - the key is valid and does not hold the permission this endpoint needs. Retrying will not help; a key with the right scopes will.
- 404 not_found, unknown_catalog, unknown_machine - no such quote, order, catalogue or machine in this account.
- 409 config_changed - somebody saved the price list between the preview and the commit, so the document being applied no longer describes what would happen. Read the configuration again, preview again, apply again.
- 409 not_analysed - the quote exists but has no readout yet. The file is still being analysed; ask again shortly.
- 422 - the spreadsheet or document was refused and nothing was written. The body names the reason, and the preview lists the rows and columns behind it.
The pattern mq_live_ belongs in whatever secret scanner the team runs, so a key committed by accident is caught the way any other credential would be.
Who can do this
Import and export follow the Quoting rules permission, so the owner and any employee granted that tab can use them. Exports are ordinary downloads through a signed-in session; no file leaves the account any other way.
Anything this page does not cover - a scheduled sync, or quotes pushed into the shop's own system as they are created - can be requested at contact@metronq.com; naming the system in use is how we decide what to build next.
