NutraSoft food manufacturing ERP software
Esc
  • GuideGetting startedThree steps to your first call and your first order, then the two shapes every response takes.
  • GuideAuthentication and keysBearer keys, test and live environments, rotation without an outage, and what each scope grants.
  • GuideTest modeA second NutraSoft account of your own, seeded with realistic data, that you can safely break.
  • PageErrorsEvery error code the API returns, what it means, what to do about it and whether to retry.
  • PageConventionsThe rules every operation follows: money, time, paging, sorting, nulls, PATCH and identifiers.
Menu

Lots and incremental sync

Base units, adjustments versus absolute updates, quality statuses, and how to poll for stock changes.
Updated

In short

  • A lot is sellable when qc_status is AVAILABLE, or RELEASED with a qc_release_expires_at that is null or still ahead.
  • A lot whose lot_number is null is the generic lot: real stock, not a data error.
  • A retired lot is archived and stays readable. Pass archived=false for live stock.
  • Adjustment adds signed base_qty; Update sets an absolute quantity_base. Both record the change as an ADJUSTMENT stock movement in traceability.
  • Send the lot's lease back when you update it, above all quantity_base: if the lot changed since your read, the update is refused with STALE_LEASE instead of overwriting stock.
  • updated_since is strictly greater than, follows the parent row only and never reports deletions. Overlap each poll by a minute.

A lot is a quantity of one product, in one location, that arrived or was made together. Everything about stock on this API is expressed in lots.

To list one product's lots, filter GET /product-lots with product_id or with its product_code, an exact match that ignores case. Send both and they must name the same product, or the list is empty.

Quantities are base units

quantity_base is in the product's base unit, on every operation, in both directions. To show a lot in the product's inventory unit, divide by both factors on the lot: quantity_base / (uom_qty * uom_base_conversion). See Units and quantities.

Quality status

qc_status has five values: AVAILABLE, RELEASED, ON_HOLD, QUARANTINE and REJECTED. Two of them are sellable: AVAILABLE and RELEASED.

Under positive release, production output starts ON_HOLD and moves to RELEASED, never back to AVAILABLE, so filtering on AVAILABLE alone misses every positively released finished good.
StatusMeans
AVAILABLEIn stock, and no release decision was needed. Sellable.
RELEASEDSigned off by quality. Sellable until qc_release_expires_at, if set.
ON_HOLDMade or received, not yet released. Yours, not sellable.
QUARANTINEHeld pending investigation. Yours, not sellable.
REJECTEDFailed. Not sellable.

Release expiry

A release can carry a retest date, published as qc_release_expires_at. Only RELEASED lots have one, and it is null when the release is open-ended. Once it has passed, NutraSoft will not ship the lot or use it in production until quality retests and releases it again. The sellable rule, in full: qc_status is AVAILABLE, or it is RELEASED and qc_release_expires_at is null or still in the future.

The generic lot

A lot whose lot_number is null is the generic lot: the unnumbered bucket created with a product to hold stock that was never lotted. It is a real row with a real quantity. Do not filter it out, and do not treat a null lot number as a data error.

Locations

A lot's location is location_id: a warehouse id or a location inside one. When you create a lot, the warehouse id you send is location_id. To list the stock of a whole warehouse, filter GET /product-lots with warehouse_id.

Once a production run is COMPLETED, its own location_id and lot_number are where the finished product went, including when the operator changed them at close. For the exact lot, its location's name and how much the run made, read output on GET /productions/{production_id}.

Retiring a lot

A lot that is used up, spoiled or written off is archived: archived becomes true and the row stays readable with its history. Without an archived filter, the list returns both, so pass archived=false for live stock and archived=true to audit what was retired. A lot can also be deleted outright, when someone removes it from a product in NutraSoft; a deleted lot simply stops appearing.

Adjustment versus Update

Use Adjustment for stock added, removed, spoiled or corrected when the change must appear in traceability. Pass the lot's id in the URL and a signed base_qty in the body. Use Update to set the balance to a total you counted, or to edit the expiry. Update records the difference from the current balance as the same ADJUSTMENT movement, so both appear in traceability. A lot's number and location are fixed once it exists.

