/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
--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 (withoutrealmand 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_SB1or1234567-sb1). - OAuth 2.0:
Authorization: Bearer <access token>with a token from the flows below. - Seeded bearer tokens:
Authorization: Bearer <token>. Tokens seeded underaccess_tokensact 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.
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 anoauth2 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 owncertificates: client credentials (M2M) mappingsemployee: the administrator when left outrole: Administrator when left outconsent_policy(not set above, for examplealways_ask): ask_first_time, never_askpublic_client(not set above, for exampletrue): no secret, PKCE required, rotating refresh tokensrefresh_token_validity_hours(not set above, for example48): public clients, 1 to 720max_token_rotation_hours(not set above, for example168): public clients, 1 to 720
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 pagePOST /services/rest/auth/oauth2/v1/token— Get token: authorization code, refresh token, or client credentials grantPOST /services/rest/auth/oauth2/v1/revoke— Revoke token
- 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 optionallycode_challengewithcode_challenge_method=S256andprompt(login,consent, or both). PKCE is required for public clients and for themcpscope, andplainis 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 withstate,role,entity,company, andcode, or witherror. - Call the token endpoint (form encoded) with
grant_type=authorization_code,code,redirect_uri, andcode_verifier, authenticated withAuthorization: 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). - 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 lastsrefresh_token_validity_hours, and the chain endsmax_token_rotation_hoursafter it started. - 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.
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 (undercertificates 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:
algPS256,PS384,PS512,ES256,ES384, orES512;kidthe certificate ID;typJWT. - Payload:
issthe client ID,scope(a list, or comma separated text),audthe token endpoint (the emulator’s, orhttps://<account>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token),iat, andexpat most an hour afteriat.
{ 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
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_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 withLocationGET /{type}/{id}— Get record: the same path takesPATCHto update it (204 withLocation) andDELETEto 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,emailreturns only those body fields.expandSubResources=trueexpands 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 byline, or byorderLineon lines made from a sales order, item pricing by currency, price level, and quantity, payment and credit memoapplylines bydoc), and other lines are added. A new quantity or rate on an existing line prices it again.?replace=addressBook,itemreplaces the named sublists, and a null sublist empties it. - A null body field in a
PATCHclears the field. WithreplaceSelectedFields=true, only the null fields named inreplaceare cleared. - Read-only fields in a body are ignored.
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
orderStatusapproves 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 tracksquantityFulfilledandquantityBilled. - An invoice is Open (A) until payments and credit memos applied to it cover the total, then Paid In Full (B), with
amountPaidandamountRemaining. - A credit memo is Open (A) until all of it is applied, then Fully Applied (B), with
appliedandunapplied. A payment keepsappliedandunappliedtoo. - An estimate turns Processed (B) when a sales order or invoice is made from it.
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
{ 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’sLocation
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 }
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: withAccept: application/vnd.oracle.resource+json; type=create-formit 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: withtype=edit-formit does the same for an update.POST /{type}orPATCH /{type}/{id}— Select options: withtype=select-optionsandfields=entityStatus,currencyit lists the values each select, multiselect, or enumeration field accepts:{ links, entityStatus: { _selectOptions: { links, items: [{ id, refName }], count, offset, hasMore, totalResults } } }. Sublist fields are nameditem.item.qfilters by option name, one condition per field separated by commas (entityStatus START_WITH PROSPECT, currency IS "Euro", withIS,CONTAIN, orSTART_WITH), andlimit(default 100) andoffsetpage every field. Inactive records are left out, and numeric IDs are numbers. A field that is not a select isINVALID_PARAMETERwitho: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:
custentityon customers, contacts, vendors, and employees;custitemon items;custbodyon transaction bodies;custcolon the item lines of estimates, sales orders, invoices, and credit memos;custrecordon a custom record type.applies_tonarrows 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, andmultiselect. A select’ssourceis a record type, acustomlist_ID, or acustomrecord_ID.mandatoryfields are named by label when missing, anddefaultfills new records. - A custom list (
customlist_region) is a record type withname(the value),abbreviation, andisInactive. A custom record type (customrecord_project) hasname,isInactive,owner(the administrator by default),created,lastModified, and itscustrecord_fields. Withautonumber, new records are numbered intonameand the entered name goes inaltName.include_name: falsedrops the Name field. - In SuiteQL, custom fields are columns (
custentity_tier), andcustomlist_regionandcustomrecord_projectare tables.
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 withPrefer: 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 collectionGET /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, andLocationGET /job/{id}/task/{taskId}/request— Get task request: as submitted, withmethod,url,body,parameters,service(RECORDorQUERY), and theprefer...flags
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, orPUTwithContent-Type: application/vnd.oracle.resource+json; type=collectionand{ "items": [...] }.PATCHitems name their record withidorexternalId;PUTitems upsert byexternalId.GET /record/v1/{type}?expandRecords=true&ids=1,eid:B2— Batch read recordsDELETE /record/v1/{type}?ids=...— Batch delete records
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
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.
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.
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 catalogGET /metadata-catalog/{type}— Get record type metadata
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 }
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 realmconcurrency_limit: reported by governanceLimitsname: IDs from 1000, in orderoauth2: OAuth 2.0; see the OAuth 2.0 sectionintegration: by name or consumer keyemployee: by email or internal ID; the administrator when left outrole: by name or ID; Administrator when left outrecords: request bodies by type, created in orderstate(not set above, for exampleblocked): refuses every requesttba(not set above, for examplefalse): turns token-based authentication offconcurrency_limit(not set above, for example2): 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.