Inside the RESTlet

Every inbound sync lands on one NetSuite endpoint: the Continuous Control (RESTlet) script, cc_control_rl. It takes POST, treats PUT the same way, and answers a handful of read-only actions over GET. It dispatches on the payload's action, resolves everything by external id before writing, and answers with the id map Salesforce uses for writebacks.

The endpoint

The endpoint is the Continuous Control (RESTlet) script record, customscript_cc_control_rl / customdeploy_cc_control_rl. The install creates it, and NetSuite registers POST, PUT, and GET from the start. There's nothing to re-save by hand.

Everything on this page is that one dispatch. The revenue engine that runs right after a sync is separate: Salesforce calls its Update POBs and Update Revenue Plans Suitelets directly, not through here. See The engine Suitelets.

One endpoint, many actions

A single deployment serves every sync type, routed by the payload's action value; the table below lists each one and its module. POST carries writes, and PUT works the same way for an endpoint that requires it. Seven actions are read-only and also answer GET: ping, setupinfo, installcheck, listrecordtypes, refdata, itemlist, and describe. An unknown action returns {ok:false, errors:["unknown action: …"]}.

describe reads a record type's shape for the two editors. It goes one level deeper, too. A sublist column that's itself a NetSuite record returns its own fields. That's what lets an address book row map field by field, not as an opaque blob. See the RESTlet actions reference for the full catalog.

A dispatcher, and a module per action

The entry script writes nothing, in about a hundred lines. It only owns the version identity, the per-call requiresVersion refusal, and the routing table below. Everything else lives in its own module, so you can read one action on its own.

ModuleAnswersWhat it owns
cc_control_ctxnoneThe shared context: deployment parameters, helpers, the external-id resolvers, the upsert and apply-sublist primitives, the write-only-what-changed helpers, the Source Org stamp, the custom-node id maps, and the booking response envelope
cc_action_bookbook, pobsThe order graph's twelve sections, and the per-node target map.
The native Sales Order leg, stood down with a note when Disable Native Sales Orders is checked in Continuous Revenue Settings.
Orphan cleanup.
The POB read-back (the pobs action): the same graph in reverse. See Writebacks & the POB mirror for the field list
cc_action_invoicesinvoicesPosted invoices and credit memos, Salesforce-owned tax, and the billing-mirror refresh
cc_action_paymentspayments, refunds, cashappsEverything that drives the apply sublist, plus the NetSuite-to-Salesforce settlement pull
cc_action_itemsitemsThe booking's items section, standalone
cc_action_customercustomerThe booking's customer and contacts sections, standalone
cc_action_orderlinesorderlinesOrder Line Sync. It updates the delivery, fulfillment and % Complete fields on RC Order Lines a booking made, writing only what changed. It answers which contracts changed
cc_action_customcustomThe admin-mapped extension nodes. It's also the engine every other action reaches for when its payload carries customNodes
cc_action_relayrelayHanding a call's payload on to another RESTlet in this account, under the same role, and wrapping its answer
cc_action_setupping, setupinfo, refdata, itemlist, describe, listrecordtypes, installcheck, settingsoptions, savesettings, postinstall, taxsetup, filesWhat an operator asks the account, not asks it to write.
itemlist pages the item catalogue for the wizard's product matching.
installcheck also reports the two Continuous Revenue Settings switches, engineEnabled and nativeSoAllowed.
setupinfo resolves the contract, order, and POB record types, and the run-invoices search, by scriptid, since SuiteQL can only look up a saved search by name, not its scriptid
cc_record_writer_libnoneThe one record writer every action's entries go through (see below)

The sharing runs one way. Standalone product and account syncs are the booking's own sections, so they call cc_action_book instead of carrying a second copy. Invoice sync re-runs three billing sections to refresh the mirrors an invoice consumed. Six actions share one custom-node engine. None of this changes the wire format.

The booking pipeline

A booking executes as an ordered list of sections. The order is a contract of the implementation.

items → customer → contacts → contract → order → lines
      → bsgs → schedules → periods → customnodes → cleanup → nativeso

Two preconditions guard the whole run: the payload must carry an order id and a contract ("Order has no Contract. Add a Contract to the order, then book again.").

Resolve by external id

Before any section runs, the RESTlet bulk-resolves every incoming external id, one SuiteQL query per table: items, customers, contacts, contracts, orders, lines, billing groups, schedules, and periods. A resolved id means update; a miss means create.

This is why re-sends are idempotent, and why the sync tolerates records made by earlier runs, other tools, or hand entry, as long as the external id convention holds. See Sync runs & idempotency.

Caller-sent NetSuite ids

Salesforce sometimes already knows a record's NetSuite id from an earlier sync, and sends it along. NetSuite checks that id before it trusts it.

The id is used only when the record exists and no other Salesforce record already claims it. A blank claim never counts as someone else's. That lets a product matched by hand in wizard step 7 keep working when a booking or Product Sync sends its id. Otherwise NetSuite ignores the id, looks the record up by its Salesforce id instead, and the run gets a note: "NetSuite id <n> belongs to a different record. It was ignored, and the record was looked up by its Salesforce id."

A line on a document you add, or on a call you build, sends no item id. This check never runs for it. Its item is found by External ID alone. See Add a transaction and its lines.

A payment's or refund's application target is checked the same way. If the target doesn't exist, or isn't the right kind of document, that one application is skipped, and the run gets an error asking you to sync the target again.

Query safety

