NetSuite API Showdown: SOAP vs REST

Eric Popivker
ENTech Solutions
Published in
12 min readJan 19, 2023

NetSuite offers two APIs for calling NetSuite functionality from external clients: REST and SOAP.

In this day and age, SOAP standard is pretty old and rusty, and most modern APIs are REST (or GraphQL if you want to get fancy). So you would think that with NetSuite — your first option for developing new applications will always be REST. Well, think again.

While REST is the new (newer) hotness, in the enterprise world, it takes a long time for API to get to the point where kinks are mostly ironed out, and documentation is not full of typos. In the case of NetSuite — REST API is still in such a state.

I will go into each NetSuite API option in a lot more detail and also discuss the 3rd approach, which is the best of both worlds but also has some drawbacks.

NetSuite SOAP API

Authentication for NetSuite SOAP API

SOAP supports TBA authentication, which is OAuth1. It is pretty old-tech, but it works well. For TBA authentication, you will need to create NetSuite integration and Access Token. After that, it is pretty standard OAuth1.

Here is what it looks like in Postman:

MetaData from NetSuite SOAP API

Metadata is all the information about standard and custom entities and fields. It is useful when you write Integration platforms like Celigo or Boomi to get all the data structures to create a nice UI/UX for calling them.

In SOAP protocol, this data is stored in a WSDL file. Unfortunately, with WSDL, you only get standard objects and standard fields.

What about custom fields?

Well, tough luck. The only way to do it with SOAP API is to create a Custom RESTLET using SuiteScript to get custom records/fields. There is no native way to get a complete Schema in REST API.

You can still use custom entities/fields in your SOAP requests; you have to know their names and types up front.

Supported Entities in NetSuite SOAP API

SOAP API supports most standard entities like Customer, SalesOrder, Employee, etc..…

It supports Saved Searches, which is very useful. In many cases, it is easier to create a Saved Search in NetSuite just for the data you would retrieve instead of making 100 API calls to join some data on the client.

SOAP API also has endpoints for querying files in FileCabinet and downloading file content. It is important if you are integrating with another system and you need to sync both, data and files.

SuiteQL in NetSuite SOAP API

SuiteQL is a NetSuite query language similar to SQL.

It is not supported in SOAP, and if you want to use SuiteQL queries, you need to create a custom Restlet (see “Restlet Way” section below).

Documentation for NetSuite SOAP API

There is a very nice (but slow) SOAP API Browser:

https://system.netsuite.com/help/helpcenter/en_US/srbrowser/Browser2022_2/schema/record/account.html

The naming of tabs is a bit confusing (but you get used to it):

  • Schema browser is for SOAP
  • Records browser is for SuiteScript
  • Connect Browser is for NetSuite connect used in Analytics

NetSuite documentation for SOAP is very detailed (with almost no typos).

https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_N3412777.html

I mean, after 10+ years, you shouldn’t have it any other way.

Syntax for NetSuite SOAP API

SOAP syntax is, in a word, horrible.

<soapenv:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Header>
<platformMsg:searchPreferences soapenv:mustUnderstand="0" soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" xmlns:platformMsg="urn:messages_2021_2.platform.webservices.netsuite.com">
<platformMsg:bodyFieldsOnly>true</platformMsg:bodyFieldsOnly>
<platformMsg:pageSize>200</platformMsg:pageSize>
</platformMsg:searchPreferences>
<ns3:tokenPassport soapenv:mustUnderstand="0" soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" xmlns:ns3="urn:messages_2021_2.platform.webservices.netsuite.com">
<ns1:account xmlns:ns1="urn:core_2021_2.platform.webservices.netsuite.com">{{ACCOUNT}}</ns1:account>
<ns2:consumerKey xmlns:ns2="urn:core_2021_2.platform.webservices.netsuite.com">{{CONSUMER_KEY}}</ns2:consumerKey>
<ns4:token xmlns:ns4="urn:core_2021_2.platform.webservices.netsuite.com">{{TOKEN_ID}}</ns4:token>
<ns5:nonce xmlns:ns5="urn:core_2021_2.platform.webservices.netsuite.com">{{nonce}}</ns5:nonce>
<ns6:timestamp xmlns:ns6="urn:core_2021_2.platform.webservices.netsuite.com">{{timestamp}}</ns6:timestamp>
<ns7:signature algorithm="HMAC_SHA256" xmlns:ns7="urn:core_2021_2.platform.webservices.netsuite.com">{{signature}}</ns7:signature>
</ns3:tokenPassport>
</soapenv:Header>
<soapenv:Body>
<platformMsg:search xmlns:platformMsg="urn:messages_2021_2.platform.webservices.netsuite.com">
<platformMsg:searchRecord xsi:type="ns2:CustomerSearch" xmlns:ns2="urn:relationships_2021_2.lists.webservices.netsuite.com" xmlns:ns3="urn:common_2021_2.platform.webservices.netsuite.com"/>
</platformMsg:search>
</soapenv:Body>
</soapenv:Envelope>

