How to update quantityAvailable of a product in an inventory via API?

I created a product and put it into the location inventory via the admin panel, but how can I get all the products in the inventory and update their quantity via API?

At the moment you cannot just set the quantity for a product. The Quantity On Hand (QoH) and Quantity Available (QATP) for a given product are derived from transactions and allocations on inventory items.

If you provide more details I can put together some sample API requests that will achieve what you’re trying to accomplish.

For example, if you just want to set the quantity for an item you’d create a new Product Inventory transaction to set the new quantity for that item. If you want to mark stock as damaged or expired, you’d create a debit transaction for the damaged or expired item. If you want to consume stock, you’d create a consumption transaction for the consumed item.

Justin

In our app we have merchants and I planned to use openboxes to manage their products in inventory. I thought about setting them as locations in openboxes, adding their products to inventory, and changing the quantity via API when they sell a product. I also wanted to get/add/update products in their inventory via API, but I can’t find a way to get products that were added to the location.

Awesome. Thanks for the explanation.

I think you’re going down the right path. Each merchant could be associated with a separate “depot” location in OpenBoxes. Whether that actually works will depend on how your warehouses are physically configured. But we can leave that issue for later.

I think the proper architecture would be for your system to send messages to OpenBoxes when items are sold. But for now, we can either use an outbound stock movement (which includes meaningful metadata around the requisition/fulfillment process) OR a plain ol’ transaction that just keeps track of the credit or debit.

Let’s start with getting an inventory.

There are a bunch of different ways to get this data but here’s the most basic flow

Authenticate into the location
Request

curl -i -c cookies.txt -X POST -H "Content-Type: application/json" \
-d '{"username":"username","password":"password","location":"locationId"}' \
https://your.server.com/openboxes/api/login

Response

Authentication was successful 

List product summary for location
Request

curl -b cookies.txt -X GET -H "Accept: application/json" \
https://openboxes.ngrok.io/openboxes/api/locations/:locationId/productSummary

Response

{
	"data": [{
		"quantityOnHand": 1600,
		"productName": "Test, Cartridge, Whole blood, Crea (Creatinine), i-STAT, 1 test",
		"productCode": "21105",
		"productId": "21105"
	}, {
		"quantityOnHand": 15,
		"productName": "Frame, for N95/FFP2 mask to improve fit",
		"productCode": "21243",
		"productId": "21243"
	}, {
		"quantityOnHand": 250,
		"productName": "Antibiotic disc, Ceftazidime, 30mcg (CAZ-30), 1 disc",
		"productCode": "21259",
		"productId": "21259"
	}, {
		"quantityOnHand": 250,
		"productName": "Antibiotic disc, Meropenem, 10mcg (MEM-10), 1 disc",
		"productCode": "21261",
		"productId": "21261"
	}, {
		"quantityOnHand": 0,
		"productName": "Laptop, Lenovo, ThinkBook 14 IIL, Core i5-1035G1",
		"productCode": "21336",
		"productId": "21336"
	}, {
		"quantityOnHand": 0,
		"productName": "Laptop, Lenovo, ThinkPad E14 Core i5 14\" Aluminium",
		"productCode": "21337",
		"productId": "21337"
	}, {
		"quantityOnHand": 0,
		"productName": "Syringe pump, Braun Perfusor Space, Standard",
		"productCode": "21347",
		"productId": "21347"
	}, {
		"quantityOnHand": 0,
		"productName": "Laptop, Lenovo, ThinkPad X1 Extreme Gen 2, 9th Generation Intel Core i7",
		"productCode": "21383",
		"productId": "21383"
	}, {
		"quantityOnHand": 0,
		"productName": "Fetal Monitor, Cardiotocography (CTG) (Edan F2)",
		"productCode": "24153",
		"productId": "24153"
	}, {
		"quantityOnHand": 225,
		"productName": "Antibiotic disc, Cefoxitin (FOX-30), 30ug, 1 disc",
		"productCode": "24180",
		"productId": "24180"
	}]
}

