How to GET invoices in JSON from the Netsuite SOAP API using Python

What is Netsuite?

Oracle Netsuite is a popular accounting software used by SMBs (small and medium businesses) and mid-market companies. Netsuite provides a unified view of a business by bringing together finance, supply chain, HR, and e-commerce services into a single system. It’s able to serve a large variety of different industries and can be customized to accommodate a wide array of business processes. 

In this article, you’ll learn how to understand Oracle Netsuite’s documentation, how to configure user permissions to begin an integration, and how to build a sample SOAP envelope with Netsuite’s API in order to GET invoices.

What is the Netsuite Developer API?

For tools and software that act as the basis of corporate accounting and resource planning or HR workflows, an [integration] {link to imaginary “what is an integration” article} with Netsuite provides a powerful source of truth. 

The way to build an integration with Netsuite is through the Netsuite Developer API. Here’s what you need to know to get started using the Netsuite API.

How to Understand Netsuite’s Documentation Like a Pro

How do you begin to integrate with Netsuite? 

To begin integrating with Netsuite, you need a user role with the proper administrative permissions. In your Netsuite portal, the first thing you’ll need to do is go to your portal >> Setup >> Users/Roles >> Manage Roles.

Image showing the "manage roles" section of the Nestuite documentation

Inside Role management, add a Role with full access to all transaction types. For the purposes of this article we’ll be pulling information from the SOAP API, so you’ll need to give the role permissions to “SOAP Web Services.”

Shows the Netsuite SOAP Web Services tab

Under the setup, tab go to “Integrations” then “Manage Integrations” and hit “New”. Look at the “Integration” page screenshot for guidance on how to fill out the form. We will use Token Based Authentication.

Token Based Authentication
Token Based

After saving your new integration, you should receive a Consumer Key and Consumer Secret.

Show integrations alter

To create the Token ID and SECRET go to "Manage Access Tokens" in your Netsuite instance.

settings

Create a New Access Token with the Role just created.

access1
access2

Creating the Access token will give you the Token ID and Secret, which along with the Consumer Key and Secret will give you all the permissions you need to access the Netsuite SOAP service.

What Netsuite data models are relevant to a general accounting use case? 

When building integrations with any Accounting API Provider, including Oracle Netsuite, one key field that almost always needs to be pulled is Invoices. Other relevant data models include Journal Entries, Tracking Categories, Contacts, Accounts, and Line Items.

Invoices are important because they signal a client’s obligation to pay for a given set of goods or services.

In the next section of this article,  you will learn how to pull invoices from Netsuite’s SOAP API with Python to receive a JSON response.

How to Build a Sample SOAP Envelope for Netsuite

Here’s an example SOAP Envelope you would send to Netsuite to pull invoices from a certain timeframe (in this example, we’ll be pulling all invoices after 2021-01-01). We’ll deconstruct the Envelope below.

<?xml version='1.0' encoding='utf-8'?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Header>
<tokenPassport >
<account xmlns:ns1="urn:core_2020_2.platform.webservices.netsuite.com">{Domain}</account>
<consumerKey xmlns:ns2="urn:core_2020_2.platform.webservices.netsuite.com">{Username}</consumerKey>
<token xmlns:ns3="urn:core_2020_2.platform.webservices.netsuite.com">{TOKEN_ID}</token>
<nonce xmlns:ns4="urn:core_2020_2.platform.webservices.netsuite.com">{nonce}</nonce>
<timestamp xmlns:ns5="urn:core_2020_2.platform.webservices.netsuite.com">{timestamp}</timestamp>
<signature xmlns:ns6="urn:core_2020_2.platform.webservices.netsuite.com" algorithm="HMAC-SHA256">{signature}</signature>
</tokenPassport>
<searchPreferences xmlns:ns7="urn:messages_2020_2.platform.webservices.netsuite.com">
<bodyFieldsOnly>true</bodyFieldsOnly>
<returnSearchColumns>true</returnSearchColumns>
<pageSize>1000</pageSize>
</searchPreferences>
</soap-env:Header>
<soap-env:Body>
<search >
<searchRecord xsi:type="ns8:TransactionSearchAdvanced" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ns8="urn:sales_2020_2.transactions.webservices.netsuite.com">
<criteria >
<basic>
<type operator="anyOf" >
<searchValue>_invoice</searchValue>
</type>
<lastModifiedDate operator="after">
<searchValue >2021-01-01T00:00:00-07:00</searchValue>
</lastModifiedDate>
</basic>
</criteria>
<columns >
<basic>
<internalId/>
<transactionNumber/>
<quantity/>
<amount/>
<amountRemaining/>
<memo/>
<currency/>
<dateCreated/>
<dueDate/>
<entity/>
<tranDate/>
<discountAmount/>
<lastModifiedDate/>
<rate/>
<item/>
<account/>
<class/>
<line/>
</basic>
</columns>
</searchRecord>
</search>
</soap-env:Body>
</soap-env:Envelope>

