Fix InventoryItem Price Replace in REST Netsuite
When you use the NETSUIT REST API to update inventory item prices, you may find a frustrating difference from the SOAP SuiteTalk API: the `replaceAll` flag doesn't exist. Instead, you use the...
Arav SharmaCore SuiteScript & Integration Engineer
When you use the NETSUIT REST API to update inventory item prices, you may find a frustrating difference from the SOAP SuiteTalk API: the replaceAll flag doesn't exist. Instead, you use the ?replace=price query parameter on a PATCH request. However, this spawns a new problem, the API applies a diff before replacing, and any rows in your payload that exactly match the current data are stripped out before the replacement happens. This article explains why that occurs and how to work around it.
The Problem: Diff Logic vs. Replace Flag
When you send a PATCH request to update the price sublist on an inventory item, NetSuite first compares your incoming payload against the existing item state. Any price level rows that match the current data exactly are treated as "no change" and are excluded from the replacement set.
Here is the actual sequence NetSuite follows:
- Diff: Compare incoming payload rows against existing sublist rows.
- Strip: Remove unchanged rows from the request.
- Replace: Apply the
?replaceparameter, clearing all existing price levels. - Insert: Add the remaining (changed) rows from your payload.
The result: unchanged rows disappear because they never make it past the diff stage. For example, if you send a priceLevel row with ID 18 and identical quantity/value to what's already stored, that row will be stripped before the replace flag clears the palette.
What Doesn't Work: Single PATCH with Replace
Attempting to update one price level while preserving another in a single request will fail. Consider this payload with ?replace=price:
{ "price": { "items": [ { "priceLevel": { "id": 18 }, "quantity": { "value": 0 }, "price": 34.98 }, { "priceLevel": { "id": 18 }, "quantity": { "value": 2 }, "price": 30.98 }, { "priceLevel": { "id": 20 }, "quantity": { "value": 0 }, "price": 34.98 }, { "priceLevel": { "id": 20 }, "quantity": { "value": 2 }, "price": 34.98 } ] } }
If any of these rows already exist exactly (same price level, quantity, and value), they will be stripped before the replace operation. Then the replace clears all existing price levels, and only the new (changed) rows are inserted. The result: your old price levels vanish and you have only partial data.
The Workaround: Two-Call Pattern
The reliable approach is to use two sequential PATCH requests to the same record. This pattern works because the first call empties the sublist, so the second call has nothing to compare against.
Step 1: Clear the Price Matrix
Send a PATCH request with an empty items array and the replace=price query parameter:
PATCH /services/rest/record/v1/inventoryItem/1234?replace=price Content-Type: application/json { "price": { "items": [] } }
This removes all price levels from the item. The empty array triggers the replacement operation without adding anything new. (Alternatively, you can set the value to null: {"price": null}.)
Step 2: Submit the Full Price Matrix
Then, send a second PATCH request with your complete set of desired price levels:
PATCH /services/rest/record/v1/inventoryItem/1234?replace=price Content-Type: application/json { "price": { "items": [ { "priceLevel": { "id": 18 }, "quantity": { "value": 0 }, "price": 34.98 }, { "priceLevel": { "id": 18 }, "quantity": { "value": 2 }, "price": 30.98 }, { "priceLevel": { "id": 20 }, "quantity": { "value": 0 }, "price": 34.98 }, { "priceLevel": { "id": 20 }, "quantity": { "value": 2 }, "price": 34.98 } ] } }
Since the sublist is now empty, the diff logic has nothing to strip, and all rows in your payload are inserted. This two-step method works reliably because the first call removes all existing data, and the second call builds the new structure from scratch.
Validating the Result
After the two-call sequence, verify the price matrix with a GET request:
GET /services/rest/record/v1/inventoryItem/1234?expandSubResources=true
Check the items array under price. You should see exactly the rows you submitted in Step 2, with no leftovers from the original state. If you see extra or missing rows, inspect the response of the second PATCH for error messages.
Key Considerations
Idempotency: The two-call pattern is not idempotent. If Step 1 succeeds but Step 2 fails, the item has no prices. Build retry logic for the second call, and log the state after each request.
Rate limiting: Each price update consumes two API requests instead of one. For bulk updates, consider batching your operations or using a different approach.
Concurrency: Between Step 1 and Step 2, another process could modify the item. Use the If-Match header with the ETag from your initial GET to prevent overwriting concurrent changes.
Audit trail: The two calls create two separate change records in the audit trail. If you need a single audit entry, you may want to use a different method.
When to Use a Script Instead
For high-volume price syncs, the two-call REST pattern doubles your request count and introduces a failure window. A RESTlet with SuiteScript 2.x can handle the entire operation in one request. However, be aware that even with the script approach, the pricing sublist has special rules.
The following RESTlet uses the N/record module's submitFields method. Note that this approach does NOT support the matrix (multi-level) pricing. It is only suitable for simple, single-level pricing. For true matrix reconfiguration, you must use the record.load and setSublistValue API, which is more complex:
/** * @NApiVersion 2.1 * @NScriptType Restlet */ define(["N/record"], (record) => { const post = (context) => { const itemId = context.itemId; const priceMatrix = context.priceMatrix; // Simple pricing only - this will NOT work for matrix (multi-level) pricing record.submitFields({ type: record.Type.INVENTORY_ITEM, id: itemId, sublistId: 'itempricing', sublistValue: priceMatrix }); return { success: true }; }; return { post }; });
For matrix pricing, you must use record.load and then setSnippetValue or the matrix-specific API, which is beyond the scope of this article. Refer to the SuiteScript Records Guide for the full details on the pricing sublist and its special handling.
Testing the Pattern Safely
Test this with a single record first. Pick an item that isn't used in production transactions. Run the two-call sequence, verify the result with a GET, then restore the original prices using the same pattern.
Also, consult the REST API Browser for the exact schema of the price sublist on the inventoryItem record. Field names and structure can vary based on your NetSuite configuration.
The two-call workaround is the current standard for REST-based price matrix replacement. There is no true replaceAll equivalent in the REST endpoint, so plan your integration around this limitation. If you need atomic updates or are syncing hundreds of items, a script-based approach with proper matrix handling is better; for occasional updates, the two-call pattern works fine. For deeper details on the data structures involved, see the SuiteTalk REST Web Services Records Guide or the SuiteScript Records Guide section on the pricing sublist.