Every value a payload, a mapping row, or a record supplies goes into a query as a bound parameter, never pasted into the query text. The only things a query can't bind are the table name and, for a transaction, its type code.

Both are checked against a fixed shape before any query runs. A bad one is refused for that field or entry, naming the value, and the rest of the payload still lands. A transaction's type code is one of NetSuite's own codes, or Custom<n> for a custom transaction type.

Upsert semantics

All writes funnel through one upsert helper.

id, externalid, and recordtype can never be written. Everything else can be lifted for one entry through write.allowFields, except entity on a document that already exists: reassigning a saved transaction to another customer always gets refused.

The Source Org stamp

Every request names the Salesforce org it came from, as orgBaseUrl and orgId on the envelope. cc_control_rl stores them once per request, and every write that follows carries the stamp.

upsert() stamps the field for its record's family, one of thirteen: the ten Continuous custom records, plus one field each for customers and contacts, items, and the applicable transaction types. The admin-mapped extension path stamps itself the same way, since it writes through cc_record_writer_lib instead of upsert().

No orgId on the wire sets nothing and clears nothing, and a mapping row can never override the stamp either way.

Per-node targets

Before the pipeline runs, the RESTlet reads the payload's nodeTargets map and gives every section a mode:

ModeMeaning
defaultThe Continuous custom record the node ships with. This is the classic path, byte for byte.
omittedThe node is switched off or deleted. The section is skipped and named in skippedNodes, with a note. Links into the record still resolve from the bare reference in the payload.
transactionThe Order lands as a native document (Sales Order, Estimate, Return Authorization, or a posting type if the admin insisted).
linesThe order lines become that document's item sublist rather than their own records.

Order and Order Line are enforced as a pair: a transaction Order whose line node points anywhere else is refused instead of half-written. An unsupported record type is reported as an error naming the version needed, and only that section is skipped.

One generic record writer

One shared module, cc_record_writer_lib, writes every record a payload entry can target: a Continuous custom record, a native transaction, a customer. No action carries its own writing logic; each hands an entry to this module, through the optional write block, and the module decides how it gets written.

The writer throws only when a document can't be written at all. Everything survivable, like an unknown field id or unresolved lookup, collects into errors[] instead, so one bad mapping row doesn't fail the run.

The transaction writer

A transaction target is written in standard, non-dynamic mode, and it's append-only. The document is resolved by external id, never by a NetSuite id Salesforce happens to be holding, since after a re-point that id could belong to a different record type. Every line is keyed by custcol_cc_sf_line_id, the Salesforce line id.

A quantity change makes NetSuite recompute amount as rate times quantity, so the Salesforce amount gets re-asserted on the next sync, needing one extra run to converge. A document type whose item sublist lacks the key column refuses its lines outright, instead of duplicating them on every re-sync.

A Sales Order the Order node lands also carries the RC contract link and the Do Not Bill (Continuous) flag, set from the Allow NetSuite billing setting. A transaction an extension node lands gets neither. It carries only the fields that node maps.

Governance and resume

NetSuite meters script work in governance units per request. The RESTlet checks the usage left before every row, not only between sections, so one big section can't run out of usage mid-write. Below a floor of 300 units, it stops cleanly and returns partial:true, with continueFrom naming where to resume.

continueFrom names a section, or a section and a row, like periods:638. Salesforce echoes it back and resends the same payload. Resume re-resolves every id first, then only the named section skips the rows already done in it.

Order Line Sync resumes by line: continueFrom is the index of the next line to update.

Invoices, payments, and refunds resume by document instead of by row. A document's id comes back only once it and everything it carries has landed, so a leg that runs low mid-document sends that whole document again next time.

Salesforce keeps resending for up to 25 legs, and stops the chain if a leg's resume point doesn't move. Operators never see the seam: it's one run with extra legs.

The response

{
  "ok": true,
  "partial": false,            // true + continueFrom on a governance pause
  "contractNsId": "…",
  "orderNsId": "…",            // the RC Order, or the transaction the Order node landed
  "counts": { "customer": 1, "contacts": 2, "items": 7, "contract": 1, "order": 1,
              "lines": 7, "bsgs": 9, "schedules": 9, "periods": 22, "orphansDeleted": 0,
              "txnLinesAdded": 7, "txnLinesUpdated": 0, "txnLinesUnmatched": 0,
              "schedulesWithoutLine": 9 },   // the last four only when a transaction was written
  "ids":    { "<each Salesforce id>": "<its NetSuite internal id>" },
  "orderNsType": "salesorder",   // present when the Order landed as a transaction
  "salesOrderNsId": "…",         // the mapped SO, or the generated one — never both
  "notes": [ "native sales order skipped - Order targets salesorder" ],   // or "… - Disable Native Sales Orders is checked in Continuous Revenue Settings"
  "skippedNodes": [ "Customer" ], // present when the payload omitted a node
  "version": "3.0.1",
  "errors": []
}

One gate applies before any of that runs. A payload can carry requiresVersion, the package version its call needs. NetSuite refuses a payload asking for more, with {ok:false, requiresVersion, errors:[…]} and nothing written, so a downgraded account fails with the upgrade instruction instead of landing part of the graph.

The ids map is the writeback contract: every record touched, keyed by Salesforce id. counts feed the card's summary, and errors append to the run's error log. The deployment also carries account-level script parameters, like the default subsidiary, the Deferred Revenue account, and tax settings. See the settings reference; the wizard's install check validates them.