---
# === IDENTITY ===
id: business/erp-integration/netsuite-suitetalk-api-capabilities/2026
canonical_question: "What are Oracle NetSuite SuiteTalk SOAP API capabilities - 1000-record limit, concurrency by tier?"
aliases:
  - "NetSuite SuiteTalk SOAP API record limits and pagination"
  - "NetSuite SOAP searchMoreWithId 1000 records per page"
  - "NetSuite concurrency limits by service tier SuiteCloud Plus"
  - "NetSuite SOAP vs REST API capabilities governance units"
entity_type: erp_integration
domain: business > erp-integration > netsuite-suitetalk-api-capabilities
region: global
jurisdiction: global
temporal_scope: 2025-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Oracle NetSuite"
    vendor: "Oracle"
    version: "WSDL 2025.2 (final SOAP endpoint)"
    edition: "Standard, Premium, Enterprise, Ultimate"
    deployment: cloud
    api_surface: "SOAP"

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.93
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2025.2 — designated as final SOAP endpoint; SOAP sunset announced for 2028.2"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Search pageSize capped at 1,000 records (sync) or 2,000 records (async) — use searchMoreWithId for pagination"
  - "addList/deleteList: max 200 records per call; updateList/upsertList: max 100 records per call (sync)"
  - "Concurrency is shared across SOAP, REST, and RESTlet requests — one pool per account"
  - "Concurrency varies by tier (new tiers, June 2020+): Standard 5, Premium 15, Enterprise 20, Ultimate 20 base slots"
  - "Each SuiteCloud Plus license adds 10 concurrent slots to account pool"
  - "SOAP request size capped at 100 MB per request"
  - "SOAP sunset: 2025.2 is last SOAP endpoint; no new SOAP integrations after 2027.1; full removal at 2028.2"
  - "OAuth 2.0 is NOT supported for SOAP — must use Token-Based Authentication (TBA) based on OAuth 1.0"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User is building a new integration from scratch in 2026+"
    use_instead: "NetSuite REST API — SOAP is deprecated; REST with OAuth 2.0 is the recommended path"
  - condition: "User needs to process >100,000 records in batch"
    use_instead: "NetSuite SuiteAnalytics or CSV Import — SOAP is not designed for bulk ETL"
  - condition: "User needs real-time event notifications from NetSuite"
    use_instead: "SuiteScript User Event Scripts or RESTlets — SOAP has no push/webhook mechanism"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to this ERP)"
      - "outbound (reading from this ERP)"
      - "bidirectional sync"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/netsuite-suitetalk-api-capabilities/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/netsuite-rest-api-capabilities/2026"
      label: "NetSuite REST API — the modern replacement for SuiteTalk SOAP"
    - id: "business/erp-integration/netsuite-suiteql-capabilities/2026"
      label: "NetSuite SuiteQL — SQL-like query language for analytics"
  solves: []
  alternative_to:
    - id: "business/erp-integration/netsuite-rest-api-capabilities/2026"
      label: "REST API — preferred for all new integrations after 2026.1"
  often_confused_with:
    - id: "business/erp-integration/netsuite-rest-api-capabilities/2026"
      label: "REST is JSON/OAuth 2.0; SOAP is XML/TBA — different auth, serialization, and record limits"

# === SOURCES ===
sources:
  - id: src1
    title: "SuiteTalk SOAP Web Services Platform Overview"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_N3412777.html
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src2
    title: "SOAP Web Services Governance Overview"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_N3418637.html
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src3
    title: "Web Services and RESTlet Concurrency Governance"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1500275531.html
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src4
    title: "SOAP Removal Plans FAQ"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/article_2104046421.html
    type: vendor_release_notes
    published: 2025-12-01
    reliability: authoritative
  - id: src5
    title: "Token-based Authentication and Web Services"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4381113277.html
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src6
    title: "NetSuite API Governance: Concurrency & Rate Limits Explained"
    author: Houseblend
    url: https://www.houseblend.io/articles/netsuite-api-governance-guide
    type: technical_blog
    published: 2025-06-01
    reliability: moderate_high
  - id: src7
    title: "NetSuite Integration Concurrency Limits 2025"
    author: Katoomi
    url: https://www.katoomi.com/netsuite-integration-concurrency-limits-2025/
    type: technical_blog
    published: 2025-01-15
    reliability: moderate_high
  - id: src8
    title: "Search Issues and Best Practices for SOAP Web Services and SuiteScript"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1519647409.html
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
---