List all available inventory items for a product and location
Request

curl -b cookies.txt -X GET -H "Accept: application/json" \
https://openboxes.ngrok.io/openboxes/api/products/:productId/productAvailability

Response

{
	"data": [{
		"class": "org.pih.warehouse.product.ProductAvailability",
		"id": "3b5579d8-3ece-4bd9-bb26-5cd4b7bca745",
		"binLocation": null,
		"binLocationName": "DEFAULT",
		"dateCreated": "2022-06-25T02:29:42Z",
		"inventoryItem": {
			"class": "InventoryItem",
			"id": "09f5e116819082200181979bdcf92b8c"
		},
		"lastUpdated": "2022-09-14T21:53:16Z",
		"location": {
			"class": "Location",
			"id": "1"
		},
		"lotNumber": "469600",
		"product": {
			"class": "Product",
			"id": "21259"
		},
		"productCode": "21259",
		"quantityAllocated": 0,
		"quantityAvailableToPromise": 250,
		"quantityNotPicked": 250,
		"quantityOnHand": 250,
		"quantityOnHold": 0
	}]
}

You can also use the availableBins subresource but these basically return the same data.

List all available bin locations
Request

curl -b cookies.txt -X GET -H "Accept: application/json" https://openboxes.ngrok.io/openboxes/api/products/:productId/availableBins

Response

{
	"data": [{
		"inventoryItem.id": "09f5e116819082200181979bdcf92b8c",
		"product.name": "Antibiotic disc, Ceftazidime, 30mcg (CAZ-30), 1 disc",
		"product": {
			"id": "21259",
			"productCode": "21259",
			"name": "Antibiotic disc, Ceftazidime, 30mcg (CAZ-30), 1 disc",
			"description": null,
			"category": "Lab: Microbiology",
			"unitOfMeasure": "EACH",
			"pricePerUnit": 0.1144,
			"dateCreated": "2020-08-11T05:00:00Z",
			"lastUpdated": "2022-04-10T19:36:19Z",
			"updatedBy": "Seyfu Desta",
			"color": "dodgerblue",
			"handlingIcons": [{
				"icon": "fa-snowflake",
				"color": "#3bafda",
				"label": "Cold chain"
			}],
			"lotAndExpiryControl": null,
			"active": true,
			"translatedName": "Antibiotic disc, Ceftazidime, 30mcg (CAZ-30), 1 disc",
			"displayNames": {
				"default": "Antibiotic disc, Ceftazidime, 30mcg (CAZ-30), 1 disc",
				"en": "Antibiotic disc, Ceftazidime, 30mcg (CAZ-30), 1 disc"
			}
		},
		"productCode": "21259",
		"lotNumber": "469600",
		"expirationDate": "04/30/2023",
		"binLocation": null,
		"zone": null,
		"quantityAvailable": 250,
		"quantityOnHand": null,
		"status": "AVAILABLE",
		"pickedRequisitionNumbers": "",
		"binLocation.id": null,
		"binLocation.name": null
	}]
}

And now that I’m trying to create the outbound transaction I’m realizing that the generic Transaction API might not work as I was expecting. We’ve built a lot of our APIs for internal purposes (React apps, mobile app, etc) so there are use cases that we’re not supporting yet.

If that’s the case, then we can probably only do this using the Stock Movement API and that sort of defeats the purpose of what you’re trying to do (i.e. streamline the outbound process with a single API request).

We built some APIs for the mobile application that might be helpful here. However, they were built for a custom implementation that required us to fork the core repo and unfortunately, those APIs have not been migrated to our core repository yet.

I’ll take a look and see if there’s something we can do in the meantime.

Thanks a lot!

A few more questions, though :slightly_smiling_face: about endpoints like this one

https://openboxes.ngrok.io/openboxes/api/locations/:locationId/productSummary

