Skip to main content

Scenario: Create, approve and post an expense report

Goal. Your expense tool (a receipt-capture app, a travel platform, a corporate card feed) files a worker's expenses in D365 as an expense report, sends it to the expense report approval workflow, and posts it once it is approved. Lattice creates the report and its lines the way the Expense reports page does, so the legal entity's expense categories, payment methods, sales tax defaults, financial dimensions, project rules and expense policies apply. Posting pays the worker through the worker's expense vendor account and charges project lines to their project.

When to use. Travel and expense reporting from an external tool, with approval in D365 (or approval driven from the tool through Workflow approvals). Project costs that need no approval are posted with a project expense journal instead (Project journals and invoicing).

Prerequisites​

  1. Security. MGCLatticeExpenseReportServiceMaintain ("Expense reports - create and post") grants createExpenseReport, postExpenseReport and their batch variants. It is in duty MGCLatticeIntegrationServiceOperationsDuty (integration operator and administrator roles). Creating a report for a worker also follows the delegate rule of Expense management: only the worker's own user, a delegate of the worker, or a user with the expense administrator, expense agent or system administrator role may file expenses for the worker. Approving through Lattice needs MGCLatticeWorkflowServiceMaintain. Reading the standard MobileExpReports, Expenses and ExpAttachedDocuments entities needs D365's own privileges. See Security model.
  2. D365 configuration in the legal entity:
    • the Expense management parameters with the number sequences for expense report numbers and expense vouchers (Expense management > Setup > General > Expense management parameters, Number sequences);
    • expense categories with the payment methods they allow. USMF: Taxi and Travel (expense type Expense, posting to 601500); payment method Cash posts to the worker's expense vendor account, CreditCard (every category's default) to the company card vendor;
    • the worker, employed in the legal entity, with an expense vendor account for employee-paid expenses. USMF: worker 000020 Julia Funderburk, the Admin user's worker, whose expenses post to vendor JULIAF;
    • an active expense report workflow (Expense management > Setup > General > Expense management workflows). USMF: "Expense report - USMF", one approval step assigned to user Admin;
    • for project lines, expense categories used in projects (USMF Travel; Entertain is not) and the project (USMF 000184);
    • an open ledger period for the expense dates.
  3. Lattice. A valid license (an expired or invalid one refuses the call with LicenseNotValid).

Steps​