# NetSuite SuiteTalk SOAP API Capabilities

## TL;DR

- **Bottom line**: SuiteTalk SOAP is NetSuite's legacy integration surface offering search, CRUD, and list operations via XML/HTTPS. It remains functional but is on a hard deprecation path — 2025.2 is the final SOAP endpoint, no new integrations after 2027.1, full shutdown at 2028.2. Migrate to REST. [src1, src4]
- **Key limit**: Search results capped at 1,000 records per page (sync); use `searchMoreWithId` for pagination. Write operations limited to 100-200 records per batch call depending on operation type. [src2]
- **Watch out for**: Concurrency is shared across SOAP, REST, and RESTlet calls in a single pool — a runaway SOAP integration can starve your REST API and RESTlet traffic. [src3, src6]
- **Best for**: Maintaining existing SOAP-based integrations that cannot be immediately migrated, or ESB platforms with mature SOAP stacks (MuleSoft, BizTalk, SAP PI/PO). [src1]
- **Authentication**: Token-Based Authentication (TBA) based on OAuth 1.0 — OAuth 2.0 is NOT supported for SOAP web services. [src5]

## System Profile

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 URL** | [SuiteTalk SOAP Web Services Platform Overview](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_N3412777.html) |
| **Status** | Supported (deprecation announced — full removal at 2028.2) |

## API Surfaces & Capabilities

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 |

## Rate Limits & Quotas

### Per-Request Limits

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 [src2] |
| Search pageSize (async) | 2,000 max, 5 min | `asyncSearch` | Use async for larger page sizes [src2] |
| `addList` records | 200 max | Synchronous create | For higher volume, use `asyncAddList` (400 max) [src2] |
| `deleteList` records | 200 max | Synchronous delete | `asyncDeleteList` allows 400 [src2] |
| `getList` records | 1,000 max | Synchronous read | `asyncGetList` allows 2,000 [src2] |
| `updateList` records | 100 max | Synchronous update | `asyncUpdateList` allows 200 [src2] |
| `upsertList` records | 100 max | Synchronous upsert | `asyncUpsertList` allows 200 [src2] |
| `getItemAvailability` | 10,000 max | Item availability queries | Largest single-operation limit [src2] |
| SOAP request body | 100 MB max | All SOAP requests | Exceeding raises `ExceededRequestSizeFault` [src2] |

### Concurrency Limits (Rolling)

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). [src3, src6, src7]

### Governance Unit Costs

NetSuite tracks API usage through governance units. Every operation consumes units from a per-script or per-request budget. While exact unit costs are operation-dependent and not fully public, the system enforces `SSS_USAGE_LIMIT_EXCEEDED` when budgets are exceeded. [src6]

| Operation Type | Approximate Governance Cost | Notes |
|---|---|---|
| Record create/update | ~10 units | Per record, varies by record type complexity |
| Search execution | ~5 units | Initial search; pagination is cheaper |
| Record read (get/getList) | ~5 units | Per call, not per record |
| Delete operation | ~5 units | Per record |
| Saved search execution | ~10 units | Depends on result set size and join complexity |

### Governance Error Faults

| Fault Code | Meaning | Resolution |
|---|---|---|
| `ExceededRecordCountFault` | Request exceeds per-operation record limit | Reduce batch size to within limits above [src2] |
| `ExceededRequestSizeFault` | SOAP request body exceeds 100 MB | Split payload into smaller requests [src2] |
| `ExceededRequestLimitFault` | Account concurrency limit reached | Implement queuing with backoff; check Integration Governance dashboard [src3] |
| `WS_REQUEST_BLOCKED` | Request throttled or blocked | Reduce request frequency; review concurrent usage [src6] |

