Scenario: Post project transactions and invoice the project
Goal. Your time-tracking, expense, field-service or billing system records work against a Dynamics 365 project: hours worked by a worker, expenses paid, items consumed and fees charged. Lattice posts each batch as the matching project journal, and when it is time to bill, creates the project invoice proposal from the posted billable transactions and, if you ask, posts it as the customer invoice.
When to use. Time and material billing driven from an external system, or any integration that has to put cost and revenue on a project. Ledger-only entries go through General and intercompany journals. For the retry-safe calling pattern, see Post a journal exactly-once.
Prerequisites
-
Security. All five operations need privilege
MGCLatticeProjectServiceMaintain, in dutyMGCLatticeIntegrationServiceOperationsDuty(integration operator and administrator roles). Without it D365 refuses the call with HTTP 403. -
A project to post to. An In process project whose project group has the ledger posting setup for its transactions. To invoice it, the project must belong to a project contract with a funding source. USMF: project
000184San Diego Subscriptions, a time and material project for customerUS-001on contract000182. -
Journal names, one kind per operation, each with a voucher number sequence:
Operation Journal kind Where it is set up USMF postHourJournalProject journal of type Hour Project management and accounting > Setup > Journals > Journal names HourpostFeeJournalProject journal of type Revenue same page FeepostExpenseJournalLedger journal of type Cost General ledger > Journal setup > Journal names PrjJrnpostItemJournalInventory journal of type Project Inventory management > Setup > Journal names > Inventory ProjItem -
Project categories of the matching type (Project management and accounting > Setup > Categories > Project categories): Hour, Cost, Item and Revenue categories respectively. USMF:
Journeyman(hour),Travel(cost),ProjItem(item),Setup(revenue), all with the billable line property. -
For hour lines, the worker exists and has a resource (USMF
000001Jodi Christiansen). For item lines, the item has on-hand where it is consumed (USMFD0001, site1, warehouse11). For expense lines, the account the expense is paid from allows manual entry (USMF110180Petty Cash). -
Lattice. A valid license (a never-registered environment is allowed through; an adverse verdict refuses posting with
LicenseNotValid). If your system knows projects or items by its own keys, register them as cross-references and sendProjIdExternalKey(header or line) and lineItemIdExternalKeywithExternalSystem; see Cross-reference keys.
Steps
The journal operations are on MGCLatticeProjectJournalService, the invoice operation on
MGCLatticeProjectInvoiceService, both in service group MGCLatticeProjectServiceGroup. Send a POST
with an OAuth 2.0 bearer token and the request in _request (see Trial and day-one guide).
The four journal operations share one request contract: header ProjId is the default project of the
lines (a line's ProjId overrides it), PostingDate the default line date. Each operation creates the
journal, posts it, and answers success only when it is posted: DocumentId = PostedDocumentId = the
journal number, message "Hour (Expense, Item, Fee) journal ... posted successfully."
-
Post hours. One line per worker and category;
Qtyis the hours.WorkerPersonnelNumberbecomes the line's resource.LinePropertyId(for exampleBillable) defaults to the project's line property setup;ActivityNumberis optional.POST https://<your-environment>/api/services/MGCLatticeProjectServiceGroup/MGCLatticeProjectJournalService/postHourJournal{"_request": {"RequestId": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e","LegalEntityId": "USMF","UserContext": "timesheets","ProjId": "000184","JournalNameId": "Hour","Description": "Week 42 timesheet","PostingDate": "2026-10-15","Lines": [{"CategoryId": "Journeyman","Qty": 8.0,"CostPrice": 75.00,"SalesPrice": 125.00,"CurrencyCode": "USD","Description": "Installation work","WorkerPersonnelNumber": "000001"}]}} -
Post expenses. Each line debits the project for
QtyxCostPrice(Qtydefaults to 1; the project's ledger account comes from its posting setup) and creditsOffsetMainAccountId, the account the expense was paid from, which every expense line needs.DocumentIdis the ledger journal number.POST https://<your-environment>/api/services/MGCLatticeProjectServiceGroup/MGCLatticeProjectJournalService/postExpenseJournal{"_request": {"RequestId": "3d4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f6a","LegalEntityId": "USMF","UserContext": "expenses","ProjId": "000184","JournalNameId": "PrjJrn","Description": "Travel expenses","Lines": [{"CategoryId": "Travel","Qty": 1.0,"CostPrice": 350.00,"CurrencyCode": "USD","Description": "Site visit travel","OffsetMainAccountId": "110180"}]}} -
Post item consumption. Each line issues
QtyofItemIdonto the project.InventDimIdnames the inventory dimension (site and warehouse) the item is taken from; leave it out to use the item's default order settings. Inventory dimension ids differ per environment: look up the id of site1/ warehouse11in yours (in USMF demo data it is typically000031).DocumentIdis the inventory journal number.POST https://<your-environment>/api/services/MGCLatticeProjectServiceGroup/MGCLatticeProjectJournalService/postItemJournal{"_request": {"RequestId": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c","LegalEntityId": "USMF","UserContext": "field-service","ProjId": "000184","JournalNameId": "ProjItem","Description": "Parts used on site","Lines": [{ "CategoryId": "ProjItem", "ItemId": "D0001", "InventDimId": "000031", "Qty": 2.0, "CostPrice": 25.00, "CurrencyCode": "USD" }]}} -
Post fees. A fee line carries revenue only:
SalesPriceper unit, usually withCostPrice0.POST https://<your-environment>/api/services/MGCLatticeProjectServiceGroup/MGCLatticeProjectJournalService/postFeeJournal{"_request": {"RequestId": "7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e","LegalEntityId": "USMF","UserContext": "billing","ProjId": "000184","JournalNameId": "Fee","Description": "Setup fee","Lines": [{ "CategoryId": "Setup", "Qty": 1.0, "CostPrice": 0, "SalesPrice": 1500.00, "CurrencyCode": "USD", "Description": "Project setup fee" }]}} -
Create the invoice proposal and post it.
createInvoiceProposalcreates the proposal the way the periodic Create invoice proposals job does: from the project's posted, chargeable hour, expense, item, fee and on-account transactions up toInvoiceDate(default: today) that are not invoiced yet. WithPostAfterCreatetrueit then posts the proposal as the project invoice.POST https://<your-environment>/api/services/MGCLatticeProjectServiceGroup/MGCLatticeProjectInvoiceService/createInvoiceProposal{"_request": {"RequestId": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a","LegalEntityId": "USMF","UserContext": "billing","ProjId": "000184","InvoiceDate": "2026-10-31","PostAfterCreate": true}} -
See whether it was posted. With
PostAfterCreatetrue,Successtruemeans the invoice was posted: the service checks that the invoice journal exists before it answers.DocumentIdis the proposal,PostedDocumentIdthe invoice, and the message reads "Project invoice proposal ... created and posted as invoice ... for project 000184." The proposal and invoice numbers come from separate number sequences and can be equal, so tell the two outcomes apart by what you sent and by the message, not by comparing the ids. WithPostAfterCreatefalse(or left out), only the proposal is created:DocumentId=PostedDocumentId= the proposal, message "Project invoice proposal ... created for project 000184."; review and post it in D365 on Project management and accounting > Project invoices > Project invoice proposals.
Bulk variants (postHourJournalBatch, postExpenseJournalBatch, postItemJournalBatch,
postFeeJournalBatch, createInvoiceProposalBatch) take a _requests list. The project operations are
not available for asynchronous submission; call them directly.
How to verify
- Each journal response:
Successtrue,Status3,DocumentId=PostedDocumentId= the journal number. - The invoice response:
Successtrue, the proposal inDocumentId, the invoice inPostedDocumentIdand the "created and posted as invoice" message. - In D365: the posted journals on the Hour, Expense, Item and Fee pages under Project management and accounting > Journals; the transactions on Project management and accounting > Transactions > Posted project transactions; the invoice on Project management and accounting > Project invoices > Project invoices.
- Over OData:
LatticeProjectPostedTransactionsfiltered onProjectIdanddataAreaId, withcross-company=true, lists the ledger postings of the project's transactions with category, date, amount and quantity. - If you subscribe to business events, Project invoice posted fires when the invoice is posted.
- The calls: System administration > Inquiries > Lattice > Services > Service call log, or
LatticeServiceCallLogsfiltered on yourRequestId.
When it goes wrong
A failed request answers Success false, Status 4, an ErrorCode and a generic Message; the
reasons are in Errors.
| ErrorCode | Typical cause (text in Errors) | What to do |
|---|---|---|
ValidationFailed (1) | No RequestId or unknown LegalEntityId; "JournalNameId is required."; "At least one journal line is required."; "Journal name ... was not found."; "Journal name ... is not a Project Hour journal (its type is ...)." (and the expense and item variants); "CategoryId is required on every ... journal line."; "Project category '...' was not found."; "Project category '...' is of type ...; a ... journal line needs a ... category."; "Worker '...' was not found (send the worker's personnel number)."; "OffsetMainAccountId is required on every Expense journal line..."; "ItemId is required on every Item journal line."; invoice: "ProjId is required."; "Project ... is not attached to an invoice project (contract), so no proposal can be created." | Correct the request or the project setup, then resend. |
DocumentNotFound (2) | "Project '...' was not found." (journals) or "Project ... was not found." (invoice); "Item '...' was not found." | Send an existing project or item of the legal entity. |
DimensionValueNotFound (5) | "Offset main account '...' could not be resolved in the chart of accounts." | Use an account of the legal entity's chart of accounts. |
ConfigurationMissing (11) | "Journal name ... has no voucher number sequence configured." (expense journals) | Set the voucher series on the journal name. |
BusinessRule (4) | D365 refused the post (no on-hand for the item, missing project posting setup, a closed period); "... journal ... was not posted: posting returned without posting it."; invoice: "No invoice proposal was created for project ... (invoice project ...): it has no posted, chargeable transactions up to ... that are not invoiced yet."; "Invoice proposal ... for project ... was created but posting it produced no invoice." | See below. |
PreviewUnsupported (10) | PreviewMode true: project operations have no preview | Send without PreviewMode. Nothing was run. |
TransientRuntime (6) | Infrastructure fault; IsRetryable true | Resend with the same RequestId. |
Failed posts leave nothing behind. When a journal operation fails after creating its journal, the
unposted journal (project, ledger or inventory journal) is deleted, and the failed request releases its
RequestId: fix the cause and resend the same request. When an invoice post fails, the proposal the
request created is deleted and its transactions return to invoiceable, so resending the same request
proposes them again.
Nothing to invoice. The proposal takes only transactions that are posted, chargeable (a billable line
property), dated on or before InvoiceDate and not yet invoiced. Post the journals first, check their line
property, or use a later InvoiceDate.
Replays and stranded requests work as for every posting service: a repeated RequestId returns the
first result, and the reconciliation sweep resolves a journal request left in progress; see
Post a journal exactly-once.
Reference
- API reference: sections 12 (Project Journal Service) and 13 (Project Invoice Service), 19 (bulk)
- Endpoint payload specification:
MGCLatticeProjectJournalService,MGCLatticeProjectInvoiceService - Data entity reference:
LatticeProjectPostedTransactions - Security model
- Cross-reference keys
- Post a journal exactly-once
- Idempotency reconcile runbook