The operations are on MGCLatticeExpenseReportService in service group MGCLatticeExpenseServiceGroup. Send a POST with an OAuth 2.0 bearer token and the request in _request (see Trial and day-one guide).

  1. Create the report and submit it.

    POST https://<your-environment>/api/services/MGCLatticeExpenseServiceGroup/MGCLatticeExpenseReportService/createExpenseReport
    {
    "_request": {
    "RequestId": "3b5d7f9a-2c4e-4a6b-8d0f-1e3a5c7e9b2d",
    "LegalEntityId": "USMF",
    "UserContext": "expense-app",
    "WorkerPersonnelNumber": "000020",
    "Purpose": "Customer visit",
    "SubmitToWorkflow": true,
    "Lines": [
    {
    "CategoryId": "Taxi",
    "TransDate": "2026-10-21",
    "Amount": 23.50,
    "CurrencyCode": "USD",
    "PaymentMethod": "Cash",
    "Merchant": "City Cabs",
    "BusinessPurpose": "Airport to customer site"
    },
    {
    "CategoryId": "Travel",
    "TransDate": "2026-10-21",
    "Amount": 41.25,
    "PaymentMethod": "Cash",
    "ProjId": "000184"
    }
    ]
    }
    }
    • WorkerPersonnelNumber names the worker; empty means the calling user's worker. The worker must be employed in the legal entity.
    • Every line needs CategoryId and a non-zero Amount. TransDate defaults to today, CurrencyCode to the accounting currency, PaymentMethod to the category's default (it must be one the category allows), Description to the category's description, and the sales tax groups as the expense line defaults them (from the worker, the project and the category).
    • A line with ProjId (and optionally ActivityNumber) is charged to the project; its category is also the project category, so it must be a category used in projects.
    • Dimensions (a list of { "DimensionName", "DimensionValue" }) on the report or on a line are merged over the dimensions the report and line default from the worker and the project.
    • Description, Location and, for the submit, Comment are optional on the report; Merchant, BusinessPurpose, AdditionalInformation, SalesTaxGroup and ItemSalesTaxGroup on a line.

    The answer: DocumentId is the expense report number. With SubmitToWorkflow the report runs the standard submit checks (policies, budget, itemization, required fields) and goes to the workflow that applies to it; it is then In review, and PostedDocumentId is the workflow's correlation id (a lower-case GUID without braces), the key the workflow operations take: Expense report 000031 created for worker 000020 with 2 line(s) and submitted to workflow (workflow instance be1322c8-5c4f-4a0e-9d0b-7f3e2a1c6d84). Without SubmitToWorkflow the report stays a draft, for the worker to complete and submit in D365.

    A request that fails after the report was created deletes the report and its lines again, so a refused request leaves nothing behind and can be sent again with the same RequestId.

  2. Approve it. The approver approves the report in D365, or your system approves the work item as its assignee with approveWorkItem on MGCLatticeWorkflowActionService (MGCLatticeWorkflowServiceGroup), with WorkflowCorrelationId = the PostedDocumentId from step 1 and ActingUserId = the assignee (USMF Admin):

    { "_request": { "RequestId": "8e0a2c4e-6f1b-4d3a-9c5e-7f9b1d3f5a8c", "LegalEntityId": "USMF",
    "WorkflowCorrelationId": "be1322c8-5c4f-4a0e-9d0b-7f3e2a1c6d84", "ActingUserId": "Admin",
    "Comment": "Approved in the expense app" } }

    Work items are created, and completed, asynchronously by the Workflow message processing batch job: poll getWorkflowStatus until the work item appears and, after the approval, until DocumentStatus is Completed. Workflow approvals has the polling, reject, delegate and recall details. The report is then Approved.

  3. Post it.

    POST https://<your-environment>/api/services/MGCLatticeExpenseServiceGroup/MGCLatticeExpenseReportService/postExpenseReport
    { "_request": { "RequestId": "5f7b9d1f-3a5c-4e7a-8b9d-2c4e6a8c0e3f", "LegalEntityId": "USMF",
    "UserContext": "expense-app", "ExpenseReportNumber": "000031" } }

    The report posts the way its Post does, in one transaction or not at all: the vendor, ledger or bank transactions of each line's payment method (employee-paid lines to the worker's expense vendor account), the project cost transactions of project lines, sales tax, and the report and its lines set to Posted. Lines post on their own transaction dates (moved to the next open date when that period is on hold or closed) unless the Expense management parameters say to post on the posting date. Each line can post under its own voucher (USMF posts one voucher per line): PostedDocumentId is the voucher of the first line, and the message lists every voucher: Expense report 000031 posted (voucher 000001, 000002). When an automatic posting workflow applies to the report, the workflow posts it instead.

  4. Attach the receipts (optional). Use the standard ExpAttachedDocuments entity, which attaches a file to an expense line by its ExpTransNumber. Read the line numbers from the standard Expenses entity (ExpenseTransactionNumber, filtered on ExpenseReportNumber). The entity first stores the receipt with the calling user's worker, so call it as the worker's own user. The Lattice document attachment entities do not cover expense reports.

The bulk variants createExpenseReportBatch and postExpenseReportBatch take { "_requests": [ ... ] }, each item a full request with its own RequestId, and answer one result per item (see Batch and async operations). The operations are synchronous only and have no preview.