Required fields for creating a Netsuite Envelope

Before actually sending off the SOAP request, we’ll need to get the fields required to build the SOAP header. These fields include your (or your customer’s) {account Domain, Username, Token_ID, nonce, timestamp, and signature} [all in code format].

Consumer Key (Username) and Token ID: The Consumer Key and Token ID will come from the integration registration, which you receive in the above section.. Account Domain (or Account ID): If you don’t already know your Account ID you can pull it from your unique Netsuite URL. Go to your Netsuite instance and the digits that prefix “app.netsuite.com” is your Account ID. 

Nonce: The nonce is a 10-digit random code that is often generated with encryption libraries. In Python, there are several libraries you could use to generate it including secrets or osrandom.  For this tutorial, we’ll just use the secrets module.

Timestamp: Generate a current timestamp in millisecond format. It is important the timestamp is created at the time of the request. We’ll use the time library to pull in the current time.

Signature:

timestamp: str = str(int(timezone.now().timestamp()))
nonce: str = secrets.token_hex(20)
base_str: str = "&".join([account_id, consumer_key, token_id, nonce, timestamp])
key: str = "&".join([consumer_secret, token_secret])
digest: bytes = hmac.new(str.encode(key), msg=str.encode(base_str), digestmod=hashlib.sha256).digest()
signature: str = base64.b64encode(digest).decode()

To create the signature, we’ll make a hash from the nonce and timestamp just generated and your auth tokens.. Create a base string by concatenating your  auth fields together, as shown in the code snippet, and create a key by concatenating your Token Secret and Consumer Secret. With your key and base string, generate a hash using the HmacSHA256 library, which will be your signature. 

Note that in this SOAP header, we also have a set of search preferences we can fill out including body fields, search columns, and page size. We’ll set the former two to true as the fields and search columns are all we’ll need from the request. The page size is set to 1000:the maximum allowed size.

Note that in this SOAP header, we also have a set of search preferences we can fill out including body fields, search columns, and page size. We’ll set the former two to true as the fields and search columns are all we’ll need from the request. The page size is set to 1000:the maximum allowed size.

<soap-env:Header>
<tokenPassport >
<account xmlns:ns1="urn:core_2020_2.platform.webservices.netsuite.com">{Domain}</account>
<consumerKey xmlns:ns2="urn:core_2020_2.platform.webservices.netsuite.com">{Username}</consumerKey>
<token xmlns:ns3="urn:core_2020_2.platform.webservices.netsuite.com">{TOKEN_ID}</token>
<nonce xmlns:ns4="urn:core_2020_2.platform.webservices.netsuite.com">{nonce}</nonce>
<timestamp xmlns:ns5="urn:core_2020_2.platform.webservices.netsuite.com">{timestamp}</timestamp>
<signature xmlns:ns6="urn:core_2020_2.platform.webservices.netsuite.com" algorithm="HMAC-SHA256">{signature}</signature>
</tokenPassport>
<searchPreferences xmlns:ns7="urn:messages_2020_2.platform.webservices.netsuite.com">
<bodyFieldsOnly>true</bodyFieldsOnly>
<returnSearchColumns>true</returnSearchColumns>
<pageSize>1000</pageSize>
</searchPreferences>
</soap-env:Header>

Now that we’ve constructed our SOAP Header, we’ll move on to the Body, where we’ll create the request.

Constructing Your Netsuite API Request Body

The searchRecord field is the object for specifying the search record types for searching records based on specific search criteria. To pull invoices, we will query transactions records here: 

“xmlns:ns8="urn:sales_2020_2.transactions.webservices.netsuite.com" There are several other items we can pull from Netsuite, and you’ll be able to find the namespace values and models of ones you can pull from here:

https://www.netsuite.com/help/helpcenter/en_US/srbrowser/Browser2016_1/schema/record/invoice.html   To further narrow down the search, add some criteria like below, including the search value which is the object we’re searching for (invoices), and a timeframe from which we wish to make our request. In this case, we’re pulling all invoices that occur after 2021-01-01.

<soap-env:Body>
<search >
<searchRecord xsi:type="ns8:TransactionSearchAdvanced" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ns8="urn:sales_2020_2.transactions.webservices.netsuite.com">
<criteria >
<basic>
<type operator="anyOf" >
<searchValue>_invoice</searchValue>
</type>
<lastModifiedDate operator="after">
<searchValue >2021-01-01T00:00:00-07:00</searchValue>
</lastModifiedDate>
</basic>
</criteria>
<columns >
<basic>
<internalId/>
<transactionNumber/>
<quantity/>
<amount/>
<amountRemaining/>
<memo/>
<currency/>
<dateCreated/>
<dueDate/>
<entity/>
<tranDate/>
<discountAmount/>
<lastModifiedDate/>
<rate/>
<item/>
<account/>
<class/>
<line/>
</basic>
</columns>
</searchRecord>
</search>
</soap-env:Body>

To make the request in Python, we’ll wrap the request in a string and create variables for the header values that you’ll need to fill out with your information.

request body =
f"""<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Header>
<tokenPassport >
<account xmlns:ns1="urn:core_2020_2.platform.webservices.netsuite.com">{Domain}</account>
<consumerKey xmlns:ns2="urn:core_2020_2.platform.webservices.netsuite.com">{Username}</consumerKey>
<token xmlns:ns3="urn:core_2020_2.platform.webservices.netsuite.com">{TOKEN_ID}</token>
<nonce xmlns:ns4="urn:core_2020_2.platform.webservices.netsuite.com">{nonce}</nonce>
<timestamp xmlns:ns5="urn:core_2020_2.platform.webservices.netsuite.com">{timestamp}</timestamp>
<signature xmlns:ns6="urn:core_2020_2.platform.webservices.netsuite.com" algorithm="HMAC-SHA256">{signature}</signature>
</tokenPassport>
<searchPreferences xmlns:ns7="urn:messages_2020_2.platform.webservices.netsuite.com">
<bodyFieldsOnly>true</bodyFieldsOnly>
<returnSearchColumns>true</returnSearchColumns>
<pageSize>1000</pageSize>
</searchPreferences>
</soap-env:Header>
<soap-env:Body>
<search >
<searchRecord xsi:type="ns8:TransactionSearchAdvanced" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ns8="urn:sales_2020_2.transactions.webservices.netsuite.com">
<criteria >
<basic>
<type operator="anyOf" >
<searchValue>_invoice</searchValue>
</type>
<lastModifiedDate operator="after">
<searchValue >2021-01-01T00:00:00-07:00</searchValue>
</lastModifiedDate>
</basic>
</criteria>
<columns >
<basic>
<internalId/>
<transactionNumber/>
<quantity/>
<amount/>
<amountRemaining/>
<memo/>
<currency/>
<dateCreated/>
<dueDate/>
<entity/>
<tranDate/>
<discountAmount/>
<lastModifiedDate/>
<rate/>
<item/>
<account/>
<class/>
<line/>
</basic>
</columns>
</searchRecord>
</search>
</soap-env:Body>
</soap-env:Envelope>"""

Sending the SOAP Envelope to Netsuite to pull invoices via API

The rest of the steps for pulling invoices is the same as our previous SOAP example here, where we create a request and parse the response form the request.

response = requests.request(method="POST", url=url, data=request_body)
parsed_response = json.loads(parse(response.content))

Like before, we pass in our SOAP envelope as a request body, and instead of receiving XML which is standard in soap, we receive a parsed JSON object that’s much simpler to work with! 

