Skip to content
nuqo

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.

Base URLhttps://backend.nuqo.io/api/v1Versionv1Sign-inBearer keyopenapi.json

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

  1. 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.
  2. Send it on every request as Authorization: Bearer nq_live_…. No cookie, no CSRF token. Every address starts with https://backend.nuqo.io/api/v1.
  3. Test it. GET /organization answers the organization the key belongs to.
Request · curl
curl https://backend.nuqo.io/api/v1/organization \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{ "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.

Request · curl
curl "https://backend.nuqo.io/api/v1/quotes?finalized_since=2026-10-01T00:00:00Z&limit=50" \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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:

op_number
integer

The operation number: 10, 20, 30 … in the order the work flows.

work_center_id
string | null

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.

name
string

The work center's name, or the operation's own for a row with no work center.

setup_seconds
number

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.

s
piece_seconds
number | null

Time per piece, rounded to 0.01; null where the quote states none.

s / piece
run_seconds
number

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.

s

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 rounded piece_seconds back 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_seconds adds up to the run time the quote charged for it, and setup_seconds + run_seconds to the assembly’s total time.
  • A row with work_center_id: null is 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).

Request · curl
curl "https://backend.nuqo.io/api/v1/events?after=20870" \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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.finalized

Fetch 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.reopened

Hold 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_after and send it as after next 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 after older than that answers the oldest events still kept; read the quotes list to catch up.

Webhooks

Coming · not available yet

Instead 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.body in a Nuqo-Signature header, 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.

  1. Send a batch of up to 1,000 rows. A row has the fields of a GET /materials row, under the same names, so a row read from nuqo can be sent back as it is; mpn and manufacturer are required. The maker, mpn and supplier say 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 as null, keeps the value nuqo holds, and a part the batch leaves out stays as it is. valid_from and valid_to are the days the row’s tiers hold, 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, mpn and supplier say which part, as on any row; its other fields are ignored. It still reads back from GET /materials, with prices_quotes false (not as obsolete: that is a part a user deleted), and sending its row again without removed lists 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 422 and 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.

  2. Poll the batch every few seconds while its status is pending or running.
    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": []} }

    done carries the summary: the rows sent, how many were imported and removed, and in not_imported each row nuqo didn’t write, by its index in your batch, with the reason: a removal of a part your list doesn’t hold is one, “nothing to remove”. failed means 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 answers 409 batch_in_progress: send it once the poll says done.

  3. Read them back with GET /materials, changed_since set 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’s listing_id and part_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.

  1. First, a full pull: no changed_since, include_removed=true, paging with next_cursor while has_more is true. A part changed while the pull runs comes again, further on, with its new values: replace the copy you hold.
  2. Then the changes: keep the last next_cursor and poll with it (and include_removed=true). You get the parts added, edited, repriced or deleted since. A change is served once it is 3 minutes old.
  3. 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.
Request · curl
curl "https://backend.nuqo.io/api/v1/materials?include_removed=true&limit=100" \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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.

Response
{
  "error": { "code": "quote_not_finalized", "message": "The quote is a draft or a revision in progress." }
}
StatusCodeWhen
400invalid_requestA parameter is malformed or out of range, or a push’s body holds no rows
401api_key_requiredinvalid_api_keyNo key, or a key that is unknown or revoked
403organization_inactiveinsufficient_scopeYour 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)
404not_foundNo such resource in your organization (another organization’s is not found too), or no such address
405method_not_allowedThe address doesn’t take this method; the Allow header names the ones it does
409quote_not_finalizedbatch_in_progresscatalogue_retiredThe 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
413batch_too_largeA batch of more than 1,000 parts
422invalid_rowA part of your batch is refused; the message names the first one by its index, e.g. rows[3]: tiers.0.price.amount: …
429rate_limitedOver 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.

  1. A1
    Events

    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.

  2. A2
    Sign-in

    A static key in an Authorization header is fine for your system.

    If not: Tell us what it needs (e.g. OAuth2 client credentials).

  3. A3
    Quotes

    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.

  4. A4
    Work 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.

  5. A5
    Revisions

    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.

  6. A6
    Parts sync

    Your ERP leads on prices, stock, lead time and article data.

    If not: Name the groups nuqo should lead.

  7. A7
    Parts sync

    Your article number (org_sku) is your key for a part.

    If not: Tell us the field that is.

  8. A8
    Parts 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

