NetSuite REST Saved Search: What It Runs and What It Won't

MokuHub9 min read
netsuiterest-apisaved-searchsoapsuiteqlmigration

The nightly export that suddenly returned every open order

Take a typical integration that pulls open sales orders from NetSuite every night. For years it has been a SOAP call: reference the saved search customsearch_open_orders, add a criterion for "last modified after the previous run", page through the result. When the team started moving off SOAP, the September 2026.2 minor release looked like perfect timing. REST web services could now run saved searches. Say the developer swaps the SOAP envelope for a GET against the new endpoint, points it at the same search, and ships.

On the first night the export returns every open order in the account, not the ones modified since the last run. The job still finishes and the target system still accepts the rows, so nothing looks wrong until the downstream system has processed the same orders several times over.

Nothing is broken. The REST endpoint does exactly what Oracle documents it to do: it runs the saved search as saved. The request-time criterion the SOAP call used to add has nowhere to go, because the REST version has no place to put it.

That scenario is the whole story of REST saved search support in one example. It is a real, useful addition, and it closes a gap that pushed many teams toward RESTlets. It is also narrower than the SOAP operation it appears to replace, and the difference only shows up in searches that were never quite "saved" in the first place.

Two endpoints: one lists searches, one runs them

Oracle's page Using Saved Search in REST Web Services describes two endpoints, both under the query service rather than the Record API.

The first lists saved searches:

GET /services/rest/query/v1/savedsearch

It returns the saved searches available to the current user and role, with each search's record type. The response is a standard REST collection. The sample in Oracle's documentation looks like this, trimmed:

{
  "count": 27,
  "hasMore": false,
  "items": [
    {
      "id": "customsearch_balance_gt_100",
      "internalid": "40",
      "recordtype": "Account",
      "title": "Account Balance gt 100"
    }
  ],
  "offset": 0,
  "totalResults": 27
}

The "current user and role" part matters more than it looks. An integration role only sees the searches it has access to. If a search your integration depends on is private to the admin who built it, it will not appear in the list, and the integration role will not be able to run it either. The listing is a quick way to check that before the first production run.

The second endpoint runs one search:

GET /services/rest/query/v1/savedsearch/customsearch_balance_gt_100/result

The identifier in the path can be the internal ID or the script ID. Use the script ID. Oracle treats internal IDs as account-specific values, which SuiteCloud Development Framework by default refuses to deploy, while script IDs are what an SDF project uses to identify objects. An integration that has to work against both sandbox and production should not depend on a number that belongs to one account.

Results come back as a collection of items keyed by lowercase field IDs, with list and record references expanded into an object with an id and a refName:

{
  "balance": 111.804,
  "displayname": "Acc. Dep-Leasehold Improvements",
  "internalid": "21",
  "isinactive": false,
  "subsidiary": { "id": "1", "refName": "Parent Company" },
  "type": { "id": "FixedAsset", "refName": "Fixed Asset" }
}

SOAP returned the same results as <Record>SearchRow and <Record>SearchRowBasic objects, so code that parsed SOAP rows will need rewriting either way.

According to Oracle, the endpoint supports standard and custom fields, joins, formulas, summary and aggregation columns, nulls, Unicode and long text. That list covers what most integration searches actually use. A search with a formula column that concatenates a customer name and a subsidiary code, or a summary search that groups invoices by customer, is in scope.

The saved search is the entire query

Oracle lists three limitations on the same page, and each one changes how you design around the endpoint:

  • You cannot create, edit or delete saved searches through REST.
  • You cannot override saved search criteria, filters or sorting.
  • REST cannot return the complete saved search definition.

The second limitation is the one from the opening incident. In SOAP, advanced search let you reference an existing saved search and still change its return columns or add criteria on top of the saved ones. A large share of real SOAP integrations were built on that: a stable saved search owned by an admin, plus a date window, a subsidiary or a status filter supplied at request time. In REST, whatever the search says is what you get.

The third limitation is quieter but just as practical. Because the definition cannot be read back, your integration code has no way to confirm what a search does. If someone edits the criteria in the UI, the endpoint keeps returning 200 with different rows. SOAP had the same exposure, but at least the request-time criteria lived in code and survived UI edits. In REST, everything that defines the result lives in the UI.

So the question to ask of every SOAP search call in your inventory is simple: does the request add anything to the saved search? If it does not, the REST endpoint is a direct replacement. If it does, you need a different design.

Three ways to handle a search that needs parameters

Move the parameter into the search. Some request-time filters were never really parameters. A filter that always says "subsidiary is US" can become a saved criterion. Relative date filters can sometimes do the same job: a saved criterion of "last modified within the last two days" run nightly covers the window, at the cost of reprocessing rows the target system has to recognise as duplicates. If your target system is idempotent, this is often enough.

