Earlier this year, NetSuite released their new REST API called SuiteTalk REST Web Services. While still incomplete, this API provides a much friendlier interface for developers than its predecessors— the SOAP API and RESTlets. We chose to use the new REST API, because we believe it is the future for building integrations to NetSuite. The REST standard is much more intuitive, with many resources across many languages and frameworks.

Tip: We do our best to write comprehensive unit tests for the entire library. Tests are a great resource for examples on inputs, outputs, and usage, not all of which may be documented here.

Options for Authentication

The REST API offers two types of authentication: Token-Based Authentication (TBA) and OAuth 2.0. We chose to use TBA for the REST client.

Simply put, TBA is based on OAuth 1.0. Ultimately, you need to generate a request header that includes a signature created using tokens retrieved from the NetSuite dashboard and the OAuth 1.0 standard. Unfortunately, at the time of development, we hit a few walls trying to authenticate:

  • The NetSuite documentation was only for their SOAP API and RESTlets
  • There were no examples in Ruby
  • Code samples relied on libraries or packages, making it hard to understand what was actually happening under the hood.
  • The nonce and timestamp were not defined in the inputs of examples we found, so outputs would naturally vary given that these values are meant to change in practice.
  • There were very few examples that had both inputs and signature output to test against.

In the end, the challenge of authenticating to NetSuite in the REST API was computing the signature. No examples we found worked for us, and regardless, we needed it in Ruby. Frustratingly, the only way to know if we were correct was to try API calls after making tweaks to our signature generation algorithm.

If you’re reading this post, hopefully this will save you a headache or two. We will clearly lay out our solution, break it down step-by-step, and we will give you some real examples. Let’s get going!

Our Solution

Let’s start with the full solution, which can be found in the LedgerSync library class LedgerSync::Ledgers::NetSuite::Token (code can be found here). The Token handles creating the authorization header that will be used to sign the request. The request header looks something like this:

Authorization: OAuth realm="TEST_REALM",oauth_consumer_key="ef40afdd8abaac111b13825dd5e5e2ddddb44f86d5a0dd6dcf38c20aae6b67e4",oauth_token="2b0ce516420110bcbd36b69e99196d1b7f6de3c6234c5afb799b73d87569f5cc",oauth_signature_method="HMAC-SHA256",oauth_timestamp="1508242306",oauth_nonce="fjaLirsIcCGVZWzBX0pg",oauth_version="1.0",oauth_signature="i7MEtGwhCTIZbTsTrNGw9LdcERn4wsjt5C7TxmKWIfU%3D"

The Inputs

To compute a signature, we need a few things first. Let’s walk through each one and how to find it:

method

The request method will be one of the following:

  • POST
  • PUT
  • PATCH
  • GET
  • DELETE

consumer_key, consumer_secret

The consumer key and secret can be retrieved from NetSuite by creating an Integration Record. Note that these values are only shown once at the end of creating a new Integration Record. Once you navigate away, you will no longer be able to see these values. You can reset the key and secret on existing Integration Records should you need to generate a new pair.

signature_method

NetSuite supports multiple signature methods. Our library uses HMAC-SHA256.

timestamp

We need to include a current timestamp in the signature and header.

nonce

We need to include a random alphanumeric string to be used in the signature and header.

oauth_version

The OAuth Version is defaults to “1.0”

realm

The realm is the NetSuite account ID. You can find this in your account or in the URL:

https://<ACCOUNT_ID>.app.netsuite.com/app/center/card.nl

If you are using a sandbox or test drive account, your account ID will include a hyphen and some other characters. For example, it may look like this: 9876543-sb1.

Once you have the account ID, we will need to transform it to the format the API expects. You replace any hyphens with underscores (a.k.a. _) and capitalize all letters. So 9876543-sb1 will become 9876543_SB1.

token_id, token_secret

These values can be found in NetSuite when you create an Access Token. Like the Integration Record, these values are only visible at the end of creating the token and will not be shown again. You can also reset these values on existing Access Tokens.

url

This is the URL of the request, which must include any query string parameters you intend to pass.

Example Values

For this guide, we will use the following values:

Input values
Variable Value
method "GET"
consumer_key "CONSUMER_KEY_VALUE"
consumer_secret "CONSUMER_SECRET_VALUE"
signature_method default, "HMAC-SHA256"
timestamp Typically, you will leave this empty, but we will use 1234567890
nonce Typically, you will leave this empty, but we will use "asdfasdf"
oauth_version default, "1.0"
realm "9876543_SB1"
token_id "TOKEN_ID_VALUE"
token_secret "TOKEN_SECRET_VALUE"
url "https://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123?expandSubResources=true"
Note: The values for nonce and timestamp should be left nil or unpassed in practice. They are included here for purposes of having a consistent input and output, where typically they would be generated on the fly.

Throughout this tutorial, examples will appear in the following style. You can copy and paste this code in sequence to get the same results.

method = 'GET'
consumer_key = 'CONSUMER_KEY_VALUE'
consumer_secret = 'CONSUMER_SECRET_VALUE'
nonce = 'asdfasdf'
oauth_version = '1.0'
realm = '9876543_SB1'
signature_method = 'HMAC-SHA256'
timestamp = 1_234_567_890
token_id = 'TOKEN_ID_VALUE'
token_secret = 'TOKEN_SECRET_VALUE'
url = 'https://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123?expandSubResources=true'

Generating the header

Now that we have our inputs, we can walk through computing the signature and header.

Before we dive in, please note that many values need to be escaped. If you see escape(...) being used below, it is shorthand for the following:

