# API Changelog

Customer-facing log of changes to the GoSmarter API. This tracks changes that affect API **consumers** — request/response shapes, new endpoints, deprecations, and removals. For internal engineering history (refactors, infrastructure, non-API-surface changes), see the repository's `CHANGELOG.md` instead.

See the [API Versioning Policy](/api-versioning-policy) for how breaking changes are classified and the deprecation notice period they follow.

Entries are grouped by release date and categorized as `Added`, `Changed`, `Deprecated`, or `Removed`.

Changes to [Preview endpoints](/api-versioning-policy#preview-apis) are listed separately under `Preview` in each release, prefixed `Added`, `Changed`, `Removed` or `Graduated`. A Preview endpoint may change or be removed without a `v2/` route or a notice period; a `Graduated` entry announces that it is now stable, and from then on it follows the versioning policy like any other endpoint.

---

## Unreleased

### Added
- `GET /api/company/{companyId}/inventory-reference/statuses` — lists the fixed inventory lifecycle statuses (`InventoryStatusId`, `Label`: Available, Reserved, Consumed, Scrapped).
- `DELETE /api/v2/company/{companyId}/inventory-slim` — idempotent version of the inventory delete. Items that belong to your company but are already deleted now count as deleted: the request succeeds (200) and lists them in a new `AlreadyDeletedIds` field, instead of failing with 404. A 404 now only means an id doesn't exist or belongs to another company, and nothing is deleted. An empty id array is rejected with 400. `DELETE /api/company/{companyId}/inventory-slim` is unchanged.
- `GET`/`PUT /api/company/{companyId}/gs1-settings` — read or set the company's GS1 Company Prefix.
- `GET /api/company/{companyId}/production-plans/{planId}` — each bar has a new optional `ReservedInventoryId` field: the inventory row its approval reservation holds, or null.
- `GET /api/company/{companyId}/production-plans` and `GET /api/company/{companyId}/production-plans/{planId}` — each plan has a new optional `ApprovedByName` field: the name of the user who approved it, or null when the plan isn't approved or that user has no name on record. In the single-plan response each bar also has new optional `StartedByName` and `CompletedByName` fields, with the same meaning for the user who started or completed the bar.
- `GET /api/company/{companyId}/subscription` and `GET /api/goadmin/companies/{companyId}/subscription` — new optional billing-state fields: `SubscriptionStatus` (`active`, `trialing`, `past_due`, `unpaid`, `paused`, `incomplete`, `incomplete_expired`, `canceled`), `PastDueSince` (when a payment failure started the dunning state, or null) and `DunningGraceEndsAt` (when the grace window ends and paid features lapse, or null). `past_due`, `unpaid` and `incomplete` keep access until `DunningGraceEndsAt`.

### Preview
- Added: `GET /api/company/{companyId}/order-line-item-tag-requirements`, `GET`/`PUT /api/company/{companyId}/order-line-items/{orderLineItemId}/tag-requirements` — read or set the inventory tags an order line item **requires** (hard: the optimisation solver only cuts the line from stock carrying every required tag) or **prefers** (soft: preferred stock scores higher). `PUT` replaces the whole set; send both `requiredTagIds` and `preferredTagIds` (empty lists clear it). A tag cannot be in both lists and must be an active inventory tag of the company (otherwise 400); an unknown line item returns 404. Responses are `{ orderLineItemId, tagId, requirement: "Required" | "Preferred" }`.
- Changed: optimisation problem/result — demand lines can now be reported as unmatched with reason `NoStockWithRequiredTags` when no stock in their group carries every required tag, and `solverSettings.priorities.tagPreference` (optional, finite and >= 0, default off) weights the preferred-tag objective; a negative or non-finite value returns 400.
- Added: `GET /api/company/{companyId}/onboarding/progress` — returns the trial onboarding checklist progress as six booleans derived from the company's real data: `millCertUploaded`, `inventoryCreated`, `orderWithLineItemCreated`, `stockReserved`, `certificatePackGenerated` and `hexUsed`. Soft-deleted records do not count. Also returns `companyCreatedAt`, `firstPackAt` and `timeToFirstPackSeconds` (time to first certificate pack; null until a pack exists).
- Added: `GET`/`PUT /api/company/{companyId}/settings` — read or set company-level settings kept outside the company profile; currently `KerfMm`, the saw-blade width lost per cut (mm) that optimisation runs apply. `PUT` is company admins only, requires `KerfMm` between 0 and 20 (rounded to 2 decimal places, otherwise 400), and changes no other company field. `PUT /api/company/{companyId}` never reads or writes `KerfMm`.
- Added: `POST /api/company/{companyId}/inventory/{inventoryId}/physical-identifiers` — issues a governed physical identifier for an inventory item, or returns the item's existing one (idempotent reprint); `reissue: true` replaces it. With no `identifierType`, the item's current identifier type is reused; an item with no identifier gets the default type: a serialised GTIN (SGTIN, returned as `GTIN`) when the company has a GS1 Company Prefix — which needs a `gtin`, otherwise 400 — or an internal code when it has none. For GTIN identifiers, `gtin` may be omitted once the item has one, and a supplied `gtin` must match it (409). Returns 404 for a deleted item.
- Added: `GET /api/company/{companyId}/inventory/{inventoryId}/physical-identifiers` and `GET /api/company/{companyId}/physical-identifiers/lookup?payload=…` — list an item's identifiers, or resolve a scanned payload. `isCurrent` reflects whether the identifier has been replaced; `inventoryActive` is false when the identified item has been deleted.
- Added: `POST /api/company/{companyId}/production-plans/{planId}/unapprove` — returns an Approved production plan to Draft and releases the stock its approval reserved. Only allowed before any bar has started (400 otherwise). Returns `PlanId`, `PlanStatus` and `ReleasedLinkCount`.
- Added: `POST /api/company/{companyId}/optimisation/runs` and `POST /api/company/{companyId}/optimisation/runs/preview` — compose an optimisation problem from filters (or dispatch a previously-previewed snapshot via `PreviewId`, within 30 minutes) and start a solver run; preview returns a per-group summary without dispatching anything. `GET /api/company/{companyId}/optimisation/runs`, `GET .../runs/{runId}` and `POST .../runs/{runId}/cancel` list, poll and cancel runs.
- Changed: `POST /api/company/{companyId}/optimisation/runs` and `POST /api/company/{companyId}/optimisation/runs/preview` — `solverSettings.algorithm` must now be omitted, blank or `ga` (the solver only runs its genetic algorithm); any other value returns 400 instead of being silently ignored. A saved filter profile's stored `algorithm` is replaced by `ga` when a run starts, and the run's result `solverInfo.algorithm` always reports `ga`. `solverSettings.priorities.stockConsumption` and `wasteVsSimplicity` are still accepted but have no effect on the solver.

### Changed
- `PUT /api/company/{companyId}/inventory/{inventoryId}/millcert-links` — a request that lists the same `(MatchKeyType, MatchedValue)` key more than once (compared case-insensitively, after trimming, whichever heat each entry names) now returns 400 naming the duplicate positions, instead of 500. If a concurrent request changes the same item's link for the same key, it returns 409 Conflict and saves nothing, instead of 500; retry after reloading. Previously both cases were server errors, so no working client is affected.
- Audit history endpoints (`.../history`, and the audit log) — `OldValue`/`NewValue` for date-time fields (for example `OrderDate`, `DeliveryDate`, `ArchivedAt`) are now ISO 8601 UTC strings such as `2026-10-09T13:43:21.000Z`, instead of US-format `10/09/2026 13:43:21`, which UK readers misread. The fields remain open-ended strings; numbers, booleans and text are unchanged. Parse the new format as UTC.
- `GET /api/company/{companyId}/millcertification/{millCertificationId}` — now fills in fields the response already declared but always returned empty: the linked supplier and mill organisations (`SupplierOrganisationId`, `SupplierOrganisationName`, `MillOrganisationId`, `MillOrganisationName`) and the archive state (`IsArchived`, `ArchivedAt`). A link to an organisation that has since been deleted is reported as unlinked (null id and name). Saving the whole certificate back no longer clears an existing organisation link.
- `POST /api/company/{companyId}/production-plans/{planId}/discard` — now also releases the stock reservations the plan's approval created (they previously stayed reserved after discard). Reservations made before approval are kept. Discarding is now refused with 400 once any bar has started, and also when the calling user can't be identified (discard is now attributed to a user).
- `DELETE /api/company/{companyId}/inventory-slim/{inventoryId}/reserve` and `DELETE /api/company/{companyId}/orders/{orderId}/line-items/{lineItemId}/reserve/{inventoryId}` — return 409 Conflict, and change nothing, when a live production plan with no started bar holds the reservation. If the plan created the reservation, discard or un-approve the plan to release it. If the plan reused a reservation you made by hand, discard or un-approve the plan first and then unreserve again: your reservation is kept. The 409 message says which case applies.

> **Applied in place (production plan reservations).** The Changed entries above (discard releasing the stock its plan reserved and being refused once a bar has started, and the two unreserve 409s) correct reservation behaviour that did not hold the platform's stock-reservation invariant: stock held by an Approved plan could be unreserved, or stay reserved after discard. Approving a plan now also rejects an inactive Draft plan. Because these fix incorrect behaviour rather than add a new contract, they are applied to the existing routes instead of new `v2/` routes.

### Deprecated
- `GET`/`POST /api/company/{companyId}/email-domains`, `DELETE /api/company/{companyId}/email-domains/{domainId}` and `POST /api/company/{companyId}/email-domains/{domainId}/restore` — retired with no replacement, because joining a company by email domain has been withdrawn. The three write routes already return `410 Gone`; the `GET` still lists existing registrations and now sends `Deprecation` and `Sunset` headers. All four now carry a sunset date in the OpenAPI document. Sunset date: 2027-01-31. See the [migration guide](/migrations/email-domains-withdrawal).

{/*
Template for a new entry — copy this block above the previous entry when a change ships:

## YYYY-MM-DD

### Added
- `POST /api/v2/company/{companyId}/example` — short description of the new capability.

### Changed
- `GET /api/company/{companyId}/example` — short description of a backward-compatible change (e.g. a new optional field).

### Deprecated
- `GET /api/company/{companyId}/example` — deprecated in favour of `GET /api/v2/company/{companyId}/example`. Sunset date: YYYY-MM-DD. See the [migration guide](/migrations/example-v2).

### Removed
- `GET /api/company/{companyId}/old-example` — removed after its notice period ended on YYYY-MM-DD.

### Preview
- Added: `POST /api/company/{companyId}/example` — a new Preview endpoint; may change before it is stable.
- Changed: `GET /api/company/{companyId}/example` — what changed, including any change that would be breaking for a stable endpoint.
- Removed: `GET /api/company/{companyId}/old-preview-example` — Preview endpoint withdrawn.
- Graduated: `GET /api/company/{companyId}/example` — now stable; from this release it follows the versioning policy (frozen v1).
*/}