This is a SOAP request to Find Customers. Unfortunately, you can’t unsee it even if you wash your eyeballs in vinegar.

Luckily you never have to look at it unless you are a sucker for devpain.

Calling NetSuite SOAP API

To call NetSuite SOAP API in Visual Studio, you would add a reference to WSDL, which generates all the proxy classes and then you write your regular OOP code. The same proxy generator should exist in Java, Php, etc… The proxies make it easy to call SOAP API.

To add customer:

var customer = new Customer();
customer.firstName = "John";
customer.lastName = "Wick";

var netSuiteService = new NetSuiteService();
netSuiteService.add(customer);

To search for customers:

var customerSearch = new CustomerSearchBasic()

var netSuiteService = new NetSuiteService();
SearchResult result = netSuiteService.search(customerSearch);

Custom fields

Custom fields require a bit more code:

var customer = new Customer();

CustomFieldRef[] customFields = new CustomFieldRef[1];

StringCustomFieldRef stringField = new StringCustomFieldRef();
stringField.scriptId = "custentity_custom_field_1";
stringField.value = "A string value";
customFields[0] = stringField;

customer.customFieldList = customFields;

Conclusion for NetSuite SOAP API

Soap API is old but reliable. It has good documentation and plenty of community support.

While you don’t want to write SOAP messages directly, you don’t have to. All modern dev GUIs generate the proxies from WSDL, and you can call SOAP API like you would any local library.

On the negative side, I wouldn’t use SOAP if I needed to retrieve custom field Metadata dynamically or if I had to use SuiteQL.

NetSuite REST API

NetSuite REST API was introduced in 2019. Before going into detail, I wanted to share my first experience with using NetSuite REST API:

I opened Postman and tried a simple request ‘Get me all customers’:

GET https://[AccountId].suitetalk.api.netsuite.com/services/rest/record/v1/customer

I expected to get a list of customers with fields for each customer, but instead, I got this:

