Skip to main content
Stateful NetSuite emulation of SuiteTalk REST web services: the record service (/services/rest/record/v1) for customers, contacts, vendors, employees, inventory, non-inventory, and service items, estimates, sales orders, item fulfillments, invoices, customer payments, credit memos, and the lists they point at, with create, read, update, upsert by external ID, and delete, sublists and subrecords, subresource paths, fields, expandSubResources, and simpleEnumFormat, collection paging and q filtering, transforms (sales order to fulfillment and invoice, invoice to payment and credit memo, and more), record actions, attaching contacts, create and edit forms, select options, asynchronous requests and batches, custom fields, lists, and record types from seed config, the metadata catalog as JSON Schema and OpenAPI 3.0, serverTime, and governanceLimits. Requests sign in with token-based authentication (OAuth 1.0a with HMAC-SHA256) or OAuth 2.0 bearer tokens from the authorization code grant (with PKCE) or the client credentials grant (a certificate-signed JWT), and every failed sign-in leaves its reason in a login audit trail the inspector shows, as NetSuite keeps one. The tests drive it with netsuite-api-client (1.1.1).

At a glance

Start

NetSuite starts only when named in config or with --service. When it is the only enabled service, it listens on http://localhost:4000. Without seed config, any bearer token the CLI accepts acts as the administrator employee, and the account starts with the standard lists described under Records. Point netsuite-api-client at the emulator with base_url and the seeded integration and token:

URL mapping

Auth

  • Token-based authentication (TBA): Authorization: OAuth realm="<account ID>", oauth_consumer_key=..., oauth_token=..., oauth_signature_method="HMAC-SHA256", oauth_timestamp=..., oauth_nonce=..., oauth_version="1.0", oauth_signature=.... The signature is checked as RFC 5849 builds it: the method, the URL without its query, and the query and OAuth parameters (without realm and the signature), keyed with the consumer secret and token secret. The timestamp must be within five minutes of the server clock, and a nonce may not be used twice with the same token and timestamp. HMAC-SHA1 is refused, as NetSuite has refused it since 2023.1. The realm is the account ID, compared without case and with _ and - alike (1234567_SB1 or 1234567-sb1).
  • OAuth 2.0: Authorization: Bearer <access token> with a token from the flows below.
  • Seeded bearer tokens: Authorization: Bearer <token>. Tokens seeded under access_tokens act as the employee and role they name, or the administrator. Any other token the CLI accepts acts as the administrator employee ([email protected], role 3 Administrator), so a quick start needs no setup.
Every refusal is HTTP 401 INVALID_LOGIN with the same detail (Invalid login attempt. For more details, see the Login Audit Trail in the NetSuite UI at Setup > Users/Roles > User Management > View Login Audit Trail.), and the reason goes to the login audit trail: MissingTokenPassportRequiredFields, UnknownAlgorithm, consumer_key_unknown, consumer_key_refused (blocked integration, or TBA turned off), token_rejected (unknown or revoked token, a token of another integration, a realm naming another account, or an unknown, revoked, or expired bearer token), InvalidTimestamp, InvalidSignature, NonceUsed, and permission_denied (inactive employee or role). Successful sign-ins are logged too; the trail keeps the latest 500. TBA is on its way out at NetSuite (no new TBA integrations from 2027.1); OAuth 2.0 is the way forward, and the emulator supports both.

Errors

Errors are NetSuite’s envelope (application/vnd.oracle.resource+json; type=error):

OAuth 2.0

An integration uses OAuth 2.0 when it has an oauth2 map in seed config. Its client ID and client secret are its consumer key and consumer secret, as on the integration record.
  • scopes: the default; suite_analytics too, or mcp on its own
  • certificates: client credentials (M2M) mappings
  • employee: the administrator when left out
  • role: Administrator when left out
  • consent_policy (not set above, for example always_ask): ask_first_time, never_ask
  • public_client (not set above, for example true): no secret, PKCE required, rotating refresh tokens
  • refresh_token_validity_hours (not set above, for example 48): public clients, 1 to 720
  • max_token_rotation_hours (not set above, for example 168): public clients, 1 to 720