## Authentication

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 — it is only available for REST web services, RESTlets, and SuiteAnalytics Connect. [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 [src5] |
| User Credentials (legacy) | Testing only | Session-based | No | Deprecated — subject to password policies and MFA issues [src5] |

### Authentication Gotchas

- TBA tokens are permanent and do not expire, but they can be revoked by an administrator at any time. Build your integration to handle sudden auth failures gracefully. [src5]
- TBA requires a signed OAuth 1.0 header with consumer key, consumer secret, token ID, and token secret. The signature method is HMAC-SHA256. Getting the signature base string wrong is the #1 cause of auth failures. [src5]
- Each integration record in NetSuite must have TBA enabled explicitly (Setup > Integration > Manage Integrations). Forgetting this step causes `INVALID_LOGIN_ATTEMPT` errors that look like credential issues. [src5]
- OAuth 2.0 cannot be used for SOAP — if you see OAuth 2.0 examples in NetSuite docs, they apply to REST only. Do not attempt to use OAuth 2.0 tokens in SOAP headers. [src5, src4]
- Starting with 2027.1, no new integrations using TBA can be created for any API surface. Existing TBA integrations continue working, but new ones must use OAuth 2.0 (REST only). [src4]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- **SOAP sunset timeline**: 2025.2 is the last SOAP endpoint. 2027.1 blocks new SOAP integrations. 2028.2 removes SOAP entirely. Any new integration should use REST. [src4]
- **No OAuth 2.0 for SOAP**: SOAP only supports TBA (OAuth 1.0). OAuth 2.0 is REST-only. [src5]
- **Shared concurrency pool**: SOAP, REST, and RESTlet requests share the same concurrency limit. A SOAP-heavy integration directly reduces capacity for other API surfaces. [src3]
- **Record limits are per-call, not per-record**: `updateList` at 100 records max means you need 10 calls to update 1,000 records, each consuming a concurrent slot. [src2]
- **Sandbox concurrency is fixed at 5**: Regardless of production tier or SC+ licenses, sandbox/developer accounts are capped at 5 concurrent requests. Load testing must account for this. [src6, src7]
- **No webhook/push mechanism**: SOAP is pull-only. For real-time notifications, you need SuiteScript User Event Scripts or RESTlets. [src1]
- **WSDL backward compatibility**: You can use an older WSDL version against a newer NetSuite release, but NOT a newer WSDL against an older release. Pin your WSDL version carefully. [src1]
- **Async operations have different limits**: Async variants (asyncAddList, asyncUpdateList) have higher record limits (2x sync) but require polling for completion status. [src2]

## Integration Pattern Decision Tree

```
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
|   |       |-- Need > 100 records per update? --> use asyncUpdateList (200 max)
|   |       +-- Need > 200 records per add? --> use asyncAddList (400 max)
|   |
|   |-- 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
```

## Quick Reference

### Core SOAP Operations

| Operation | Method | Records/Call | Async Variant | Async Records/Call | Notes |
|---|---|---|---|---|---|
| `get` | Read single | 1 | N/A | N/A | By internal ID or external ID [src1] |
| `getList` | Read multiple | 1,000 | `asyncGetList` | 2,000 | Returns full records [src2] |
| `search` | Query | 1,000/page | `asyncSearch` | 2,000/page | Use SearchPreferences for pageSize [src2] |
| `searchMoreWithId` | Pagination | 1,000/page | N/A | N/A | Requires searchId from initial search [src8] |
| `add` | Create single | 1 | N/A | N/A | Returns internal ID on success [src1] |
| `addList` | Create multiple | 200 | `asyncAddList` | 400 | Partial success possible [src2] |
| `update` | Update single | 1 | N/A | N/A | Requires internal ID [src1] |
| `updateList` | Update multiple | 100 | `asyncUpdateList` | 200 | Partial success possible [src2] |
| `upsert` | Create or update | 1 | N/A | N/A | Uses external ID for matching [src1] |
| `upsertList` | Batch upsert | 100 | `asyncUpsertList` | 200 | External ID required on each record [src2] |
| `delete` | Delete single | 1 | N/A | N/A | By internal ID [src1] |
| `deleteList` | Delete multiple | 200 | `asyncDeleteList` | 400 | Partial success possible [src2] |
| `getDeleted` | Deleted records | N/A | N/A | N/A | Returns records deleted within a date range [src1] |
| `initialize` | Initialize record | 1 | N/A | N/A | Pre-populates a new record from a source [src1] |
| `attach`/`detach` | Link records | 1 | N/A | N/A | Creates/removes relationships between records [src1] |
| `getSelectValue` | Picklist values | N/A | N/A | N/A | Returns valid values for select fields [src1] |

## Step-by-Step Integration Guide

### 1. Set up TBA credentials in NetSuite

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. These four values form your TBA credential set. [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.

### 2. Construct the SOAP request with TBA header

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
<?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.

### 3. Execute a search with pagination

Use `search` for the first page, then `searchMoreWithId` for subsequent pages. Each page returns up to 1,000 records (sync). [src2, src8]

```xml
<!-- 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>

<!-- Response includes searchId and totalPages -->
<!-- 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 the response. Page through all pages until `pageIndex == totalPages`.

### 4. Implement error handling and retry logic

Handle the three governance fault types and implement exponential backoff for concurrency limits. [src2, src3]

```python
# Input:  SOAP client configured with TBA
# Output: Robust retry wrapper for NetSuite SOAP calls

import time
import 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:
            # Concurrency limit hit - back off
            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:
            # Too many records in request - this is a logic error, don't retry
            raise ValueError("Reduce batch size below operation limit")
        except ExceededRequestSizeFault:
            # Payload too large - this is a logic error, don't retry
            raise ValueError("Request exceeds 100MB - split into smaller requests")
    raise RuntimeError(f"Failed after {MAX_RETRIES} retries - concurrency exhausted")
```

**Verify**: Trigger a deliberate `ExceededRequestLimitFault` by sending concurrent requests exceeding your tier limit. Confirm retry logic fires.

## Code Examples

### Python: Search all customers modified since a date with pagination

```python
# 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
from zeep.transports import Transport
import hashlib, hmac, time, secrets, base64

WSDL_URL = "https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/wsdl/v2025_2_0/netsuite.wsdl"
ACCOUNT_ID = "YOUR_ACCOUNT_ID"
CONSUMER_KEY = "YOUR_CONSUMER_KEY"
CONSUMER_SECRET = "YOUR_CONSUMER_SECRET"
TOKEN_ID = "YOUR_TOKEN_ID"
TOKEN_SECRET = "YOUR_TOKEN_SECRET"

def generate_tba_header(client):
    """Generate OAuth 1.0 TBA token passport for SOAP header."""
    nonce = secrets.token_hex(16)
    timestamp = str(int(time.time()))

    # Signature base string: account + consumer_key + token + nonce + timestamp
    base_string = "&".join([ACCOUNT_ID, CONSUMER_KEY, TOKEN_ID, nonce, timestamp])
    signing_key = f"{CONSUMER_SECRET}&{TOKEN_SECRET}"
    signature = base64.b64encode(
        hmac.new(signing_key.encode(), base_string.encode(), hashlib.sha256).digest()
    ).decode()

    token_passport = client.get_type("ns0:TokenPassport")(
        account=ACCOUNT_ID,
        consumerKey=CONSUMER_KEY,
        token=TOKEN_ID,
        nonce=nonce,
        timestamp=timestamp,
        signature={"_value_1": signature, "algorithm": "HMAC-SHA256"}
    )
    return token_passport

def search_all_customers(client, since_date):
    """Search customers with pagination (1,000 per page max)."""
    # Set search preferences
    search_prefs = client.get_type("ns0:SearchPreferences")(
        pageSize=1000,
        bodyFieldsOnly=False
    )

    # Build search criteria
    search_record = {
        "basic": {
            "lastModifiedDate": {
                "operator": "after",
                "searchValue": since_date
            }
        }
    }

    # Initial search
    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

    # Paginate remaining pages
    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)  # Be kind to concurrency pool

    return all_records  # All customer records across all pages
```

### JavaScript/Node.js: Upsert records with batch splitting

```javascript
// Input:  Array of record objects to upsert
// Output: Results with success/failure per record

// npm install soap@1.1.0
const soap = require('soap');

const WSDL_URL = 'https://ACCOUNT_ID.suitetalk.api.netsuite.com/wsdl/v2025_2_0/netsuite.wsdl';
const BATCH_SIZE = 100; // upsertList max is 100 sync

async function upsertInBatches(client, records) {
  const results = [];

  // Split records into chunks of 100 (upsertList sync limit)
  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
        });
      });

      console.log(`Batch ${Math.floor(i/BATCH_SIZE)+1}: ${batch.length} records processed`);
    } catch (err) {
      if (err.root?.Envelope?.Body?.Fault?.faultstring?.includes('ExceededRequestLimitFault')) {
        // Concurrency limit - wait and retry this batch
        console.log('Concurrency limit hit, waiting 5s...');
        await new Promise(r => setTimeout(r, 5000));
        i -= BATCH_SIZE; // Retry this batch
      } else {
        throw err;
      }
    }

    // Respect concurrency - pause between batches
    await new Promise(r => setTimeout(r, 1000));
  }

  return results;
}
```

### cURL: Test TBA authentication

```bash
# Input:  TBA credentials (consumer key/secret, token ID/secret, account ID)
# Output: Server time response confirming auth works