How to verify​

  • The responses: Success true, Status 3; create: DocumentId the report number (and the workflow correlation id in PostedDocumentId after a submit); post: DocumentId the report, PostedDocumentId a voucher.
  • The report: MobileExpReports with $filter=dataAreaId eq 'usmf' and ExpenseReportNumber eq '000031'&cross-company=true shows DocumentStatus Pending (In review) after the submit, Approved after the approval and Ledger (Posted) after the posting, and EmployeePersonnelNumber 000020.
  • The lines: Expenses with the same filter lists one row per line (ExpenseCategory, TransactionAmount, PaymentMethod, ProjectID); after posting, every line has ApprovalStatus Ledger and a Voucher.
  • The transactions: LatticeVendorTransactions for vendor JULIAF under the line vouchers carries the employee-paid amounts; LatticeProjectPostedTransactions has a cost transaction for the project line on the project, with its category and date. Posting gives that transaction an id of its own, so look it up by project, category and date rather than by the line's ProjectTransactionID.
  • In D365: Expense management > My expenses > Expense reports (or All expense reports) shows the report and its lines, In review, then Approved, then Posted; the posted report's voucher shows the vendor transaction of the worker's expense vendor and, for project lines, the project's posted cost transactions.

When it goes wrong​

A failed call answers Success false, Status 4, an ErrorCode and the message "Expense report creation failed due to a validation or business rule error." (or "Expense report posting failed ..."); the reasons are in Errors.

ErrorCodeTypical cause (text in Errors)What to do
ValidationFailed (1)Create: no RequestId or unknown LegalEntityId; "At least one expense line is required."; "Expense line 1 is empty."; "Expense line 1: CategoryId is required."; an unknown or inactive category; "Expense line 1: Amount must not be zero."; an unknown currency, payment method, sales tax group, item sales tax group, project or activity; "Expense line 2: payment method ... is not allowed for expense category ..."; a category with no usable default payment method and no PaymentMethod; "Expense line 2: expense category Entertain is not used in projects, so the line cannot be charged to project 000184."; ActivityNumber without ProjId; an unknown worker; "Worker ... has no employment in legal entity ..."; no WorkerPersonnelNumber and the calling user has no worker. Post: no ExpenseReportNumberCorrect the request and resend. Nothing was created.
DimensionValueNotFound (5)An unknown financial dimension or valueUse dimension names and values of the legal entity.
AccessDenied (14)"User ... may not create expense reports for worker ..."Call as the worker's user, a delegate, an expense administrator or an expense agent.
DocumentNotFound (2)Post: "Expense report ... does not exist in legal entity ..."Check the number and the legal entity.
InvalidStatus (3)Post: "Expense report ... is already posted (voucher ...)."; "Expense report ... cannot be posted: its approval status is Pending. Only an approved expense report can be posted." (Draft, In review, Returned, Canceled...); a line that is not approvedWait for the approval, or approve it (step 2). A posted report needs nothing more.
ConfigurationMissing (11)No number sequence for expense report numbers or expense vouchers; "No expense report workflow applies to expense report ..."Fix the setup, or create the report without SubmitToWorkflow.
BusinessRule (4)The standard line or report validation, or the submit checks (expense policies, budget, itemization, required fields), refused the report; the posting checks or the posting refused it (a payment method without offset account, the project, an exchange rate, a closed period, budget). The standard messages are in ErrorsFix the cause in D365 or in your data, then resend.
PreviewUnsupported (10)PreviewMode trueSend without PreviewMode. Nothing was run.
TransientRuntime (6)A runtime fault; IsRetryable trueResend with the same RequestId.
LicenseNotValid (12)The Lattice license is expired or invalidRenew the license, then resend.

Retries are safe. Resending a successful request with the same RequestId creates or posts nothing again: the answer repeats the original result with the message Duplicate request detected. Original request processed at .... A new RequestId for a report that is already posted is refused with InvalidStatus.

The approval never arrives. See Workflow approvals, When it goes wrong: a faulted workflow instance or a stopped Workflow message processing job leaves the report In review.

Reference​