its ending - productSummary - where I can find all available methods like this one?

A similar question about generic API, the documentation says

GET https://openboxes.ngrok.io/openboxes/api/generic/resource

where is any of the domain classes in the system and is the primary key

I’m not familiar with Java/Groovy, where I can find all of these domain classes?

And is there a way to create a product that belongs/is visible to only a specific location?

One workaround would be to use the Stock Transfer API to move items from the default internal location to a “hold” location that represents the “sold” goods. I’ll put together an example, if that’s something you’d be interested in.

Also, to clarify my point about the Stock Movement process, you could conceivably create an outbound request for a product through the API and then fulfill that request either manually through the Stock Movement feature or programmatically via the API. In either case, it’s just more complicated than what you are looking to do so I’d like to come up with a more streamlined approach as well.

1 Like

Disclaimer: Since I don’t have a ton of time to document the APIs I’m sort of hoping that we can use this forum as a way to identify and prioritize the API documentation needs as well as to figure out what APIs need a little love in terms of development.

All that is to say that this conversation is exactly what I want. So thank you for bringing this up.


As you have probably discovered, the main documentation for the API is here (and not very good).

We started to work on OpenAPI (Swagger) documentation, but it uses our Grails 3 migration branch so it might be outdated and potentially useless until the migration is done.

However, I can see whether we can get it updated to the latest version (0.8.21) just so all of the known endpoints are there.

A few of the product subresources are listed in the Swagger docs, but for other domains, you need to dig around the source code.

So for now, just ask for recommendations here and I’ll do my best to provide a solution. And eventually, those will get merged back into the docs. The cycle of life.

Lastly, the error I’m seeing on the generic Transaction API is actually a small bug which can be solved by adding a property editor for the Inventory domain.

Request

curl -b cookies.txt -X POST -H "Content-Type: application/json" \
-d '{"transactionDate": "03/10/2023 00:00:00", "transactionType": "9", "inventory": "1", "destination":"1"}' \ 
https://openboxes.ngrok.io/openboxes/api/generic/transaction

Response

{
	"errorCode": 400,
	"errorMessage": "Validation error. Cannot create product due to validation errors:\n- Field error in object 'org.pih.warehouse.inventory.Transaction' on field 'inventory': rejected value [1]; codes [typeMismatch.org.pih.warehouse.inventory.Transaction.inventory,typeMismatch.inventory,typeMismatch.org.pih.warehouse.inventory.Inventory,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [org.pih.warehouse.inventory.Transaction.inventory,inventory]; arguments []; default message [inventory]]; default message [Failed to convert property value of type 'java.lang.Integer' to required type 'org.pih.warehouse.inventory.Inventory' for property 'inventory'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.Integer] to required type [org.pih.warehouse.inventory.Inventory] for property 'inventory': no matching editors or conversion strategy found]\n",
	"errorMessages": ["Failed to convert property value of type java.lang.Integer to required type org.pih.warehouse.inventory.Inventory for property inventory; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.Integer] to required type [org.pih.warehouse.inventory.Inventory] for property inventory: no matching editors or conversion strategy found"]
}

So I can work on fixing that as well as any other issues that stand in the way

but a proper solution is also in order.

Here are a few suggestions.

Let me know how you were expecting to handle your “sales” use case. For example, did you have an API request in mind when you started the process? Would you prefer to handle this interaction using messaging (i.e. EDI message) or direct API requests?

I thought to make it quite straightforward, to be honest :slightly_smiling_face:
Without creating transaction records in openboxes but with funds transaction records in our app. For example, the customer adds some products to a cart, the merchant scans product QR codes (which are IDs of the inventory items) with our terminal app, the customer pays and after that I just call openboxes API with inventory item IDs to decrease their quantity and create corresponding funds transaction records in our system.

But I think merchants would like to see the history of their inventory items in openboxes too, so your suggestion about using Stock Transfer API or Transaction API sounds good to me. Now I think that just updating the inventory items quantity is not a really good idea.