{
"soapenv:Envelope": {
"@xmlns:soapenv": "http://schemas.xmlsoap.org/soap/envelope/",
"@xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"soapenv:Header": {
"platformMsgs:documentInfo": {
"@xmlns:platformMsgs": "urn:messages_2020_2.platform.webservices.netsuite.com",
"platformMsgs:nsId": "WEBSERVICES_7600508_04082022182110359418257319_d3406a8646b75"
}
},
"soapenv:Body": {
"searchResponse": {
"@xmlns": "",
"platformCore:searchResult": {
"@xmlns:platformCore": "urn:core_2020_2.platform.webservices.netsuite.com",
"platformCore:status": {
"@isSuccess": "true"
},
"platformCore:totalRecords": "13",
"platformCore:pageSize": "1000",
"platformCore:totalPages": "1",
"platformCore:pageIndex": "1",
"platformCore:searchId": "WEBSERVICES_7600508_04082022182110359418257319_d3406a8646b75",
"platformCore:searchRowList": {
"platformCore:searchRow": [
{
"@xsi:type": "tranSales:TransactionSearchRow",
"@xmlns:tranSales": "urn:sales_2020_2.transactions.webservices.netsuite.com",
"tranSales:basic": {
"@xmlns:platformCommon": "urn:common_2020_2.platform.webservices.netsuite.com",
"platformCommon:account": {
"platformCore:searchValue": {
"@internalId": "119"
}
},
"platformCommon:amount": {
"platformCore:searchValue": "4520.0"
},
"platformCommon:amountRemaining": {
"platformCore:searchValue": "20.0"
},
"platformCommon:class": {
"platformCore:searchValue": {
"@internalId": "1"
}
},
"platformCommon:dueDate": {
"platformCore:searchValue": "2022-01-05T00:00:00.000-08:00"
},
"platformCommon:entity": {
"platformCore:searchValue": {
"@internalId": "10"
}
},
"platformCommon:internalId": {
"platformCore:searchValue": {
"@internalId": "113"
}
},
"platformCommon:lastModifiedDate": {
"platformCore:searchValue": "2022-04-06T11:22:00.000-07:00"
},
"platformCommon:line": {
"platformCore:searchValue": "0"
},
"platformCommon:memo": {
"platformCore:searchValue": "memoline"
},
"platformCommon:tranDate": {
"platformCore:searchValue": "2021-12-06T00:00:00.000-08:00"
},
"platformCommon:transactionNumber": {
"platformCore:searchValue": "CUSTINVC1"
}
}
}
]
}
}
}
}
}
}

We’ve shortened our response here to a more manageable bite, but in the body section, we’re able to see the fields we’re interested in pulling under the section which includes the amount, amount remaining, and the due dates as shown below. There will also be additional fields relevant to the invoice such as modified date, memos, and general transaction information.

"platformCommon:amount": {
"platformCore:searchValue": "4520.0"
},
"platformCommon:amountRemaining": {
"platformCore:searchValue": "20.0"
},
"platformCommon:class": {
"platformCore:searchValue": {
"@internalId": "1"
}
},
"platformCommon:dueDate": {
"platformCore:searchValue": "2022-01-05T00:00:00.000-08:00"
},

With this data at the tip of your fingers, you’re able to utilize all the data inside your or your customer's NetSuite instances. 

Beyond Netsuite

Looking to integrate multiple accounting providers beyond Netsuite? Merge’s Unified Accounting API provides easy, REST-based API integration with platforms like Netsuite, Quickbooks, Sage Intaact, and more. 

Merge’s Unified API returns standardized data across all platforms, and handles the long-term of integration maintenance for you. 

One integration with Merge is all you’ll need to pull and push accounting data on behalf of your customers

Building your Netsuite integration with Merge

With Merge, offering an integration with Netsuite (or any other accounting platform) inside your product becomes a trivial task.

All your developers need to do is add our drop-in Merge Link component to your front end, and integrate it with Merge’s Unified API in your backend. We provide SDKs in NodeJS, C#/ .NET, Python, and Ruby to make your development process easy., though if there’s a specific language you need, you can get in touch with us through Intercom to request it. 

You’re also able to make requests to the Merge API.

To test with Merge, we have a public postman space that allows you to get a cleaned version of the JSON we’ve shown above with Merge’s API, you no longer need to do any additional work of normalizing the soap response and will receive responses in a standard format that is consistent across all accounting API providers, whether they use SOAP or a REST architecture.


Read these next

Guide to Payroll API Integrations
Your July 2022 Product Update
Learn to handle any edge case with Authenticated Passthrough Requests

Email Updates

Subscribe to the Merge Blog

Get stories from Merge straight to your inbox