Rewrite the search as SuiteQL. When the filter truly varies per request, SuiteQL is the REST tool that accepts it. The call is a POST with Prefer: transient and the query in the body, and values can be bound through params instead of being concatenated into the SQL (Executing SuiteQL Queries Through REST Web Services):

POST /services/rest/query/v1/suiteql
Prefer: transient
Content-Type: application/json

{
  "q": "SELECT id, tranid, status FROM transaction WHERE type = 'SalesOrd' AND lastmodifieddate > TO_DATE(?, 'YYYY-MM-DD')",
  "params": ["2026-09-21"]
}

The cost is translation. Every formula column and every join in the saved search has to be expressed in SQL against the analytics schema, and the result no longer tracks what an admin changes in the UI. That is sometimes exactly what you want.

Keep a RESTlet. A RESTlet can load a saved search with the N/search module, add filters in code and return JSON. It was the standard workaround before September, and it still is the one option that gives you both a UI-owned search and request-time filters. It also brings SuiteScript governance back into the picture: a RESTlet that walks a large search has to page with runPaged() and watch its usage units.

We covered the RESTlet and SuiteQL paths in more depth in our SOAP to REST migration plan. The new endpoint changes that plan in one place: searches that run unchanged no longer need either workaround.

Paging is documented for the list, not stated for the results

Oracle's saved search page says that pagination is supported "when you run a query for a list of saved searches" and links to the general Collection Paging rules. Those rules are: 1,000 results per page by default, a limit parameter to change that, an offset that must be divisible by the limit, and next and last links in the response to walk the pages. The same page caps collections at 100,000 results, or 1,000,000 with SuiteAnalytics Connect.

The result endpoint's sample response carries the same count, hasMore, offset and totalResults fields, so it is reasonable to expect it to page the same way. Oracle's page does not say so explicitly, though, and it does not say whether the 100,000 cap applies to search results. Before you move a large extract to this endpoint, run the largest search you have through it in a sandbox and check that totalResults matches what the UI shows.

One SOAP habit is worth keeping. Oracle's SOAP guidance for searches that return duplicate or missing rows across pages is to sort the saved search by internal ID. Since REST cannot override sorting, the sort order has to be in the saved search itself. If a search pages through records that change while the job runs, give it an internal ID sort before you rely on offsets.

Long searches should run asynchronously

Oracle states that retrieving or running saved searches can be done synchronously or asynchronously. Asynchronous execution uses the same mechanism as every other REST request (REST Web Services Request Processing): add the Prefer: respond-async header, and NetSuite answers with 202 Accepted and a job URL in the Location header.

GET /services/rest/query/v1/savedsearch/customsearch_open_orders/result
Prefer: respond-async

HTTP/1.1 202 Accepted
Location: /services/rest/async/v1/job/1

From there you poll the job at /services/rest/async/v1/job/1, find its task under /job/1/task/, and read the output at /job/1/task/1/result. If a retry could submit the same job twice, send an X-NetSuite-idempotency-key header. A repeated key is rejected with an IDEMPOTENCY_ERROR instead of starting a second run.

Asynchronous jobs are processed by REST async processors, whose number depends on your SuiteCloud Plus licenses, and Oracle notes they do not consume SuiteScript asynchronous governance. For a nightly export, async is the safer default: the HTTP connection no longer has to stay open for the length of the search, and a network timeout on your side no longer means a lost run.

Our view: a saved search endpoint is a contract with an admin

The endpoint puts the whole definition of the result in the NetSuite UI. That is its strength and its risk. The strength is that a business user can fix a column, add a status to the criteria or adjust a formula without a deployment. The risk is that the same business user can change what your integration receives, and nothing on the API side will tell you.

Our recommendation is to treat every saved search an integration calls through REST as an interface, not as a report. Give it a script ID that says it belongs to an integration, restrict who can edit it, deploy it through SDF so the definition exists in version control, and add a check in the integration for the columns it expects. Searches that the finance team edits every month should not sit behind an export job, whichever protocol calls them.

The tempting shortcut is to fake parameters by cloning a search for every variant: one for each subsidiary, one for each date window. It works on day one and turns into dozens of near-identical searches nobody can safely delete. If a search needs parameters, it belongs in SuiteQL or a RESTlet, and the endpoint should be kept for searches that are genuinely fixed.

This matters on the calendar as well. SOAP web services are switched off in 2028.2, and new SOAP integrations cannot be created from 2027.1. The dates, and the authentication changes that run alongside them, are in our SOAP, TBA and OAuth 2.0 timeline. Sorting your SOAP search calls into "runs unchanged" and "adds criteria" is a one-afternoon job, and it tells you how much of your search migration the new endpoint has already done for you.