after that I just call openboxes API with inventory item IDs to decrease their quantity

Ok thanks, that should be easy to implement.

Now I think that just updating the inventory items quantity is not a really good idea.

There’s no way to do this in OB without a transaction. But from your perspective, there shouldn’t be much of a difference. You can just pass us a list of sold items (i.e. inventory item, quantity, unit of measure) and we should be able to create a transaction for you.

For example, we could send us the following request

request

POST /api/transactions

payload

{
    transactionType: "SALES",
    transactionDate: "2022-03-10 21:49:00",
    items: [
        { inventoryItemId: "09f5e116819082200181979bdcf92b8c", quantity: 1, unitOfMeasure: "EA" },
        { inventoryItemId: "8a8a9e9666194c8901661d2e06340167", quantity: 10, unitOfMeasure: "EA" }]
}

NOTE: The transaction type (SALES) tell us whether we should add or subtract the quantities from the quantity on hand.

From what you said earlier, we can do this as a straight-up transaction (i.e. Debit 1 EA of Inventory Item 001 of Product ABC from Location A). We can always build in the additional layers later.

For example, we can get more complicated if you eventually want to track other data elements like customer info, sales reference number, sales interaction type (POS vs e-commerce), or if you need to track the fulfillment process (allocation, picking, packing, shipping) as disparate events within a workflow.

The transaction type (SALES) tell us whether we should add or subtract the quantities from the quantity on hand.

So in this case

{ inventoryItemId: "09f5e116819082200181979bdcf92b8c", quantity: 1, unitOfMeasure: "EA" }

this inventory item will decrease from 10 (for example) to 9, I mean we are decreasing its quantity by 1, not setting it to 1, right? And after that openboxes will automatically create a transaction?

But until this API is implemented I need to use Transaction API or Stock Transfer API to create transactions myself, correct?

this inventory item will decrease from 10 (for example) to 9, I mean we are decreasing its quantity by 1, not setting it to 1, right? And after that openboxes will automatically create a transaction?

Correct. But we can also create an API that will allow you to adjust the transaction by telling us what the current Quantity on Hand for an inventory item and we’ll create the adjustment transaction.

But until this API is implemented I need to use Transaction API or Stock Transfer API to create transactions myself, correct?

Correct.

Using the generic Transaction API would basically be the same as above but would require two API requests (one to create the transaction header and another to create the transaction entry for each item).
In other words, the only difference between the two is that the streamlined API above would basically merge those two requests into a single request.

Using the Stock Transfer API would be a hack that allows you to move quantity from available to unavailable like you can do using either of the transfer features available from the UI.

The end result would look something like this.

1 Like

The document above is about Transaction API or Stock Transfer API?

If not, then could you give me an example with both API?

No, Stock Movement API is the one that would require you to go through a multi-step fulfillment process (add items, revise quantities, allocate stock, pick stock, pack stock, ship stock). This API is used for our internal stock movement workflow. You need something very simple “debit quantity X from location A for inventory item 123 at time Y”.

I just remembered a third API that is closer to what you originally wanted. I’ll write up an example of the Transaction and Stock Adjustment APIs once I fix the bugs preventing their use and the Stock Transfer API once I run through a few examples in Postman.

In time meantime, this is a screenshot of a Stock Transfer API request I created sometime last year in Postman. I can’t say this will work but it’s what I plan to use as the starting point for an example.

So right now there is no way to use openboxes for what I need?

You can use the Stock Transfer API until we fix the bugs on the other two APIs. I just didn’t have time to test the Stock Transfer API yesterday. However, I was able to get it working locally tonight. I just need a bit more time to make sure this will work for your use case i.e.

  • origin bin location = null
  • destination bin location = Hold Bin