# Note: SuiteTalk SOAP requires OAuth 1.0 signed requests.
# Generate signature components first, then send:
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 response: <getServerTimeResponse> with <getServerTimeResult>
# containing <serverTime>2026-03-01T12:00:00.000Z</serverTime>
```

## Data Mapping

### Key Record Type Internal Names

| 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 |
| Item (Non-inventory) | `NonInventoryResaleItem` | `noninventoryresaleitem` | Drop-ship or service items |
| 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 |

### Data Type Gotchas

- NetSuite datetime fields use Pacific Time (PT) by default unless the user's timezone preference is set differently. Always convert to UTC before comparing with external systems. [src8]
- Currency amounts in NetSuite are stored as decimals with the currency's precision. Multi-currency transactions require specifying the `currency` field — omitting it defaults to the subsidiary's base currency. [src1]
- Boolean fields in SOAP use `true`/`false` strings, not 1/0. Sending `1` for a boolean field may cause silent data corruption. [src1]
- Custom fields use the `customFieldList` element with `internalId` matching the custom field's script ID (e.g., `custbody_my_field`). Wrong internal IDs are silently ignored — no error, no update. [src1]
- External IDs (`externalId`) are case-sensitive and must be unique per record type. They are the primary mechanism for idempotent upserts. [src1]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| `ExceededRequestLimitFault` | Concurrency limit exceeded | Too many simultaneous requests for account tier | Exponential backoff + queue pending requests [src2, src3] |
| `ExceededRecordCountFault` | Too many records in request | Batch size exceeds operation limit (e.g., >100 for updateList) | Reduce batch size to within per-operation limits [src2] |
| `ExceededRequestSizeFault` | Request body >100 MB | Oversized XML payload | Split into multiple requests [src2] |
| `INVALID_LOGIN_ATTEMPT` | Authentication failure | Wrong credentials, TBA not enabled, or role lacks web services permission | Verify consumer/token keys, enable TBA on integration, check role permissions [src5] |
| `INSUFFICIENT_PERMISSION` | Role lacks access | Integration user's role does not have permission for the record type | Add missing permissions to the integration role [src1] |
| `SSS_USAGE_LIMIT_EXCEEDED` | Governance units exhausted | Script or operation consumed too many governance units | Optimize operation — reduce search complexity, use getList instead of search for known IDs [src6] |
| `INVALID_KEY_OR_REF` | Invalid internal/external ID | Record referenced by ID does not exist or is of wrong type | Validate IDs exist before referencing; check record type matches [src1] |

### Failure Points in Production

- **searchMoreWithId pagination drift**: If records matching the search criteria are created or modified between `search` and `searchMoreWithId` calls, records can shift between pages — causing duplicates or missed records. Fix: `Use a timestamp-based filter (lastModifiedDate) and track processed records externally`. [src8]
- **Silent custom field failures**: Setting a custom field value with an incorrect `internalId` does not raise an error — the field is simply ignored. Fix: `Validate custom field script IDs against getCustomizationId before deployment`. [src1]
- **Concurrency starvation from long-running searches**: A complex saved search that takes 30+ seconds holds a concurrency slot the entire time, reducing available capacity for all other integrations. Fix: `Break complex searches into narrower criteria; use async search for large result sets`. [src3, src6]
- **TBA token revocation without notification**: An admin can revoke TBA tokens at any time with no notification to the integration. Fix: `Monitor for INVALID_LOGIN_ATTEMPT errors; implement alerting on auth failures; store backup token pairs`. [src5]
- **WSDL version mismatch after NetSuite upgrade**: If your integration pins a WSDL version that is retired, requests will fail. Fix: `Monitor Oracle release notes for WSDL deprecation announcements; test against release preview accounts`. [src1, src4]
- **Timezone mismatches in date comparisons**: Searching by date without accounting for NetSuite's Pacific Time default causes off-by-one errors for records modified near midnight. Fix: `Always use full datetime (not date-only) in search criteria, explicitly in UTC`. [src8]

## Anti-Patterns

### Wrong: Fetching all records to find changes

```python
# BAD -- Pulls every customer record to detect changes
# Wastes concurrency slots and governance units
result = client.service.search(searchRecord={"basic": {}})
for record in result.searchResult.recordList.record:
    if record.lastModifiedDate > last_sync_time:
        process(record)
