Document Service
Note: The sales order model has been replaced by the generic Document model. All endpoints are now under
/api/v1/Documents. The document approach supports multiple document types (sales orders, credit notes, quotes, etc.) through theDocumentTypeIdfield.
Overview
Documents represent transactional records — sales orders, credit notes, quotes, and any other document type defined in ref.DocumentType.
Documents provide a consistent reference for:
- recording and processing customer orders
- tracking status, fulfillment, and dispatch
- stamping immutable billing and shipping address snapshots at creation time
- attaching line items, files, and time entries
- reporting and analytics
Soft delete is supported — deleted documents have IsDeleted = true and a DeletedAt timestamp rather than being physically removed.
All document operations are tenant-aware.
Database tables
sales.Documentsales.DocumentLinesales.DocumentAddressSnapshotref.DocumentTyperef.DocumentCategory
Endpoints
GET
/api/v1/Documents
Returns a paginated, filterable list of documents.
Query parameters (pagination):
page(int, default: 1) — page number (1-based)pageSize(int, default: 20, max: 100) — items per page. Pass0to receive onlytotalCountwith an emptyitemsarraysortBy(string, optional) — field to sort by:ordernumber,orderdate,grandtotal,createdatsortDesc(bool, default: false) — sort descending
Query parameters (filters):
search(string, optional) — search across order number and courier referenceentityId(long, optional) — filter by customer/entity IDdocumentTypeId(int, optional) — filter by document typeisComplete(bool, optional) — filter by completion statusorderDate(yyyy-MM-dd, optional) — filter by exact order date; ignored whendateFrom/dateToare used
Query parameters (date window):
dateFrom(yyyy-MM-dd, optional) — start of date range, inclusivedateTo(yyyy-MM-dd, optional) — end of date range, inclusivedateProperty(string, default:"orderdate") — column to filter on:"orderdate"or"createdat"
Example — all documents for entity 5 in April 2026:
GET /api/v1/Documents?entityId=5&dateFrom=2026-04-01&dateTo=2026-04-30&dateProperty=orderdate&pageSize=0
Response:
PagedResponse<DocumentModel>
Authorization:
- Requires Bearer Token
- Permission:
OrdersRead
GET
/api/v1/Documents/{id}
Returns a single document by its numeric identifier, including all lines.
Route parameters:
id(long, required) — document identifier
Behavior:
- Returns
404if not found
Response:
DocumentModel(includesLineslist)
Authorization:
- Requires Bearer Token
- Permission:
OrdersRead
GET
/api/v1/Documents/ref/{orderNumber}/entity/{entityId}
Returns a document matching both order reference and entity ID.
Route parameters:
orderNumber(string, required) — order reference numberentityId(long, required) — entity identifier
Behavior:
- Returns
404if no match found
Response:
DocumentModel
Authorization:
- Requires Bearer Token
- Permission:
FullRead
GET
/api/v1/Documents/{id}/files
Returns a paginated list of file attachments linked to a document.
Route parameters:
id(long, required) — document identifier
Query parameters: see FileFilterRequest (standard pagination + search)
Response:
PagedResponse<FileMetadataModel>
Authorization:
- Requires Bearer Token
POST
/api/v1/Documents
Creates a new document.
Request body (DocumentModel):
- See Document Model for full field reference
OrderNumber,EntityId,OrderDate,Currency,DocumentTypeId,GrandTotal >= 0are requiredStatusIdis not part of the request — the initial status is always derived from the document type'sInitialStatusGuid(falls back toNEW_ORDER), never supplied by the caller
Behavior:
- Validation is handled in the service layer
- Successful creation writes an audit log entry
Response:
201 Created—DocumentModel
Authorization:
- Requires Bearer Token
- Permission:
OrdersWrite
POST
/api/v1/Documents/{id}/convert
Converts or clones an existing document into a different document type.
Route parameters:
id(long, required) — source document identifier
Query parameters:
documentTypeGuid(string, required) — target document type code (e.g."SALES_ORDER","QUOTE")clone(bool, default: false) — iffalse, updates the existing document in place; iftrue, creates a new document copy and leaves the original unchanged
Behavior:
- Validates the conversion is actually allowed from the source document's current type/status (via
DocumentActionEngine) — returns409(DocumentConversionNotAllowed) if not - Resolves the target document type by
documentTypeGuid - The resulting document status is always derived from the target type's
InitialStatusGuidconfiguration; falls back toNEW_ORDERif not set - When
clone=falsethe same document record is updated and returned - When
clone=truea new document with its own order number is created; the original remains unchanged, unless the matching conversion action defines a post-conversion status for the source (e.g. a Sales Order converted to an Invoice moves the source Sales Order toCOMPLETED)
Response:
200 OK—DocumentModel
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
POST
/api/v1/Documents/{id}/actions/{actionCode}
Performs a lifecycle action on a document (e.g. BEGIN_PROCESSING, CANCEL, EMAIL_DOCUMENT_SUMMARY). This is
the only way a document's status changes outside of creation/conversion — the caller names the action, not
the destination status, and the server resolves the actual result (which may depend on live stock levels
or tenant feature flags). See DocumentModel.DocumentActionState.AllowedActions for what's currently invokable
on a given document.
Route parameters:
id(long, required) — document identifieractionCode(string, required) — e.g.BEGIN_PROCESSING,CANCEL,MARK_DISPATCHED,ISSUE,MARK_PAID,CONVERT_TO_SALES_ORDER,CONVERT_TO_INVOICE,EMAIL_PAYMENT_REMINDER,EMAIL_DOCUMENT_SUMMARY
Behavior:
- Returns
404(DocumentActionNotFound) ifactionCodeisn't a recognised action - Returns
409(DocumentActionNotAllowed) if the action isn't currently valid for the document's status - Returns
409(DocumentActionRequiresOrderLines) forBEGIN_PROCESSINGif the Sales Order has no order lines CONVERT_TO_*actions delegate into the same conversion logic asPOST {id}/convert, per-action:CONVERT_TO_SALES_ORDERtransforms a Quote into the same document in place (clone=false— it becomes the Sales Order, it isn't cloned into one);CONVERT_TO_INVOICEclones the Sales Order into a new Invoice document (clone=true) and also moves the source Sales Order toCOMPLETED, only invokable while the Sales Order isREADY_TO_INVOICEEMAIL_PAYMENT_REMINDER/EMAIL_DOCUMENT_SUMMARYare pure side effects (delegate to the same logic asPOST {id}/email?type=PaymentReminder|Document) — the document's status is unchanged- Otherwise resolves and applies the resulting status (and any associated date field, e.g.
CANCELsetsCancelledDate) - There is no document-level "mark picked" action — picking happens per line via
POST /api/v1/DocumentLines/{id}/pick, which automatically resolves the document's next status once every line is picked
Response:
200 OK—DocumentModel
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
POST
/api/v1/Documents/PurchaseOrders
Generates Purchase Orders from Sales Orders that have lines with insufficient stock.
Behavior:
- Scans all Sales Order documents in
NEW_ORDERorPROCESS_AWAITINGSTOCKstatus - For each line where
Quantity > stock on hand, calculates the shortfall - Groups shortfalls by supplier entity; creates one PO per supplier or adds lines to an existing open PO (status:
NEW_ORDER) for that supplier - Copies
UnitPriceandTaxRatefrom the originating Sales Order line - Recalculates PO totals after all lines are added
- Returns a summary of the operation
Response:
200 OK—GeneratePurchaseOrdersResult
Authorization:
- Requires Bearer Token
- Permission:
DocumentsWrite
POST
/api/v1/Documents/{documentId}/files/assign
Assigns one or more existing files to a document.
Route parameters:
documentId(long, required)
Request body (FileIdsRequest):
FileIds(List, required)
Response:
204 No Content
Authorization:
- Requires Bearer Token
- Permission:
FullManage
PUT
/api/v1/Documents/{id}
Updates an existing document.
Route parameters:
id(int, required) — document identifier
Request body (DocumentModel):
Idis set from the routeId > 0,EntityId > 0,DocumentTypeId > 0are requiredStatusIdand the status-linked date fields (CancelledDate,DispatchDate,CustomisationDate,CompletedDate,InProgressDate,PaidDate) are not part of the request — they only ever change as a side effect ofPOST {id}/actions/{actionCode}
Behavior:
- Updates all provided fields (excluding status/status-linked dates — see above)
- Successful update writes an audit log entry
Response:
200 OK—DocumentModel
Authorization:
- Requires Bearer Token
- Permission:
OrdersWrite
DELETE
/api/v1/Documents/{id}
Soft-deletes a document.
Route parameters:
id(long, required) — document identifier
Behavior:
- Sets
IsDeleted = trueandDeletedAtto current UTC time - Record is retained in the database
- Successful deletion writes an audit log entry
Response:
204 No Content
Authorization:
- Requires Bearer Token
- Permission:
FullManage
DELETE
/api/v1/Documents/{documentId}/files/remove
Removes one or more file associations from a document (does not delete the file itself).
Route parameters:
documentId(long, required)
Request body (FileIdsRequest):
FileIds(List, required)
Response:
204 No Content
Authorization:
- Requires Bearer Token
- Permission:
FullManage
Models
DocumentModel
Represents a document (sales order, credit note, quote, etc.).
Fields:
Id (type: long)— internal identifierOrderNumber (type: string?)— order reference numberEntityId (type: long?)— customer entity identifierUserId (type: long?)— user who created the documentStatusId (type: int)— order status; read-only — always derived from the document type on creation/conversion, or resolved by an action (seeDocumentActionState.AllowedActions), never caller-suppliedOrderDate (type: DateTime?)— order creation dateCurrency (type: string?)— order currencyBillingDocumentAddressSnapshotId (type: long?)— FK to billing address snapshotShippingDocumentAddressSnapshotId (type: long?)— FK to shipping address snapshotPriceListId (type: long?)— price list appliedSubtotal (type: decimal?)— subtotal amountTaxTotal (type: decimal?)— tax totalShippingTotal (type: decimal?)— shipping totalGrandTotal (type: decimal?)— final totalNotes (type: string?)— order notesCreatedAt (type: DateTime?)— creation timestampDueDate (type: DateTime?)— payment due datePromotionId (type: int?)— applied promotionPaidDate (type: DateTime?)— payment dateCompletedDate (type: DateTime?)— completion dateDispatchDate (type: DateTime?)— dispatch/shipping dateCourier (type: string?)— courier nameCourierRef (type: string?)— courier reference numberDocumentTypeId (type: int?)— document type FKInProgressDate (type: DateTime?)— date moved to in-progressPurchaseOrderRef (type: string?)— customer PO referenceAdditionalRef (type: string?)— secondary referenceDocumentCategoryId (type: int?)— document category FKTags (type: string?)— comma-separated tagsDescription (type: string?)— document descriptionActionNotes (type: string?)— internal action notesOther1 (type: string?)— configurable field 1Other2 (type: string?)— configurable field 2Other3 (type: string?)— configurable field 3OtherNotes (type: string?)— additional notesLines (type: List<DocumentLineModel>?)— line items (populated on read, ordered byLineNumber)Status (type: OrderStatus?)— resolved status objectDocumentType (type: DocumentTypeModel?)— resolved document type objectEntity (type: EntityModel?)— resolved entity objectDocumentCategory (type: DocumentCategoryModel?)— resolved category objectBillingAddress (type: DocumentAddressSnapshotModel?)— immutable billing address snapshotShippingAddress (type: DocumentAddressSnapshotModel?)— immutable shipping address snapshotDocumentActionState (type: DocumentActionState?)— the full computed action/permission state for this document's current type/status, as returned byDocumentActionEngine.GetDocumentActionState:IsValid (type: bool)—falseif the document has no type/status or no action configuration exists for its type;Errorsexplains whyErrors (type: IReadOnlyList<string>)— populated only whenIsValidisfalseAllowedActions (type: IReadOnlyList<DocumentActionDescriptor>)— what the caller can currently do to this document (Code,Label,Trigger); the primary contract for driving action buttons. Invoke viaPOST {id}/actions/{Code}FieldPermissions (type: DocumentStatusFieldPermissions)— which of the date fields above are ever relevant to show for this document's current type/status (one bool per date field:OrderDate,PaidDate,CompletedDate,DispatchDate,InProgressDate,DueDate,CustomisationDate,CancelledDate,QuoteDate,InvoicedDate). This is a status-driven permission, not a "has a value" check — the frontend should only display a date field when its flag here istrueAND the field itself is non-null. Computed byDocumentActionEngine, e.g. a Sales Order only exposesCompletedDate/DispatchDateonce it reachesAWAITING_PICK/AWAITING_STOCK, andCustomisationDateadditionally requires the tenant to have thedocument:customisationfeatureHasFinancialDetails (type: bool)— whether this document type carries financial details (subtotal/tax/grand total) worth showing; currently onlytruefor InvoicesHasManpackDetails (type: bool),HasStockDetails (type: bool),HasCustomisationDetails (type: bool)— whether manpacking/stock/customisation details are relevant for this document's current type/statusCanViewEntireCatalog (type: bool)— whether this document's type is exempt from the entity's product allow-list (currentlytrueonly for Quotes and Purchase Orders); informs the frontend whether to scope product pickers to the entity's allowed products or show the full catalogueFullyPicked (type: bool)— whether all stock-required lines are picked
Validation:
IsReadyToAdd()— requiresEntityId,OrderNumber,OrderDate,Currency,DocumentTypeId,GrandTotal >= 0IsReadyToUpdate()— requiresId > 0,EntityId > 0,DocumentTypeId > 0
DocumentLineModel
Represents a single line item within a document.
Fields:
Id (type: long?)— internal identifierDocumentId (type: long?)— parent documentLineNumber (type: int?)— line sequence numberProductId (type: long?)— product identifierProductVariantId (type: long?)— product variant identifierSku (type: string?)— product SKU at time of orderTitle (type: string?)— product title at time of orderDescription (type: string?)— line descriptionQuantity (type: decimal?)— quantity orderedUnitPrice (type: decimal?)— price per unitDefaultSalePrice (type: decimal?)— catalogue price before any overrideDiscountAmount (type: decimal?)— discount appliedTaxRate (type: decimal?)— tax rate appliedTaxAmount (type: decimal?)— tax amountLineTotal (type: decimal?)— line total after discounts and taxWarehouseId (type: long?)— warehouse the goods are allocated fromEntityUserId (type: long?)— user who created the lineNotes (type: string?)— additional notesQuantityFulfilled (type: decimal?)— fulfilled quantityQuantityReturned (type: decimal?)— returned quantityReturnedDate (type: DateTime?)— date of return
Validation:
IsReadyToAdd()— requiresDocumentId,LineNumber,ProductId,Quantity > 0,UnitPrice >= 0,LineTotal >= 0IsReadyToUpdate()— requiresId > 0,DocumentId > 0,ProductId > 0,Quantity >= 0,UnitPrice >= 0
GeneratePurchaseOrdersResult
Returned by POST /api/v1/Documents/PurchaseOrders.
Fields:
PurchaseOrdersCreated (type: int)— number of new PO documents createdPurchaseOrdersUpdated (type: int)— number of existing PO documents that had lines addedLinesAdded (type: int)— total number of PO lines inserted
DocumentAddressSnapshotModel
An immutable copy of an address stamped at the time the document was created (via cart convert or manual creation).
Fields:
Id (type: long)— internal identifierName (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?)— phoneEmail (type: string?)— emailCreatedAt (type: DateTime?)— snapshot creation timestamp
Notes
- All operations are tenant-aware
- All state-changing operations are audited
pageSize=0returns onlytotalCountwith an emptyitemsarray — useful for count-only dashboard queries- Document lines are always returned ordered by
LineNumber, soft-deleted lines are excluded - Address snapshots are immutable — they do not reflect subsequent changes to the entity's address
- Internal errors are logged but not exposed to clients