Every change to the Frostbyte API, newest first. Additive changes ship on v1 without notice periods; anything breaking is flagged here with a migration note, and deprecations get a sunset date before they take effect.
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 serialized 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} — 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} — 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 (pushing fulfilment to a linked Shopify / WooCommerce / BigCommerce order and auto-sending the Foodstuffs ASN), 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.
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 serializers — 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 — 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 pushes fulfilment to linked Shopify / WooCommerce / BigCommerce orders), 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 — 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.
How we version the API
New fields, new optional parameters, and new endpoints ship on v1 without a version bump — build your client to ignore response fields it doesn't recognise. Breaking changes get a new path version (/api/v2) running alongside v1, and deprecations are announced on this page with a sunset date. Read the full versioning policy
Ready to Take Control of Your Inventory?
Join businesses across New Zealand who trust Frostbyte Pro to manage their inventory. Start your free 14-day trial today, no credit card required.