```

### Correct: Use lastModifiedDate filter in search criteria

```python
# GOOD -- Only fetches records modified since last sync
# Dramatically reduces result set size and API consumption
result = client.service.search(searchRecord={
    "basic": {
        "lastModifiedDate": {
            "operator": "after",
            "searchValue": last_sync_time.isoformat()
        }
    }
})
for record in result.searchResult.recordList.record:
    process(record)
```

### Wrong: Using search for known internal IDs

```python
# BAD -- Using search to retrieve records when you know the IDs
# Search costs more governance units and is slower
for internal_id in known_ids:
    result = client.service.search(searchRecord={
        "basic": {"internalId": {"operator": "is", "searchValue": internal_id}}
    })
```

### Correct: Use getList for known IDs (up to 1,000 per call)

```python
# GOOD -- getList is faster, cheaper, and supports up to 1,000 records
# One call instead of N calls
ref_list = [{"type": "customer", "internalId": id} for id in known_ids[:1000]]
result = client.service.getList(baseRef=ref_list)
```

### Wrong: Ignoring ExceededRequestLimitFault

```python
# BAD -- No retry logic; integration fails permanently on concurrency limit
try:
    result = client.service.addList(record=records)
except Exception as e:
    log.error(f"Failed: {e}")
    return None  # Gives up immediately