So I’ll look into that tomorrow and either provide you with a working example to get you started or link to yet another ticket that will need to be completed before you can continue. The good news is that now of the issues we’ve encountered so far will take more than a day or two to complete.

As I alluded to in a previous comment, we are building our APIs for our own internal use so while most of them work fine for us, we haven’t had much feedback from developers on how they would like to interact with the OpenBoxes API.

So your feedback and patience are very much appreciated.


Step 1. Create a stock transfer by specifying what items need to be transferred

POST https://openboxes.ngrok.io/openboxes/api/stockTransfers

Payload

{
    "stockTransferItems": [
        {
            "product.id": "09f5e1167e6ee8ec017e741ff8a637a5",
            "product.productCode": "24465",
            "inventoryItem.id": "09f5e1167f2c6949017f658d60953859",
            "location.id": "1",
            "lotNumber": "FMQWR77N00",
            "originBinLocation.id": "09f5e1168103c8ba0181496a1a416df2",
            "destinationBinLocation.id": "09f5e1167e6ee8ec017e7414e83e36f0",
            "quantity": 1
        }
    ]
}

This will create a PENDING stock transfer

{
    "data": {
        "id": "ff80818186cd348e0186d943eb3f0019",
        "description": null,
        "stockTransferNumber": "NMR354",
        "status": "PENDING",
        "dateCreated": "March 12, 2023",
        "origin.id": "8a8a9e9665c4f59d0165c54ec6b10027",
        "origin.name": "Distribution Center",
        "destination.id": "8a8a9e9665c4f59d0165c54ec6b10027",
        "destination.name": "Distribution Center",
        "stockTransferItems": [
            {
                "id": "ff80818186cd348e0186d943eb3f001a",
                "productAvailabilityId": "6617209c-31e7-4fbc-a13d-b0becdd3f047",
                "product.id": "09f5e1167e6ee8ec017e741ff8a637a5",
                "product.productCode": "24465",
                "product.name": "Laptop, MacBook Pro, 14in",
                "product.translatedName": "Laptop, MacBook Pro, 14in",
                "product.handlingIcons": [],
                "inventoryItem.id": "09f5e1167f2c6949017f658d60953859",
                "lotNumber": "FMQWR77N00",
                "expirationDate": null,
                "recalled": false,
                "originBinLocation.id": "09f5e1168103c8ba0181496a1a416df2",
                "originBinLocation.name": "Speed Cart",
                "originZone": null,
                "onHold": false,
                "destinationBinLocation.id": "09f5e1167e6ee8ec017e7414e83e36f0",
                "destinationBinLocation.name": "Hold Bin",
                "destinationZone.id": null,
                "destinationZone.name": null,
                "quantity": 1,
                "quantityOnHand": 1,
                "quantityNotPicked": 1,
                "status": "PENDING",
                "recipient": null,
                "splitItems": [],
                "picklistItems": [],
                "sortOrder": 0
            }
        ],
        "orderedBy": "Justin Miranda",
        "type": "TRANSFER_ORDER",
        "dateShipped": "",
        "expectedDeliveryDate": "",
        "shipmentType": "",
        "trackingNumber": "",
        "driverName": "",
        "comments": "",
        "documents": ""
    }
}

Step 2. Post a status update on the newly create stock transfer

POST https://openboxes.ngrok.io/openboxes/api/stockTransfers/ff80818186cd348e0186d943eb3f0019

Payload

{
    "status": "COMPLETED",
}

And that will return the full stock transfer object again with the COMPLETED status.

