Published on February 6, 2023.
Most of the NetSuite integration projects that I get involved with involve developing custom RESTlets, and more recently, customizations to SuiteAPI. However, I also get involved in projects where SuiteTalk REST is being used.
In this post, I'm going to discuss some of the common challenges that my clients run into when using SuiteTalk REST, and share some of the tips and techniques that I've used to help them overcome those challenges.
Let's start with what a very basic SuiteTalk REST API call. In this example, I'm going to request the record for a single Customer, and do so based on its internal NetSuite ID.
Here's the API call:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236
So I'm making an HTTP GET request to the Record Service, specifying that I want a customer record, and that the internal ID of the customer is 236.
Here's what the response looks like.
{ "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236" } ], "addressBook": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/addressBook" } ] }, "alcoholRecipientType": { "id": "CONSUMER", "refName": "Consumer" }, "balance": 0.0, "companyName": "Centizu LLP", "contactRoles": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/contactRoles" } ] }, "creditHoldOverride": { "id": "AUTO", "refName": "Auto" }, "currency": { "links": [], "id": "1", "refName": "US Dollar" }, "currencyList": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/currencyList" } ] }, "custentity_bronto_duplicate": false, "custentity_bronto_imported": false, "custentity_bronto_is_restlet": false, "custentity_bronto_suppressed": false, "custentity_esc_last_modified_date": "2024-02-14", "custentity_solupay_cust_bypass_surcharge": false, "dateCreated": "2021-11-16T01:15:00Z", "defaultAddress": "Centizu LLP
Fort Washington and 175th Street
New York NY 11368
United States", "depositBalance": 0.0, "email": "agarcia@Centizu.com", "emailPreference": { "id": "DEFAULT", "refName": "Default" }, "emailTransactions": false, "entityId": "Centizu LLP", "entityStatus": { "links": [], "id": "13", "refName": "CUSTOMER-Closed Won" }, "externalId": "J170", "faxTransactions": false, "giveAccess": false, "globalSubscriptionStatus": { "id": "1", "refName": "Soft Opt-In" }, "groupPricing": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/groupPricing" } ] }, "id": "236", "isBudgetApproved": false, "isInactive": false, "isPerson": false, "itemPricing": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/itemPricing" } ] }, "lastModifiedDate": "2024-02-14T19:50:00Z", "overdueBalance": 0.0, "partners": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/partners" } ] }, "phone": "(202) 555-0190", "printTransactions": false, "receivablesAccount": { "links": [], "id": "-10", "refName": "Use System Preference" }, "salesRep": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/employee/121" } ], "id": "121", "refName": "Tom Singor" }, "shipComplete": false, "shippingCarrier": { "id": "nonups", "refName": "More" }, "startDate": "2019-11-30", "subscriptionMessageHistory": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/subscriptionMessageHistory" } ] }, "subscriptions": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/subscriptions" } ] }, "subsidiary": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/subsidiary/2" } ], "id": "2", "refName": "United States" }, "syncPartnerTeams": false, "terms": { "links": [], "id": "2", "refName": "Net 30" }, "territory": { "links": [], "id": "1", "refName": "East" }, "unbilledOrders": 0.0 }
Notice that the "addressBook" (which is actually a sublist) looks like this:
"addressBook": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/addressBook" } ] },
That's not very helpful, is it? It seems that in order to get the actual address, you'd have to make a second API call using the href that is returned. And that's one of the common frustrations that my clients have.
Thankfully, there is a way to get the details of sublist values without making additional API calls. To do so, you pass an "expandSubResources" URL parameter with a value of true.
Here's the updated API call:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236?expandSubResources=true
And here's a snippet of the updated response, showing the expanded addressBook.
"addressBook": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/addressBook" } ], "items": [ { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/addressBook/2526" } ], "addressBookAddress": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/addressBook/2526/addressBookAddress" } ], "addr1": "Fort Washington and 175th Street", "addressee": "Centizu LLP", "addrText": "Centizu LLP Fort Washington and 175th Street New York NY 11368 United States", "city": "New York", "country": { "id": "US", "refName": "United States" }, "override": false, "state": "NY", "zip": "11368" }, "addressBookAddress_text": "Centizu LLP
Fort Washington and 175th Street
New York NY 11368
United States", "addressId": "2526", "defaultBilling": true, "defaultShipping": true, "id": 2526, "internalId": 2526, "isResidential": false, "label": "Fort Washington and 175th Street" } ], "totalResults": 1 }
Note that the expandSubResources parameter will expand both sublists and subrecords.
The responses to those first two requests include a lot of data. But what if you really only need a few fields from the record? For example, what if you only need the customer's Entity ID, Company Name, Status, and Sales Rep?
SuiteTalk REST supports this type of request, and does so via a special "fields" URL parameter. The API call would look like this:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236?fields=entityId,companyName,entityStatus,salesRep
Here's the response.
{ "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236?fields=entityId%2CcompanyName%2CentityStatus%2CsalesRep" } ], "companyName": "Centizu LLP", "entityId": "Centizu LLP", "entityStatus": { "links": [], "id": "13", "refName": "CUSTOMER-Closed Won" }, "salesRep": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/employee/121" } ], "id": "121", "refName": "Tom Singor" } }
It's important to note that you cannot combine the expandSubResources and fields paramaters. For example, this API call...
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236?expandSubResources=true&fields=entityId,companyName,entityStatus,salesRep,addressBook... will result in an "INVALID_PARAMETER" error.
In the examples above, the customer's sales rep has been returned like this:
"salesRep": { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/employee/121" } ], "id": "121", "refName": "Tom Singor" }
To get the sales rep's full employee record by making a call to that href.
Another way to get the record would be to use a "subresource" URL, like this:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/236/salesRep
The results will be exactly the same.
You can also use subresource URLs to get sublist values. For example, if you want the first item on a sales order (with internal ID of 325), the URL that you'd use would like something like this:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/salesOrder/325/item/1
This is an example of when SuiteTalk REST's "RESTful" nature can be helpful and convenient.
In the examples above, I showed how you can get a specific record based on its internal NetSuite ID. But what if you want to get a record based on its external ID?
SuiteTalk makes that easy to do. Instead of specifying the internal ID in the URL, you specify the external ID, like this.
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/eid:J170
In that example, I'm requesting the customer record that has an external ID of J170.
You can use external IDs with the fields URL parameter. For example:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/eid:J170?fields=entityId,companyName,entityStatus,salesRep,externalId%20
The API requests that I've shown above all involve getting a single record. But what about getting multiple records? For example, what if you wanted to get a list of the customer records?
To do so, you'd make an API call that only specifies the record type. For example:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/
Here's an abbreviated version of the response.
{ "links": [ { "rel": "next", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=5&offset=5" }, { "rel": "last", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=5&offset=455" }, { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/?limit=5" } ], "count": 457, "hasMore": false, "items": [ { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/1566" } ], "id": "1566" }, { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/1570" } ], "id": "1570" }, ... { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/1573" } ], "id": "1573" }, { "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/1580" } ], "id": "1580" } ], "offset": 0, "totalResults": 457 }
The response includes the internal IDs of customer records, and the URL to use if you want to get details on each one.
You can use "limit" and "offset" parameters to paginate SuiteTalk REST's results. If you've used other REST APIs in the past, then you're likely already familiar with those parameters.
For example, suppose that you want to get the first 10 customer records. The request would look like this.
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/?limit=10
To get the next 10 records, you'd make a call like this.
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=10&offset=10
You can request filtered lists of records, and you do so by using the "q" query parameter. For example, to get all customers whose company names contain "LTD" you would make a call like this:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/?q=companyName%20CONTAIN%20%22LTD%22
To get all customers assigned to salesrep 121, you'd make a call like this.
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/?q=salesRep%20EQUAL%20121
You can specify multiple filter conditions using AND and OR keywords. For example, to get all customers that are assigned to salesrep 121, AND whose company name contains "LTD," you'd make this call:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/?q=salesRep%20EQUAL%20121%20AND%20companyName%20CONTAIN%20%22LTD%22
Similarly, to get all customers that are assigned to salesrep 121, OR whose company name contains "LTD," you'd make this call:
GET https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/record/v1/customer/?q=salesRep%20EQUAL%20121%20OR%20companyName%20CONTAIN%20%22LTD%22
The SuiteTalk REST Record Service has a number of limitations that you should be aware of.
When retrieving a list of records, you cannot use the "fields" or "expandSubResources" URL parameters. So essentially, you get a list of records, and have to make additional API calls to get their details.
You cannot specify the order in which the records are returned. In other words, you cannot specify the equivalent of an "ORDER BY" clause.
You can get a maximum of 1,000 records per page, and you can only get the first 1,000 pages of results. For NetSuite accounts that have a very large number of records, this can be problematic.
That all being said, you might find that using SuiteQL queries to retrieve data via SuiteTalk REST is a better approach.
SuiteTalk REST also supports SuiteQL queries, and if you've been following my blog for awhile, you're probably not surprised to hear that for me, this is where SuiteTalk REST really shines.
With SuiteQL, you can overcome many of the Record Service's limitations that I listed above. You can specify the fields that you want to return. You can specify an ORDER BY clause. You can also use SQL JOINs to get to related records.
And while SuiteQL can return a maximum of 100,000 results, you can get an unlimited number of records in accounts that have SuiteAnalytics Connect enabled.
Here's an example of a SuiteTalk REST API call that uses SuiteQL.
POST https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql
The request's body looks like this.
{ "q": "SELECT TOP 5 ID, EntityID, CompanyName, BUILTIN.DF( SalesRep ) AS SalesRep FROM Customer ORDER BY CompanyName" }
The response will look like this.
{ "links": [ { "rel": "self", "href": "https://tstdrv2533109.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql" } ], "count": 5, "hasMore": false, "items": [ { "links": [], "companyname": "Abbott Inc.", "entityid": "Abbott Inc.", "salesrep": "Scott Torman", "id": "224" }, { "links": [], "companyname": "Altima Technology", "entityid": "Altima Technology", "salesrep": "Edwin Goldwasser", "id": "240" }, { "links": [], "companyname": "Anonymous Customer Parent Company", "entityid": "Anonymous Customer Parent Company", "id": "4" }, { "links": [], "companyname": "Blue Pumpkin", "entityid": "Blue Pumpkin", "salesrep": "Edwin Goldwasser", "id": "329" }, { "links": [], "companyname": "Botique 2021", "entityid": "Botique 2021", "salesrep": "Will Clark", "id": "858" } ], "offset": 0, "totalResults": 5 }
For additional information on using SuiteQL with SuiteTalk REST, please check out NetSuite / SuiteTalk REST Web Services: Putting SuiteQL to Work.
In this post, I've provided an introduction to NetSuite's SuiteTalk REST service, and also discussed some of the common challenges that my clients run into while using it.
When it comes to SuiteTalk REST, my general advice is that if you are going to use it, lean on its ability to run SuiteQL queries. I think you'll find that it is more powerful and easier to use than the Record Service, and it is usually a little better in terms of speed. And while I didn't discuss it in this post, SuiteTalk's ability to provide metadata can also be very helpful, depending on the type of integration project that you're using it on.
That all being said, given the choice between using SuiteTalk REST or a custom RESTlet, I'd much rather use a custom RESTlet. That's primarily because, with a RESTlet, I have total control over the integration. And if you're interested in seeing what a RESTlet looks like, I encourage you to check out SuiteAPI.
As always, I hope you've found this to be helpful.
Hello, I'm Tim Dietrich. I develop custom software for businesses that are running on NetSuite, including mobile apps, Web portals, Web APIs, and more.
I'm the developer of several popular NetSuite open source solutions, including the SuiteQL Query Tool, SuiteAPI, and more.
I founded SuiteStep, a NetSuite development studio, to provide custom software and AI solutions - and continue pushing the boundaries of what's possible on the NetSuite platform.
Copyright © 2025 Tim Dietrich.