```

### Correct: Implement exponential backoff with jitter

```python
# 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 -- reduce concurrent load")
```

## Common Pitfalls

- **Sandbox concurrency is fixed at 5**: Production may have 50+ concurrent slots via SC+ licensing, but sandbox is always 5. Integration tests that pass in production timing may fail in sandbox due to queuing. Fix: `Design load tests with sandbox's 5-slot limit in mind; use async operations for parallel workloads`. [src6, src7]
- **searchMoreWithId results are not stable**: Creating/modifying records between pagination calls causes result set drift. Fix: `Track processed record IDs externally; use narrow date ranges to minimize result set changes during pagination`. [src8]
- **updateList at 100 vs addList at 200**: The asymmetric limits catch developers who assume all list operations share the same cap. Fix: `Always check the per-operation limits table; asyncUpdateList allows 200 if you need parity`. [src2]
- **WSDL version pinning vs auto-upgrade**: Not pinning your WSDL version means NetSuite auto-upgrades may change behavior. Pinning too old a version risks hitting end-of-support. Fix: `Pin to the current WSDL version (2025.2); test against release preview before each NetSuite upgrade`. [src1, src4]
- **Saved search via SOAP ignores SOAP SearchPreferences**: When executing a saved search via `search` with a `savedSearchId`, the saved search's own row limit and columns override `SearchPreferences`. Fix: `Set the saved search in NetSuite to return all results; control pagination via SOAP SearchPreferences pageSize`. [src8]
- **Custom record type naming**: Custom record types use `customrecord_{id}` as their SOAP type name, where `{id}` is the numeric internal ID, not the script ID. Using the script ID results in `INVALID_RECORD_TYPE`. Fix: `Look up the numeric ID via Setup > Customization > Record Types in the NetSuite UI`. [src1]

