Cart Service
For validation semantics (errors/warnings, the atomic upsert contract, preview/convert correlation) see the Cart Validation & Checkout — Frontend Integration Guide. This page covers the endpoints and session/merge mechanics only.
Overview
The Cart Service manages shopping carts and their line items. Carts support anonymous guest flows, authenticated user sessions, and seamless merge on login.
Each cart is identified by a client-generated GUID and can be associated with a user after login.
Cart types are seeded as follows:
| Id | Title |
|---|---|
| 1 | Guest |
| 2 | Wishlist |
| 3 | Saved For Later |
| 4 | Quote |
All cart endpoints are [AllowAnonymous] with rate limiting applied per IP (AnonCart policy), except cart deletion which requires FullManage.
All operations are tenant-aware.
Database tables
sales.Cartsales.CartItemref.CartType
Endpoints
GET
/api/v1/Cart
Returns a cart by its GUID or by user GUID.
Query parameters:
guid(string, optional) — cart GUIDuserGuid(Guid, optional) — looks up the cart associated with the user
Behavior:
- Returns
404if not found - Only one of
guidoruserGuidshould be supplied
Response:
CartModel(includesItemslist)
Authorization:
[AllowAnonymous]— no token required
POST
/api/v1/Cart
Unified cart session endpoint — creates, claims, or merges a cart depending on the fields supplied.
Request body (CartSessionDTO):
UserGuid(Guid?, optional) — authenticated user's GUID. If present, the request must be authenticated.CartTypeId(int?, optional) — cart type to assign on creationMergeFromGuid(string?, optional) — GUID of a guest cart to merge into the user's cart
Behavior modes:
| Scenario | Fields supplied | Result |
|---|---|---|
| Create (guest) | {} |
Creates a new anonymous cart |
| Create (logged-in) | { "userGuid": "..." } |
Creates a cart owned by the user |
| Claim | { "userGuid": "...", "mergeFromGuid": "..." } |
Assigns the guest cart to the user if they have no existing cart |
| Merge | { "userGuid": "...", "mergeFromGuid": "..." } |
Merges the guest cart into the user's existing cart |
Response:
200 OK—CartModel
Authorization:
[AllowAnonymous]— but ifUserGuidis provided the request must include a valid Bearer token
POST
/api/v1/Cart/items
Adds, updates, or removes cart items by cart GUID.
Request body (UpsertCartItemsDTO):
Guid(string, required) — cart GUIDItems(List, required) — items to upsert: Sku(string, required) — product SKUQuantity(int, required) — new quantity. Set to0or negative to remove the item.UserId(long?, optional) — entity-user placing this line (customisation scoping)Notes(string?, optional) — customisation notes
Behavior:
- An empty
Itemslist deletes the entire cart - Quantity
<= 0removes that item from the cart - New additions are validated atomically — if any new SKU is unavailable/not allowed, the
entire batch is rejected (nothing applied) and the response is
422with a fullCartModelbody (operationErrorset,validation.rejectedAdditionspopulated). Updates/ removals of lines already in the cart are never blocked this way. See the frontend guide for the full contract.
Response:
200 OK—CartModel(success)422 Unprocessable Entity—CartModel(batch rejected; still a full cart body)
Authorization:
[AllowAnonymous]— no token required
POST
/api/v1/Cart/guid/{guid}/invalid-items/remove
Removes exactly the error-severity items excluded for the supplied EntityId, PriceListId,
and DocumentTypeGuid context. Send all their CartItemId values as CartItemIds. The API
revalidates before removal and returns 409 CartConcurrencyConflict if the set changed.
Success returns the updated CartModel; valid items remain untouched.
POST
/api/v1/Cart/guid/{guid}/preview
Calculates prices, tax, and totals for a cart without creating a document.
Route parameters:
guid(string, required) — cart GUID
Request body (PreviewCartRequest):
EntityId(long, required) — customer entity to price againstUserId(long?, optional) — user placing the orderPriceListId(long?, optional) — price list to apply
Behavior:
- Resolves product prices using the given entity and price list
- Applies price tier fallback: uses the closest tier at or below the requested quantity
- Returns a fully calculated document preview without changing cart contents or creating a document; validation metadata on existing cart items may be refreshed
Response:
200 OK—DocumentModel(unsaved,Id = 0)
Authorization:
[AllowAnonymous]— no token required
POST
/api/v1/Cart/guid/{guid}/convert
Converts a cart into a saved document. The cart is deleted on success.
Route parameters:
guid(string, required) — cart GUID
Request body (ConvertCartRequest — extends PreviewCartRequest):
EntityId(long, required) — customer entityUserId(long?, optional) — user placing the orderPriceListId(long?, optional) — price list to applyDocumentTypeGuid(string?, optional) — target document type code (e.g."SALES_ORDER","QUOTE"). Determines document type and initial status; defaults toSALES_ORDERbehaviour if omittedDocumentCategoryId(int?, optional) — document category FKBillingAddressSnapshots(DocumentBillingAddressSnapshotModel?, optional) — full billing address to stamp on the documentShippingAddressSnapshots(DocumentShippingAddressSnapshotModel, required) — full shipping address to stamp on the documentNotes(string?, optional) — order notesPurchaseOrderRef(string?, optional) — customer PO referenceAdditionalRef(string?, optional) — secondary referenceTags(string?, optional) — comma-separated tagsDescription(string?, optional) — document descriptionActionNotes(string?, optional) — internal action notesOther1/Other2/Other3(string?, optional) — configurable fieldsOtherNotes(string?, optional) — additional notesIsBillingSameAsShipping(bool) — iftrue, billing snapshot is copied from shippingAcknowledgedValidationSignature(string?, optional) — current warning signature after explicit customer acceptance; a preview alone does not acknowledge warningsRequireAllItemsConvertible(bool, optional) — when true, reject with409 / 6051if any item is excluded by current validation
Behavior:
- Creates
DocumentandDocumentLinerecords from cart contents - Items with validation errors are excluded from the document, not a reason to refuse
conversion — convert only fails if nothing in the cart is convertible
(
422 CartHasNoConvertibleItems). See the frontend guide for the full contract, including the (currently dormant) warning-acknowledgement gate. - Address snapshots are saved and linked to the document — they are immutable copies of the address at time of conversion, independent of any future address changes
- Prices are resolved using the same tier logic as preview
- WarehouseId is taken from the stock item associated with each variant
- Document status is determined by the target type's
InitialStatusGuidconfiguration; falls back toNEW_ORDERif not set - Source cart is deleted on success, atomically with document creation
Response:
201 Created—DocumentModel(fully populated, same shape as/api/v1/Documents/{id})422 Unprocessable Entity—CartHasNoConvertibleItemsif nothing in the cart is convertible409 Conflict—CartContainsExcludedItemsif strict conversion encounters invalid items
Authorization:
[AllowAnonymous]— no token required
DELETE
/api/v1/Cart/{id}
Deletes a cart by its internal numeric ID.
Route parameters:
id(long, required) — cart identifier
Response:
204 No Content
Authorization:
- Requires Bearer Token
- Permission:
FullManage
Models
CartModel
Fields:
Guid (type: string?)— client-assigned GUID (used for all cart operations)DateAdded (type: DateTime?)— creation timestampDeviceId (type: string?)— device identifierUserId (type: long?)— associated userCartTypeId (type: int?)— FK toCartTypeDateLastUpdated (type: DateTime?)— last update timestampItems (type: List<CartItemModel>?)— cart line itemsValidation (type: CartValidationModel?)— aggregate + per-item validation state (see frontend guide)OperationError (type: CartOperationError?)— set only on a rejectedPOST /itemscall
Note: Id is present in the DB but omitted from JSON responses.
Each CartItemModel does include its own Id in JSON so preview cartItemId values can be
joined back to cart rows.
CartItemPostDTO
Fields used when upserting items via POST /api/v1/Cart/items:
Sku (type: string)— product SKUQuantity (type: int)— quantity (<= 0removes the item)UserId (type: long?)— entity-user placing this lineNotes (type: string?)— customisation notes
PreviewCartRequest
Fields used for preview and as base for convert:
EntityId (type: long)— customer entityUserId (type: long?)— user placing the orderPriceListId (type: long?)— price list to apply
ConvertCartRequest
Extends PreviewCartRequest. Additional fields:
DocumentTypeGuid (type: string?)— target document type code (e.g."SALES_ORDER","QUOTE"). Determines the document type and initial status on conversionDocumentCategoryId (type: int?)— document category FKBillingAddressSnapshots (type: DocumentBillingAddressSnapshotModel?)— billing address snapshotShippingAddressSnapshots (type: DocumentShippingAddressSnapshotModel)— shipping address snapshotNotes (type: string?)— order notesPurchaseOrderRef (type: string?)— customer PO referenceAdditionalRef (type: string?)— secondary referenceTags (type: string?)— comma-separated tagsDescription (type: string?)— document descriptionActionNotes (type: string?)— internal action notesOther1 / Other2 / Other3 (type: string?)— configurable fieldsOtherNotes (type: string?)— additional notesIsBillingSameAsShipping (type: bool)— copy billing from shippingAcknowledgedValidationSignature (type: string?)— the currentcartValidation.signatureafter explicit customer acceptance; required once warning detection shipsRequireAllItemsConvertible (type: bool)— rejects excluded items with409 / 6051
DocumentAddressSnapshotModel
Used in both billing and shipping snapshot fields:
Name (type: string?)— address name / companyLine1 (type: string?)— street address line 1Line2 (type: string?)— street address line 2City (type: string?)— cityStateRegion (type: string?)— state or regionPostalCode (type: string?)— postal/ZIP codeCountryCode (type: string?)— ISO country codePhone (type: string?)— phone numberEmail (type: string?)— email
Notes
- All cart endpoints are rate-limited via the
AnonCartpolicy - Cart
Idis hidden from API responses; useGuidfor all client-side operations POST /api/v1/CartwithUserGuidrequires a valid JWT — the controller returns401ifUserGuidis provided but no token is present- Convert and preview use the same price resolution logic — preview is a dry run of convert
- Address snapshots are point-in-time copies; they do not link back to a live address record
- All data is tenant-scoped
- Internal errors are logged but not exposed to API consumers