Developers
Build on the nuqo API
Your own systems read what your organization decided in nuqo: finalized quotes with each assembly’s work plan and the parts list, the events that say when a quote is finalized or reopened, and your parts, which your ERP can also send. Connect your ERP, a production-planning tool, a report or a script of your own. JSON over HTTPS, signed in with a key your admin creates in nuqo.
The reference is generated from the code that answers the requests.
1What the API does
The API lets your own systems — an ERP, the middleware in front of it, a planning tool or a script — read what your organization decided in nuqo, without anyone copying it from the screen. It is mostly for reading: your system asks, nuqo answers. The one thing it takes is your parts, from your ERP.
It does
- Read your finalized quotes: the header, each assembly’s work plan, the parts list.
- Tell your system when a quote is finalized or reopened — by polling today, by webhook soon.
- Read the parts your organization keeps in nuqo, in full and as the changes since your last read.
- Take your parts from your ERP: prices, stock and article data, in batches.
It doesn’t
- Read drafts. A draft’s price moves as its inputs change, so it answers
409. - Change quotes. Nothing is created, edited or finalized through the API.
- Write into your systems. nuqo answers your calls; soon it will also notify you of an event, and nothing more.
2Sign in
- Create a key. In nuqo, an owner or admin of your organization opens Settings → API keys, names the key (say, “ERP middleware”) and creates it. The key is shown once:
nq_live_followed by 43 characters. Put it in your secret store. - Send it on every request as
Authorization: Bearer nq_live_…. No cookie, no CSRF token. Every address starts withhttps://backend.nuqo.io/api/v1. - Test it.
GET /organizationanswers the organization the key belongs to.
curl https://backend.nuqo.io/api/v1/organization \
-H "Authorization: Bearer $NUQO_KEY"{ "id": 318, "name": "Example Assemblies Inc" }Revoke it under Settings → API keys; it stops at once. A lost key can’t be shown again: create a new one, switch your system over, then revoke the old one.
A key acts as your organization, never as a person, and sees only your organization’s data. Every key can read. A key your admin created with “Can also push parts” ticked can also send your parts to nuqo: see Push your parts.
3Read finalized quotes
The list
GET /quotes answers your finalized quotes, oldest finalization first, 50 a page (limit up to 100). Page with next_cursor while has_more is true. Then keep the last next_cursor and poll with it later: it resumes after the last quote you saw, so you get only what was finalized since. A cursor is opaque — pass it back unchanged, never build one. To start from a date instead, send finalized_since; sent together with a cursor, both apply.
curl "https://backend.nuqo.io/api/v1/quotes?finalized_since=2026-10-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer $NUQO_KEY"{
"quotes": [
{
"id": 5087,
"number": "139-1",
"revision": 1,
"finalized_at": "2026-10-14T08:52:40.611093Z",
"valid_until": null,
"customer": null,
"total": { "amount": 3920.5, "currency": "EUR" }
},
{
"id": 5120,
"number": "147-2",
"revision": 2,
"finalized_at": "2026-10-20T14:03:11.204518Z",
"valid_until": "2026-11-19T22:59:59Z",
"customer": { "id": 903, "name": "Sample Machines Ltd" },
"total": { "amount": 21480, "currency": "EUR" }
}
],
"next_cursor": "MjAyNi0xMC0yMFQxNDowMzoxMS4yMDQ1MTgrMDA6MDB8NTEyMA",
"has_more": false
}A quote reopened, revised and finalized again comes back later with a higher revision and a new number (147-2 after 147-1): replace your copy. While it is being revised it is not in the list, and reading it answers 409.
One quote and its work plan
GET /quotes/{quote_id} answers the header, each assembly with its work plan, and the parts list. The names nuqo gives (assemblies, work centers) come in English; add ?lang=de for German. Text from your customer’s documents, such as a line’s description, stays as written. A draft, or a quote reopened for a revision, answers 409 quote_not_finalized. A finalized quote that has no work plan answers work_plan_available: false and no assemblies. Every field is in the reference; the work plan is the part most systems map first:
The operation number: 10, 20, 30 … in the order the work flows.
Your own code for the work center, as set up in nuqo (your ERP's number where you number your work centers); null for an operation no work center was given — the row stays, with its name and time.
The work center's name, or the operation's own for a row with no work center.
Setup time. Setup is charged once per order: this is this assembly's share of it, by quantity, on the first row that needs it — so the setup over all assemblies adds up to the setup the quote charged.
sTime per piece, rounded to 0.01; null where the quote states none.
The row's run time for all piece_count pieces. setup_seconds + run_seconds is the row's total time; use it rather than multiplying the rounded piece_seconds back out. Over an assembly's rows, run_seconds adds up to the run time the quote charged for it.
How the times add up to what the quote charged
Op 10 · Cutting · work center 1010 · piece_count 250
- setup_seconds
- 180.00 s
- run_seconds (250 × 42.50 s)
- 10,625.00 s
- the row’s total time
- 10,805.00 s
- Use
setup_seconds + run_seconds. Don’t multiply the roundedpiece_secondsback out. - Setup is charged once per order. Each assembly carries its share by quantity, so the setup over all assemblies adds up to the setup the quote charged.
- Over an assembly’s rows,
run_secondsadds up to the run time the quote charged for it, andsetup_seconds + run_secondsto the assembly’s total time. - A row with
work_center_id: nullis an operation no work center was given: it keeps its name and time, so no time is dropped.
One sales-order line per assembly
Each assembly states its price as its row on the quote PDF: unit_price for one piece (per: "pcs") and amount for the quantity ordered, net, in the currency of total. amount is quantity × unit_price, so a sales-order line built from the three matches the PDF. The rest of the quote is in total only: setup the rows don’t carry, shipping, extra charges, the discount, and on an older quote its overall margin. An assembly nobody ordered a quantity of has unit_price: null and amount: null.
The parts list
parts lists every line of the quote’s bill of materials, in its order — including a line that matched nothing (part: null). quantity is what the whole order needs of the part; ordered is what is bought, in the same unit: the need rounded up to the supplier’s pack, minimum order and order multiple. Order ordered. unit_price is the supplier’s list price per unit, in the currency the part is bought in; line_total is what buying ordered costs, in the quote’s currency.
Since 29 October 2026, a line counted in feet or inches states unit_price per foot or per inch, and per says which ("ft" or "in"); before, it was stated per metre. This is a one-off exception to the rule that nothing under /v1/ changes its meaning, made while no integrator uses the API.
4Events
Events tell your system which quote to fetch and which order to hold. Poll GET /events every minute or so, sending the last event id you handled as after (0 the first time).
curl "https://backend.nuqo.io/api/v1/events?after=20870" \
-H "Authorization: Bearer $NUQO_KEY"{
"data": [
{
"id": 20871,
"type": "quote.reopened",
"created_at": "2026-10-19T08:41:55.120334Z",
"quote": { "id": 5120, "revision": 2 }
},
{
"id": 20872,
"type": "quote.finalized",
"created_at": "2026-10-20T14:03:11.394120Z",
"quote": { "id": 5120, "revision": 2 }
}
],
"next_after": 20872
}quote.finalizedFetch the quote. A higher revision of the same quote replaces the plan you built on the earlier one. One you already hold at that revision is a repeat: skip it.
quote.reopenedHold any order built on the quote until the next quote.finalized. Meanwhile the quote answers 409 quote_not_finalized; keep your copy of the last finalized revision. revision is the new one being worked on. If you hold no copy of the quote, there is nothing to hold: wait for its quote.finalized.
- Store
next_afterand send it asafternext time. A full page (limit, 100 by default) means more are waiting: ask again at once. A shorter page is the end for now. - Dedupe on
id. Delivery is at least once: if your system fails between handling an event and storing its id, it gets the event again. - Ignore types you don’t know. New ones may be added.
- Events are kept 30 days. An
afterolder than that answers the oldest events still kept; read the quotes list to catch up.
Webhooks
Coming · not available yetInstead of polling, your system can be called. You’ll enter an HTTPS address in Settings; nuqo will POST each event to it — the same body as an entry of data above — the moment a quote is finalized or reopened.
- Signed: an HMAC-SHA256 of
timestamp.bodyin aNuqo-Signatureheader, with a secret shown once in Settings. - Retried with back-off for 24 hours until your address answers 2xx. Dedupe on the event
id. - Polling keeps working alongside; both read the same events.
Your input shapes this: see assumption A1.
5The parts sync
Push your parts
Your ERP sends nuqo its parts — prices, stock, article data — instead of someone exporting a file and uploading it. They land on your ERP sync price list, which nuqo ranks against your other price lists like any of them. You need a key an owner or admin created under Settings → API keys with “Can also push parts” ticked; it can’t be added to a key later, and a key without it answers 403.
- Send a batch of up to 1,000 rows. A row has the fields of a
GET /materialsrow, under the same names, so a row read from nuqo can be sent back as it is;mpnandmanufacturerare required. The maker,mpnandsuppliersay which part a row is, so send the same ones every time: a row that leaves its supplier out is another part. Any other field a row leaves out, or sends asnull, keeps the value nuqo holds, and a part the batch leaves out stays as it is.valid_fromandvalid_toare the days the row’stiershold, so send them with those tiers: tiers without them hold from now with no end, and dates without tiers are refused.A part you no longer sell: send its row with
"removed": true, e.g.{"mpn": "1-968849-1", "manufacturer": "TE Connectivity", "supplier": {"name": "Example Supply", "sku": "ES-968849"}, "removed": true}. It stops being priced from your ERP sync list; your other price lists keep theirs. Its maker,mpnandsuppliersay which part, as on any row; its other fields are ignored. It still reads back fromGET /materials, withprices_quotesfalse (not asobsolete: that is a part a user deleted), and sending its row again withoutremovedlists it again. Removals apply after the batch’s other rows, so a batch that sends and removes the same part ends with it removed.Request · curl curl https://backend.nuqo.io/api/v1/materials/batches \ -H "Authorization: Bearer $NUQO_KEY" \ -H "Content-Type: application/json" \ -d '{ "rows": [ { "org_sku": "A-10442", "mpn": "1-968849-1", "manufacturer": "TE Connectivity", "description": "Receptacle 2.8 mm, tin", "supplier": { "name": "Example Supply", "sku": "ES-968849" }, "tiers": [{"min_qty": 1, "max_qty": null, "price": {"amount": 0.42, "currency": "EUR"}}], "valid_from": "2026-11-01", "valid_to": "2027-03-31", "quantity_in_stock": 4200 } ] }'Response202 { "batch_id": 88214 }The whole batch is checked before any of it is taken. A refused row answers
422and nothing is queued; the message names the first bad row, e.g.rows[3]: tiers.0.price.amount: Input should be a valid number …. Fix it and send the batch again. - Poll the batch every few seconds while its
statusispendingorrunning.Request · curl curl https://backend.nuqo.io/api/v1/materials/batches/88214 \ -H "Authorization: Bearer $NUQO_KEY"Response200 { "id": 88214, "status": "done", "summary": {"rows": 1, "imported": 1, "removed": 0, "not_imported": []} }donecarries thesummary: therowssent, how many wereimportedandremoved, and innot_importedeach row nuqo didn’t write, by itsindexin your batch, with the reason: a removal of a part your list doesn’t hold is one, “nothing to remove”.failedmeans the import stopped: send the batch again. That is safe: a row sent twice writes the same part, and a removal sent twice finds nothing left to remove. While a batch is pending or running, another answers409 batch_in_progress: send it once the poll saysdone. - Read them back with
GET /materials,changed_sinceset a minute before you sent the batch, so a clock of yours running ahead misses nothing: every part that changed since, the batch’s among them, as nuqo holds them, with nuqo’slisting_idandpart_id. A row that changed nothing doesn’t come back. A change is served once it is 3 minutes old.Request · curl curl "https://backend.nuqo.io/api/v1/materials?changed_since=2026-10-30T08:00:00Z" \ -H "Authorization: Bearer $NUQO_KEY"Response200 { "materials": [ { "listing_id": 918377, "part_id": 4410951, "catalogue": { "id": 131, "name": "ERP-Sync" }, "org_sku": "A-10442", "mpn": "1-968849-1", "manufacturer": "TE Connectivity", "description": "Receptacle 2.8 mm, tin", "category_leaf": null, "supplier": { "name": "Example Supply", "sku": "ES-968849" }, "tiers": [{"min_qty": 1, "max_qty": null, "price": {"amount": 0.42, "currency": "EUR"}}], "unit_price": { "amount": 0.42, "currency": "EUR", "per": "pcs" }, "valid_from": "2026-11-01", "valid_to": "2027-03-31", "order_unit": null, "content_unit": "pcs", "no_cu_per_ou": 1, "price_quantity": 1, "minimum_order_quantity": null, "order_multiple": null, "lead_time_days": null, "quantity_in_stock": 4200, "notes": null, "prices_quotes": true, "status": "active", "changed_at": "2026-10-30T08:00:41.208113Z" } ], "next_cursor": "MjAyNi0xMC0zMFQwODowMDo0MS4yMDgxMTMrMDA6MDB8NDQxMDk1MXw5MTgzNzc", "has_more": false }
The first full load is the same, many times: send batches of up to 1,000 rows one after another, each polled to done before the next.
Read your parts from nuqo
GET /materials answers the rows of your Materials tab in nuqo, with the same values, least recently changed first.
- First, a full pull: no
changed_since,include_removed=true, paging withnext_cursorwhilehas_moreis true. A part changed while the pull runs comes again, further on, with its new values: replace the copy you hold. - Then the changes: keep the last
next_cursorand poll with it (andinclude_removed=true). You get the parts added, edited, repriced or deleted since. A change is served once it is 3 minutes old. - Nightly, a full pull and compare. It catches the few changes no delta can show: a part of nuqo’s global catalog you removed from your parts is gone, not marked, and nuqo’s own housekeeping (a manufacturer’s name merged, a category refiled) doesn’t move a part’s change time.
curl "https://backend.nuqo.io/api/v1/materials?include_removed=true&limit=100" \
-H "Authorization: Bearer $NUQO_KEY"{
"materials": [
{
"listing_id": 918204,
"part_id": 4410273,
"catalogue": { "id": 112, "name": "Example Supply 2026" },
"org_sku": "300-118",
"mpn": "EX-H4",
"manufacturer": "Example Housings",
"description": "Housing, 4 pole, black",
"category_leaf": "Connectors > Housings",
"supplier": { "name": "Example Supply", "sku": "ES-H4-01" },
"tiers": [
{ "min_qty": 1, "max_qty": 499, "price": {"amount": 1.58, "currency": "EUR"} },
{ "min_qty": 500, "max_qty": null, "price": {"amount": 1.42, "currency": "EUR"} }
],
"unit_price": { "amount": 1.58, "currency": "EUR", "per": "pcs" },
"valid_from": "2026-01-01",
"valid_to": "2026-12-31",
"order_unit": null,
"content_unit": "pcs",
"no_cu_per_ou": 1,
"price_quantity": 1,
"minimum_order_quantity": 100,
"order_multiple": 50,
"lead_time_days": 14,
"quantity_in_stock": 2400,
"notes": null,
"prices_quotes": true,
"status": "active",
"changed_at": "2026-10-21T16:20:05.774812Z"
}
],
"next_cursor": "MjAyNi0xMC0yMVQxNjoyMDowNS43NzQ4MTIrMDA6MDB8NDQxMDI3Mw",
"has_more": false
}A part’s identity is part_id. org_sku is your article number where you set one, but two parts can share it, so it isn’t a key. A full pull is one request per 100 parts: 24,000 parts are 240 requests, about two minutes under the rate limit.
6Errors, limits and changes
An error answers a stable code for your program and a message for people, which may change.
{
"error": { "code": "quote_not_finalized", "message": "The quote is a draft or a revision in progress." }
}| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A parameter is malformed or out of range, or a push’s body holds no rows |
| 401 | api_key_requiredinvalid_api_key | No key, or a key that is unknown or revoked |
| 403 | organization_inactiveinsufficient_scope | Your organization is inactive; the key lacks the permission (a key that only reads, sending parts). A plain-text answer error code: 1010 instead comes from Cloudflare, in front of nuqo: it refuses your client’s User-Agent, so send your own (your system’s name, say) |
| 404 | not_found | No such resource in your organization (another organization’s is not found too), or no such address |
| 405 | method_not_allowed | The address doesn’t take this method; the Allow header names the ones it does |
| 409 | quote_not_finalizedbatch_in_progresscatalogue_retired | The quote is a draft or being revised; a batch of your parts is still importing (send yours again once it is done); your ERP sync price list is retired in nuqo |
| 413 | batch_too_large | A batch of more than 1,000 parts |
| 422 | invalid_row | A part of your batch is refused; the message names the first one by its index, e.g. rows[3]: tiers.0.price.amount: … |
| 429 | rate_limited | Over the rate limit; wait the Retry-After header’s seconds |
Besides the 1010 answer above, only a server error, 500, doesn’t carry this body. Treat any answer without a code by its status. Retry a 500, a 502–504 or a network error with a growing wait (say 2, 4, 8 … seconds, five times), then give up until your next run.
Rate limit
120 requests a minute per key, across every address. Over it, the answer is 429 rate_limited with a Retry-After header giving the whole seconds to wait.
How the API changes
- Additions arrive without notice. A new field, address, parameter or event type can appear any day. Your system ignores what it doesn’t read.
- A field is retired slowly. It is marked deprecated here and in a response header, announced in the changelog, and removed no sooner than 6 months later.
- A real break ships as
/v2/next to/v1/, which keeps running while you move. A check on every change of ours refuses anything else.
7What we assume about your system
We built v1 on these assumptions. Tell us which hold for your system and correct the ones that don’t — the numbers are enough.
- A1Events
Your system polls
GET /events, or it can take HTTPS calls from the internet.If not: Tell us which. If it takes inbound HTTPS, webhooks move up our list.
- A2Sign-in
A static key in an
Authorizationheader is fine for your system.If not: Tell us what it needs (e.g. OAuth2 client credentials).
- A3Quotes
Your ERP keys an assembly on your own number for it,
shop_part_number, and it is set on every quote you finalize.If not: Tell us which number your ERP files a work plan under.
- A4Work plan
A work-plan row with no work center (
work_center_id: null, with its name and time) is acceptable.If not: Every row must name a work center, and we’ll need a default one from you.
- A5Revisions
A higher revision of a quote replaces the plan you hold for it.
If not: Tell us how your ERP keeps revisions side by side.
- A6Parts sync
Your ERP leads on prices, stock, lead time and article data.
If not: Name the groups nuqo should lead.
- A7Parts sync
Your article number (
org_sku) is your key for a part.If not: Tell us the field that is.
- A8Parts sync
A few thousand parts, synced once a day, and parts added in nuqo don’t need to become new articles in your ERP.
If not: Tell us your row count, how often, and whether new parts should flow back.
Copy the list as a plain-text reply, mark each one holds or not, and send it to your nuqo contact.
Reference
Generated from the code that answers the requests, so it can’t drift from what the API sends. All times are seconds; every timestamp is ISO 8601 in UTC; money carries its currency. Ignore fields you don’t read: new ones may appear. Addresses are written in full here; the base URL above already ends in /api/v1.
GET/api/v1/organizationThe key's organization
The organization the key belongs to. Use it to check a key.
- Sign-in
- Bearer key · permission
read - Rate limit
- Shared, 120 a minute per key
Responses
- 200
- Organization
- 401
api_key_requiredinvalid_api_key- 403
organization_inactiveinsufficient_scope- 405
method_not_allowed- 429
rate_limited
Response fields · 200 · Organization
Your organization's id in nuqo.
Your organization's name.
Example
curl https://backend.nuqo.io/api/v1/organization \
-H "Authorization: Bearer $NUQO_KEY"{ "id": 318, "name": "Example Assemblies Inc" }GET/api/v1/quotesFinalized quotes, oldest first
Your finalized quotes, oldest finalization first, headers only: read each quote's work plan with GET /quotes/{quote_id}. Page with next_cursor while has_more is true, then keep the last next_cursor and poll with it: it answers only what was finalized since. A quote revised and finalized again comes again, later, with a higher revision. Every finalized quote is listed, also when your organization doesn't share finalized quotes with all its members.
- Sign-in
- Bearer key · permission
read - Rate limit
- Shared, 120 a minute per key
Query parameters
Only quotes finalized at or after this time. Without an offset it is UTC.
A next_cursor this API answered, passed back unchanged: reads on after the last row it ended on. Never build one.
How many to answer at most.
Responses
- 200
- QuoteList
- 400
invalid_request- 401
api_key_requiredinvalid_api_key- 403
organization_inactiveinsufficient_scope- 405
method_not_allowed- 429
rate_limited
Response fields · 200 · QuoteList
This page, oldest finalization first.
The quote's id, what GET /quotes/{id} takes.
The number your customer reads, <quote number>-<revision>, e.g. "42-3".
1 for the first; counts up each time the quote is reopened and finalized again.
When this revision was finalized.
UTCWhen the offer expires.
UTCWho the quote is for; null when it names nobody.
Your customer's id in nuqo.
The customer's company, else the contact's name.
The quote's total as finalized, net, in the currency nuqo priced it in (the PDF may show it converted).
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
Opaque: pass it back unchanged as cursor to read on after this page's last quote. On a page with no quotes it is the cursor sent (null when none was).
Whether the next page already holds quotes; false once you have caught up.
Example
curl https://backend.nuqo.io/api/v1/quotes \
-H "Authorization: Bearer $NUQO_KEY"{
"quotes": [
{
"id": 5087,
"number": "139-1",
"revision": 1,
"finalized_at": "2026-10-14T08:52:40.611093Z",
"valid_until": null,
"customer": null,
"total": { "amount": 3920.5, "currency": "EUR" }
},
{
"id": 5120,
"number": "147-2",
"revision": 2,
"finalized_at": "2026-10-20T14:03:11.204518Z",
"valid_until": "2026-11-19T22:59:59Z",
"customer": { "id": 903, "name": "Sample Machines Ltd" },
"total": { "amount": 21480, "currency": "EUR" }
}
],
"next_cursor": "MjAyNi0xMC0yMFQxNDowMzoxMS4yMDQ1MTgrMDA6MDB8NTEyMA",
"has_more": false
}GET/api/v1/quotes/{quote_id}One finalized quote
One finalized quote: its header, each assembly with its work plan, and its parts list. A finalized quote with no work plan answers work_plan_available: false and assemblies: []. A draft, or a quote reopened for a revision, answers 409 quote_not_finalized: keep your copy of the last finalized revision. Every finalized quote is served, also when your organization doesn't share finalized quotes with all its members.
- Sign-in
- Bearer key · permission
read - Rate limit
- Shared, 120 a minute per key
Path parameters
The quote's id, from the list or from an event.
Query parameters
The language of the name fields.
Responses
- 200
- Quote
- 400
invalid_request- 401
api_key_requiredinvalid_api_key- 403
organization_inactiveinsufficient_scope- 404
not_found- 405
method_not_allowed- 409
quote_not_finalized- 429
rate_limited
Response fields · 200 · Quote
The quote's id, what GET /quotes/{id} takes.
The number your customer reads, <quote number>-<revision>, e.g. "42-3".
1 for the first; counts up each time the quote is reopened and finalized again.
When this revision was finalized.
UTCWhen the offer expires.
UTCWho the quote is for; null when it names nobody.
Your customer's id in nuqo.
The customer's company, else the contact's name.
The quote's total as finalized, net, in the currency nuqo priced it in (the PDF may show it converted).
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
Whether the quote has a work plan; false for a quote that was never priced, or priced on nuqo's older labor model.
The quote's assemblies, in the order the Work plan tab lists them; empty when there is no work plan.
The assembly's reference on the quote; null for a quote that declares no assemblies (one plan for the whole order).
The assembly's name, else its reference.
Your own number for the assembly, what your ERP files the plan under; null when unset.
The quantity ordered of this assembly; null where the quote states none per assembly.
The pieces each row's piece_seconds is per (the count the run was charged across); null where its operations were charged across different counts.
One row per work-center visit, in the order the work flows.
The operation number: 10, 20, 30 … in the order the work flows.
Your own code for the work center, as set up in nuqo (your ERP's number where you number your work centers); null for an operation no work center was given — the row stays, with its name and time.
The work center's name, or the operation's own for a row with no work center.
Setup time. Setup is charged once per order: this is this assembly's share of it, by quantity, on the first row that needs it — so the setup over all assemblies adds up to the setup the quote charged.
sTime per piece, rounded to 0.01; null where the quote states none.
The row's run time for all piece_count pieces. setup_seconds + run_seconds is the row's total time; use it rather than multiplying the rounded piece_seconds back out. Over an assembly's rows, run_seconds adds up to the run time the quote charged for it.
The price of one piece of the assembly, net, as the quote PDF prints its row (per is "pcs"), in the currency of total. amount is quantity × unit_price, as on the PDF (on a quote priced before October 2026 a row can be a cent off, as it printed). Null where the PDF prints no row for it (no quantity ordered).
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
The unit amount is a price of: "pcs" for a piece, "m" for a meter, "ft" for a foot, "in" for an inch, "kg" for a kilogram; null for any other unit (the amount stands, look the line up). The list grows by addition only.
The assembly's row on the quote PDF: its price for the quantity ordered, net. The quote's other charges are in total only: setup the row does not carry, shipping, extra charges, the discount, and on a quote priced before September 2026 its overall margin. Null where unit_price is.
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
One per BOM line, in BOM order, matched or not.
Show the 12 fields of PartLine
The line's number in the BOM.
The line's id on the quote.
What your customer's BOM asks for.
The manufacturer part number the BOM states.
The manufacturer the BOM states.
Your customer's own number for the part.
The line's description.
Where the line's part search ended: selected, direct_match, manual (a user chose the part), customer_supplied (the customer provides it), not_found, unavailable, needs_search, pending; null where the line was never searched. New values may be added.
Whether the chosen part is another article than the line asks for (an alternate your customer approved is not).
The part chosen to buy; null when the line matched nothing.
The chosen part's manufacturer part number.
Its manufacturer.
Its product name.
Your own number for the part, from your listing of it in nuqo; null when unset.
Who the part is bought from; null when the line has no offer.
Who the part is bought from, as nuqo names them (e.g. "DigiKey"); a part of your own catalog reads "<short name> parts".
The supplier's order number for the part.
How much of the part the whole order needs.
The amount, in unit; null when the line has no offer.
Its unit: "pcs", or the line's measure ("m", "mm", …).
How much of the part is bought for the whole order, in the same unit as quantity: what it needs, rounded up to the supplier's pack, minimum order and order multiple. value null on a quote finalized before October 2026, whose record does not state it.
The amount, in unit; null when the line has no offer.
Its unit: "pcs", or the line's measure ("m", "mm", …).
The supplier's list price per unit, in the currency the part is bought in; null where the offer states no price per unit (only a pack's), and on a quote finalized before October 2026, whose record does not state it.
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
The unit amount is a price of: "pcs" for a piece, "m" for a meter, "ft" for a foot, "in" for an inch, "kg" for a kilogram; null for any other unit (the amount stands, look the line up). The list grows by addition only.
What buying ordered costs, in the quote's currency, at list price before your discounts and markups: the over-order beyond quantity included; amount null when the line has no offer.
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
Days to delivery: the supplier's lead time, its delivery time from stock when stock covers the order, or the default lead time of your price list where its row states none; null when unknown.
Example
curl https://backend.nuqo.io/api/v1/quotes/5120 \
-H "Authorization: Bearer $NUQO_KEY"{
"id": 5120,
"number": "147-2",
"revision": 2,
"finalized_at": "2026-10-20T14:03:11.204518Z",
"valid_until": "2026-11-19T22:59:59Z",
"customer": { "id": 903, "name": "Sample Machines Ltd" },
"total": { "amount": 21480, "currency": "EUR" },
"work_plan_available": true,
"assemblies": [
{
"id": "A1",
"name": "Control cabinet assembly",
"shop_part_number": "KS-40-117",
"quantity": 250,
"piece_count": 250,
"work_plan": [
{
"op_number": 10,
"work_center_id": "1010",
"name": "Cutting",
"setup_seconds": 180,
"piece_seconds": 42.5,
"run_seconds": 10625
},
{
"op_number": 20,
"work_center_id": "2030",
"name": "Crimping",
"setup_seconds": 300,
"piece_seconds": 96.12,
"run_seconds": 24030
},
{
"op_number": 30,
"work_center_id": null,
"name": "Special labeling",
"setup_seconds": 0,
"piece_seconds": 3.6,
"run_seconds": 900
}
],
"unit_price": { "amount": 84.32, "currency": "EUR", "per": "pcs" },
"amount": { "amount": 21080, "currency": "EUR" }
}
],
"parts": [
{
"line_number": 1,
"spec_id": "k7p2q9",
"requested": {
"mpn": "EX-H4",
"manufacturer": "Example Housings",
"internal_part_number": "C-100",
"description": "Housing, 4 pole"
},
"status": "selected",
"is_substitute": false,
"part": {
"mpn": "EX-H4",
"manufacturer": "Example Housings",
"name": "Housing 4p",
"shop_part_number": "300-118"
},
"supplier": { "name": "Example Supply", "sku": "ES-H4-01" },
"quantity": { "value": 480, "unit": "pcs" },
"ordered": { "value": 500, "unit": "pcs" },
"unit_price": { "amount": 1.42, "currency": "EUR", "per": "pcs" },
"line_total": { "amount": 710, "currency": "EUR" },
"lead_time_days": 14
},
{
"line_number": 2,
"spec_id": "m3r8t1",
"requested": {
"mpn": null,
"manufacturer": null,
"internal_part_number": "C-215",
"description": "Cable tie, black, 100 mm"
},
"status": "not_found",
"is_substitute": false,
"part": null,
"supplier": null,
"quantity": { "value": null, "unit": "pcs" },
"ordered": { "value": null, "unit": "pcs" },
"unit_price": null,
"line_total": { "amount": null, "currency": "EUR" },
"lead_time_days": null
}
]
}GET/api/v1/eventsWhat happened to your quotes
Your organization's events after the id you send, oldest first: a quote finalized, or reopened for a revision. Store next_after and send it as after next time; a full page means more are waiting. Events are kept 30 days.
- Sign-in
- Bearer key · permission
read - Rate limit
- Shared, 120 a minute per key
Query parameters
Answer only events with a higher id.
How many to answer at most.
Responses
- 200
- EventList
- 400
invalid_request- 401
api_key_requiredinvalid_api_key- 403
organization_inactiveinsufficient_scope- 405
method_not_allowed- 429
rate_limited
Response fields · 200 · EventList
The events after after, oldest first.
The event's id: dedupe on it, and send the last one handled as after.
quote.finalized or quote.reopened. Ignore a type you don't know: new ones may be added.
When it happened.
UTCThe quote it happened to.
The quote's id: read it with GET /quotes/{id}.
quote.finalized: the revision finalized. quote.reopened: the new revision being worked on.
What to send as after next time: the last event's id, or the after you sent when nothing is new.
Example
curl https://backend.nuqo.io/api/v1/events \
-H "Authorization: Bearer $NUQO_KEY"{
"data": [
{
"id": 20871,
"type": "quote.reopened",
"created_at": "2026-10-19T08:41:55.120334Z",
"quote": { "id": 5120, "revision": 2 }
},
{
"id": 20872,
"type": "quote.finalized",
"created_at": "2026-10-20T14:03:11.394120Z",
"quote": { "id": 5120, "revision": 2 }
}
],
"next_after": 20872
}GET/api/v1/materialsYour parts, least recently changed first
The rows of your Materials tab in nuqo, a page at a time. Page with next_cursor while has_more is true, then keep the last next_cursor and poll with it: it answers only the parts changed since. A change is served once it is 3 minutes old.
- Sign-in
- Bearer key · permission
read - Rate limit
- Shared, 120 a minute per key
Query parameters
Only parts whose row changed at or after this time. Without an offset it is UTC.
true also answers the parts a user deleted, with status "obsolete".
A next_cursor this API answered, passed back unchanged: reads on after the last row it ended on. Never build one.
How many to answer at most.
Responses
- 200
- MaterialList
- 400
invalid_request- 401
api_key_requiredinvalid_api_key- 403
organization_inactiveinsufficient_scope- 405
method_not_allowed- 429
rate_limited
Response fields · 200 · MaterialList
This page, least recently changed first.
This row's id in nuqo: its identity, for as long as it exists. Two of your catalogues listing one part are two rows with one part_id, each with its own listing_id and price.
The part's id in nuqo, for as long as it exists.
Which of your catalogues the row's price comes from; null for a price typed in or a part kept from nuqo's catalog, and once the catalogue is deleted.
The catalogue's id in nuqo.
Its name, as nuqo shows it: the one you gave it, else the supplier and edition (e.g. "Stocko 2025").
Your own article number for it; null when unset. Two parts can share one, so it is not a key.
The manufacturer part number.
The manufacturer, by the name the Materials tab shows.
Its description, else its name; "" when it has neither.
The category nuqo files it under, as a path, e.g. "Protection > Sleeving"; while nuqo files it under none, the category your file stated.
Where it is bought.
Who the part is bought from, as nuqo names them (e.g. "DigiKey"); a part of your own catalog reads "<short name> parts".
The supplier's order number for the part.
Your price, one entry per quantity break; empty when unpriced.
The least quantity the break's price applies to.
The most; null for no upper end.
The price for price_quantity order units.
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
The first break's price per content unit (per is the part's own content unit, never scaled); null when unpriced.
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
The unit amount is a price of: "pcs" for a piece, "m" for a meter, "ft" for a foot, "in" for an inch, "kg" for a kilogram; null for any other unit (the amount stands, look the line up). The list grows by addition only.
The first day the tiers' prices hold; null when no start is stated.
The last day the tiers' prices hold; null when no end is stated.
What one is ordered as, e.g. "stick", "reel"; null when unstated.
The unit one order unit holds ("pcs", "m", "ft", "kg"); null when unstated or none of them.
How many content units one order unit holds.
How many order units a break's price is for. A break's price buys price_quantity order units of no_cu_per_ou content units each.
The least you can order, in order units.
The step an order goes up in, in order units.
Delivery time.
daysStock as last stated; null when unknown.
Your notes on it.
Whether this price may price new quotes: false for a listing an import switched off or one from a price list a user retired; the part and its last prices are still served. Where two of your price lists price the same part, both can read true: quotes use one of them, the one your purchasing rule ranks first, else one not past its end date, else the one priced last.
"active", or "obsolete" for a part a user deleted (served with include_removed=true); a part of nuqo's global catalog you added can also be "soft_deleted" or "confirmed_gone" once its supplier stops listing it.
When the row last changed: what the list is ordered by.
UTCOpaque: pass it back unchanged as cursor to read on after this page's last part. On a page with no parts it is the cursor sent (null when none was).
Whether the next page already holds parts; false once you have caught up.
Example
curl https://backend.nuqo.io/api/v1/materials \
-H "Authorization: Bearer $NUQO_KEY"{
"materials": [
{
"listing_id": 918204,
"part_id": 4410273,
"catalogue": { "id": 112, "name": "Example Supply 2026" },
"org_sku": "300-118",
"mpn": "EX-H4",
"manufacturer": "Example Housings",
"description": "Housing, 4 pole, black",
"category_leaf": "Connectors > Housings",
"supplier": { "name": "Example Supply", "sku": "ES-H4-01" },
"tiers": [
{ "min_qty": 1, "max_qty": 499, "price": {"amount": 1.58, "currency": "EUR"} },
{ "min_qty": 500, "max_qty": null, "price": {"amount": 1.42, "currency": "EUR"} }
],
"unit_price": { "amount": 1.58, "currency": "EUR", "per": "pcs" },
"valid_from": "2026-01-01",
"valid_to": "2026-12-31",
"order_unit": null,
"content_unit": "pcs",
"no_cu_per_ou": 1,
"price_quantity": 1,
"minimum_order_quantity": 100,
"order_multiple": 50,
"lead_time_days": 14,
"quantity_in_stock": 2400,
"notes": null,
"prices_quotes": true,
"status": "active",
"changed_at": "2026-10-21T16:20:05.774812Z"
}
],
"next_cursor": "MjAyNi0xMC0yMVQxNjoyMDowNS43NzQ4MTIrMDA6MDB8NDQxMDI3Mw",
"has_more": false
}POST/api/v1/materials/batchesPush your parts
Up to 1000 of your parts, onto your ERP sync price list in nuqo: a new part is added, one you sent before is updated, a field you leave out keeps its value, and a row with removed: true takes its part off the list. Answers the batch's id at once; poll GET /materials/batches/{id} until it is done. One batch at a time: send more parts as consecutive batches.
- Sign-in
- Bearer key · permission
catalogue:write - Rate limit
- Shared, 120 a minute per key
Request body · MaterialBatch
Your parts, at most 1000; send more as consecutive batches. A part you leave out stays as it is.
Your own article number for it.
The manufacturer part number. With manufacturer and supplier, it says which of your parts the row is.
The manufacturer, as your system names it or as GET /materials answers it. One nuqo doesn't know yet is added as you spell it.
Its description.
Where you buy it: name and your supplier's order number. Leave it out for a part you make or stock yourself.
Who the part is bought from, as nuqo names them (e.g. "DigiKey"); a part of your own catalog reads "<short name> parts".
The supplier's order number for the part.
Your price, one entry per quantity break, all in one currency; the breaks may not overlap.
The least quantity the break's price applies to.
The most; null for no upper end.
The price for price_quantity order units.
The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).
ISO 4217 currency code, e.g. "EUR".
The first day the tiers' prices hold. Sent with the tiers; tiers sent without it hold from now.
The last day the tiers' prices hold. Sent with the tiers; tiers sent without it hold with no end.
What one is ordered as, e.g. "stick", "reel".
The unit one order unit holds.
How many content units one order unit holds.
How many order units a break's price is for.
The least you can order, in order units.
The step an order goes up in, in order units.
Delivery time.
daysStock; 0 is out of stock.
Your notes on it.
true: you no longer sell it, and it stops being priced from your ERP sync price list. Your other price lists keep theirs. mpn and manufacturer say which part, supplier which of two offers of it; the row's other fields are ignored. It still reads back from GET /materials, with prices_quotes false; pushing it again without removed lists it again. Removals apply after the batch's other rows: a batch that sends and removes one part ends with it removed.
Responses
- 202
- BatchCreated
- 400
invalid_request- 401
api_key_requiredinvalid_api_key- 403
organization_inactiveinsufficient_scope- 405
method_not_allowed- 409
batch_in_progresscatalogue_retired- 413
batch_too_large- 422
invalid_row- 429
rate_limited
Response fields · 202 · BatchCreated
The batch's id: poll GET /materials/batches/{id} with it.
Example
curl https://backend.nuqo.io/api/v1/materials/batches \
-H "Authorization: Bearer $NUQO_KEY" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{
"org_sku": "A-10442",
"mpn": "1-968849-1",
"manufacturer": "TE Connectivity",
"description": "Receptacle 2.8 mm, tin",
"supplier": { "name": "Example Supply", "sku": "ES-968849" },
"tiers": [
{ "min_qty": 1, "max_qty": 999, "price": {"amount": 0.42, "currency": "EUR"} },
{ "min_qty": 1000, "max_qty": null, "price": {"amount": 0.31, "currency": "EUR"} }
],
"valid_from": "2026-11-01",
"valid_to": "2027-03-31",
"order_unit": "bag",
"content_unit": "pcs",
"no_cu_per_ou": 100,
"price_quantity": 1,
"minimum_order_quantity": 1,
"order_multiple": 1,
"lead_time_days": 5,
"quantity_in_stock": 4200,
"notes": null
},
{ "mpn": "FLRY-B 0.5", "manufacturer": "Coroplast", "lead_time_days": 10, "quantity_in_stock": 0 },
{
"mpn": "GR-1100",
"manufacturer": "Example Grommets",
"supplier": { "name": "digikey.com", "sku": null }
},
{
"mpn": "1-968850-1",
"manufacturer": "TE Connectivity",
"supplier": { "name": "Example Supply", "sku": "ES-968850" },
"removed": true
}
]
}'{ "batch_id": 88214 }GET/api/v1/materials/batches/{batch_id}A batch of your parts
Where a batch you pushed is, and once it is done, what it did.
- Sign-in
- Bearer key · permission
catalogue:write - Rate limit
- Shared, 120 a minute per key
Path parameters
The batch_id POST /materials/batches answered.
Responses
- 200
- Batch
- 401
api_key_requiredinvalid_api_key- 403
organization_inactiveinsufficient_scope- 404
not_found- 405
method_not_allowed- 429
rate_limited
Response fields · 200 · Batch
The batch's id.
"pending" (waiting its turn), "running", "done" or "failed" (nothing more will be taken; send it again). Poll until done or failed.
What the batch did, once it is done; null until then, and for a failed batch.
How many rows the batch had.
How many of them are in nuqo now, new or updated.
How many of them took a part off your ERP sync price list (removed: true).
The rows that were not taken, each with why — a removal that found nothing to remove included.
The row's place in rows, from 0.
Its manufacturer part number.
Why it wasn't taken, for people; its wording may change.
Example
curl https://backend.nuqo.io/api/v1/materials/batches/88214 \
-H "Authorization: Bearer $NUQO_KEY"{
"id": 88214,
"status": "done",
"summary": {
"rows": 4,
"imported": 2,
"removed": 1,
"not_imported": [
{
"index": 2,
"mpn": "GR-1100",
"reason": "'digikey.com' is on a domain several markets' storefronts share — name the storefront you buy from"
}
]
}
}Not in the reference yet: webhooks. They appear here, generated like the rest, on the day they ship.