{
    "data": {
        "id": "ff80818186cd348e0186d9345f720011",
        "description": "Stock Transfer Description (if desired)",
        "stockTransferNumber": "VST066",
        "status": "COMPLETED",
        "dateCreated": "March 12, 2023",
        "origin.id": "1",
        "origin.name": "Boston: Haiti Stock",
        "destination.id": "1",
        "destination.name": "Boston: Haiti Stock",
        "stockTransferItems": [
            {
                "id": "ff80818186cd348e0186d9345f720012",
                "productAvailabilityId": "ff80818186cd348e0186d9345f720012",
                "product.id": "09f5e1167e6ee8ec017e741ff8a637a5",
                "product.productCode": "24465",
                "product.name": "Laptop, MacBook Pro, 14in",
                "product.translatedName": "Laptop, MacBook Pro, 14in",
                "product.handlingIcons": [],
                "inventoryItem.id": "09f5e1167f2c6949017f658d449b3856",
                "lotNumber": null,
                "expirationDate": null,
                "recalled": false,
                "originBinLocation.id": "09f5e1168103c8ba0181496a1a416df2",
                "originBinLocation.name": "Speed Cart",
                "originZone": null,
                "onHold": false,
                "destinationBinLocation.id": "09f5e1167e6ee8ec017e7414e83e36f0",
                "destinationBinLocation.name": "Hold Bin",
                "destinationZone.id": null,
                "destinationZone.name": null,
                "quantity": 1,
                "quantityOnHand": 0,
                "quantityNotPicked": 0,
                "status": "COMPLETED",
                "recipient": null,
                "splitItems": [],
                "picklistItems": [],
                "sortOrder": 0
            }
        ],
        "orderedBy": "Justin Miranda",
        "type": "TRANSFER_ORDER",
        "dateShipped": "",
        "expectedDeliveryDate": "",
        "shipmentType": "",
        "trackingNumber": "",
        "driverName": "",
        "comments": "",
        "documents": ""
    }
}

Here’s what the operation looks like from the UI perspective.

Stock Card (Before)

Stock Card (After)

Stock History

1 Like

In Payload of Step 1 what is inventoryItem.id and where can I get it?

I have created two Bin Locations - Store and Sold - under my location and after moving the product from Default location to Store location I tried to get inventoryItem.id from
GET /openboxes/api/products/2c96808386c648e70186c69efed0000b/availableItems
Response