id
integer

Your organization's id in nuqo.

name
string

Your organization's name.

Example

Request · curl
curl https://backend.nuqo.io/api/v1/organization \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{ "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

finalized_since
timestamp

Only quotes finalized at or after this time. Without an offset it is UTC.

cursor
string

A next_cursor this API answered, passed back unchanged: reads on after the last row it ended on. Never build one.

limit
integer · 1–100 · default 50

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

quotes[]
list of QuoteHeader

This page, oldest finalization first.

id
integer

The quote's id, what GET /quotes/{id} takes.

number
string

The number your customer reads, <quote number>-<revision>, e.g. "42-3".

revision
integer

1 for the first; counts up each time the quote is reopened and finalized again.

finalized_at
timestamp

When this revision was finalized.

UTC
valid_until
timestamp | null

When the offer expires.

UTC
customer
Customer | null

Who the quote is for; null when it names nobody.

id
integer

Your customer's id in nuqo.

name
string

The customer's company, else the contact's name.

total
Money

The quote's total as finalized, net, in the currency nuqo priced it in (the PDF may show it converted).

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

next_cursor
string | null

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).

has_more
boolean

Whether the next page already holds quotes; false once you have caught up.

Example

Request · curl
curl https://backend.nuqo.io/api/v1/quotes \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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

quote_idrequired
integer

The quote's id, from the list or from an event.

Query parameters

lang
"de" | "en" · default "en"

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

id
integer

The quote's id, what GET /quotes/{id} takes.

number
string

The number your customer reads, <quote number>-<revision>, e.g. "42-3".

revision
integer

1 for the first; counts up each time the quote is reopened and finalized again.

finalized_at
timestamp

When this revision was finalized.

UTC
valid_until
timestamp | null

When the offer expires.

UTC
customer
Customer | null

Who the quote is for; null when it names nobody.

id
integer

Your customer's id in nuqo.

name
string

The customer's company, else the contact's name.

total
Money

The quote's total as finalized, net, in the currency nuqo priced it in (the PDF may show it converted).

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

work_plan_available
boolean

Whether the quote has a work plan; false for a quote that was never priced, or priced on nuqo's older labor model.

assemblies[]
list of Assembly

The quote's assemblies, in the order the Work plan tab lists them; empty when there is no work plan.

id
string | null

The assembly's reference on the quote; null for a quote that declares no assemblies (one plan for the whole order).

name
string

The assembly's name, else its reference.

shop_part_number
string | null

Your own number for the assembly, what your ERP files the plan under; null when unset.

quantity
integer | null

The quantity ordered of this assembly; null where the quote states none per assembly.

pieces
piece_count
integer | null

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.

pieces
work_plan[]
list of WorkPlanRow

One row per work-center visit, in the order the work flows.

op_number
integer

The operation number: 10, 20, 30 … in the order the work flows.

work_center_id
string | null

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.

name
string

The work center's name, or the operation's own for a row with no work center.

setup_seconds
number

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.

s
piece_seconds
number | null

Time per piece, rounded to 0.01; null where the quote states none.

s / piece
run_seconds
number

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.

s
unit_price
UnitPrice | null

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).

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

per
string | null

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.

amount
Money | null

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.

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

parts[]
list of PartLine

One per BOM line, in BOM order, matched or not.

Show the 12 fields of PartLine
line_number
integer | null

The line's number in the BOM.

spec_id
string | null

The line's id on the quote.

requested
RequestedPart

What your customer's BOM asks for.

mpn
string | null

The manufacturer part number the BOM states.

