Summary and recommendation
NetSuite exposes user lifecycle management through its SuiteTalk REST Record API at `https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee`. Authentication supports OAuth 2.0 (client credentials or authorization code) for REST and Token-Based Authentication (TBA/OAuth 1.0a) for SOAP. OAuth 2.0 client credentials flow requires the Integration record to have 'Client Credentials (Machine to Machine)' explicitly enabled - it is off by default.
NetSuite does not expose a SCIM 2.0 endpoint. There is no native webhook mechanism for employee record changes; change detection requires either SuiteScript User Event scripts (beforeSubmit/afterSubmit) triggering HTTP callouts, or polling via SuiteQL.
For teams building against an identity graph, NetSuite's employee object carries subsidiary, department, location, supervisor, and role arrays - making it a viable node in a cross-system identity graph when polled incrementally via SuiteQL filtered on lastModifiedDate.
Governance is concurrency-based, not rate-based. Standard accounts allow 10 concurrent web service requests; SuiteCloud Plus License raises this to up to 100. Errors surface as HTTP 429 or `CONCURRENCY_LIMIT_EXCEEDED` SOAP faults - not via standard rate-limit headers - so retry logic must be implemented explicitly with exponential backoff.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (client credentials or authorization code) for REST; Token-Based Authentication (TBA/OAuth 1.0a) for SOAP SuiteTalk |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Enterprise |
Authentication
Auth method: OAuth 2.0 (client credentials or authorization code) for REST; Token-Based Authentication (TBA/OAuth 1.0a) for SOAP SuiteTalk
Setup steps
- In NetSuite, navigate to Setup > Company > Enable Features > SuiteCloud tab and enable 'REST Web Services' and 'OAuth 2.0'.
- Create an Integration record at Setup > Integration > Manage Integrations > New. Note the Client ID and Client Secret.
- For client credentials flow, assign the integration a role with appropriate permissions (e.g., Employee & Access role).
- Request an access token via POST to https://
.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token with grant_type=client_credentials. - Include the Bearer token in the Authorization header for all subsequent REST API calls.
- For TBA (SOAP), generate Consumer Key/Secret on the Integration record, then create Access Token records under Setup > Users/Roles > Access Tokens.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| rest_webservices | Grants access to SuiteTalk REST Web Services endpoints. | All REST API calls including employee/user CRUD |
| suite_analytics | Grants access to SuiteAnalytics Connect and workbook data. | Reporting and analytics queries; not required for user management |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | Internal NetSuite record ID for the Employee. | system-assigned | read-only | Used as path parameter in REST URLs. |
| firstName | string | Employee first name. | required | optional | |
| lastName | string | Employee last name. | required | optional | |
| string | Primary email address; used as login identifier. | required | optional | Must be unique across the account. | |
| isInactive | boolean | Marks the employee record as inactive (deprovisioned). | optional (default false) | optional | Setting true disables login access. |
| giveAccess | boolean | Controls whether the employee can log in to NetSuite. | optional | optional | Must be true for the user to have portal/UI access. |
| role | array of {id, name} | List of NetSuite roles assigned to the employee. | optional | optional | Role IDs are account-specific internal IDs. |
| subsidiary | object {id, name} | Primary subsidiary the employee belongs to. | required (OneWorld accounts) | optional | Required when OneWorld module is enabled. |
| department | object {id, name} | Department assignment. | optional | optional | |
| location | object {id, name} | Office/location assignment. | optional | optional | |
| supervisor | object {id, name} | Manager/supervisor record reference. | optional | optional | |
| title | string | Job title. | optional | optional | |
| phone | string | Primary phone number. | optional | optional | |
| mobilePhone | string | Mobile phone number. | optional | optional | |
| hireDate | date (YYYY-MM-DD) | Employee hire date. | optional | optional | |
| employeeType | enum string | Employment classification (e.g., FULLTIME, PARTTIME, CONTRACTOR). | optional | optional | |
| password | string (write-only) | Initial password for the employee login. | optional | optional | Write-only; never returned in GET responses. |
| sendEmail | boolean | Triggers a welcome/access email to the employee on create/update. | optional | optional |
Core endpoints
List Employees
- Method: GET
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee - Watch out for: Response items contain only summary fields and links; fetch individual records for full field data.
Request example
GET /services/rest/record/v1/employee?limit=20&offset=0
Authorization: Bearer <token>
Response example
{
"links": [...],
"count": 150,
"hasMore": true,
"items": [
{"id": "123", "links": [...], "email": "jdoe@example.com"}
]
}
Get Employee
- Method: GET
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee/{id} - Watch out for: Use the numeric internal ID, not the employee name or email, as the path parameter.
Request example
GET /services/rest/record/v1/employee/123
Authorization: Bearer <token>
Response example
{
"id": "123",
"firstName": "Jane",
"lastName": "Doe",
"email": "jdoe@example.com",
"isInactive": false,
"giveAccess": true,
"subsidiary": {"id": "1", "refName": "Parent Company"}
}
Create Employee
- Method: POST
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee - Watch out for: Successful creation returns HTTP 204 with a Location header, not a body. Parse Location to get the new record ID.
Request example
POST /services/rest/record/v1/employee
Content-Type: application/json
{
"firstName": "Jane",
"lastName": "Doe",
"email": "jdoe@example.com",
"subsidiary": {"id": "1"},
"giveAccess": true
}
Response example
HTTP 204 No Content
Location: /services/rest/record/v1/employee/456
Update Employee (partial)
- Method: PATCH
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee/{id} - Watch out for: PATCH only updates supplied fields. Use PUT to replace the full record (requires all mandatory fields).
Request example
PATCH /services/rest/record/v1/employee/456
Content-Type: application/json
{
"title": "Senior Engineer",
"department": {"id": "10"}
}
Response example
HTTP 204 No Content
Deactivate Employee
- Method: PATCH
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee/{id} - Watch out for: Setting isInactive=true alone may not revoke login; also set giveAccess=false to ensure access is removed.
Request example
PATCH /services/rest/record/v1/employee/456
Content-Type: application/json
{
"isInactive": true,
"giveAccess": false
}
Response example
HTTP 204 No Content
Delete Employee
- Method: DELETE
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee/{id} - Watch out for: NetSuite rarely allows hard deletion of Employee records due to transaction history linkage. Prefer isInactive=true instead.
Request example
DELETE /services/rest/record/v1/employee/456
Authorization: Bearer <token>
Response example
HTTP 204 No Content
Search Employees (SuiteQL)
- Method: POST
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql - Watch out for: SuiteQL field names are lowercase and differ from REST record field names (e.g., 'firstname' vs 'firstName'). Boolean fields use 'T'/'F' strings.
Request example
POST /services/rest/query/v1/suiteql
Content-Type: application/json
{
"q": "SELECT id, email, firstname, lastname FROM employee WHERE isinactive = 'F' LIMIT 100 OFFSET 0"
}
Response example
{
"items": [
{"id": 123, "email": "jdoe@example.com", "firstname": "Jane", "lastname": "Doe"}
],
"hasMore": false,
"totalResults": 1
}
Assign Role to Employee
- Method: POST
- URL:
https://<accountId>.suitetalk.api.netsuite.com/services/rest/record/v1/employee/{id}/roles - Watch out for: Role IDs are account-specific internal IDs; query the role record type to resolve names to IDs before assignment.
Request example
POST /services/rest/record/v1/employee/456/roles
Content-Type: application/json
{
"role": {"id": "15"}
}
Response example
HTTP 204 No Content
Rate limits, pagination, and events
- Rate limits: NetSuite enforces concurrency-based governance rather than per-minute rate limits. Each account has a concurrency limit on simultaneous web service requests. Exceeding the limit returns a SOAP/REST governance error. SuiteCloud Plus License increases the concurrency allowance.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: NetSuite does not return standard rate-limit HTTP headers. Governance errors return HTTP 429 or a SOAP fault with code CONCURRENCY_LIMIT_EXCEEDED. Implement exponential backoff. REST also enforces a per-request timeout of 15 minutes.
- Pagination method: offset
- Default page size: 20
- Max page size: 1000
- Pagination pointer: offset / limit
| Plan | Limit | Concurrent |
|---|---|---|
| Standard NetSuite | 10 concurrent web service requests | 10 |
| SuiteCloud Plus License | Up to 100 concurrent web service requests (configurable) | 100 |
- Webhooks available: No
- Webhook notes: NetSuite does not offer native outbound webhooks for record change events. There is no built-in webhook subscription mechanism for user/employee record changes.
- Alternative event strategy: Use SuiteScript 2.x User Event scripts (beforeSubmit/afterSubmit) deployed on the Employee record to trigger HTTP callouts to external systems on create/update/delete. Alternatively, poll the SuiteQL endpoint or use the SuiteTalk SOAP ChangeLog/getSelectValue approach for change detection.
SCIM API status
- SCIM available: No
- SCIM version: 2.0
- Plan required: Enterprise
- Endpoint: Not documented
Limitations:
- NetSuite does not natively expose a SCIM 2.0 endpoint.
- Microsoft Entra ID SCIM provisioning connector is not officially supported by Oracle NetSuite.
- Okta integration uses a proprietary NetSuite connector (not SCIM) leveraging SuiteTalk SOAP or REST APIs.
- JIT (Just-In-Time) SAML provisioning is available as an alternative for SSO-based user creation.
- Third-party middleware (e.g., Workato, Boomi) can bridge SCIM to NetSuite REST/SOAP APIs.
Common scenarios
Provisioning a new employee requires a POST to /employee with at minimum firstName, lastName, email, subsidiary (mandatory on OneWorld accounts), giveAccess=true, and at least one role ID.
The successful response is HTTP 204 with no body; the new record's internal ID must be parsed from the Location response header. Role IDs are account-specific internal integers with no global name-to-ID mapping - resolve them via the role record type before provisioning.
Deprovisioning requires a PATCH to /employee/{id} setting both isInactive=true and giveAccess=false. Setting only isInactive may not fully revoke login access. Hard DELETE of Employee records is blocked by NetSuite when transaction history exists; isInactive=true is the correct deprovisioning pattern. Any TBA tokens tied to the inactivated user are immediately invalidated - downstream integrations using those tokens will fail silently without explicit token rotation handling.
For bulk sync against an identity graph, POST to /query/v1/suiteql with a SELECT against the employee table filtered by isinactive = 'F'. SuiteQL field names are lowercase (firstname, lastname) and do not match REST record API camelCase field names (firstName, lastName) - mapping must be handled explicitly. Maximum LIMIT per SuiteQL query is 1000 rows; paginate via OFFSET and prefer filtering on lastModifiedDate for incremental syncs to avoid full-table scans on large accounts.
Provision a new employee with login access
- POST to /employee with firstName, lastName, email, subsidiary, giveAccess=true, and desired role IDs.
- Parse the Location header from the 204 response to extract the new employee internal ID.
- Optionally PATCH the record with additional fields (department, title, supervisor).
- If sendEmail=true was included, the user receives a welcome email with login instructions.
Watch out for: If the subsidiary field is omitted on a OneWorld account, the POST returns a 400 validation error. Always resolve subsidiary ID before provisioning.
Deprovision (offboard) an employee
- PATCH /employee/{id} with isInactive=true and giveAccess=false to revoke login and mark inactive.
- Optionally remove role assignments by DELETE to /employee/{id}/roles/{roleId} for each assigned role.
- Verify via GET /employee/{id} that isInactive=true and giveAccess=false are reflected.
Watch out for: Hard DELETE of Employee records is typically blocked by NetSuite when transaction history exists. isInactive=true is the standard deprovisioning pattern.
Bulk-list active employees for sync
- POST to /query/v1/suiteql with query: SELECT id, email, firstname, lastname, isinactive FROM employee WHERE isinactive = 'F' LIMIT 1000 OFFSET 0.
- Check hasMore in the response; if true, increment OFFSET by 1000 and repeat.
- Map SuiteQL lowercase field names to your system's schema (note: firstname ≠ firstName in REST record API).
Watch out for: SuiteQL has a maximum LIMIT of 1000 per query. For accounts with >1000 employees, pagination via OFFSET is required. Large offsets can be slow; consider filtering by lastModifiedDate for incremental syncs.
Why building this yourself is a trap
The most consequential API caveat is the dual-field deprovisioning requirement: isInactive=true alone does not guarantee login revocation. Systems that set only one flag and assume access is removed will leave accounts partially active. This is not documented prominently and is a common integration defect.
The REST and SuiteQL field name mismatch (firstName vs firstname, boolean true/false vs string 'T'/'F') creates silent data mapping errors in pipelines that treat the two interfaces as interchangeable. Validate field names against the specific API surface being used - they are not equivalent.
Concurrency governance errors do not surface via standard HTTP headers, so off-the-shelf retry libraries that inspect Retry-After or X-RateLimit-* headers will not function correctly. Implement retry logic that catches HTTP 429 and SOAP fault code CONCURRENCY_LIMIT_EXCEEDED explicitly.
For high-volume provisioning pipelines, the SuiteCloud Plus License is a hard dependency - without it, 10 concurrent requests is the ceiling, and bulk operations will hit governance limits quickly.
Automate Netsuite workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.