def escape(str)
  CGI.escape(str.to_s).gsub(/\+/, '%20')
end

Now let’s walk through each step:

1. Build data string

We need to create a string that will be used to compute a digest (a.k.a. signature) from. Using our values above, we can retrieve the string using the following:

token = LedgerSync::Ledgers::NetSuite::Token.new(
  method: method,
  consumer_key: consumer_key,
  consumer_secret: consumer_secret,
  realm: realm,
  token_id: token_id,
  token_secret: token_secret,
  url: url
)

puts token.signature_data_string
"GET&https%3A%2F%2F9876543-sb1.suitetalk.api.netsuite.com%2Fservices%2Frest%2Frecord%2Fv1%2Fcustomer%2F123&expandSubResources%3Dtrue%26oauth_consumer_key%3DCONSUMER_KEY_VALUE%26oauth_nonce%3Dasdfasdf%26oauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3D1234567890%26oauth_token%3DTOKEN_ID_VALUE%26oauth_version%3D1.0"

But how is this created? It is composed of three values that are escaped and joined with &. The values are as follows:

  • method: Described in the inputs above.
  • url_without_params: The url with all query parameters removed. In our example, this would be https://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123
  • parameters_string: A string representation of all URL parameters as well as the oauth parameters.

Let’s dive a level deeper and look into creating the parameters string.

1a. Build parameters string

The parameters string is a sorted list of key/value pairs each joined with an ampersand (&). The key/value pairs come from two sources:

  • Query parameters parsed out of the URL, in our case { "expandSubResources" => "true }
  • OAuth parameters

For our example, we would have the following:

url_params = {
  "expandSubResources" => "true"
}

oauth_parameters_array = {
  oauth_consumer_key: consumer_key,
  oauth_nonce: nonce,
  oauth_signature_method: signature_method,
  oauth_timestamp: timestamp,
  oauth_token: token_id,
  oauth_version: oauth_version
}.to_a

parameters_string = url_params.to_a
                              .concat(oauth_parameters_array)
                              .map { |k, v| [escape(k), escape(v)] }
                              .sort { |a, b| a <=> b }
                              .map { |e| "#{e[0]}=#{e[1]}" }
                              .join('&')

puts parameters_string
"expandSubResources=true&oauth_consumer_key=CONSUMER_KEY_VALUE&oauth_nonce=asdfasdf&oauth_signature_method=HMAC-SHA256&oauth_timestamp=1234567890&oauth_token=TOKEN_ID_VALUE&oauth_version=1.0"

1b. Put it together

Now that we have our parameters_string, we can generate the following:

url_without_params = "https://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123"

signature_data_string = [
  method,
  escape(url_without_params),
  escape(parameters_string)
].join('&')
"GET&https%3A%2F%2F9876543-sb1.suitetalk.api.netsuite.com%2Fservices%2Frest%2Frecord%2Fv1%2Fcustomer%2F123&expandSubResources%3Dtrue%26oauth_consumer_key%3DCONSUMER_KEY_VALUE%26oauth_nonce%3Dasdfasdf%26oauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3D1234567890%26oauth_token%3DTOKEN_ID_VALUE%26oauth_version%3D1.0"

2. Build signature key

The final piece needed to generate a signature is the key. Our key is made by joining our consumer_secret to token_secret with an ampersand (&):

key ||= [
  consumer_secret,
  token_secret
].join('&')
"CONSUMER_SECRET_VALUE&TOKEN_SECRET_VALUE"

3. Compute signature

Now with our signature_data_string, we will use the desired signature_method to compute a digest. We will assume we are using HMAC-SHA256.

signature ||= Base64.encode64(
  OpenSSL::HMAC.digest(
    OpenSSL::Digest.new('sha256'),
    key,
    signature_data_string
  )
).strip
"cId0B3hP0sFVQw/gjQ/P6YiOSx76u0WfyO8umOlq3gg="

4. Generate Header

Last but not least, we can now generate our header. The header is made by combining all of the inputs and our signature together in comma-separated, key-value pairs:

authorization_parts = [
  [:realm, realm],
  [:oauth_consumer_key, escape(consumer_key)],
  [:oauth_token, escape(token_id)],
  [:oauth_signature_method, signature_method],
  [:oauth_timestamp, timestamp],
  [:oauth_nonce, escape(nonce)],
  [:oauth_version, oauth_version],
  [:oauth_signature, escape(signature)]
]

headers = {
  'Authorization' => "OAuth #{authorization_parts.map { |k, v| "#{k}=\"#{v}\"" }.join(',')}"
}

puts headers
{"Authorization"=>"OAuth realm=\"9876543_SB1\",oauth_consumer_key=\"CONSUMER_KEY_VALUE\",oauth_token=\"TOKEN_ID_VALUE\",oauth_signature_method=\"HMAC-SHA256\",oauth_timestamp=\"1234567890\",oauth_nonce=\"asdfasdf\",oauth_version=\"1.0\",oauth_signature=\"cId0B3hP0sFVQw%2FgjQ%2FP6YiOSx76u0WfyO8umOlq3gg%3D\""}

Wrapping up

All that is left is to use this header in a request. You will need a new signature (and therefore new header) per-request. And that’s it!

While not difficult in practice, it was by trial-and-error we were ultimately able to authenticate to NetSuite. Hopefully this post saves you some time!

It is highly recommended that you write comprehensive unit tests for this code. We wrote some tests in RSpec. These test are a great resource for examples you can test against.