28 September 2026
v1.16: lot expiry settings on products
A product can now require an expiry date on the lots production makes, and can carry a shelf life that fills the expiry in.
Added- expiryRequired on products: when true, completing production of the product needs an expiry date for the output lot.
- shelfLifeDays on products: days from the made date to expiry, or null. The web app fills the expiry in with it on the completion screens.
- Both can be set when you create or update a product.
28 September 2026
v1.15: line discounts in money
A line's discount can now be entered as an amount of money off the whole line, as well as a percentage. Sales order, quote and invoice lines report the amount when it was entered that way.
Added- discountAmount on sales order and invoice lines: the line's discount in money when it was entered as an amount, or null when it was entered as a percentage. It is the whole line's discount, not per unit.
- POST /sales-orders accepts discountAmount on a line. It wins over discountPct, which is derived from it (discountAmount divided by quantity times unitPrice). An amount more than the line itself is a 422.
Notes- discountPct is still on every line and is still what the totals use, so existing integrations need no change. It can now carry up to ten decimal places, so an amount lands on the exact cent.
- An invoice raised for part of an order line carries the same share of a money discount, to the cent.
28 September 2026
v1.14: customer product discounts in the price lookup
A customer can now have their own discount on particular products, a percentage off whatever price applies to them. GET /products/{id}/prices reports it when you pass a customerId, so a webstore can price a line the way Frostbyte Pro does.
Added- appliedLineDiscountPct on GET /products/{id}/prices: the customer's discount on that product as a FRACTION (0.0935 = 9.35%), or null. Frostbyte Pro applies it as the line discount, so the line's net is its total times (1 minus appliedLineDiscountPct), before any order-level discount.
Notes- resolvedPrice is unchanged and is still the price before any discount. appliedDiscountPct is still the customer's standing, order-level discount.
- POST /sales-orders does not look discounts up: send each line's discountPct as before.
16 September 2026
v1.13: JOB_USE stock adjustment reason
Stock adjustments have a new reason, JOB_USE, for stock taken off by a job materials import. Like STOCK_REQUISITION it is written by Frostbyte Pro itself and cannot be set through the API.
Added- JOB_USE in the reason enum of stock adjustments returned by GET /stock-adjustments, and in the values its reason filter accepts.
Notes- POST /stock-adjustments rejects reason JOB_USE with 400, as it already rejects STOCK_REQUISITION.
15 September 2026
v1.12: zone types on warehouse zones
Each zone returned by GET /warehouses can now carry a zone type: what the area is, such as Freezer or Cool room, or a type the organisation added. It is additive and independent of zoneType, which stays the workflow role.
Added- zones[].zoneCategory on GET /warehouses: { id, name, builtInKey, needsTemperatureRecords }, or null when the zone has no type. builtInKey is FREEZER, COOL_ROOM, REFRIGERATED, AMBIENT, RACKING, PICKING or QUARANTINE for the built-in types, and null for a type the organisation added.
Notes- zones[].zoneType is unchanged: the same seven values, and it still decides where returned stock is put back. It now has a description in the OpenAPI document saying so.
- Zone temperature readings are not available through the API yet.
29 August 2026
v1.11: service lines for Non stocked products
A sales-order line whose product is NON_STOCKED (a service, fee or add-on such as labour or a call-out charge) is now a service line. Nothing changes in the request or response shapes; the change is in what the order-to-cash transitions do with those lines.
Changed- POST /sales-orders/{id}/confirm no longer stock-checks or reserves lines for NON_STOCKED products, so an order can carry labour or a fee alongside stocked lines (or on its own) and still confirm. Stocked lines are checked and reserved exactly as before.
- Service lines report pickedQty, packedQty and dispatchedQty equal to quantity as the order reaches PICKING, PACKED and DISPATCHED. POST /sales-orders/{id}/dispatch ignores any per-line quantity supplied for a service line and always ships its whole packed balance with the wave, so a service line never appears as a backorder.
- POST /sales-orders/{id}/invoice bills a service line in full even while the order is PARTIALLY_DISPATCHED; stocked lines keep billing what has shipped.
- Shipments: an order rolls up to DISPATCHED once every stocked line is fully shipped; service lines never need a shipment of their own.
- POST /sales-orders/{id}/cancel releases and returns stock for stocked lines only; service lines hold no stock, so nothing moves for them and no stock.level_changed event is raised for their product.
- PUT /products/{id} returns 422 when productType would change to or from NON_STOCKED while the product has stock on hand, lot quantities, reservations or lines on an open (CONFIRMED to PARTIALLY_DISPATCHED) sales order. Writing the same type back is still accepted.
1 August 2026
v1.10: credit notes, production orders, BOM reads, lot-level order detail, allocation policy, and batch webhooks
v1.10 opens the manufacturing side of Frostbyte Pro to integrators: read-only credit notes and production orders, a product BOM endpoint, per-lot availability on batches, lot allocations on sales-order lines, and the product's pick-time allocation policy. It also adds three webhook events, widens stock.level_changed coverage, and publishes two overdue breaking-change notices for ledger-integrity enforcement that went live on 26 July. Everything below is live on /api/v1 now.
Breaking- Stock adjustments: a line for a batch-tracked product must now carry batchId, naming the lot the adjustment applies to; a lot-less line returns 422 invalid_reference. This applies to positive and negative lines, on DRAFT and COMPLETED adjustments alike (a draft's lines persist and complete later). It enforces the stock-ledger invariant that quantities and lots always agree, since picking, FEFO, and recalls work off lots. Migration: fetch the product's lots from GET /batches?productId=... and pass the batchId on each line for batch-tracked products. This enforcement took effect on 26 July 2026 with a ledger-integrity release; the OpenAPI document and this notice are catching up now, and we should have published it at the time.
- Products: PUT /products/{id} no longer accepts a change to isBatchTracked while the product has stock on hand or lot quantities; the change returns 422 with a message naming the direction and the fix. Writing the same value back is still accepted (idempotent PUTs keep working), and a zero-stock product flips freely. Enabling tracking over anonymous stock would create quantities with no lots behind them, and disabling it would orphan the lots, so stock must be adjusted to zero first (the same rule the app and comparable platforms enforce). This gate also took effect on 26 July 2026 and is being documented now rather than at the time.
Added- Products now report allocationPolicy (FEFO or MANUAL), the pick-time lot-selection policy for batch-tracked products: FEFO auto-selects earliest-expiring lots, MANUAL means a person picks the lot (built for roll and dye-lot stock). It is accepted on POST /products and PUT /products/{id} with one coercion, matching the app: a product that is not batch tracked always stores FEFO regardless of the payload, and a PUT that disables batch tracking (permitted only at zero stock, see above) resets the stored policy to FEFO.
- Credit notes (read-only, new credit-notes scope family): GET /credit-notes and GET /credit-notes/{id} expose credit notes raised in the app: status (DRAFT, APPROVED, VOID), customer, the linked invoice and its originating sales order where they exist, totals, the returnStock flag with the restock warehouse, void metadata, and lines on the detail. List filters: status (comma list), customerId, dateFrom/dateTo on creditDate, search, modifiedSince, sortBy/sortDir. There is no stored per-credit-note applied amount: an APPROVED credit note linked to an invoice was applied in full at approval time (historical approvals were clamped at the invoice total). Creating, approving, and voiding stay in-app.
- GET /products/{id}/bom (products.view): the ordered recipe for a BOM product: ingredient product references, quantity per one base unit of the parent, wastagePercent (a percentage 0 to 100, not a fraction), isPhantom for sub-recipes exploded inline, sortOrder, and the optional grouping label. A product that is not a BOM product returns 200 with isBomProduct: false and empty lines, so any id can be probed without special-casing; a foreign or unknown id returns 404.
- Production orders (read-only, new production-orders scope family, manufacturing.view): GET /production-orders lists runs newest first with the output product, planned quantity, schedule dates, warehouse, priority, and the linked sales order id for make-to-order runs; filters status, productId (validated in-org, 404 on a foreign id), dateFrom/dateTo on plannedStartDate, search, modifiedSince. GET /production-orders/{id} adds the INPUT (ingredient demand) and OUTPUT (goods produced) lines with planned and actual quantities, unit costs, and the lots consumed from or minted into where allocated. Lines are capped at 1000 with linesTruncated/linesTotal beside the array.
- Per-lot availability on GET /batches/{id}: each item in the per-warehouse breakdown now carries availableQuantity, the item's quantity minus unpicked sales-order allocations against that lot, so you can see committed lot stock. The existing quantity field is unchanged (gross on hand).
- Lot allocations on GET /sales-orders/{id}: each line now carries lots, an array of { batchId, batchNumber, quantity, isPicked } aggregated from the line's allocations (empty when none). The list endpoint is unchanged, so incremental syncs stay cheap.
- Three new webhook events: credit_note.approved (the credit was applied to its linked invoice and any returned stock booked back in), credit_note.voided (its financial and stock effects reversed), and batch.status_changed (an existing lot changed status through an explicit action, with batchNumber, previousStatus, newStatus, and the affected productIds in the payload). batch.status_changed fires only on a change to a lot that already exists: creating a lot never fires it, even when a credit-note return mints the lot directly into quarantine. Emission covers manual status changes, quality pass/fail transitions, the nightly expiry sweep, credit note and return reversals, merges, and adjustment-void reactivations. A lot that reaches zero through ordinary picking or adjustments flips to consumed without firing it; use stock.level_changed for quantity-driven lifecycle.
- Wider stock.level_changed coverage: the event now also fires for credit-note approval restocks and void reversals, stock-take completion (per product and warehouse with an applied non-zero variance), and lot stocktake imports. The webhook section on /developers now states exactly which flows emit it and which app-only flows do not yet.
Notes- Per-resource API-key scopes now span twelve families with credit-notes and production-orders joining the ten from v1.5. Existing keys are unaffected: global read/write keys reach the new endpoints already, and family-scoped keys stay confined to their family.
- Everything new in v1.10 is a read surface or an additive response field. The two Breaking items above are notices for enforcement that has been live since 26 July, not new behaviour shipping today.
24 June 2026
Allergen-conformance product flag (requiresAllergenCheck)
v1.9 adds the requiresAllergenCheck product flag, backing the new allergen-conformance gate: a production order for a flagged product can't start until the product's declared allergen label covers every allergen its recipe introduces and all recipe ingredients are reviewed. Additive field only.
Added- Products now report requiresAllergenCheck (boolean) and accept it on POST /products and PUT /products/{id} (omit to default false). When true, the Frostbyte Pro app blocks starting a production order for that product until its own declared allergens (containsAllergens / mayContainAllergens) cover everything the BOM rolls up, so no undeclared allergen can reach the finished good. The effective rolled-up label is already exposed on GET /products/{id} as effectiveAllergens (v1.8).
23 June 2026
Allergen declarations + rolled-up allergen label
v1.8 exposes the new allergen model: each product declares the allergens it contains or may contain, and a manufactured product's effective label is rolled up automatically from its recipe.
Added- Products now report containsAllergens and mayContainAllergens (arrays of the EU/Codex declarable set: GLUTEN, CRUSTACEANS, EGGS, FISH, PEANUTS, SOYBEANS, MILK, TREE_NUTS, CELERY, MUSTARD, SESAME, SULPHITES, LUPIN, MOLLUSCS) plus allergenReviewed (boolean). All three are accepted on POST /products and PUT /products/{id}; these are the product's OWN declarations.
- GET /products/{id} adds effectiveAllergens: the rolled-up label = the product's own declarations unioned with every BOM ingredient's, walked recursively (CONTAINS always wins over MAY_CONTAIN). It carries contains, mayContain, reviewed, and unreviewedIngredientCount (when > 0 the roll-up is provisional because some ingredient's allergen data is unreviewed). Computing this on read means a finished-good's allergen label can never drift out of sync with its recipe.
23 June 2026
Final-QC product flag on the product shape
v1.7 adds the requiresFinalQC product flag, which mirrors the new mandatory final-QC gate in the app: a production order for a flagged product can't be completed until a FINAL quality inspection for it has passed. Additive field only.
Added- Products now report requiresFinalQC (boolean) and accept it on POST /products and PUT /products/{id} (omit to default false). When true, the Frostbyte Pro app blocks completing a production order for that product until a FINAL quality inspection linked to the order has a PASS result, so an integration can provision the gate when it creates the product. The inspection workflow itself is performed in the app; the API exposes the product-level requirement flag.
23 June 2026
Cold-chain and line-clearance fields on products and batches
v1.6 surfaces the food-safety attributes the Frostbyte Pro app gained for cold-chain handling and production line clearance, so an integration can read and set them over REST. The only changes are additive fields on the product and batch shapes; everything below is live on /api/v1 now.
Added- Products now report requiresInspection and requiresLineClearance (booleans) alongside the existing storageCondition. requiresLineClearance, when true, blocks a production order for that product from starting until a sanitation / line-clearance sign-off is approved in the app; requiresInspection has a final quality inspection created automatically when a production order for the product is completed (there is no on-receipt trigger). Both are also writable on POST /products and PUT /products/{id} (omit to default false), so you can provision a chilled, clearance-gated product entirely over the API.
- Batches now report storageCondition (AMBIENT | CHILLED | FROZEN, or null) on GET /batches and GET /batches/{id}. A new lot inherits its product's storage condition when it is created, so the cold-chain requirement travels with the stock through receipt, production, and recall traceability.
Notes- These are additive response fields, so existing clients are unaffected. storageCondition was already returned on products; this release adds it to batches and adds the two production / QC flags to products. The line-clearance sign-off itself (the two-party operator + QA approval) is performed in the Frostbyte Pro app; the API exposes the product-level requirement flag, not the sign-off workflow.
12 June 2026
Hardening: per-resource API-key scopes, org rate limits, key rotation
v1.5 hardens the key and rate-limit layer without touching any endpoint. API keys can now be scoped to a single resource family, a per-organisation rate ceiling sits over the existing per-key one, and keys rotate with a grace window so you can roll a new secret out with zero downtime. Everything below is live on /api/v1 now; the full prose is under Authentication on /developers.
Added- Per-resource API-key scopes: alongside the global read and write scopes, a key can now be issued with a <family>:read or <family>:write scope for any of the ten families: products, inventory, customers, suppliers, sales-orders, purchase-orders, invoices, shipments, reports, webhooks. The family is derived from the request URL's first path segment, so a resource-scoped key is granted only its own family and returns 403 insufficient_scope on every other one (a :write scope grants read and write on its family). Combine several on one key to widen its reach without handing out a global scope. No competitor inventory API offers per-resource key scopes. This is a Frostbyte differentiator for least-privilege integrations.
- Organisation-wide rate limit: a 600 requests-per-minute ceiling now applies across all of your organisation's keys, in addition to the existing 120-per-minute-per-key limit. Either limit surfaces as 429 rate_limit_exceeded with a Retry-After header, so spreading a burst over more keys no longer raises the ceiling. Repeated failed authentication from one IP address is throttled independently to blunt key-guessing.
- Key rotation: rotating a key from Settings → API Keys mints a fresh token and keeps the previous token valid for a grace window (24 hours by default) so you can deploy the new secret across your fleet and cut over without a window of downtime. Both tokens authenticate (with identical scopes) until the grace period lapses, after which only the new one works. The rotated key, like a freshly created one, is shown only once.
Changed- Capped collections inside a detail response now flag truncation explicitly. Where an embedded list is capped (for example an invoice's payments or a supplier's price list), the response carries a <collection>Truncated boolean and a <collection>Total count beside the array (paymentsTruncated / paymentsTotal, priceListTruncated / priceListTotal) so you can tell a complete list from a clipped one and know how many rows exist in total rather than inferring it from the cap. Point-in-time reports remain intentionally unpaginated and still truncate silently. Narrow their filters to stay under the cap.
- The 429 status-code reference and the Authentication section on /developers now document both the per-key and the per-organisation limits.
12 June 2026
Outbound webhooks: stop polling, get signed push events
The fifth release of 12 June, v1.4, closes the last big gap between the Frostbyte API and the inventory platforms it competes with: outbound webhooks. Subscribe an HTTPS endpoint to the events you care about and we push a small, HMAC-signed payload the moment a matching change commits, whether it came through this API or someone working in the Frostbyte Pro app, so your integration can stop polling. Unlike some competitors, every delivery is signed, so you can prove it came from us. Everything below is live on /api/v1 now; the payload shape, signature scheme, and delivery semantics are documented under Webhooks in the Conventions section on /developers.
Added- Webhook subscriptions: POST /webhooks (read-write) creates a subscription from { url (a public HTTPS endpoint; loopback, RFC-1918 private, and link-local / cloud-metadata hosts are rejected by an SSRF guard), events (one or more catalogue strings, or ["*"] for all), description? }. The 201 returns the signing secret EXACTLY ONCE. Afterwards only secretPrefix is ever returned, and there is no endpoint to re-read or rotate it (delete and recreate to roll it). Up to 25 active subscriptions per organisation. GET /webhooks and GET /webhooks/{id} list and fetch subscriptions (secret never included); PUT /webhooks/{id} updates url / events / description / isActive (setting isActive: true re-enables an auto-disabled endpoint and resets its failure count); DELETE /webhooks/{id} stops all further deliveries.
- The event catalogue (fifteen events): stock.level_changed; sales_order.created / .confirmed / .dispatched / .completed / .cancelled; purchase_order.created / .approved / .received; invoice.created / .payment_recorded; product.created / .updated; customer.created / .updated. Subscribe to any exact string or to "*" (alone) for all of them. Events fire for changes made through the API AND for the same actions performed by a person in the Frostbyte Pro app: they reflect the data, not the channel.
- Thin, fetch-the-resource payloads: each delivery is a POST of { id, event, occurredAt, orgId, data: { id, ... } }, just what changed and which record. Use data.id to GET the full resource over REST, so you always read current state and never act on a stale serialised copy. The event type and a stable delivery id also ride in the X-Frostbyte-Event and X-Frostbyte-Delivery-Id headers.
- Stripe-style signatures: every delivery carries X-Frostbyte-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<rawBody>")>. Recompute the HMAC over the raw request body (before any JSON parse / re-serialise) and compare in constant time to prove the request came from us; a short verification recipe is on /developers. This is the differentiator over competitors that send unsigned, spoofable webhooks.
- Delivery log + test ping: GET /webhooks/{id}/deliveries is the per-subscription delivery log (each attempt's event, status PENDING | DELIVERED | DEAD, response code, last error, and timestamps; delivery metadata only, not the payload body), and POST /webhooks/{id}/ping fires a signed synthetic delivery so you can validate your receiver and signature check before real events flow.
Notes- Delivery is at-least-once and near-real-time: events are queued at commit and sent by a dispatch job that runs every minute (~1 min latency). Acknowledge with any 2xx. Verify the signature on every request AND tolerate duplicates: dedupe on X-Frostbyte-Delivery-Id, which repeats across retries of one delivery.
- Retries use exponential backoff (1m → 5m → 15m → 1h → 6h), after which the delivery is marked DEAD. An endpoint that keeps failing is auto-disabled so it stops consuming retries; re-enable it with PUT /webhooks/{id} { isActive: true }, which resets the failure count. URLs are HTTPS-only and re-validated against the SSRF guard at delivery time, and a redirect to an internal host is refused.
- This brings the v1 surface to fifteen resources. Webhooks are emitted post-commit and fire-and-forget, so a delivery problem can never roll back or slow down the underlying operation.
12 June 2026
Surface round-out: reference data, pricing, traceability, shipments, reports, and new write paths
The fourth release of 12 June, v1.3, broadens the API from the order-to-cash core to the rest of your data: reference-data lookups for the ids the write endpoints ask for, price tiers and an effective-price resolver, batch and serial traceability, the shipment pick / pack / dispatch view, and read-only reports. It also opens new write paths: addressable customer contacts and addresses (the first DELETE endpoints), shipment pack / dispatch, and invoice generation from a sales order. Everything below is live on /api/v1 now; full request/response details are on the API reference and in the OpenAPI document.
Added- Reference data (read-only): GET /categories and GET /units-of-measure (products.view), GET /customer-groups and GET /sales-reps (sales.view), and GET /suppliers/{id}/locations (purchasing.view; 404 if the supplier isn't in your organisation). These resolve the ids the create/update endpoints ask for: a categoryId, a unit-of-measure id, a customerGroupId, the user id to assign as a sales rep, or a supplier's ship-to location. /sales-reps returns only id (the user id), name, email, and role.
- Pricing (read-only, products.view): GET /price-tiers (list, ranking order) and GET /price-tiers/{id} (detail with per-product tier prices and quantity breaks), plus GET /products/{id}/prices, an effective-price resolver that mirrors the sales-order form's chain (defaultPrice → customer tier → quantity break, where a break overrides the tier and never stacks). Optional customerId (an unknown id returns 404, since it changes the answer) and quantity (default 1). The response reports basePrice, resolvedPrice, and source. The customer's standing discount is surfaced as appliedDiscountPct (a fraction 0-1, informational) but never folded into resolvedPrice; the app applies it to the line total, not the unit price.
- Traceability (read-only, inventory.view): GET /batches and GET /batches/{id} return lot/expiry batches in FEFO order (earliest expiry first) with a per-warehouse / bin quantity breakdown and a rolled-up total; filters productId / warehouseId (validated in-org → 404), status, expiryAfter / expiryBefore, search (batchNumber), modifiedSince. GET /serial-numbers and GET /serial-numbers/{id} return individual serial numbers with their current placement and full status-transition history (oldest first, capped at 500); filters productId (→ 404), status, serial (exact, case-insensitive), modifiedSince.
- Shipments: GET /shipments and GET /shipments/{id} (shipments.view) expose the fulfilment view: pick / pack / dispatch state, carrier and tracking, and the shipment lines. POST /shipments/{id}/pack (shipments.create; PICKING or PICKED → PACKED, no body) and POST /shipments/{id}/dispatch (shipments.dispatch; PACKED → DISPATCHED, stamping the ship date) write back the two transitions a 3PL owns. Dispatch takes an optional { carrier?, trackingNumber? } body persisted in the same transaction, rolls the parent sales order up to DISPATCHED once every line is fully shipped (marking a linked Shopify / WooCommerce / BigCommerce order fulfilled and sending the Foodstuffs ASN where each is enabled for the organisation, on request), and accepts an Idempotency-Key so a retry replays instead of re-running the rollup and pushes.
- Reports (read-only, reports.view): GET /reports/stock-movement (signed inventory ledger over a required date range), /reports/stock-on-hand-at-date (per-location stock at a historical date), /reports/valuation (current on-hand value per product plus a total), /reports/receivable-aging (outstanding invoices aged into buckets; amounts in each invoice's own currency, so the report mixes currencies), /reports/expiry (active batches expiring within a window, with locations), and /reports/low-stock (products at or below their minimum with a suggested reorder). These are point-in-time snapshots, not paginated lists: each caps its result and truncates silently beyond the cap. A filter id that isn't in your organisation returns 422 invalid_reference; there are no path {id} resources, so no 404.
- Customer contacts and addresses are now addressable sub-resources: GET / POST /customers/{id}/contacts and PUT / DELETE /customers/{id}/contacts/{contactId} (read on sales.view, writes on sales.edit), and the same four verbs for /customers/{id}/addresses. These are our first DELETE endpoints. Creating or flagging one primary contact (or one default-billing / default-shipping address) clears the previous one in the same transaction (single-primary, single-default-per-dimension), the customer's scalar email / phone / address mirror is re-synced from the current primary/defaults, and every write returns the refreshed customer detail (201 on create, 200 on replace and delete). PUT is a full-resource replace: omit an optional field and it is cleared. Deleting the last contact or address is allowed.
- Invoice generation from a sales order: POST /sales-orders/{id}/invoice (invoices.create) creates an invoice from a CONFIRMED-or-later order and returns 201 with the invoice in the GET /invoices/{id} detail shape. Optional body { suppressEdi? (default false), lines?: [{ soLineId, quantity }] }: with no lines each line is billed for its not-yet-invoiced balance, so repeat calls bill only the outstanding amount; a fully-invoiced order or an over-invoiced line returns 422 (the cap re-checks under a row lock), and a foreign soLineId returns 422 invalid_reference. Invoice creation can auto-send over Foodstuffs EDI when the order carries a Foodstuffs PO and your integration has auto-send on; suppressEdi: true skips it. Supports the Idempotency-Key header (the key is bound to the order).
Notes- The fourteen-resource count: products, customers, suppliers, sales orders, invoices, purchase orders, stock on hand, warehouses, stock adjustments, shipments, traceability (batches + serials), pricing, reference data, and reports.
- Sub-resource writes return the refreshed parent customer detail rather than the child alone, so a webstore sees the updated children and the recomputed scalar mirror in one round-trip. The contact / address POSTs do not take an Idempotency-Key (child creation is low-risk); the shipment dispatch and invoice-from-sales-order POSTs do.
- Permission gating follows the resource domain: reference-data reads sit on the domain that owns the id (categories / units of measure on products.view, customer groups / sales reps on sales.view, supplier locations on purchasing.view); pricing reads on products.view; traceability on inventory.view; reports on reports.view. There is no separate settings permission for reference data; a read key that can see the parent resource can resolve its ids.
12 June 2026
Developer tooling: OpenAPI 3.1 spec + machine-readable docs
The third release of 12 June is about how you consume the docs, not the endpoints. Nothing on the API surface changed. v1.2 publishes the reference in machine-readable forms: an OpenAPI 3.1 document generated from the very validation schemas the API enforces, a markdown mirror for AI agents, and an llms.txt pointer at the site root.
Added- OpenAPI 3.1 specification at GET /api/v1/openapi.json: public, no API key required (it is documentation, not data), CORS-enabled so browser-based tooling can fetch it directly. Request bodies are generated from the same validation schemas the API parses requests with, and response schemas are transcribed from the serialisers, so the spec describes what the server actually does. A conformance test walks the route tree on every build and fails it if an endpoint exists that the document doesn't describe.
- Postman import by URL: Import → Link → paste https://frostbytesoftware.co.nz/api/v1/openapi.json and Postman builds a collection with every endpoint, parameter, and request body.
- SDK generation: the spec works with standard OpenAPI client generators (openapi-generator, orval, and friends) to produce a typed client for your stack. We don't ship an official SDK. The spec is the contract; pick whichever generator fits your toolchain.
- Markdown mirror of the full API reference at /developers/api-reference.md: one plain-text page for AI agents and LLMs (and anyone who prefers plain text), with no JavaScript to render.
- llms.txt at the site root, pointing AI tools at the markdown reference and the OpenAPI spec.
12 June 2026
Order-to-cash: workflow transitions, goods receipts, payments, and idempotency keys
The second release of 12 June, and the big one: v1.1 takes the API from data sync to workflow. Orders created via the API no longer dead-end in DRAFT: you can confirm, dispatch, complete, and cancel sales orders, submit, approve, cancel, and receive purchase orders, and record invoice payments, with opt-in idempotency keys on every side-effecting POST. Everything below is live on /api/v1 now; full request/response details are on the API reference.
Added- Sales orders: POST /sales-orders/{id}/confirm (DRAFT → CONFIRMED; reserves stock for every line and enforces credit: a customer on credit hold always blocks, going over the credit limit blocks when your organisation's credit enforcement mode is BLOCK), POST /sales-orders/{id}/dispatch (PACKED or PARTIALLY_DISPATCHED → DISPATCHED / PARTIALLY_DISPATCHED; optional { lines: [{ soLineId, quantity }] } body for partial waves, or omit it for a full dispatch; quantities clamp to the packed-not-yet-dispatched balance, allocated serial numbers move to SOLD, and a full dispatch marks linked Shopify / WooCommerce / BigCommerce orders fulfilled where order status push is enabled for the organisation, on request), POST /sales-orders/{id}/complete (DISPATCHED → COMPLETED; every line must be fully invoiced by non-VOID invoices), and POST /sales-orders/{id}/cancel (DRAFT / CONFIRMED / PICKING / PACKED → CANCELLED with optional { reason }; releases reservations, returns picked stock, and frees allocated serials; blocked once any units have shipped).
- Purchase orders: POST /purchase-orders/{id}/submit (DRAFT → SUBMITTED; rejected with no lines), POST /purchase-orders/{id}/approve (SUBMITTED → APPROVED; rejected if the supplier has been deactivated), and POST /purchase-orders/{id}/cancel (DRAFT / SUBMITTED / APPROVED → CANCELLED with optional { reason }, recorded in the audit log; blocked while active supplier returns exist).
- Goods receipts: POST /purchase-orders/{id}/receipts records a receipt against an APPROVED or PARTIALLY_RECEIVED order: { warehouseId, receivedAt?, notes?, lines } with full batch intake (an existing batch number or an auto-created batch; expired/recalled batches rejected) and serial intake (explicit serial numbers matching the quantity, or auto-generate). Returns 201 with the receipt (server-generated GR-… reference) and the order's post-receipt status, RECEIVED or PARTIALLY_RECEIVED. Over-receipt is allowed and flagged in the audit log.
- Invoice payments: POST /invoices/{id}/payments records a payment against a SENT, PARTIALLY_PAID, or OVERDUE invoice ({ amount, currency?, method?, reference?, notes?, paidAt? }), returning 201 with the created payment and an embedded invoice summary (updated paidAmount, status, balanceDue). paidAt must lie between the invoice date and now. Over-payments and currency mismatches return 422, and the balance check runs under a row lock so concurrent payments can never jointly overpay.
- Idempotency-Key request header (optional, Stripe-style) on the six side-effecting POSTs: /sales-orders, /purchase-orders, /stock-adjustments, /purchase-orders/{id}/receipts, /invoices/{id}/payments, and /sales-orders/{id}/dispatch (the one transition where a retry would otherwise re-execute). Retrying with the same key and byte-identical body returns the stored response with an Idempotency-Replayed: true header (24-hour replay window); reusing a key with a different body or endpoint returns 422 idempotency_key_reuse; a concurrent duplicate returns 409 conflict_in_progress with Retry-After: 5. A failed request releases its key so the same key can retry.
- Two new error codes in the status-code reference: 422 idempotency_key_reuse and 409 conflict_in_progress.
Notes- Transition endpoints return 200 with the same detail shape as the resource's GET /{id}. 404 stays reserved for a path {id} that doesn't exist in your organisation; calling a transition in the wrong current status returns 422 unprocessable with a message naming the actual status, which is also what a concurrent double-transition sees, since every transition runs in a single transaction holding a row lock on the order.
- Every write is audit-logged and attributed to the API key that made it, and fires the same in-app notifications as the equivalent action in the app.
- API keys are organisation-level credentials: one write key can create, submit, and approve the same purchase order. If your organisation relies on two-person PO approval, keep the approve step in the app or give the approving system its own key and process.
- Still in-app only: picking and packing sales orders, generating invoices, adding landed costs to a goods receipt, and marking a fully received purchase order COMPLETED.
12 June 2026
Filtering, sorting, and incremental-sync quick wins
The first wave of integrator feedback, two days after launch: exact-match lookups, consistent sorting on every list, an incremental-sync recipe that works across all resources, and a more debuggable error envelope. Everything below is live on /api/v1 now.
Added- Products: new sku and barcode query params for exact, case-insensitive matches (?sku=PUMP-1 matches “pump-1” but never “PUMP-10”; values are trimmed, empty values ignored). Built for connectors resolving order lines by SKU and for scan-driven flows. Both AND-combine with every existing filter, including search.
- Customers and suppliers: sortBy (name | code | createdAt | updatedAt) and sortDir (asc | desc) on the list endpoints. Defaults are unchanged (name asc); invalid values return 400 invalid_parameter.
- Invoices: sortBy (invoiceNumber | createdAt | updatedAt) and sortDir on the list endpoint. Defaults are unchanged (createdAt desc, following the /sales-orders convention).
- Stock on hand: modifiedSince filter, includeInactiveProducts (reveals archived products' residual stock for reconciliation), validated sortBy (productName | updatedAt) and sortDir, and a new updatedAt field on every row, the checkpoint value for incremental sync. Caveat: batch-only changes (a batch quarantined, merged, or split) update the batch breakdown without bumping the row's updatedAt, so they do not surface under modifiedSince.
- Stock adjustments: new GET /stock-adjustments (list) and GET /stock-adjustments/{id} (detail) endpoints, returning the same shape as the existing POST. List filters: status and reason (comma lists), warehouseId (adjustments with any line in that warehouse), search (adjustment number), dateFrom / dateTo, modifiedSince. The adjustment shape, including the POST 201 response, now carries updatedAt.
- Rate-limit visibility: every authenticated /api/v1 response now carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix epoch seconds), and 429 responses include a Retry-After header.
- Richer validation errors: 400 validation_error responses now include error.details, an array of { path, message } covering every failing field (up to 20), alongside the existing first-issue message.
Changed- A rate-limiter infrastructure failure on our side now returns 503 service_unavailable with Retry-After: 30, instead of a misleading 429 that sent well-behaved clients into pointless back-off loops. A 429 now always means you genuinely exhausted your window.
- Two more database error conditions map to proper codes: a write against a record deleted mid-flight returns 404 not_found, and a broken reference returns 422 invalid_reference. Raw database errors never appear in response bodies.
- Explicit JSON null (plus booleans, arrays, and objects) on required numeric fields (unitPrice, taxRate, amount, defaultPrice, costPrice, costPerUnit) is now rejected with 400 validation_error instead of being silently coerced to 0 or 1. Omitting the field still applies the documented default. If you were sending null, you were almost certainly storing zeros you didn't intend.
Breaking- Purchase orders: when sortBy is provided without sortDir, results now sort descending (previously ascending), aligning with /sales-orders. The no-sortBy default (orderDate desc) and explicit sortDir values behave exactly as before. If you relied on the implicit ascending order, pass sortDir=asc. We are shipping this as a day-one correction (two days after launch, while integrations are still being written) rather than carrying an inconsistent default into v1's stable surface; under the versioning policy it would otherwise have waited for /api/v2.
10 June 2026
API v1 launch
The first public release of the Frostbyte Pro REST API.
Added- Nine resources under /api/v1: products, customers, suppliers, sales orders, invoices, purchase orders, stock on hand, warehouses, and stock adjustments.
- Per-organisation API keys (Settings → API Keys) with read and read-write scopes. The full key is shown once at creation; only a hash is stored on our side.
- Shared list conventions on every resource: page / pageSize, search, modifiedSince, the { data, pagination } response envelope, and { error: { code, message } } on every error.
- Rate limit of 120 requests per minute per key.
- Full reference documentation at /developers.