Skip to main content
Wildo.ai Coming soon

Systems of record

Work with NetSuite records in your application

Connect a company's NetSuite account so an application can show its records live, keep a copy up to date however large the table, and create, change or delete records in NetSuite. Editing the same records on both sides is not supported.

NetSuite customers are copied nightly and linked to local tickets, while only a corrected billing contact is sent back to NetSuite.

Work with NetSuite records in your application

A company that runs its finance and operations on Oracle NetSuite can build internal tools on those records. The application declares the NetSuite account once and reaches it as a registered integration, authenticated by a certificate, never as one of its own users.

Read in place: a screen queries NetSuite through SuiteQL when it opens. Copy kept current: a scheduled pipeline copies chosen fields, walking the table by record id. NetSuite will not page past 100,000 results by offset; walking by id never reaches that ceiling, so a large table can be copied whole, and a record inserted or deleted during a long copy is neither read twice nor skipped.

Write back: a resource can create, change and delete NetSuite records through its record service. Each write is sent exactly once and recorded; a lost answer is reported as an unknown outcome rather than retried into a duplicate. A pipeline can also send records created or changed in the application to NetSuite.

The same records cannot be edited on both sides with Wildo deciding who wins: the facet declares no record version, so a two-way pipeline is refused when the application starts.

Example — Keep a customer list beside service tickets

A support tool copies NetSuite customers every night so tickets can link to them and carry the team’s own triage fields. When the team corrects a billing contact, the tool updates that field in NetSuite through the record service; the customer’s other fields stay as NetSuite holds them.

For engineers

The facet offers one credential kind, OAuth client credentials with a client assertion signed by a private key, so the instance does not name one. A design example:

systemsOfRecord: {
  erp: { vendor: 'netsuite', dialect: HttpApiTransportDialect.REST_JSON },
},

Both NetSuite hosts are account-specific, so neither is a constant: baseUrl is the REST services host (https://<accountId>.suitetalk.api.netsuite.com) and tokenUrl the token endpoint. A sandbox changes both. The environment also supplies clientId and certificateKeyId, the key id NetSuite assigns when the certificate is uploaded and expects in the assertion’s header, and the secret privateKeyPem. An encrypted private key is refused by name.

Copy a table through a keyset

The binding below is taken from the engine’s keyset integration test, which runs it against the SuiteQL double on MongoDB and PostgreSQL. The destination schema, mapping and registration are omitted.

binding: {
  dialect: HttpApiTransportDialect.REST_JSON, system: 'netsuite', entityRef: 'customer',
  keyFields: [{ localField: 'id', remoteField: 'id', codec: HttpApiKeyComponentCodec.INTEGER }],
  subjectRights: { posture: SystemOfRecordSubjectRightsPosture.NO_SUBJECT_DATA },
  operations: {
    read: { method: 'GET', pathTemplate: '/services/rest/record/v1/customer/{key}' },
    list: {
      method: 'POST', pathTemplate: '/services/rest/query/v1/suiteql', resultPath: 'items',
      pagingParams: { limitParam: 'limit', offsetParam: 'offset', placement: HttpApiRestParameterPlacement.QUERY_STRING },
      staticHeaders: { Prefer: 'transient' },
      statement: { language: HttpApiQueryStatementLanguage.SUITEQL, parameterName: 'q', keyset: { keyRemoteField: 'id' } },
    },
  },
},

The statement travels in the body of a POST and the paging in the URL, which is why placement is declared: placed by method, the limit would land in the body and bound nothing, so such a member is refused where it is declared. Each page is built as … WHERE id > <last id> ORDER BY id ASC with limit=<n>&offset=0. NetSuite serves ids as text; the integer codec decodes them so they compare as numbers, and a page served out of order is refused rather than trusted. A short page that NetSuite flags with hasMore does not end the pass.

The test’s subject-rights posture describes its fixture. A customer record can describe a person, so a real binding declares its fields’ personal-data categories and keeps the default posture, under which NetSuite answers for its own records; the framework refuses NO_SUBJECT_DATA beside a field declared as personal data.

Write through the record service

Each opened verb declares one request: a create as POST on the record type, with createdKey: { source: HttpApiRestCreatedKeySource.LOCATION_HEADER }; an update as PATCH and a delete as DELETE on …/{key}. NetSuite answers a create with 204 and the new record’s URL in Location. The engine reads the key only when that URL names a record of the collection the create was sent to, then reads the row back by it; any other Location is an unknown outcome, never a guessed key. writeConcurrency must be LAST_WRITER_WINS, because the facet declares no record version.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.