{
"links": [
{
"rel": "next",
"href": "https://xxxxxx.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1000&offset=1000"
},
{
"rel": "last",
"href": "https://xxxxxx.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1000&offset=194000"
},
{
"rel": "self",
"href": "https://xxxxxx.suitetalk.api.netsuite.com/services/rest/record/v1/customer"
}
],
"count": 1000,
"hasMore": true,
"items": [
{
"links": [
{
"rel": "self",
"href": "https://xxxxxx.suitetalk.api.netsuite.com/services/rest/record/v1/customer/125173"
}
],
"id": "125173"
},
{
"links": [
{
"rel": "self",
"href": "https://xxxxxx.suitetalk.api.netsuite.com/services/rest/record/v1/customer/125174"
}
],
"id": "125174"
},
....

The “Items” is a long list of ids and HATEOAS to the actual entity.

I took a double take and a triple take, but the results didn’t change. I checked documentation to see if I was missing some parameters to get Customer Fields, but to no avail.

I mean, I like HATEOAS as much as the next guy (Actually, I never use it even if API provides it. I mean, it stands for: Hypermedia as the Engine of Application State. WHAT? The longest acronym ever. You need an acronym for HATEOAS. How do you even pronounce it? It starts with the word HATE.).

Coming back to NetSuite REST API: why would anyone make a GET search request that only returns IDs. I see a couple of reasons:

  • It is an intern from Big 4 who has never used any API before
  • It was made so the developers would keep using SOAP
  • It was a test case on how NOT to do HATEOAS, but the deadline was getting close, so NetSuite just deployed it

Why do you think they did it like this?

Anyways, the workaround, for now, is to use SuiteQL which is accessible with REST API.

Authentication for NetSuite REST API

Rest API calls can use TBA/OAUTH1 authentication, the same as for SOAP.

In addition, we can also use OAUTH2. But I would stick to TBA, and I will tell you why.

There are two OAUTH2 approaches supported in NetSuite.

  • OAuth 2 Standard (Authorization Code Grant)
  • OAuth 2 Machine to Machine

I covered them in detail in an older blog post:

https://medium.com/entech-solutions/how-to-use-netsuite-rest-api-with-oauth-2-and-c-net-126ac118919c

OAUTH2 Standard approach, with customer interaction, generates an Access Token that expires every hour. But but but there is a refresh token, you say, and the access token can be easily generated again. The reply to this is, the refresh token expires in 1 week. So if you are using OAUTH2 standard approach, an end user would have to re-authenticate by entering a login/password every week. EVERY WEEK!

With the OAUTH2 M2M approach, it is a bit better. The token expires every 2 years, but the setup is quite a bit more complicated and requires generating a certificate using the command line tool. I describe exactly how to do it in a blog post:

https://medium.com/entech-solutions/how-to-use-netsuite-rest-api-with-oauth-2-and-c-net-126ac118919c

So TBA is probably still the best approach, followed by OAUTH2 M2M. The last one, OAUTH2 Standard you can use only if you want your users to HATEaus you.

Meta Data from NetSuite REST API

NetSuite was very kind to provide a REST API endpoint that returns a complete OpenAPI schema for a specific client account.

So if you want to get full OpenAPI for all your entities, including custom, you would make a GET request to

https://[AccountId].suitetalk.api.netsuite.com/services/rest/record/v1/metadata-catalog

It can take a long 1-2 minutes to get results, and the OpenAPI response will be pretty huge. Luckily you can pass parameters to this endpoint and specify only the entities that you would like to include.

For example:

GET https://[AccountId].suitetalk.api.netsuite.com/services/rest/record/v1/metadata-catalog?select=customer,salesorder,invoice,inventoryitem,noninventorysaleitem,custom_record1

Returns OpenApi spec that only includes:

  • customer
  • salesorder
  • invoice
  • inventoryitem
  • noninventorysaleitem
  • custom_record1

You do need to use proper TBA or OAUTH2 authentication for this call and also pass “Accept”: “application/swagger+json” in the header. But it is worth it.

Supported Entities in NetSuite REST API

NetSuite REST API supports most of the entities that SOAP API supports, plus some others, like Subscription, SubscriptionPlan, and others.

Unlike SOAP API, it doesn’t support SavedSearch or FileCabinet operations. These are pretty major omissions and require custom restlets to overcome. See SuiteAPI below that includes this functionality.

The records accessible through REST API are split into:

  • Supported Records
  • Beta Records

Supported records include the most common entities like SalesOrder, Customer, Invoice, InventoryItem, etc.

Beta records add all other entities. But, to use Beta Records, you need to apply to the “Beta Testing Program” and accept the “ Oracle Cloud Services Beta Trial License Agreement.” This doesn’t instill too much confidence in using the Beta records for production integrations.

Here is the warning in NetSuite documentation from Nov 11, 2020, about the risks of using BETA records:

Doesn’t look very inviting.

You can easily see which Records are standard and which are Beta by going to the REST API Browser:

https://system.netsuite.com/help/helpcenter/en_US/APIs/REST_API_Browser/record/v1/2022.2/index.html

You have to wait a minute for it to load fully. Then click on the “Beta” checkbox on top to include/exclude Beta records:

SuiteQL in NetSuite REST API

There is an endpoint in REST API to call SuiteQL:

POST https://[AccountId].suitetalk.api.netsuite.com/services/rest/query/v1/suiteql
Prefer: transient
{
"q": "SELECT * FROM CustomField where fieldtype = 'ENTITY'"
}

This is currently the workaround for only getting Ids in REST searches.

Documentation for NetSuite REST API

NetSuite provides a REST API Browser. You can access it here:

https://system.netsuite.com/help/helpcenter/en_US/APIs/REST_API_Browser/record/v1/2022.2/index.html

It is built using OpenAPI schema that can be retrieved using an endpoint in the “Meta Data from NetSuite REST API” section above.

It is pretty slow, but has pretty much all the data you need. Unlike SwaggerUI-based documentation, there is no way to try out the calls directly in the browser.

Postman

There is also a nice postman collection to help you get started. It is available here:

https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1545059880.html

There are many topics covered in this collection that make it easy to get started with REST API:

Syntax in NetSuite REST API

The syntax is pretty standard REST API syntax. To Get data, you use GET, to create, you use POST with JSON body, to update PATCH, and to delete DELETE.

There are two major issues that I have discovered so far:

  1. The GET search for any entity only returns Ids. Even REST documentation thinks that GET should be returning collection with actual data values:

But no matter what — you just get a list of Ids.

The only workaround to retrieving each entity by Id is to get the data using the SuiteQL endpoint.

2. Second issue is that the Insert Records endpoint doesn’t return the Id in response, just HttpCode 204.

Once again, the designer of the API either has never used another REST API before or just wants to see how much it would take for a developer to cry “Uncle.”

Fortunately, there is a workaround around for this problem.

The headers returned from this call include a Location key with a value that includes the new entity Id:

Calling NetSuite REST API

To call the NetSuite REST API you can use standard libraries for calling any REST API. For example, in .NET/C#, you can use code like this:

  public async Task<NsCustomer> GetCustomer(int customerId)
{
var url = RecordApiRoot + "/customer/" + customerId;

if (_accessToken == null)
_accessToken = await GetAccessToken();

using var httpRequest = new HttpRequestMessage(HttpMethod.Get, url);
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);

var httpResponse = await _httpClient.SendAsync(httpRequest);
var responseJson = await httpResponse.Content.ReadAsStringAsync();

var customer =
JsonSerializer.Deserialize<NsCustomer>(responseJson);

return customer;
}

The full code is here:

https://github.com/ericpopivker/entech-blog-netsuite-oauth-m2m-demo/blob/main/NetSuiteOauthM2mDemo/NetSuiteOauthM2mDemo.Core/NetSuiteApiClient.cs

The article that goes into more detail about calling NetSuite API with C# is here:

https://medium.com/entech-solutions/how-to-use-netsuite-rest-api-with-oauth-2-and-c-net-126ac118919c

Custom fields

The custom fields and records are exposed through OpenAPI, just like regular records/fields.

In this case, “companyName”is standard field on Customer and “custentity_sii_id” is the custom field.

So to create a new customer, you would pass:

{
"companyName": "Hooli",
"custentity_sii_id": "SomeSiiId2"
}

Of course, it would be much better if there was some SDK in .NET, Java, or PHP, so you wouldn’t have to look up documentation for each call. There is no such SDK officially provided by NetSuite, probably because it would need to account for custom fields/records that are different for each client account.

I am working on a way to quickly generate such custom SDK in any language using OpenAPI spec, and I will share it in the near future.

Conclusion for NetSuite REST API

NetSuite RESP API has some major issues, because the designer probably didn’t actually try using his API for most common cases like getting data and creating new records. There are hacks/workarounds for those issues, but that shouldn’t be the case for basic usage.

If you are ok with the workarounds, REST API works pretty much like standard REST API. There is good documentation provided, and it is easy to get Metadata for standard and custom entities.

While the most common entities are supported, many are still in BETA (and have been for years). Hopefully, NetSuite will make them available for General Use in the near future. NOTE that enterprise API updating is calculated in dog years.

I would suggest using REST API if you are used to REST and don’t have much experience with SOAP. It is also a good option if you need to get metadata dynamically or if you need to call SuiteQL.

Restlet Way

Restlet is a third approach to calling NetSuite functionality from external clients.

It is the most flexible way, but it requires NetSuite development.

Let’s say you need to get Customer names and emails from NetSuite. You could create a Restlet for that. It would look like this:

/*
* @NApiVersion 2.1
* @NScriptType restlet
*/
define(['N/search'], function(search) {
return {
get: function(context) {
var customerSearch = search.create({
type: search.Type.CUSTOMER,
filters:
[
search.createFilter({name: 'isperson', operator: search.Operator.IS, values: [true] })
],
columns:
[
search.createColumn({ name: "firstName", label: "First Name" }),
search.createColumn({ name: "lastName", label: "Last Name" }),
search.createColumn({ name: "email", label: "Email" })
]
});

var resultSet = customerSearch.run();
results = resultSet.getRange( { start: 0, end: 10 } );

return JSON.stringify(results);
}
};
});

This Restlet is deployed to a url like this:

https://[AccountId].restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=xxxxx&deploy=1

And can be called with the same Authentication as REST API.

The script above uses Saved Search functionality that is not accessible directly through REST API.

Restlets allow you to build custom APIs on top of NetSuite and give you a full range of functionality of the latest SuiteScript, which is more than either REST or SOAP APIs.

But who has time to implement both server-side and client-side logic for each request to NetSuite? So Restlets are best used as complimentary tool to either SOAP or REST API.

For example, if you need to use SuiteQL in SOAP, write a restlet specific to that. If you need to run SavedSearch in REST API, write a restlet for that.

SuiteAPI

A nice NetSuite open-source package built by Tim Dietrich extends and exposes a lot of NetSuite functionality through Restlets. It is called SuiteAPI and is available here:

https://suiteapi.com/

Using this package, you can:

• Access the results of saved searches.
• Upload and download files to and from the file cabinet.
• Send email messages via NetSuite.
• Generate PDFs based on transactions or ad-hoc XML code.
• And more.

Conclusion

In this article, we compared NetSuite SOAP and REST APIs.

Both have pluses and minuses, but SOAP wins out at this time because it is old and stable, and entity search actually returns complete entity data and not just ids.

The limitations of SOAP and REST APIs can be overcome by writing custom Restlets.

Let’s hope NetSuite continues improving REST API, and maybe next time, it will be The ONE to use.

--

--

Eric Popivker
ENTech Solutions

Living in .NET world for 20+ years. Founder of .NET Fiddle.