searchMoreWithId for pagination. Write operations limited to 100-200 records per batch call depending on operation type. [src2]This card covers Oracle NetSuite SuiteTalk SOAP Web Services, the original programmatic API for NetSuite. SuiteTalk uses document-style SOAP over HTTPS with XML serialization. It supports all major NetSuite record types (customers, transactions, items, custom records) and provides search, CRUD, and bulk list operations. The WSDL is versioned in lockstep with NetSuite releases (e.g., 2025.2 corresponds to WSDL 2025_2). All NetSuite editions (Standard, Premium, Enterprise, Ultimate) support SOAP web services, but concurrency limits vary dramatically by tier and SuiteCloud Plus licensing. [src1, src3]
Critical deprecation notice: Oracle has announced that SOAP web services will be fully removed in the 2028.2 release. The 2025.2 WSDL is the last planned SOAP endpoint. Starting with 2027.1, no new SOAP integrations can be created. All existing SOAP integrations must migrate to REST web services with OAuth 2.0 before 2028.2. [src4]
| Property | Value |
|---|---|
| Vendor | Oracle |
| System | Oracle NetSuite (WSDL 2025.2) |
| API Surface | SOAP (Document-style, XML/HTTPS) |
| Current API Version | WSDL 2025.2 (final planned) |
| Editions Covered | Standard, Premium, Enterprise, Ultimate |
| Deployment | Cloud |
| API Docs | SuiteTalk SOAP Web Services Platform Overview |
| Status | Supported (deprecation announced — full removal at 2028.2) |
NetSuite offers multiple API surfaces. This card focuses on SuiteTalk SOAP, but understanding where it fits relative to alternatives is critical for integration architecture decisions. [src1, src4]
| API Surface | Protocol | Best For | Max Records/Request | Concurrency | Real-time? | Status |
|---|---|---|---|---|---|---|
| SuiteTalk SOAP | HTTPS/XML | Legacy integrations, SOAP-centric ESBs | 1,000 (search), 200 (addList) | Shared pool | Yes | Deprecated (removal 2028.2) |
| SuiteTalk REST | HTTPS/JSON | All new integrations post-2026.1 | 1,000 (default), configurable | Shared pool | Yes | GA — recommended |
| SuiteQL | HTTPS/JSON | Analytics, complex queries, reporting | 100,000 rows max | Shared pool | Yes | GA |
| RESTlets (SuiteScript) | HTTPS/JSON | Custom endpoints, business logic | Script-dependent | 5 per user | Yes | GA |
| CSV Import | File-based | Bulk data loads, migrations | Unlimited (file-size limited) | N/A | No | GA |
NetSuite governs SOAP web services through record limiting and request limiting. These are hard caps — exceeding them raises SOAP faults. [src2]
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Search pageSize (sync) | 1,000 max, 5 min | search, searchMoreWithId | Default is 1,000 if not specified |
| Search pageSize (async) | 2,000 max, 5 min | asyncSearch | Use async for larger page sizes |
addList records | 200 max | Synchronous create | asyncAddList allows 400 |
deleteList records | 200 max | Synchronous delete | asyncDeleteList allows 400 |
getList records | 1,000 max | Synchronous read | asyncGetList allows 2,000 |
updateList records | 100 max | Synchronous update | asyncUpdateList allows 200 |
upsertList records | 100 max | Synchronous upsert | asyncUpsertList allows 200 |
getItemAvailability | 10,000 max | Item availability queries | Largest single-operation limit |
| SOAP request body | 100 MB max | All SOAP requests | Exceeding raises ExceededRequestSizeFault |
Concurrency is the number of simultaneous API requests NetSuite will process for your account. This pool is shared across all SOAP, REST, and RESTlet requests. [src3, src6, src7]
| Service Tier | Base Concurrent Requests | With 1 SC+ License | With 3 SC+ Licenses | With 5 SC+ Licenses |
|---|---|---|---|---|
| Standard | 5 | 15 | 35 | 55 |
| Premium | 15 | 25 | 45 | 65 |
| Enterprise | 20 | 30 | 50 | 70 |
| Ultimate | 20 | 30 | 50 | 70 |
| Developer/Sandbox | 5 (fixed) | 5 (fixed) | 5 (fixed) | 5 (fixed) |
Formula: Total concurrency = Base tier limit + (10 x number of SuiteCloud Plus licenses).
| Fault Code | Meaning | Resolution |
|---|---|---|
ExceededRecordCountFault | Request exceeds per-operation record limit | Reduce batch size to within limits above |
ExceededRequestSizeFault | SOAP request body exceeds 100 MB | Split payload into smaller requests |
ExceededRequestLimitFault | Account concurrency limit reached | Implement queuing with backoff; check Integration Governance dashboard |
WS_REQUEST_BLOCKED | Request throttled or blocked | Reduce request frequency; review concurrent usage |
SuiteTalk SOAP uses Token-Based Authentication (TBA), which is based on OAuth 1.0. OAuth 2.0 is explicitly NOT supported for SOAP web services. [src5]
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Token-Based Authentication (TBA) | All SOAP integrations | Until revoked | No (permanent tokens) | Based on OAuth 1.0; recommended for all SOAP |
| User Credentials (legacy) | Testing only | Session-based | No | Deprecated — subject to password policies and MFA issues |
INVALID_LOGIN_ATTEMPT errors. [src5]updateList at 100 records max means you need 10 calls to update 1,000 records, each consuming a concurrent slot. [src2]START -- User needs to integrate with NetSuite via SOAP
|-- Is this a NEW integration (2026+)?
| |-- YES --> STOP. Use REST API with OAuth 2.0 instead (SOAP deprecated)
| +-- NO --> Continue (maintaining existing SOAP integration)
|
|-- What is the integration pattern?
| |-- Real-time individual record operations
| | |-- Single record? --> use get/add/update/upsert/delete
| | +-- Multiple records (< 200)? --> use addList/updateList/upsertList
| |
| |-- Search/query operations
| | |-- Simple lookup by internal ID? --> use get or getList (up to 1,000)
| | |-- Complex criteria search? --> use search with SearchPreferences
| | | |-- Expect < 1,000 results? --> single search call
| | | +-- Expect > 1,000 results? --> search + searchMoreWithId pagination
| | +-- Need to reuse existing saved search? --> use search with savedSearchId
| |
| |-- Batch/bulk processing
| | |-- < 10,000 records? --> Loop with list operations + rate control
| | |-- < 100,000 records? --> Async operations + parallel execution
| | +-- > 100,000 records? --> CSV Import (not SOAP) or SuiteQL for reads
| |
| +-- Event-driven (real-time notifications)
| +-- SOAP cannot push events. Use SuiteScript User Event Scripts + RESTlets
|
+-- What is the error tolerance?
|-- Zero-loss required --> Implement idempotent upsert + external tracking table
+-- Best-effort --> Fire-and-forget with retry on ExceededRequestLimitFault
| Operation | Method | Records/Call | Async Variant | Async Records/Call | Notes |
|---|---|---|---|---|---|
get | Read single | 1 | N/A | N/A | By internal ID or external ID |
getList | Read multiple | 1,000 | asyncGetList | 2,000 | Returns full records |
search | Query | 1,000/page | asyncSearch | 2,000/page | Use SearchPreferences for pageSize |
searchMoreWithId | Pagination | 1,000/page | N/A | N/A | Requires searchId from initial search |
add | Create single | 1 | N/A | N/A | Returns internal ID on success |
addList | Create multiple | 200 | asyncAddList | 400 | Partial success possible |
update | Update single | 1 | N/A | N/A | Requires internal ID |
updateList | Update multiple | 100 | asyncUpdateList | 200 | Partial success possible |
upsert | Create or update | 1 | N/A | N/A | Uses external ID for matching |
upsertList | Batch upsert | 100 | asyncUpsertList | 200 | External ID required on each record |
delete | Delete single | 1 | N/A | N/A | By internal ID |
deleteList | Delete multiple | 200 | asyncDeleteList | 400 | Partial success possible |
getDeleted | Deleted records | N/A | N/A | N/A | Returns records deleted within date range |
initialize | Init record | 1 | N/A | N/A | Pre-populates from source record |
attach/detach | Link records | 1 | N/A | N/A | Creates/removes relationships |
getSelectValue | Picklist values | N/A | N/A | N/A | Returns valid values for select fields |
Navigate to Setup > Integration > Manage Integrations > New. Enable Token-Based Authentication. Note the Consumer Key and Consumer Secret. Then create a token: Setup > Users/Roles > Access Tokens > New. Select the integration, user, and role. Note the Token ID and Token Secret. [src5]
Consumer Key: abc123...
Consumer Secret: def456...
Token ID: ghi789...
Token Secret: jkl012...
Verify: Go to Setup > Integration > Integration Governance. Your integration should appear with its concurrency allocation.
Build an OAuth 1.0-signed SOAP request. The signature base string must include the account ID, consumer key, token, nonce, and timestamp. [src5]
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="urn:messages_2025_2.platform.webservices.netsuite.com"
xmlns:core="urn:core_2025_2.platform.webservices.netsuite.com">
<soap:Header>
<ns:tokenPassport>
<core:account>YOUR_ACCOUNT_ID</core:account>
<core:consumerKey>YOUR_CONSUMER_KEY</core:consumerKey>
<core:token>YOUR_TOKEN_ID</core:token>
<core:nonce>RANDOM_NONCE</core:nonce>
<core:timestamp>UNIX_TIMESTAMP</core:timestamp>
<core:signature algorithm="HMAC-SHA256">COMPUTED_SIGNATURE</core:signature>
</ns:tokenPassport>
<ns:searchPreferences>
<ns:pageSize>1000</ns:pageSize>
<ns:bodyFieldsOnly>false</ns:bodyFieldsOnly>
</ns:searchPreferences>
</soap:Header>
<soap:Body>
<!-- Operation goes here -->
</soap:Body>
</soap:Envelope>
Verify: Send a simple getServerTime call. Expected response: <getServerTimeResponse> with a timestamp.
Use search for the first page, then searchMoreWithId for subsequent pages. Each page returns up to 1,000 records (sync). [src2, src8]
<!-- Initial search -->
<soap:Body>
<ns:search>
<ns:searchRecord xsi:type="listRel:CustomerSearchBasic">
<listRel:lastModifiedDate operator="after">
<core:searchValue>2026-01-01T00:00:00Z</core:searchValue>
</listRel:lastModifiedDate>
</ns:searchRecord>
</ns:search>
</soap:Body>
<!-- For page 2+: -->
<soap:Body>
<ns:searchMoreWithId>
<ns:searchId>WEBSERVICES_TSTDRV1234567_01012026...</ns:searchId>
<ns:pageIndex>2</ns:pageIndex>
</ns:searchMoreWithId>
</soap:Body>
Verify: Check <totalRecords> and <totalPages> in response. Page through all pages until pageIndex == totalPages.
Handle the three governance fault types and implement exponential backoff for concurrency limits. [src2, src3]
import time, random
MAX_RETRIES = 5
BASE_DELAY = 2 # seconds
def call_with_retry(soap_client, operation, *args):
for attempt in range(MAX_RETRIES):
try:
return operation(*args)
except ExceededRequestLimitFault:
delay = BASE_DELAY * (2 ** attempt) + random.uniform(0, 1)
print(f"Concurrency limit hit, retry {attempt+1}/{MAX_RETRIES} in {delay:.1f}s")
time.sleep(delay)
except ExceededRecordCountFault:
raise ValueError("Reduce batch size below operation limit")
except ExceededRequestSizeFault:
raise ValueError("Request exceeds 100MB - split into smaller requests")
raise RuntimeError(f"Failed after {MAX_RETRIES} retries")
# Input: NetSuite account with TBA credentials, target date
# Output: All customer records modified after the given date
# pip install zeep==4.2.1
from zeep import Client
import hashlib, hmac, time, secrets, base64
WSDL_URL = "https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/wsdl/v2025_2_0/netsuite.wsdl"
def search_all_customers(client, since_date):
search_prefs = client.get_type("ns0:SearchPreferences")(
pageSize=1000, bodyFieldsOnly=False
)
search_record = {
"basic": {
"lastModifiedDate": {
"operator": "after",
"searchValue": since_date
}
}
}
result = client.service.search(searchRecord=search_record,
_soapheaders={"tokenPassport": generate_tba_header(client),
"searchPreferences": search_prefs})
all_records = list(result.searchResult.recordList.record)
total_pages = result.searchResult.totalPages
search_id = result.searchResult.searchId
for page in range(2, total_pages + 1):
page_result = client.service.searchMoreWithId(
searchId=search_id, pageIndex=page,
_soapheaders={"tokenPassport": generate_tba_header(client)})
all_records.extend(page_result.searchResult.recordList.record)
time.sleep(0.5) # Respect concurrency pool
return all_records
// Input: Array of record objects to upsert
// Output: Results with success/failure per record
// npm install [email protected]
const soap = require('soap');
const BATCH_SIZE = 100; // upsertList max is 100 sync
async function upsertInBatches(client, records) {
const results = [];
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE);
try {
const response = await client.upsertListAsync({ record: batch });
const writeResponse = response[0].writeResponseList.writeResponse;
writeResponse.forEach((wr, idx) => {
results.push({
index: i + idx,
success: wr.status.isSuccess,
internalId: wr.baseRef?.internalId || null,
error: wr.status.statusDetail?.[0]?.message || null
});
});
} catch (err) {
if (err.root?.Envelope?.Body?.Fault?.faultstring?.includes('ExceededRequestLimitFault')) {
await new Promise(r => setTimeout(r, 5000));
i -= BATCH_SIZE; // Retry this batch
} else { throw err; }
}
await new Promise(r => setTimeout(r, 1000));
}
return results;
}
# Input: TBA credentials (consumer key/secret, token ID/secret, account ID)
# Output: Server time response confirming auth works
curl -X POST \
"https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/NetSuitePort_2025_2" \
-H "Content-Type: text/xml; charset=utf-8" \
-H 'SOAPAction: "getServerTime"' \
-d '<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="urn:messages_2025_2.platform.webservices.netsuite.com"
xmlns:core="urn:core_2025_2.platform.webservices.netsuite.com">
<soap:Header>
<ns:tokenPassport>
<core:account>ACCOUNT_ID</core:account>
<core:consumerKey>CONSUMER_KEY</core:consumerKey>
<core:token>TOKEN_ID</core:token>
<core:nonce>RANDOM_32_CHAR_NONCE</core:nonce>
<core:timestamp>UNIX_TIMESTAMP</core:timestamp>
<core:signature algorithm="HMAC-SHA256">BASE64_SIGNATURE</core:signature>
</ns:tokenPassport>
</soap:Header>
<soap:Body><ns:getServerTime/></soap:Body>
</soap:Envelope>'
# Expected: <getServerTimeResponse> with <serverTime>2026-03-01T12:00:00.000Z</serverTime>
| Business Object | SOAP Record Type | Internal Name | Notes |
|---|---|---|---|
| Customer | Customer | customer | Includes leads, prospects, and customers (lifecycle stages) |
| Sales Order | SalesOrder | salesorder | Transaction record — requires item lines |
| Invoice | Invoice | invoice | Transaction record — can be created from sales order |
| Item (Inventory) | InventoryItem | inventoryitem | Stock items with quantity tracking |
| Vendor | Vendor | vendor | Supplier records |
| Purchase Order | PurchaseOrder | purchaseorder | Transaction record |
| Journal Entry | JournalEntry | journalentry | GL-level financial entries |
| Custom Record | CustomRecord | customrecord_{id} | Custom record types use dynamic internal names |
| Employee | Employee | employee | HR records — restricted by role permissions |
currency field. [src1]true/false strings, not 1/0. Sending 1 may cause silent data corruption. [src1]customFieldList element with internalId matching the custom field's script ID. Wrong internal IDs are silently ignored. [src1]externalId) are case-sensitive and must be unique per record type. They are the primary mechanism for idempotent upserts. [src1]| Code | Meaning | Cause | Resolution |
|---|---|---|---|
ExceededRequestLimitFault | Concurrency limit exceeded | Too many simultaneous requests | Exponential backoff + queue pending requests |
ExceededRecordCountFault | Too many records in request | Batch size exceeds operation limit | Reduce batch size to within per-operation limits |
ExceededRequestSizeFault | Request body > 100 MB | Oversized XML payload | Split into multiple requests |
INVALID_LOGIN_ATTEMPT | Authentication failure | Wrong credentials or TBA not enabled | Verify consumer/token keys, enable TBA on integration |
INSUFFICIENT_PERMISSION | Role lacks access | Integration role missing permissions | Add missing permissions to integration role |
SSS_USAGE_LIMIT_EXCEEDED | Governance units exhausted | Too many governance units consumed | Optimize operations — reduce search complexity |
INVALID_KEY_OR_REF | Invalid ID reference | Record does not exist or wrong type | Validate IDs exist before referencing |
Use timestamp-based filters and track processed records externally. [src8]Validate custom field script IDs against getCustomizationId before deployment. [src1]Break complex searches into narrower criteria; use async search for large result sets. [src3, src6]Monitor for INVALID_LOGIN_ATTEMPT errors; implement alerting on auth failures. [src5]Monitor Oracle release notes; test against release preview accounts. [src1, src4]Always use full datetime (not date-only) in search criteria, explicitly in UTC. [src8]# BAD -- Pulls every customer record to detect changes
result = client.service.search(searchRecord={"basic": {}})
for record in result.searchResult.recordList.record:
if record.lastModifiedDate > last_sync_time:
process(record)
# GOOD -- Only fetches records modified since last sync
result = client.service.search(searchRecord={
"basic": {
"lastModifiedDate": {
"operator": "after",
"searchValue": last_sync_time.isoformat()
}
}
})
# BAD -- Using search when you know the IDs
for internal_id in known_ids:
result = client.service.search(searchRecord={
"basic": {"internalId": {"operator": "is", "searchValue": internal_id}}
})
# GOOD -- getList is faster, cheaper, and supports up to 1,000 records
ref_list = [{"type": "customer", "internalId": id} for id in known_ids[:1000]]
result = client.service.getList(baseRef=ref_list)
# BAD -- No retry logic; integration fails on concurrency limit
try:
result = client.service.addList(record=records)
except Exception as e:
log.error(f"Failed: {e}")
return None # Gives up immediately
# GOOD -- Retries with increasing delay on concurrency limits
import time, random
for attempt in range(5):
try:
result = client.service.addList(record=records)
break
except ExceededRequestLimitFault:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
else:
raise RuntimeError("Exhausted retries")
Design load tests with sandbox's 5-slot limit in mind. [src6, src7]Track processed record IDs externally; use narrow date ranges. [src8]Always check per-operation limits; use asyncUpdateList (200) if needed. [src2]Pin to 2025.2; test against release preview before upgrades. [src1, src4]Set saved search to return all results; control pagination via SOAP pageSize. [src8]customrecord_{id} where {id} is the numeric internal ID, not the script ID. Fix: Look up the numeric ID via Setup > Customization > Record Types. [src1]# Check current concurrency usage
# Navigate in NetSuite: Setup > Integration > Integration Governance
# Test TBA authentication
curl -X POST \
"https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/NetSuitePort_2025_2" \
-H "Content-Type: text/xml" -H 'SOAPAction: "getServerTime"' \
-d '<soap:Envelope...><soap:Header><!-- TBA header --></soap:Header><soap:Body><getServerTime/></soap:Body></soap:Envelope>'
# Check WSDL availability
curl -s "https://ACCOUNT_ID.suitetalk.api.netsuite.com/wsdl/v2025_2_0/netsuite.wsdl" | head -5
# Monitor async job status
# Use getAsyncResult with jobId from async operation
# Poll until status is "complete" or "failed"
| WSDL Version | NetSuite Release | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| 2025.2 | 2025.2 | Current (final SOAP endpoint) | None | Last planned SOAP WSDL — pin to this |
| 2025.1 | 2025.1 | Supported | Minor field additions | Supported until 2027.2 |
| 2024.2 | 2024.2 | Supported | None | Will lose support in stages through 2028.1 |
| 2024.1 | 2024.1 | Supported | None | Plan migration to 2025.2 |
| Pre-2024.1 | Various | Available (unsupported) | Varies | All removed by 2028.2 |
| Release | What Happens |
|---|---|
| 2025.2 | Last planned SOAP endpoint published |
| 2026.1 | All new integrations should use REST + OAuth 2.0 |
| 2027.1 | No new SOAP integrations; no new TBA integrations |
| 2027.2 | Only 2025.2 endpoint supported with bug fixes |
| 2028.2 | All SOAP endpoints disabled — integrations stop working |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Maintaining an existing SOAP integration | Building a new integration (2026+) | SuiteTalk REST API with OAuth 2.0 |
| ESB platform is SOAP-centric (MuleSoft, BizTalk, SAP PI/PO) | Need OAuth 2.0 authentication | REST API (OAuth 2.0 supported) |
| Need saved search execution via API | Daily volume exceeds 100,000 records | CSV Import for writes, SuiteQL for reads |
| Record types better supported in SOAP for your version | Need real-time push notifications | SuiteScript User Event Scripts + RESTlets |
| Short-term project decommissioning before 2028.2 | Long-term integration (3+ years) | REST API (SOAP removed 2028.2) |