Dutchie Inventory Intake via Backoffice API
Mint stores receive physical inventory every day. The canonical source of truth for what's on the shelf is Dutchie.
This course walks through the Backoffice REST intake flow end-to-end against the AZ sandbox (LocId 1989) — the same path production uses, with zero risk to real stock.
Learning objectives
- Authenticate against the Dutchie Backoffice REST API and reuse a cached session.
- Identify which of the three sandboxes to use for AZ-dispensary testing.
- Fetch the prerequisite IDs — vendor, room, product — that a receive call needs.
- POST an inventory receive with the correct field shape and explain why each field is required.
- Verify a receive materialized using the right read endpoints (and the subtle parameter gotcha).
- Diagnose the three most common failures: orphaned receives, empty reads, SQL errors.
Prerequisites: Familiarity with HTTP/REST and JSON. Access to Dutchie Backoffice credentials. Clone of letsgomint-us repo.
Source: docs/elearning/dutchie-inventory-intake-course.md
| Responsible | Juan Palomino |
|---|---|
| Last Update | 04/18/2026 |
| Completion Time | 50 minutes |
| Members | 2 |
Module 3 — Prerequisite IDs
Vendor, room, product
A receive call needs three IDs you cannot guess. Pull them first.
3.1 Vendor
const vendors = await client.post('vendor/get-vendors', { LocId: 1989 });
// [{ VendorId: 39195, VendorName: 'PALOMINO PRINTING', ... }]
Note the path has no v2/ prefix. Common misguess: v2/vendor/get-vendors returns 404.
3.2 Room
const rooms = await client.post('v2/room/get-rooms', { LocId: 1989 });
// [{ RoomId: 24772, RoomName: 'Sales Floor', PosRoom: 'yes', InventoryRoom: 'yes' }]
At the AZ sandbox there are 4 rooms. Pick one whose InventoryRoom === yes.
3.3 Product
const lite = await client.post('product-master/get-products-lite', { LspId: 575 });
// [{ ProductId: 11484902, Sku: '3B4791', Name: 'Tommy Chong Joint (H)', ... }]
get-products-lite is LSP-scoped (~17k products). For richer detail use get-product-master.
Vendor, room, product
A receive call needs three IDs you cannot guess. Pull them first.
3.1 Vendor
const vendors = await client.post('vendor/get-vendors', { LocId: 1989 });
// [{ VendorId: 39195, VendorName: 'PALOMINO PRINTING', ... }]
Note the path has no v2/ prefix. Common misguess: v2/vendor/get-vendors returns 404.
3.2 Room
const rooms = await client.post('v2/room/get-rooms', { LocId: 1989 });
// [{ RoomId: 24772, RoomName: 'Sales Floor', PosRoom: 'yes', InventoryRoom: 'yes' }]
At the AZ sandbox there are 4 rooms. Pick one whose InventoryRoom === yes.
3.3 Product
const lite = await client.post('product-master/get-products-lite', { LspId: 575 });
// [{ ProductId: 11484902, Sku: '3B4791', Name: 'Tommy Chong Joint (H)', ... }]
get-products-lite is LSP-scoped (~17k products). For richer detail use get-product-master.
When it breaks
| Symptom | Root cause | Fix |
|---|---|---|
Cannot insert the value NULL into column 'Status' | Missing header Status | Add Status: "Received" at top level |
Receive returns true, preorder/inventory unchanged | Missing RoomId per product | Add RoomId to every Products[] item |
get-packages-from-receive returns [] | Used Id instead of ReceiveInventoryHistoryId | Rename the param |
User is not permitted | Endpoint needs elevated permissions | Not fatal for intake — use scope-appropriate endpoints |
| Receive succeeds but location has no rooms | Using IL (2862) or OH (2869) sandbox | Switch to AZ sandbox (1989) |
Final assessment
Work through these against the AZ sandbox (LocId 1989). Use scripts/_probe/sandbox-az-retry.cjs as reference.
- Authenticate and confirm your session sees at least 18 locations.
- Pick a vendor, a room (
InventoryRoom === "yes"), and two cannabis products. - POST a receive for quantity 3 of each. Capture the returned
ReceiveInventoryHistoryId. - Verify: two new packages, history row with
Products: 2, both SKUs inpreorder/inventoryat Qty 3. - Summarize what you'd do if step 4c failed but 4a and 4b passed.
Grading rubric
- Steps 1-3 complete with
Result: true— basic pass. - Step 4 all green — working knowledge.
- Step 5 identifies that packages exist but are in a non-sellable room, and points to checking
RoomNameon package records — mastery.
Not done until you've read it back
5.1 Packages
POST api/v2/inventory/get-packages-from-receive
{ "LocId": 1989, "ReceiveInventoryHistoryId": 1379127 }
Use ReceiveInventoryHistoryId — not Id. Passing Id silently returns [] with no error.
Healthy response: one package per product line, each with a PackageId, matching SKU, and Quantity equal to what you sent.
5.2 Receive history
POST api/v2/inventory/get-receive-history
{ "LocId": 1989, "StartDate": "<ISO>", "EndDate": "<ISO>" }
Your new row has Status: "Received", your VendorName, OrderTitle, and Products count.
5.3 Sellable inventory
POST api/preorder/inventory
{ "LocId": 1989 }
Your SKUs appear with Quantity equal to what you received. If not — the packages exist but are in a non-sellable room.
The single POST that lands physical stock
POST api/v2/inventory/receive
{
"LocId": 1989,
"ReceiveType": "Purchase",
"Status": "Received",
"VendorId": 39195,
"VendorName": "PALOMINO PRINTING",
"InvoiceNumber": "INV-12345",
"OrderTitle": "Intake — manifest #12345",
"ReceivedBy": "Jane Doe",
"DeliveredBy": "Acme Logistics",
"DeliveredOn": "2026-04-17T15:30:00Z",
"Note": "Free-form note",
"Products": [
{
"ProductId": 11484902,
"Sku": "3B4791",
"ProductName": "Tommy Chong Joint (H)",
"Category": "Flower",
"Quantity": 5,
"UnitCost": 1.00,
"UnitId": 1,
"NetWeight": 0,
"Status": "Complete",
"RoomId": 24772
}
]
}
Response: { "Result": true, "Data": 1379127 }. Data is the ReceiveInventoryHistoryId — hold onto it for verification.
Two Status fields, two different values
- Header
Status: "Received"— controlsReceiveInventoryHistory.StatusSQL column. Missing → NULL constraint error. Wrong value ("Complete") → accepted but orphans. - Per-product
Status: "Complete"— marks each line item as finalized.
Don't forget RoomId
Without RoomId on a product line, the receive header lands in SQL but no PackageInventory row is created. You get Result: true and zero new packages. Always set RoomId.
Don't write your own auth
All Backoffice endpoints are POST, JSON in and out, and session-authenticated via a cookie named LLSession.
POST https://themint.backoffice.dutchie.com/api/posv3/user/EmployeeLogin
Content-Type: application/json
appname: Backoffice
{ "UserName": "<employee email>", "Password": "<password>" }
The response contains Data.SessionGId. Use it as Cookie: LLSession=<SessionGId> on every subsequent call. Sessions expire at ~60 min.
Use the production client at packages/inventory-service/api/dutchieBackoffice.js:
const Client = require('./packages/inventory-service/api/dutchieBackoffice.js');
const client = new Client();
await client.getSession(); // cached + auto-refreshes at 50 min
const result = await client.post('v2/inventory/get-inventory', { LocId: 1989 });
Env vars: DUTCHIE_BACKOFFICE_USERNAME, DUTCHIE_BACKOFFICE_PASSWORD.
Know where you're writing to
Before writing a line of code, know where you're writing to. Our Dutchie session (UserId=60672) sees three locations named "Sandbox":
| LocId | State | Company | When to use |
|---|---|---|---|
| 1989 | AZ | Dispensary | Default for intake work. Mirrors Tempe's retail setup — rooms, registers, vendors already configured. |
| 2862 | IL | Manufacturing | Only for IL-specific manufacturing flows. Has no rooms — intake writes orphan. |
| 2869 | OH | Dispensary | Only for OH-specific flows. |
Why this matters
The first version of our memory doc listed LocId 2862 as "the sandbox" because a naïve filter (GetLspLocationsBackend with LspId=575) only returned IL locations. The AZ sandbox wasn't visible under that filter. Always call GetLspLocationsBackend with an empty body {} to see all 18 locations across LSPs.
Check for understanding
- A teammate asks you to "test against the sandbox." What's your first question?
- If
v2/inventory/receivereturnsResult: truebut no packages appear, which LocId did they probably send it to?
What you'll learn
Mint stores receive physical inventory every day — flower from Swallowtail Cultivation, edibles from Wyld, carts from Stiiizy. The canonical source of truth for what's on the shelf is Dutchie. If you're writing any integration that places orders, mirrors stock into Odoo, or reconciles on-hand counts, you need to know how inventory gets into Dutchie in the first place.
This course walks you through the Backoffice REST intake flow end-to-end against the AZ sandbox (LocId 1989).
By the end you will be able to
- Authenticate against the Dutchie Backoffice REST API and reuse a cached session.
- Identify which sandbox to use for AZ-dispensary testing (and which two to avoid).
- Fetch the prerequisite IDs — vendor, room, product — that a receive call needs.
- POST an inventory receive with the correct field shape.
- Verify a receive materialized using the right read endpoints.
- Diagnose the three most common failures.