{
    "data": [
        {
            "inventoryItem.id": "2c96808386c648e70186c6adf57c000c",
            "product.name": "test product 1",
            "product": {
                "id": "2c96808386c648e70186c69efed0000b",
                "productCode": "tp1",
                "name": "test product 1",
                "description": null,
                "category": "test category",
                "unitOfMeasure": null,
                "pricePerUnit": 1,
                "dateCreated": "2023-03-09T13:45:54Z",
                "lastUpdated": "Mar 10, 2023",
                "updatedBy": "Miss Administrator",
                "color": null,
                "handlingIcons": [],
                "lotAndExpiryControl": false,
                "active": true
            },
            "productCode": "tp1",
            "lotNumber": null,
            "expirationDate": null,
            "binLocation": {
                "id": "2c96808386da37c80186dc7cb9f90017",
                "name": "Store",
                "description": null,
                "locationNumber": null,
                "locationGroup": null,
                "parentLocation": {
                    "id": "2c96808386da37c80186dc7c32520015",
                    "name": "test merchant",
                    "description": null,
                    "locationNumber": null,
                    "locationGroup": null,
                    "parentLocation": null,
                    "locationType": {
                        "id": "2c96808386c648e70186c87661f1002d",
                        "name": "super",
                        "description": null,
                        "locationTypeCode": "DISTRIBUTOR"
                    },
                    "sortOrder": null,
                    "hasBinLocationSupport": true,
                    "hasPackingSupport": true,
                    "hasPartialReceivingSupport": true,
                    "hasCentralPurchasingEnabled": true,
                    "organizationName": "tst",
                    "organizationCode": "TST",
                    "backgroundColor": "FFFFFF",
                    "zoneName": null,
                    "zoneId": null,
                    "active": true,
                    "organization": {
                        "id": "2c96808386c648e70186c67a9c6b0004",
                        "name": "tst",
                        "description": null,
                        "code": "TST",
                        "dateCreated": "2023-03-09T13:06:10Z",
                        "lastUpdated": "2023-03-09T13:06:10Z",
                        "defaultLocation": null,
                        "partyType": {
                            "id": "1",
                            "name": "Organization",
                            "code": "ORG",
                            "partyTypeCode": "ORGANIZATION"
                        },
                        "roles": [
                            {
                                "id": "2c96808386c648e70186c67c40fa0006",
                                "roleType": "ROLE_ORGANIZATION",
                                "startDate": null,
                                "endDate": null
                            }
                        ],
                        "sequences": {}
                    },
                    "manager": {
                        "id": "1",
                        "name": "Miss Administrator",
                        "firstName": "Miss",
                        "lastName": "Administrator",
                        "email": "admin@openboxes.com",
                        "username": "admin"
                    },
                    "address": null,
                    "supportedActivities": [
                        "PARTIAL_RECEIVING",
                        "DYNAMIC_CREATION",
                        "EXTERNAL",
                        "PLACE_ORDER",
                        "ENABLE_NOTIFICATIONS",
                        "ADJUST_INVENTORY",
                        "HOLD_STOCK",
                        "SUBMIT_REQUEST",
                        "REQUIRE_ACCOUNTING",
                        "MANAGE_INVENTORY",
                        "APPROVE_ORDER",
                        "PICK_STOCK",
                        "FULFILL_REQUEST",
                        "PLACE_REQUEST",
                        "PUTAWAY_STOCK",
                        "SEND_STOCK",
                        "CROSS_DOCKING",
                        "ENABLE_CENTRAL_PURCHASING",
                        "CONSUME_STOCK",
                        "PACK_SHIPMENT",
                        "FULFILL_ORDER",
                        "RECEIVE_STOCK",
                        "APPROVE_REQUEST"
                    ]
                },
                "locationType": {
                    "id": "cab2b4f35ba2d867015ba2e17e390001",
                    "name": "Bin Location",
                    "description": "Default bin location type",
                    "locationTypeCode": "BIN_LOCATION"
                },
                "sortOrder": null,
                "hasBinLocationSupport": true,
                "hasPackingSupport": false,
                "hasPartialReceivingSupport": false,
                "hasCentralPurchasingEnabled": false,
                "organizationName": "tst",
                "organizationCode": "TST",
                "backgroundColor": "FFFFFF",
                "zoneName": null,
                "zoneId": null,
                "active": true,
                "organization": {
                    "id": "2c96808386c648e70186c67a9c6b0004",
                    "name": "tst",
                    "description": null,
                    "code": "TST",
                    "dateCreated": "2023-03-09T13:06:10Z",
                    "lastUpdated": "2023-03-09T13:06:10Z",
                    "defaultLocation": null,
                    "partyType": {
                        "id": "1",
                        "name": "Organization",
                        "code": "ORG",
                        "partyTypeCode": "ORGANIZATION"
                    },
                    "roles": [
                        {
                            "id": "2c96808386c648e70186c67c40fa0006",
                            "roleType": "ROLE_ORGANIZATION",
                            "startDate": null,
                            "endDate": null
                        }
                    ],
                    "sequences": {}
                },
                "manager": null,
                "address": null,
                "supportedActivities": [
                    "PICK_STOCK",
                    "PUTAWAY_STOCK"
                ]
            },
            "zone": null,
            "quantityAvailable": 10,
            "quantityOnHand": null,
            "status": "AVAILABLE",
            "pickedRequisitionNumbers": "",
            "binLocation.id": "2c96808386da37c80186dc7cb9f90017",
            "binLocation.name": "Store"
        }
    ]
}

and use it in payload for
POST /openboxes/api/stockTransfers
Payload