## Diagnostic Commands

```bash
# Check current concurrency usage
# Navigate in NetSuite UI: Setup > Integration > Integration Governance
# Programmatically: No direct API — monitor via HTTP 429/ExceededRequestLimitFault frequency

# Test TBA authentication (requires computing OAuth 1.0 signature)
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>'

# Verify record type accessibility (check role permissions)
# Use getList with a known internal ID for the target record type
# If INSUFFICIENT_PERMISSION is returned, the integration role lacks access

# Check WSDL availability and version
curl -s "https://ACCOUNT_ID.suitetalk.api.netsuite.com/wsdl/v2025_2_0/netsuite.wsdl" | head -5
# Expected: <?xml version="1.0" encoding="UTF-8"?> <definitions ...>

# Monitor async job status (for asyncAddList, asyncSearch, etc.)
# Use getAsyncResult with the jobId returned by the async operation
# Poll until status is "complete" or "failed"
```

## Version History & Compatibility

| WSDL Version | NetSuite Release | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| 2025.2 | 2025.2 | Current (final SOAP endpoint) | None | Last planned SOAP WSDL — all SOAP integrations must pin to this [src4] |
| 2025.1 | 2025.1 | Supported | Minor field additions | Supported until 2027.2, then available without support [src4] |
| 2024.2 | 2024.2 | Supported | None | Will lose support in stages through 2028.1 [src4] |
| 2024.1 | 2024.1 | Supported | None | Plan migration to 2025.2 [src4] |
| Pre-2024.1 | Various | Available (unsupported) | Varies | Progressively disabled; all removed by 2028.2 [src4] |

### SOAP Removal Timeline

| Release | What Happens |
|---|---|
| 2025.2 | Last planned SOAP endpoint published [src4] |
| 2026.1 | All new integrations should use REST + OAuth 2.0 (SOAP still available) [src4] |
| 2027.1 | No new SOAP integrations can be created; no new TBA integrations for any surface [src4] |
| 2027.2 | Only 2025.2 endpoint supported with bug fixes; older endpoints available without support [src4] |
| 2028.2 | All SOAP endpoints disabled — SOAP integrations stop working entirely [src4] |

### Deprecation Policy

Oracle provides multi-year deprecation notice for major API changes. The SOAP removal timeline gives approximately 3 years of advance notice (announced ~2025, full removal 2028.2). Each WSDL version is supported for at least 3 releases after publication. [src4]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Maintaining an existing SOAP integration that works | Building a new integration from scratch (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 for existing saved searches | Daily data volume exceeds 100,000 records | CSV Import for writes, SuiteQL for reads |
| Record types better supported in SOAP for your account version | Need real-time push notifications | SuiteScript User Event Scripts + RESTlets |
| Short-term project (< 2 years) that will decommission before 2028.2 | Long-term integration expected to run 3+ years | REST API (SOAP removed 2028.2) |

## Important Caveats

- Concurrency limits vary dramatically by service tier and SC+ licensing. A Standard tier account with no SC+ licenses has only 5 concurrent slots shared across all API surfaces — this can bottleneck even moderate integrations. [src3, src6]
- SOAP is on a hard deprecation path. The 2028.2 removal date is firm per Oracle's FAQ. Do not start new SOAP integrations — invest migration effort toward REST. [src4]
- Governance unit costs per operation are not fully documented publicly. The approximate values in this card are based on community analysis and may vary by record type complexity and customization. [src6]
- Sandbox/developer accounts are capped at 5 concurrent requests regardless of production tier. Performance testing in sandbox does not accurately represent production throughput. [src6, src7]
- Rate limits (requests per minute/day) are enforced but thresholds are not publicly documented. Oracle uses fair-use policies with 60-second and 24-hour rolling windows. Plan for thousands of calls per minute for large accounts. [src6]

## Related Units

- [NetSuite REST API Capabilities](/business/erp-integration/netsuite-rest-api-capabilities/2026) — the modern replacement for SuiteTalk SOAP
- [NetSuite SuiteQL Capabilities](/business/erp-integration/netsuite-suiteql-capabilities/2026) — SQL-like query language for analytics and reporting