manufacturer
string | null

The manufacturer the BOM states.

internal_part_number
string | null

Your customer's own number for the part.

description
string | null

The line's description.

status
string | null

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.

is_substitute
boolean

Whether the chosen part is another article than the line asks for (an alternate your customer approved is not).

part
ChosenPart | null

The part chosen to buy; null when the line matched nothing.

mpn
string

The chosen part's manufacturer part number.

manufacturer
string | null

Its manufacturer.

name
string | null

Its product name.

shop_part_number
string | null

Your own number for the part, from your listing of it in nuqo; null when unset.

supplier
Supplier | null

Who the part is bought from; null when the line has no offer.

name
string | null

Who the part is bought from, as nuqo names them (e.g. "DigiKey"); a part of your own catalog reads "<short name> parts".

sku
string | null

The supplier's order number for the part.

quantity
Quantity

How much of the part the whole order needs.

value
number | null

The amount, in unit; null when the line has no offer.

unit
string | null

Its unit: "pcs", or the line's measure ("m", "mm", …).

ordered
Quantity

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.

value
number | null

The amount, in unit; null when the line has no offer.

unit
string | null

Its unit: "pcs", or the line's measure ("m", "mm", …).

unit_price
UnitPrice | null

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.

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

per
string | null

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.

line_total
Money

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.

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

lead_time_days
integer | null

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.

days

Example

Request · curl
curl https://backend.nuqo.io/api/v1/quotes/5120 \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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

after
integer · ≥ 0 · default 0

Answer only events with a higher id.

limit
integer · 1–100 · default 100

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

data[]
list of Event

The events after after, oldest first.

id
integer

The event's id: dedupe on it, and send the last one handled as after.

type
string

quote.finalized or quote.reopened. Ignore a type you don't know: new ones may be added.

created_at
timestamp

When it happened.

UTC
quote
EventQuote

The quote it happened to.

id
integer

The quote's id: read it with GET /quotes/{id}.

revision
integer

quote.finalized: the revision finalized. quote.reopened: the new revision being worked on.

next_after
integer

What to send as after next time: the last event's id, or the after you sent when nothing is new.

Example

Request · curl
curl https://backend.nuqo.io/api/v1/events \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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

changed_since
timestamp

Only parts whose row changed at or after this time. Without an offset it is UTC.

include_removed
boolean · default false

true also answers the parts a user deleted, with status "obsolete".

cursor
string

A next_cursor this API answered, passed back unchanged: reads on after the last row it ended on. Never build one.

limit
integer · 1–100 · default 50

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

materials[]
list of Material

This page, least recently changed first.

listing_id
integer

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.

part_id
integer

The part's id in nuqo, for as long as it exists.

catalogue
Catalogue | null

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.

id
integer

The catalogue's id in nuqo.

name
string

Its name, as nuqo shows it: the one you gave it, else the supplier and edition (e.g. "Stocko 2025").

org_sku
string | null

Your own article number for it; null when unset. Two parts can share one, so it is not a key.

mpn
string | null

The manufacturer part number.

manufacturer
string | null

The manufacturer, by the name the Materials tab shows.

description
string

Its description, else its name; "" when it has neither.

category_leaf
string | null

The category nuqo files it under, as a path, e.g. "Protection > Sleeving"; while nuqo files it under none, the category your file stated.

supplier
Supplier

Where it is bought.

name
string | null

Who the part is bought from, as nuqo names them (e.g. "DigiKey"); a part of your own catalog reads "<short name> parts".

sku
string | null

The supplier's order number for the part.

tiers[]
list of PriceBreak

Your price, one entry per quantity break; empty when unpriced.

min_qty
integer

The least quantity the break's price applies to.

max_qty
integer | null

The most; null for no upper end.

price
Money

The price for price_quantity order units.

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

unit_price
UnitPrice | null

The first break's price per content unit (per is the part's own content unit, never scaled); null when unpriced.

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

per
string | null

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.

valid_from
string | null