The authorization code grant is on when redirect_uris are given (authorization_code: false turns it off). Redirect URIs must be https or a custom scheme (myapp://callback), as on NetSuite; Baseline also takes http on localhost and 127.0.0.1.

Authorization code grant

  • GET /app/login/oauth2/authorize.nl — Authorize: the consent page
  • POST /services/rest/auth/oauth2/v1/token — Get token: authorization code, refresh token, or client credentials grant
  • POST /services/rest/auth/oauth2/v1/revoke — Revoke token
  1. Send the user to the authorize page with response_type=code, client_id, redirect_uri (one of the integration’s), scope (space separated), state (22 to 1024 printable characters), and optionally code_challenge with code_challenge_method=S256 and prompt (login, consent, or both). PKCE is required for public clients and for the mcp scope, and plain is refused. The page lists every active employee with each active role; choosing one allows access (Deny refuses it). The browser returns to the redirect URI with state, role, entity, company, and code, or with error.
  2. Call the token endpoint (form encoded) with grant_type=authorization_code, code, redirect_uri, and code_verifier, authenticated with Authorization: Basic base64(client_id:client_secret). Public clients send the client ID alone, in the body or in Basic with an empty secret. The answer is { access_token, refresh_token, expires_in: 3600, token_type: "Bearer" }. Codes work once and for ten minutes (Baseline’s choice; Oracle says only that they are short lived).
  3. Refresh with grant_type=refresh_token&refresh_token=.... Confidential clients get a new access token and keep their refresh token, which lasts seven days. Public clients get a new refresh token each time and the old one stops working; each lasts refresh_token_validity_hours, and the chain ends max_token_rotation_hours after it started.
  4. The revoke endpoint with token=<refresh token> revokes it and every access token issued with or from it (an access token alone can be revoked too). It answers 200.
The consent screen follows the integration’s consent_policy and prompt as Oracle’s table describes: Always Ask always shows it, Never Ask shows only the sign in (except for mcp), and prompt=consent always shows it. The page signs in and asks at once, so Ask First Time stops asking once someone has allowed the same scopes for the integration (Baseline’s choice). There is never a NetSuite session, so prompt=none returns error=login_required (Baseline’s wording). Step one problems, as Oracle lists them: an unknown client (unauthorized_client) or a redirect URI the integration does not list (invalid_request) is shown on the page; missing parameters or a bad state or PKCE value are invalid_request, a response type other than code is unsupported_response_type, a scope the integration does not enable is invalid_scope, a blocked integration or one without the code grant is unauthorized_client, and Deny is access_denied, all sent to the redirect URI. Token endpoint errors are { "error": "..." }: invalid_request (missing or malformed parameters, a body that is not form encoded, or Basic and a body client_id together), invalid_client (401 with WWW-Authenticate: Basic realm="<account ID>"), invalid_grant (a code or refresh token that is unknown, used, expired, revoked, issued to another client, or for another redirect URI, or a code_verifier that does not match), unauthorized_client, unsupported_grant_type, and invalid_scope.

Client credentials grant

Map a certificate to the integration, an employee, and a role (under certificates in seed config, or with the rotation endpoint below), then POST /services/rest/auth/oauth2/v1/token with grant_type=client_credentials, client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer, and client_assertion, a JWT signed with the certificate’s private key:
  • Header: alg PS256, PS384, PS512, ES256, ES384, or ES512; kid the certificate ID; typ JWT.
  • Payload: iss the client ID, scope (a list, or comma separated text), aud the token endpoint (the emulator’s, or https://<account>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token), iat, and exp at most an hour after iat.
The answer is { access_token, expires_in: 3600, token_type: "Bearer" } with no refresh token, acting as the mapped employee and role. A certificate ID is the base64url SHA-256 of the certificate’s DER (Baseline’s choice; Oracle generates its own). Assertion problems are invalid_grant with the audit trail reasons UnsupportedAlgo, MissingKeyId, MissingKey (unknown, revoked, or expired certificate), SignatureValidation, InvalidIssuer, TokenExpired, TokenNotAcceptable (issued in the future, or for another audience), and TokenValidityTooBroad; scopes are invalid_scope (Baseline’s choice of error values, since Oracle does not list them for this flow).

Certificate rotation

  • GET /services/rest/auth/oauth2/v1/clients/{clientId}/certificates — List certificates: the integration’s
With an OAuth 2.0 access token (or another bearer token), the list call shows the integration’s certificates, POST to the same path with { "fileContent": "<PEM>", "role": 3, "entity": 12 } sets up a new one (at most five active per integration), and POST .../certificates/{certificateId}/revoke revokes one (Successfully revoked). TBA requests are refused. Errors here use the REST error envelope (Baseline’s choice).

Tokens

Access and refresh tokens are JWTs shaped as NetSuite’s: RS256 with a company key (kid c.<account>.<created>), sub the role and entity (1000;12), aud the application ID with the account and the client ID, scope a list, iss https://system.netsuite.com, oit the start of the token chain, iat, exp, and jti. Oracle’s text says PS256; its example tokens are RS256, which Baseline follows. Tokens are also kept in the emulator’s state, so revoking one takes effect at once, and keys, tokens, and certificates survive a restart.
  • GET /services/rest/auth/oauth2/v1/keys — Get signing keys: the public key as a JWKS
REST calls need the rest_webservices scope. A refused OAuth 2.0 token answers 401 INVALID_LOGIN with WWW-Authenticate: Bearer realm="<account ID>", error="invalid_token", error_description="Invalid login attempt.", and the login audit trail gets Oracle’s reason: TokenRejected (a refresh token), TokenBlocked (revoked), AccessTokenExpired, InvalidSignature (a JWT the account did not issue), InvalidIntegration, IntegrationBlocked, ScopeMismatched, or EntityOrRoleDisabled. The authorize and token endpoints log their outcomes too (ClientAuthenticationFailed, InvalidGrant, ClientIdMismatch, RefreshTokenExpired, InvalidRefreshToken, AuthorizationExplicitlyDenied, …).

Records

  • POST /services/rest/record/v1/{type} — Create record: answers 204 with Location
  • GET /{type}/{id} — Get record: the same path takes PATCH to update it (204 with Location) and DELETE to remove it (204)
  • PUT /customer/eid:CID002 — Upsert record by external ID: creates the record or updates the one with that external ID
PUT works only with an external ID, as an upsert. Anywhere an internal ID goes, eid:<external ID> works too; external IDs hold letters, digits, _, and -. Type names and field names match without case. Records come back as NetSuite writes them (application/vnd.oracle.resource+json; type=singular): links first, then the fields in alphabetical order, with empty fields left out. A select field is a reference { links, id, refName }, an enumeration is { id, refName } (or the ID alone with simpleEnumFormat=true), a multiselect is { links, items, totalResults }, and datetimes are UTC at the minute (2026-03-01T14:05:00Z). In request bodies, a reference takes { "id": "1" } or { "externalId": "ACME" }, and an enumeration takes its ID or { "refName": "..." }.
  • fields=companyName,email returns only those body fields. expandSubResources=true expands sublists (each line with its own link) and subrecords; without it they are links. The two cannot be combined (INVALID_PARAMETER).
  • Subresources: GET /{type}/{id}/{select field} reads the referenced record, /{sublist} and /{sublist}/{line} read lines, /{subrecord} and /{sublist}/{line}/{subrecord} read subrecords.
  • Sublists in a body merge by key: a line whose key matches an existing line updates it (the address book by id, transaction items by line, or by orderLine on lines made from a sales order, item pricing by currency, price level, and quantity, payment and credit memo apply lines by doc), and other lines are added. A new quantity or rate on an existing line prices it again. ?replace=addressBook,item replaces the named sublists, and a null sublist empties it.
  • A null body field in a PATCH clears the field. With replaceSelectedFields=true, only the null fields named in replace are cleared.
  • Read-only fields in a body are ignored.
Saves are checked as NetSuite checks them: missing mandatory fields are one USER_ERROR naming each by its label (Error while accessing a resource. Please enter value(s) for: First Name, Last Name.), a value of the wrong shape or a reference to a missing record is INVALID_CONTENT with o:errorPath (item.items[0].item.id), and too long a text is a USER_ERROR. Entity IDs, item names, and external IDs are unique; deleting a record other records point at is refused (This record cannot be deleted because it has dependent records.).

Record types

Transactions work out what NetSuite works out: each line’s rate from the item’s base price in the order’s currency (a rate or amount in the body sets a custom price, price level -1), the amount as quantity times rate, the subtotal and total, the document number, and the customer’s default addresses. Invoices get a due date from their terms and track the amount paid and remaining. Every save or delete of a transaction brings the linked ones up to date:
  • A sales order is Pending Approval until orderStatus approves it. After that its status follows its fulfillments and invoices: Pending Fulfillment (B) until something ships, Partially Fulfilled (D), Pending Billing/Partially Fulfilled (E) when shipped lines wait to be billed, Pending Billing (F) when every item that ships has shipped, and Billed (G). Service lines need no fulfillment, so an approved order of services alone is Pending Billing. Each line tracks quantityFulfilled and quantityBilled.
  • An invoice is Open (A) until payments and credit memos applied to it cover the total, then Paid In Full (B), with amountPaid and amountRemaining.
  • A credit memo is Open (A) until all of it is applied, then Fully Applied (B), with applied and unapplied. A payment keeps applied and unapplied too.
  • An estimate turns Processed (B) when a sales order or invoice is made from it.
A customer’s balance adds up its open invoices less money received but not yet applied, overdueBalance counts the invoices past due, and unbilledOrders is what its approved orders have left to bill. Payments (customerPayment, with customer and payment) and credit memos (creditMemo, with entity and item lines) pay invoices through their apply sublist: { "doc": { "id": "12" }, "apply": true, "amount": 50 }. The amount defaults to what the invoice still owes; more than that is a USER_ERROR at apply.items[0].amount, an invoice of another customer is INVALID_CONTENT, and a payment that applies more than payment is a USER_ERROR at payment. With autoApply: true the money goes to the customer’s open invoices oldest first. Only applied lines are kept, and an applied payment or memo can be deleted to free the invoice again. An item fulfillment (itemFulfillment) can only be made by transforming a sales order, and cannot ship more than is left on a line. Every account starts with these records (IDs are Baseline’s): currencies 1 USD, 2 GBP, 3 CAD, 4 EUR; subsidiary 1 Parent Company; terms 1 Net 15, 2 Net 30, 3 Net 60, 4 Due on receipt; customer statuses 6 to 13 (13 is CUSTOMER-Closed Won, the default for new customers); accounts 1 Checking, 2 Accounts Receivable, 3 Inventory Asset, 4 Sales, 5 Cost of Goods Sold, 6 Accounts Payable; price level 1 Base Price; the administrator employee; and role 3 Administrator. Entities share one internal ID sequence, as do items and transactions; lists number on their own. Every account also has contact role -10 Primary Contact. Baseline choices where NetSuite’s documentation is silent: an unknown record type is 404 NONEXISTENT_ID (Record type 'x' does not exist.), an unknown body field is INVALID_CONTENT naming it, document numbers are SO1, INV1, and so on, an entity ID defaults to the company or person name, and PUT without eid: is 405.

Collections

  • GET /services/rest/record/v1/{type} — List records: in ascending internal ID order
The list is { links, count, hasMore, items: [{ links, id }], offset, totalResults }. limit is 1 to 1000 (the default), offset must be a multiple of limit, and the links are prev and first, next and last when there are more, then self, keeping q. q filters with N/query operators by field type: EMPTY and EMPTY_NOT for any field; IS and IS_NOT for checkboxes; ANY_OF, BETWEEN, EQUAL, GREATER, GREATER_OR_EQUAL, LESS, LESS_OR_EQUAL, and WITHIN (each with _NOT) for numbers, select fields (by internal ID), and id; CONTAIN, IS, START_WITH, and END_WITH (each with _NOT) for text and enumerations; and AFTER, BEFORE, ON, ON_OR_AFTER, and ON_OR_BEFORE (each with _NOT) for dates, written "1/1/2019" or 2019-01-01. AND binds tighter than OR and parentheses group; values with spaces are quoted, and BETWEEN, WITHIN, and ANY_OF take a list (id ANY_OF [1, 2, 3]). Text compares without case, and an operator ending in _NOT also matches empty fields. A query that does not parse is INVALID_PARAMETER with o:errorQueryParam: q.

Transforms

  • POST /services/rest/record/v1/{type}/{id}/!transform/{target} — Transform record: makes a new record from an existing one, as Transform Record does in the UI, and answers 204 with the new record’s Location
The body sets or overrides fields on the new record; lines made from an order carry orderLine (the order’s line), so a body line { "orderLine": 1, "quantity": 2 } changes that line and quantity: 0 drops it. A sales order turns into an item fulfillment of what is left to ship and an invoice of what is ready to bill: the shipped but unbilled quantity of items that ship, and the unbilled quantity of services. Once nothing is left, the transform is a USER_ERROR (Error while accessing a resource. You can not initialize invoice: invalid reference 7.), as is transforming an order still pending approval. A transform from an invoice applies the new payment or credit memo to that invoice. A transform from a customer starts a transaction for that customer, which takes the customer’s defaults as a direct create does; customer to vendor (and back) copies the name, contact details, subsidiary, currency, terms, and address book. The other pairs NetSuite lists (such as salesOrder to cashSale, employee to expenseReport, or invoice to returnAuthorization) end in record types Baseline does not model, so they are 404 NONEXISTENT_ID (Record type 'cashSale' does not exist.), as for any unknown type. A pair NetSuite does not support is 400 INVALID_PARAMETER (Invalid transformation. A customer payment record cannot be transformed into a invoice record.). Send Accept: application/vnd.oracle.resource+json; type=create-form to get the record the transform would make without saving it.

Record actions

  • POST /services/rest/record/v1/{type}/{id}/@{action} — Run record action: answers 200 { "links": [...], "result": true }
Of the types Baseline models, only invoices have actions: markforgrouping (sets forInvoiceGrouping; refused with This invoice is already marked., The invoice is partially or fully paid., or There is no sales order associated with this invoice.), unmarkforgrouping (This is already a standalone invoice. when not marked), and removeinvoicefromgroup (always This is already a standalone invoice., since Baseline does not model invoice groups). Refusals are 400 USER_ERROR with Oracle’s messages; an unknown action is 400 INVALID_PARAMETER.

Attach and detach

  • POST /services/rest/record/v1/{type}/{id}/!attach/contact/{contactId} — Attach contact: to a customer, vendor, or transaction, with an optional role ({ "role": { "id": "-10" } } for Primary Contact); answers 204
!detach/contact/{contactId} removes it. On a customer the attachment shows in the contactRoles sublist. Attaching a file is 404 (Baseline has no file cabinet); any other record type, or a target that takes no contacts, is 400 INVALID_PARAMETER, and an unknown role is a USER_ERROR at role. Deleting a contact detaches it everywhere.

Forms and select options

  • POST /{type} — Create form: with Accept: application/vnd.oracle.resource+json; type=create-form it answers 200 with the record a create would make from the body (defaults, prices, totals, and addresses filled in), without saving it or checking mandatory fields.
  • PATCH /{type}/{id} — Edit form: with type=edit-form it does the same for an update.
  • POST /{type} or PATCH /{type}/{id} — Select options: with type=select-options and fields=entityStatus,currency it lists the values each select, multiselect, or enumeration field accepts: { links, entityStatus: { _selectOptions: { links, items: [{ id, refName }], count, offset, hasMore, totalResults } } }. Sublist fields are named item.item. q filters by option name, one condition per field separated by commas (entityStatus START_WITH PROSPECT, currency IS "Euro", with IS, CONTAIN, or START_WITH), and limit (default 100) and offset page every field. Inactive records are left out, and numeric IDs are numbers. A field that is not a select is INVALID_PARAMETER with o:errorQueryParam: fields.

Custom fields and records

Seed config defines custom lists, custom record types, and custom fields on standard records. They show in REST under their script IDs in lower case, as NetSuite shows them, and work everywhere standard fields and types do: bodies, fields, q, sublists, select options, SuiteQL, the metadata catalog, and the inspector.
  • The prefix says where a field goes: custentity on customers, contacts, vendors, and employees; custitem on items; custbody on transaction bodies; custcol on the item lines of estimates, sales orders, invoices, and credit memos; custrecord on a custom record type. applies_to narrows it to some record types.
  • Types: text, textarea, long_text, email, phone, url, integer, decimal, currency, percent, checkbox (a JSON boolean, false by default), date (YYYY-MM-DD), datetime, select, and multiselect. A select’s source is a record type, a customlist_ ID, or a customrecord_ ID. mandatory fields are named by label when missing, and default fills new records.
  • A custom list (customlist_region) is a record type with name (the value), abbreviation, and isInactive. A custom record type (customrecord_project) has name, isInactive, owner (the administrator by default), created, lastModified, and its custrecord_ fields. With autonumber, new records are numbered into name and the entered name goes in altName. include_name: false drops the Name field.
  • In SuiteQL, custom fields are columns (custentity_tier), and customlist_region and customrecord_project are tables.
Baseline choices: Oracle does not publish the REST JSON of custom lists and custom records, so their field names (name, altName, abbreviation, owner) are Baseline’s, taken from the SuiteScript record fields. Custom lists and record types each number their own internal IDs from 1.

Asynchronous requests

Any REST request sent with Prefer: respond-async answers 202 with no body, Preference-Applied: respond-async, and Location: .../services/rest/async/v1/job/{id}. The job resources are:
  • GET /services/rest/async/v1/job/{id} — Get job: { links, completed, id, progress, task: { links } }
  • GET /job/{id}/task — List job tasks: as a collection
  • GET /job/{id}/task/{taskId} — Get job task: { links, completed, id, progress, startTime, endTime }
  • GET /job/{id}/task/{taskId}/result — Get task result: the request’s own response, its status, body, Content-Type, and Location
  • GET /job/{id}/task/{taskId}/request — Get task request: as submitted, with method, url, body, parameters, service (RECORD or QUERY), and the prefer... flags
Baseline runs a job as soon as it is submitted, so it is already complete on the first poll. A job is visible only to the user who submitted it. X-NetSuite-idempotency-key refuses a second job with the same key: 400 IDEMPOTENCY_ERROR with the first job in Location. Batches run as asynchronous jobs with one task per record, up to 100:
  • PUT /record/v1/{type} — Batch write records: POST, PATCH, or PUT with Content-Type: application/vnd.oracle.resource+json; type=collection and { "items": [...] }. PATCH items name their record with id or externalId; PUT items upsert by externalId.
  • GET /record/v1/{type}?expandRecords=true&ids=1,eid:B2 — Batch read records
  • DELETE /record/v1/{type}?ids=... — Batch delete records
Baseline choices: a job’s progress is succeeded, or failed when any task answered an error, and an item without an ID becomes a failed task.

SuiteQL

  • POST /services/rest/query/v1/suiteql — Run SuiteQL query
The Prefer: transient header is required, as Oracle documents it; without it the answer is 400 with o:errorHeader: Prefer (Baseline’s wording and INVALID_HEADER code). The body is { "q": "...", "params": ["...", ...] }, where params fills anonymous ? placeholders in order. limit (1 to 1000, the default) and offset (a multiple of limit) in the URL page the result, with the same links as collections, and at most 100,000 rows are returned. netsuite-api-client’s query() and queryAll() work unchanged.
As on the service, every value is text, keys are lowercase (aliases too), NULL columns are left out of the item, booleans are T and F, and dates print as M/D/YYYY (datetimes too; use TO_CHAR for the time). An expression without an alias is named expr1, expr2, and so on. Supported: SELECT [TOP n] [DISTINCT], * and alias.*, AS aliases, FROM with table aliases, [INNER] JOIN, LEFT [OUTER] JOIN, and RIGHT [OUTER] JOIN with ON, WHERE (=, <>, !=, <, >, <=, >=, LIKE with % and _, IN, BETWEEN, IS [NOT] NULL, AND, OR, NOT), GROUP BY, HAVING, ORDER BY with ASC, DESC, NULLS FIRST, NULLS LAST, select aliases, or positions, FETCH FIRST n ROWS ONLY, ROWNUM, SYSDATE, CURRENT_DATE, CASE, ||, arithmetic (dates plus or minus days), '...' strings with '' for a quote, "..." identifiers, and -- comments. Functions: COUNT, SUM, AVG, MIN, MAX (with DISTINCT), BUILTIN.DF (a reference’s or list value’s display name; a transaction status shows as Sales Order : Pending Approval), BUILTIN.CF, COALESCE, NVL, NVL2, NULLIF, DECODE, CONCAT, UPPER, LOWER, INITCAP, LENGTH, SUBSTR, INSTR, REPLACE, TRIM, LTRIM, RTRIM, LPAD, RPAD, LEFT, RIGHT, ABS, CEIL, FLOOR, ROUND, TRUNC (numbers and dates), MOD, POWER, SQRT, SIGN, GREATEST, LEAST, TO_CHAR, TO_DATE, TO_NUMBER, ADD_MONTHS, LAST_DAY, and MONTHS_BETWEEN. Oracle’s rules apply: NULL is unknown in comparisons (so NOT IN with a NULL in the list matches nothing), '' is NULL, text compares case sensitively, ascending order puts NULLs last and descending order puts them first, columns outside aggregates must be in GROUP BY, and a text value compared with a date is read as M/D/YYYY. Table and field names match without case. Baseline choices where Oracle’s documentation is silent: datetimes print as dates, text compares case sensitively (as Oracle does), and multiselect columns hold comma separated internal IDs. Tables, with columns named after the REST fields in lower case (companyname, entityid, isinactive, trandate, …), references as internal IDs, and id and externalid on each: Item lines of sales transactions post as credits, so their quantity, netamount, and foreignamount are negative, as on the service; write -tl.quantity or ABS(tl.quantity) for the ordered amount. A credit memo is the other way round (main line negative, item lines positive), and item fulfillment lines have negative quantities and no amounts. On payments, the customer is the entity column.
A query that does not parse or names something unknown is 400 INVALID_PARAMETER with o:errorPath: q. The detail starts Invalid search query. Detailed unprocessed description follows. Search error occurred: as the service’s does, followed by Baseline’s description, such as Record 'x' was not found., Field 'x' for record 'customer' was not found., Column 'id' is ambiguously defined., 'x' is not a GROUP BY expression., or Function 'X' is not supported in SuiteQL..

Metadata

  • GET /services/rest/record/v1/metadata-catalog — List metadata catalog
  • GET /metadata-catalog/{type} — Get record type metadata
The catalog lists the record types, each with canonical (application/json), OpenAPI (application/swagger+json), JSON Schema (application/schema+json), and describes links; ?select=customer,invoice narrows it. A record type’s metadata answers JSON Schema (draft-06) by default and OpenAPI 3.0 with Accept: application/swagger+json, which also works on the catalog itself for the selected types. Filterable fields carry x-ns-filterable.

Governance limits

  • GET /services/rest/system/v1/governanceLimits — Get governance limits: { accountConcurrencyLimit, accountUnallocatedConcurrencyLimit, integrationConcurrencyLimit, integrationLimitType }
The account limit comes from account.concurrency_limit (default 5), an integration’s own limit from integrations[].concurrency_limit, and the unallocated limit is the account limit less every integration’s own. integrationLimitType is integrationSpecific for an integration with its own limit, accountLimit for one without, and internal for a bearer token. Baseline does not enforce the limits.

Server time

  • GET /services/rest/system/v1/serverTime — Get server time: { "serverTime": "..." } in UTC
OPTIONS requests (which netsuite-api-client’s connect() sends) are answered 204 by the shared CORS layer without checking credentials.

Seed config

  • id: the TBA realm
  • concurrency_limit: reported by governanceLimits
  • name: IDs from 1000, in order
  • oauth2: OAuth 2.0; see the OAuth 2.0 section
  • integration: by name or consumer key
  • employee: by email or internal ID; the administrator when left out
  • role: by name or ID; Administrator when left out
  • records: request bodies by type, created in order
  • state (not set above, for example blocked): refuses every request
  • tba (not set above, for example false): turns token-based authentication off
  • concurrency_limit (not set above, for example 2): this integration’s own share of the account limit
integrations[].oauth2 and certificates are described under OAuth 2.0. custom_lists, custom_records, and custom_fields are described under Custom fields and records; they are set up before records, so seed records can use them. Seed records go through the same checks as POST, so a bad one stops the start with an error that starts with netsuite seed: and names the entry. A record with an externalId that already exists is updated rather than created again.

Inspector

GET / serves a tabbed inspector with a tab per entity, item, and transaction type (each record expandable to its full JSON), the lists (custom lists too), the custom record types and their records, asynchronous jobs with each task’s request and status, a SuiteQL console that runs a query read only and shows the rows as the API prints them, the account with its integrations (TBA and OAuth 2.0 settings), TBA tokens, bearer and OAuth 2.0 tokens, client credentials certificates, and roles, and the login audit trail with each sign-in’s status, reason, method, request, entity, role, and integration.

Limits

Transforms build only the pairs listed under Transforms; there are no cash sales, return authorizations, vendor bills, purchase orders, deposits, refunds, or invoice groups, and record actions exist only for invoices. Custom fields cannot be added at run time, and there are no custom segments, custom transactions, or custom forms. Async jobs run at once and concurrency limits are reported, not enforced. There is no NetSuite as OpenID Connect provider (openid scope, ID tokens, userinfo), no dynamic client registration, and no RESTlets or SuiteAnalytics Connect behind the restlets and suite_analytics scopes. SuiteQL has no subqueries, UNION, WITH, window functions, LISTAGG, EXTRACT, Oracle’s (+) join marker, or BUILTIN functions other than DF and CF. There are no saved searches, SuiteAnalytics workbooks, file cabinet, RESTlets, or SOAP web services, no role permissions beyond an active role (every session can read and change every record), no tax, discounts, inventory counts, or GL postings, and no multi-currency exchange. NetSuite has no outbound webhooks of its own, so neither does the emulator.

Conformance

See CONFORMANCE.md for every provider.