{
    "stockTransferItems": [
        {
            "product.id": "2c96808386c648e70186c69efed0000b",
            "product.productCode": "tp1",
            "inventoryItem.id": "2c96808386c648e70186c6adf57c000c",
            "location.id": "2c96808386da37c80186dc7c32520015",
            "lotNumber": "Default",
            "originBinLocation.id": "2c96808386da37c80186dc7cb9f90017",
            "destinationBinLocation.id": "2c96808386da37c80186dc7ccfd70019",
            "quantity": 1
        }
    ]
}

It seems that it correctly creates stock transfer in PENDING status and I can see it in UI
Response

{
    "data": {
        "id": "2c96808386da37c80186dc84235a0022",
        "description": null,
        "stockTransferNumber": "827MHK",
        "status": "PENDING",
        "dateCreated": "March 13, 2023",
        "origin.id": "2c96808386da37c80186dc7c32520015",
        "origin.name": "test merchant",
        "destination.id": "2c96808386da37c80186dc7c32520015",
        "destination.name": "test merchant",
        "stockTransferItems": [
            {
                "id": "2c96808386da37c80186dc84235a0023",
                "productAvailabilityId": "2c96808386da37c80186dc84235a0023",
                "product.id": "2c96808386c648e70186c69efed0000b",
                "product.productCode": "tp1",
                "product.name": "test product 1",
                "product.handlingIcons": [],
                "inventoryItem.id": "2c96808386da37c80186dc73fb6c000e",
                "lotNumber": "Default",
                "expirationDate": null,
                "recalled": false,
                "originBinLocation.id": "2c96808386da37c80186dc7cb9f90017",
                "originBinLocation.name": "Store",
                "originZone": null,
                "onHold": false,
                "destinationBinLocation.id": "2c96808386da37c80186dc7ccfd70019",
                "destinationBinLocation.name": "Sold",
                "destinationZone.id": null,
                "destinationZone.name": null,
                "quantity": 1,
                "quantityOnHand": 0,
                "quantityNotPicked": 0,
                "status": "PENDING",
                "recipient": null,
                "splitItems": [],
                "picklistItems": [],
                "sortOrder": 0
            }
        ],
        "orderedBy": "Miss Administrator",
        "type": "TRANSFER_ORDER",
        "dateShipped": "",
        "expectedDeliveryDate": "",
        "shipmentType": "",
        "trackingNumber": "",
        "driverName": "",
        "comments": "",
        "documents": ""
    }
}

but when I try to update this transfer with ID I get from the previous step
POST /openboxes/api/stockTransfers/2c96808386da37c80186dc84235a0022
Payload

{
    "status": "COMPLETED"
}

I always get a response with an empty stockTransferItems array
Response

{
    "data": {
        "id": "2c96808386da37c80186dc8454940024",
        "description": null,
        "stockTransferNumber": "355YBV",
        "status": "COMPLETED",
        "dateCreated": "March 13, 2023",
        "origin.id": "2c96808386da37c80186dc7c32520015",
        "origin.name": "test merchant",
        "destination.id": "2c96808386da37c80186dc7c32520015",
        "destination.name": "test merchant",
        "stockTransferItems": [],
        "orderedBy": "Miss Administrator",
        "type": "TRANSFER_ORDER",
        "dateShipped": "",
        "expectedDeliveryDate": "",
        "shipmentType": "",
        "trackingNumber": "",
        "driverName": "",
        "comments": "",
        "documents": ""
    }
}

And in UI I see a new completed transfer with 0 items in it.

Can you double check the status update request and make sure you’re posting to the right stock transfer?

In your previous message you state that you are posting a COMPLETED status to stock transfer with ID 2c96808386da37c80186dc84235a0022 (which is the PENDING one with stock transfer number 827MHK).

POST /openboxes/api/stockTransfers/2c96808386da37c80186dc84235a0022
{
    "status": "COMPLETED"
}

But the response shows a different stock transfer with a different ID and stock transfer number.

{
    "data": {
        "id": "2c96808386da37c80186dc8454940024",
        "description": null,
        "stockTransferNumber": "355YBV",
        ...