Adjustment: POST /product-lots/{lot_id}/adjustmentsUpdate: PATCH /product-lots/{lot_id}
Quantity fieldSigned base_qty: a changequantity_base: the new total
Starting at 100, send 25Balance becomes 125Balance becomes 25
Starting at 100, remove 25Send base_qty = -25; balance becomes 75Send quantity_base = 75; negative totals are refused
TraceabilityCreates an ADJUSTMENT movement with optional notes and a Created via API labelCreates an ADJUSTMENT movement for the difference with a Created via API label; sending the current balance creates none
AccountingPosts applicable adjustment entries; enabled QuickBooks sync runs after commitSame, for the difference
Lot detailsKeeps the lot number, location and expiryCan edit the expiry; lot_number and location_id are refused with 400
ConcurrencyApplies the delta while holding a lot lock; optional leaseRecords the difference while holding a lot lock; send lease to protect a total worked out from a stale read
bash
curl -X POST 'https://public-api.nutrasoft.ca/v1/product-lots/123/adjustments' \
  -H "Authorization: Bearer $NUTRASOFT_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: lot-123-damaged-2026-10-09-001' \
  -d '{"base_qty": -5.5, "notes": "Damaged stock removed"}'

Adjustment requires inventory:write and returns the updated lot in data. Quantities are rounded to six decimal places; a change that rounds to zero is refused with 400. Insufficient stock and archived lots return 409, while a missing lot returns 404. Unarchive a retired lot first. Send an optional lease to require that it still matches your last read; otherwise the delta applies to the current balance. A successful adjustment updates linked Shopify stock and runs enabled QuickBooks adjustment sync after commit; integration failures do not undo the stock change.

Use a unique Idempotency-Key for each adjustment and reuse it when retrying that same request. Without it, every successful retry applies the delta again. API-created stock movements carry a server-written source marker and show Created via API in NutraSoft lot traceability. An Update that changes quantity records the same movement for the difference, so retrying an Update with the same total records nothing more. Like Adjustment, an Update that changes quantity returns 409 on an archived lot.

Updating a lot without losing stock

Every lot carries a lease: a token for the state you read. It changes whenever the lot number, location, quantity, expiration date or archived state does, whoever made the change. Send it back in the body of PATCH /product-lots/{lot_id} and the update is applied only if the lot is still as you read it. Otherwise nothing is written and you get 409 STALE_LEASE.

json
{
  "quantity_base": 18250,
  "lease": "3f9a1c0b7e2d4a6f8b1c9e0d"
}
Send the lease whenever you write quantity_base. A quantity worked out from a read is wrong as soon as NutraSoft ships, receives or consumes from the lot, and written without a lease it undoes that movement with an adjustment of its own.
  1. On STALE_LEASE, read the lot again.
  2. Redo your change against the values you get back, not the ones you held.
  3. Send it with the new lease.

The lease is optional: an update without one is applied as before. Send it back exactly as received. A change to the lot's quality status leaves it as it is.

Incremental sync with updated_since

Seven resources accept updated_since: products, product lots, sales orders, invoices, purchase orders, customers and suppliers. Each publishes created_at and updated_at and can be sorted by them. Four rules are not guessable, so read them before you build a sync:

  • It is strictly greater than. A row whose updated_at equals the value you pass is not returned again.
  • It tracks the parent row only. Editing a sales order's lines does not necessarily move the order's updated_at. If you care about lines, re-fetch the documents you hold on a slower cycle.
  • Timestamps are written when the row is saved, not when its transaction commits. A slow transaction can land a row whose updated_at is slightly earlier than rows you have already seen. Overlap each poll by a minute and de-duplicate by id.
  • Deletions are invisible. Archiving a lot moves its updated_at, so an archived lot shows up in the feed. A deleted row, a lot included, simply stops appearing, and polling will never tell you about it.

A sync that holds up

  1. On the first run, page through the whole resource with sort=updated_at:asc.
  2. Record the highest updated_at you saw.
  3. Poll with updated_since set to that value minus a minute, sorted the same way, and upsert by id.
  4. On a slow cycle, nightly or weekly, re-list the resource in full to catch deletions.

Offset paging stops at an offset of 10,000. For a large first run, sort by updated_at and move updated_since forward to the last updated_at of each page, minus the same overlap, instead of paging deeper.

Polling tells you what changed since you last asked, but not that it changed. Webhooks push each change to you within seconds, including line edits and deletions, which updated_since cannot report. Most integrations run both: webhooks to react, a periodic sweep to reconcile.