The first day the tiers' prices hold; null when no start is stated.

valid_to
string | null

The last day the tiers' prices hold; null when no end is stated.

order_unit
string | null

What one is ordered as, e.g. "stick", "reel"; null when unstated.

content_unit
string | null

The unit one order unit holds ("pcs", "m", "ft", "kg"); null when unstated or none of them.

no_cu_per_ou
number

How many content units one order unit holds.

price_quantity
number

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.

minimum_order_quantity
integer | null

The least you can order, in order units.

order_multiple
integer | null

The step an order goes up in, in order units.

lead_time_days
integer | null

Delivery time.

days
quantity_in_stock
integer | null

Stock as last stated; null when unknown.

notes
string | null

Your notes on it.

prices_quotes
boolean

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.

status
string

"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.

changed_at
timestamp

When the row last changed: what the list is ordered by.

UTC
next_cursor
string | null

Opaque: 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).

has_more
boolean

Whether the next page already holds parts; false once you have caught up.

Example

Request · curl
curl https://backend.nuqo.io/api/v1/materials \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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

rows[]
list of MaterialRow

Your parts, at most 1000; send more as consecutive batches. A part you leave out stays as it is.

org_sku
string | null

Your own article number for it.

mpn
string

The manufacturer part number. With manufacturer and supplier, it says which of your parts the row is.

manufacturer
string

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.

description
string | null

Its description.

supplier
Supplier | null

Where you buy it: name and your supplier's order number. Leave it out for a part you make or stock yourself.

name
string | null

Who the part is bought from, as nuqo names them (e.g. "DigiKey"); a part of your own catalog reads "<short name> parts".

sku
string | null

The supplier's order number for the part.

tiers[]
list of PriceBreak | null

Your price, one entry per quantity break, all in one currency; the breaks may not overlap.

min_qty
integer

The least quantity the break's price applies to.

max_qty
integer | null

The most; null for no upper end.

price
Money

The price for price_quantity order units.

amount
number | null

The amount, net, in currency; null where there is none to state (a quote never priced, a line with no offer).

currency
string

ISO 4217 currency code, e.g. "EUR".

valid_from
string | null

The first day the tiers' prices hold. Sent with the tiers; tiers sent without it hold from now.

valid_to
string | null

The last day the tiers' prices hold. Sent with the tiers; tiers sent without it hold with no end.

order_unit
string | null

What one is ordered as, e.g. "stick", "reel".

content_unit
string | null

The unit one order unit holds.

no_cu_per_ou
number | null

How many content units one order unit holds.

price_quantity
number | null

How many order units a break's price is for.

minimum_order_quantity
integer | null

The least you can order, in order units.

order_multiple
integer | null

The step an order goes up in, in order units.

lead_time_days
integer | null

Delivery time.

days
quantity_in_stock
integer | null

Stock; 0 is out of stock.

notes
string | null

Your notes on it.

removed
boolean | null

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

batch_id
integer

The batch's id: poll GET /materials/batches/{id} with it.

Example

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": 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
    }
  ]
}'
Response202
{ "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

batch_idrequired
integer

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

id
integer

The batch's id.

status
string

"pending" (waiting its turn), "running", "done" or "failed" (nothing more will be taken; send it again). Poll until done or failed.

summary
BatchSummary | null

What the batch did, once it is done; null until then, and for a failed batch.

rows
integer

How many rows the batch had.

imported
integer

How many of them are in nuqo now, new or updated.

removed
integer

How many of them took a part off your ERP sync price list (removed: true).

not_imported[]
list of RowNotImported

The rows that were not taken, each with why — a removal that found nothing to remove included.

index
integer

The row's place in rows, from 0.

mpn
string | null

Its manufacturer part number.

reason
string

Why it wasn't taken, for people; its wording may change.

Example

Request · curl
curl https://backend.nuqo.io/api/v1/materials/batches/88214 \
  -H "Authorization: Bearer $NUQO_KEY"
Response200
{
  "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.