RESTlet actions

One endpoint handles every sync, dispatched on the payload's action. Every packaged sync pushes with POST. PUT works as an alias, and eight read-only actions also answer GET. This is the wire-level catalog. For the pipeline mechanics, see Inside the RESTlet.

Calling the endpoint

The endpoint is the Continuous Control (RESTlet) script record, the one entry point for every action below. Salesforce's revenue engine also calls the Update POBs and Update Revenue Plans Suitelets directly, outside this dispatch. See The engine Suitelets for those two, and Inside the RESTlet for how a call reaches an action here.

POST https://<account>.restlets.api.netsuite.com/app/site/hosting/restlet.nl
     ?script=customscript_cc_control_rl&deploy=customdeploy_cc_control_rl
Authorization: OAuth 1.0a (HMAC-SHA256; query params included in the signature)
Content-Type: application/json

{ "action": "…", … }

PUT  same URL, same body                // alias of POST, for endpoints that require it
GET  same URL &action=ping             // read-only actions only: ping, setupinfo,
                                        // installcheck, listrecordtypes, refdata, itemlist, describe,
                                        // settingsoptions

Each call can be pointed at a different URL, method, and action. The values above are the defaults every packaged call uses. The install registers GET and PUT on the script record along with POST.

Every push call's body carries orgBaseUrl and orgId. These are the Salesforce org's My Domain host and its 18-character org id. Together they let NetSuite build "Open in NetSuite" deep links and stamp the Source Org field. See Anatomy of the payload for the full envelope shape.

Action reference

actionPurposeRequest carries → response returns
book (default)Book an order, the full graphThe booking payload, including nodeTargets →
{ok, partial?, continueFrom?, contractNsId, orderNsId,
counts, ids, errors, notes, skippedNodes?,
orderNsType?, salesOrderNsId, version}
invoicesLand invoices + credit memos (one chunk)invoices[] docs (typed invoice/creditmemo, per-line taxRate1, CM applications[]) + optional billing mirror refresh → ids per document. A document lands whole, with its billing mirrors and extension records, or not at all this leg
paymentsCustomer payments with applicationspayments[] with applications[] and replace semantics → ids. A payment lands whole, with its extension records, or not at all this leg
refundsCustomer refunds against open creditsrefunds[] with credit applications → ids. A refund lands whole, with its extension records, or not at all
itemsStandalone product syncThe booking payload's items section alone → ids. A product lands whole, with its extension records, or not at all
customerStandalone account/contacts syncThe customer section, with its addresses riding customNodes as the address nodes' carrier entry. → ids. The account lands whole, with its contacts and extension records, or not at all
orderlinesOrder Line Sync: a booked order's line status (ns-v3.3.0+){requiresVersion:"3.3.0", orderId, orderNumber,
lines:[{sfId, delivered, deliveredDate,
fulfilled, fulfilledDate, pctComplete}], continueFrom} →
{ok, version, partial, continueFrom,
counts:{lines, updated, unchanged, skipped},
ids, skipped, affectedContracts, errors, notes}.
Updates only the RC Order Lines a booking made, and only the fields that changed. A blank clears the field. It never creates a record. See the orderlines payload.
customRun a call you've built yourselfA root entry inside customNodes[], shaped like any call's extension node. An entry with nested lines[] is a document, whether it's a transaction with item rows or a customer with address rows. Each row names its sublist. A node key prefixed __sublist: is a reserved carrier. It sets no fields of its own. It only writes sublist rows onto a record another node, or an earlier sync, already landed.
→
{ok, partial?, customNodesFrom?, customIds,
customIdsByNode, counts, errors, notes, version}.
counts carries customNodes (records and documents landed), one custom:<nodeKey> per node, and addresses when address rows landed.
pobsRead a contract's POBs (the mirror source){contractNsId} →
{ok, pobs:[…]}, each row
{id, name, contract, item, booked, billed, recognized, qty,
rule, startdate, enddate, delivered, delivereddate,
fulfilled, fulfilleddate, pct}.
pct is the POB's % Complete in points, such as 40, or null (ns-v3.3.0+).
Read-only. Runs whether or not the Continuous Revenue Engine is on.
cashappsReverse settlement pullWatermark + scope → changed payments/CMs/refunds with applications, paged, maxLastModified
filesAttach Salesforce documentsFile content + target record →
File Cabinet id, attached.
A file lands only in the Continuous Documents folder, or a subfolder of it named by the payload's folderName (one plain name, no path separators). The folder's id is kept on Continuous Revenue Settings. Only a document or image extension is accepted; a name with no extension is stored as MISCBINARY, and a script or markup extension (.js, .html, .svg, and similar) is refused. The attach target must be a record type Continuous syncs, with a numeric NetSuite id. Every check runs before the same-name file it would replace is deleted, so a refused file never removes anything. See Files, folders, and extensions below.
pingConnection test→
{ok, pong:true, script, version, writeSchema, featureVersion, sublistModes, role, remainingUsage}.
sublistModes lists the sublist write modes this package understands: append, match, replace, subrecord. Salesforce reads this floor instead of assuming it.
role is {id, scriptId, name} for the connection's NetSuite role, or null when no user answers (never fails the call).
setupinfoEnvironment discovery (wizard step 5)→
{contractTypeId, orderTypeId, pobRecordTypeId, invoiceSearchId, …}.
These are the RC Contract, RC Order, and RC POB custom record-type ids. They also include the numeric internal id of customsearch_cc_run_invoices, found by its scriptid so it resolves even on a fresh install. The pobRecordTypeId field is what lets a mirrored Performance Obligation get an "Open in NetSuite" link. See the POB mirror.
refdataReference data (wizard step 7)→ subsidiaries (with addresses) + currencies
itemlistItem catalogue (wizard step 7 product matching)Optional {page, pageSize, includeInactive}. page is 0-based, pageSize ranges 5–1000 and defaults to 1000, and inactive items are left out unless asked for.
→
{ok, items:[{id, itemId, name, externalId, type, inactive}],
page, pageSize, pageCount, total, hasMore, version}.
Every item in the account, ordered by Name/Number, one page per call. The itemId field is the NetSuite Name/Number the wizard matches product codes against. The type field is the NetSuite item type code, as reported. Read-only, GET-able.
describeLive field metadata for the mapping and call editorsRecord type →
{ok, fields, types, mandatory, sublists:[{id, fields?}],
sublistSource, sublistFields, subrecords?, options?}.
Returns field ids and their types, the fields NetSuite won't save without, and the record's sublists.
Sublist ids come back always. Their fields come back only when the request sets deep, since that costs one metadata read per sublist. The sublistSource field says how the ids were found. getSublists means the account enumerated them. probe means a curated list of common ids was checked one by one.
fieldId asks for one list field's options alone. That narrower request skips sublists, sublistSource, and mandatory, since it never runs the full sweep that fills them.
subrecords answers one level deeper: a sublist column that's itself a NetSuite record, filed as subrecords[sublistId][columnId] = {fields, types, mandatory}. That's how the editor offers an address book row its own addr1, city, state, and country. Naming both sublist and subrecord probes that one column directly, even without deep. A miss there lands in errors while ok stays true.
listrecordtypesLive record-type list for the call wizards' picker→
{ok, types:[{scriptid, name}],
transactions:[{scriptid, name, txnType, lineSublist}],
standardTypes:[{family, scriptid, name, txnType?, lineSublist?}]}.
Returns every active custom record type in the account, plus the native transaction types the writer supports, each naming the sublist its lines land on. It also returns a curated catalogue of standard types grouped by family: entity, item, transaction, billing, other. Each entry is filtered against the features the account has switched on.
The catalogue is a convenience, not an authority. There's no allow-list, so a type it doesn't list is still usable by typing it. describe is the real gate. The feature filter fails open, so a probe that errors can never hide a type the account has. Read-only.
installcheckComponent inventory (wizard step 4)→
Present and missing record types, scripts, and searches. Package version. Parameters still on demo defaults. The fields key lists the transaction body and column fields the sync needs. That includes the three Source Org probes: custbody_cc_source_org on Sales Order, custentity_cc_source_org on Customer, and custitem_cc_source_org on a Service Item standing in for every item type. From ns-v3.3.0 it also probes custrecord_cc_line_pct_complete on the RC Order Line, the field Order Line Sync writes.
The sublistModes key reports the same modes as ping. entryPoints comes back as {post, get, put}. The two Continuous Revenue Settings switches come back as booleans, engineEnabled and nativeSoAllowed, absent (meaning both on) when the settings row doesn't exist yet.
role is the same {id, scriptId, name} object ping reports. When it's anything other than Administrator or the Continuous Control Integration role, notes carries: "This connection uses the <name> role. Move it to the Continuous Control Integration role. A later release limits the endpoint to that role and Administrator." This never fails the check. Every RESTlet deployment stays open to all roles in this release.
Also notes, always present and possibly empty, for non-fatal diagnostics: any field or entry-point probe that couldn't run this pass. Read-only. The wizard shows a hint for a false switch, but never overrides NetSuite.
settingsoptionsThe eight account settings and their choices (wizard step 4, ns-v3.1.0+){requiresVersion:"3.1.0"} →
{ok, version, settingsId,
settings:[{key, label, value, valueText, onDefault, options, truncated}], errors}
Each setting's current value, whether it's still on a shipped default, and the choices NetSuite allows. Read-only. Also answers GET.
savesettingsSave the account settings (wizard step 4, ns-v3.1.0+){requiresVersion:"3.1.0", values:{key: id, …}} →
The settingsoptions answer plus saved. All or nothing: one bad value writes nothing and names the setting.
postinstallPost-install provisioning (wizard step 4)→
Creates the settings record, and creates any missing saved searches. Idempotent. Each entry in searches[] carries a detail of present, meaning the search already existed and was left alone, or created, meaning this run built it. It's never "refreshed": a rebuild only fills what's missing.
taxsetupOne-time US nexus provisioningOptional state list → tax agency, nexuses, zero-rate codes per state
relayHand the call on to another RESTlet in this accounttarget:{scriptId, deployId, action?} plus the payload to forward, or an explicit body.
→
{ok, relay:{scriptId, deployId, action, sent, status, body},
errors, notes, customIds?, customIdsByNode?, counts?,
partial?, customNodesFrom?}.
See Relaying to another RESTlet below.

Any other action value returns {ok:false, errors:["unknown action: …"]}.

Files, folders, and extensions

The files action attaches a Salesforce-generated document to a NetSuite record. Everything lands under one shared folder:

Every check runs before the same-name file it would replace is deleted, so a refused file never removes an existing one. A same-name file that passes every check replaces it, since a regenerated document should refresh in place.

Governance and dispatcher errors

NetSuite meters script work per request. Every write action checks the usage left before each row, so a large request stops cleanly instead of failing outright. A call that resumes by document, rather than by section, reports a document synced only once everything under it landed. A single-document call that can't finish fails with a message telling you to sync it again.

An error the RESTlet can't otherwise handle answers {ok:false, version, errors:[…]} instead of NetSuite's own error page. The message names the action, and tells you to check the script log and run it again.

Relaying to another RESTlet

When an admin points a call at another RESTlet in this account, a script your team or a vendor already owns, Salesforce doesn't call it directly. It posts to the Continuous endpoint as usual, with action:'relay' and the target from the endpoint row. NetSuite forwards the payload from the inside:

{ "action": "relay",
  "target": { "scriptId": "customscript_partner_thing",
              "deployId": "customdeploy_partner_thing",
              "action":   "ingest" },        // optional: what the TARGET calls it
  "callKey": "…", "customNodes": [ … ]       // or "body": { … } to send exactly that
}

Going through us means the forward happens inside NetSuite, under the same role. There's no second credential to store, no second Remote Site entry, no OAuth signature for someone else's endpoint, and no secret ever leaves the account. Both target ids are required, since NetSuite's own RESTlet-to-RESTlet call needs the deployment as well as the script.

The target's own deployment audience must include this connection's role. A relay carries no second credential, so it runs as whichever role the connection's token uses. If the target RESTlet's deployment is scoped to a different set of roles, NetSuite refuses the in-account call itself. It comes back as an HTTP error under relay.status, not a Continuous-worded refusal. Check the target's deployment audience before pointing a call at it.

Two more limits are checked before anything is sent, both from ns-v3.2.0 (audit N16):

What goes over the wire is your payload minus the two routing keys that are ours. The target key is removed. The action key becomes target.action, or is dropped entirely when the target doesn't use one, since relay is our word, not theirs. Everything else rides verbatim. Send body instead when the call's own shape and the target's differ. Then that object is sent as-is.

What comes back is the target's answer, wrapped and never rewritten. relay.sent says whether the request ever left NetSuite. When it's true, relay.status is the HTTP status, and relay.body is its answer: parsed when it's JSON, raw text otherwise.

If the target speaks the same wire format Continuous does, its customIds, customIdsByNode, counts, ids, pobs, errors, notes, partial, and customNodesFrom are lifted to the top level. That's what lets Salesforce's writeback and continuation loops treat a relayed call the same way as a direct one.

Don't relay Order Line Sync. The relay doesn't lift its skipped, affectedContracts or continueFrom keys, so a relayed run loses its skipped-line warnings and can't resume.

If it speaks something else, nothing is lifted. ok follows the HTTP status, and a note says so. The run records "sent" rather than failing.

relay.body is capped at 65,536 characters. Without the cap, a chatty target could carry its answer twice: once whole, and once as the lifted keys. That could turn a working relay into an opaque "response too large" failure. The lift happens first, so the machine-readable half always survives. Past the cap, the body is replaced by a marker saying how big it was, and relay.truncated is set.

One leg per call. The relay never chunks a payload. It forwards what it was given, cursor and all, and passes the target's own partial and customNodesFrom back up. Governance sizing belongs to the two ends, not to the courier between them.

NetSuite refuses a relay before sending anything, for any of these reasons:

Nothing answered means no status. Every refusal comes back ok:false, partial:false, relay.sent:false, and relay.status:null. It never fakes a 0, which downstream would read as "the target answered badly" when really it was never asked. Salesforce reports these as not sent, with NetSuite's reason, and fails the run. The same payload would be refused the same way again.

There's one other not-sent answer, and it means the opposite, told apart by partial. A leg NetSuite stopped for governance comes back ok:true, partial:true, with the same customNodesFrom cursor, and the run re-posts it.

The relay is a courier, not an adapter. It doesn't know or care what the target's payload means, and it doesn't translate. Vendor-specific adapters aren't built until a specific vendor needs one.

The write block

Every push action can carry record entries an admin configured, in customNodes[]. The book action carries them alongside the fixed order graph. The invoices, payments, refunds, items, and customer actions carry them alongside their own documents. The custom action is nothing but those entries. They all go through one engine and one record writer, so an entry means the same thing no matter which action carries it.

Any entry can add an optional write block saying how the record should be written. Every key is optional, and an entry without a write block uses the defaults. The full schema is annotated in Anatomy of the payload.

What write controls
External idWhich table resolves it, and the type code for a transaction
ResolutionWhether to resolve by external id at all
SublistsWhich ones it drives, and in what mode: append, match, replace, subrecord
CreationWhether the entry may be created, and what a create needs
FormA custom form to use
Field namingWhich fields are set by label instead of internal id
ClearingWhich fields may be cleared
Field accessWhich default-blocked field ids are lifted for this entry
LookupsField-level lookups

A sublist row can be a record. Alongside append, match, and replace, a sublist can be driven in subrecord mode. An entity's address book works this way. The row itself holds only a label and the two default flags. The address lives in a nested record hanging off one of its columns.

Two keys go with the mode. subrecordId names that column. It defaults to addressbookaddress on an addressbook, and is required on anything else, since guessing wrong would write the address into a plain column. The ensureDefaults key, on by default for an addressbook, settles the default billing and shipping flags across the record after the rows are written.

The rules, in short:

Settling the flags is a whole-record pass, not a per-row one. The row the payload marks default is the only one that keeps that flag. A record left with only one address carries both flags. A lone existing row already carrying a flag gets adopted and relabeled instead of duplicated. That's what keeps a matched customer's existing NetSuite addresses from being copied.

Lookups resolve by external id, and only by external id. A lookups entry, and a nested row's keyRef, names a table and the external id of the record it wants. There's no by key: the writer doesn't read one, and Salesforce doesn't emit one, so a resolution mode isn't something a payload can select. A miss becomes a per-field error naming the value, and the record still writes.

Any action's payload can also carry requiresVersion, the package version the call needs. Salesforce stamps it from the activated configuration and from what the built payload turned out to need. NetSuite checks it first, ahead of the action. A payload asking for more than the installed package can do is refused with {ok:false, requiresVersion, errors:[…]}, and nothing is written.

Response envelope

Every write action shares this envelope:

FieldHolds
okOverall success
errors[]Accumulated errors, including non-fatal ones
countsPer-section record counts
idsThe Salesforce id to NetSuite internal id map that drives writebacks
versionThe NetSuite package version that answered

Two informational arrays ride alongside. The notes[] array covers things worth knowing that aren't errors: a section skipped, a line NetSuite kept that the payload stopped sending, or the native Sales Order generator standing down. That last one reads native sales order skipped - Order targets salesorder when a Sales Order node booked the run. It reads native sales order skipped - Disable Native Sales Orders is checked in Continuous Revenue Settings when NetSuite refuses it instead. The skippedNodes[] array names each node the payload deliberately omitted.

Long-running writes can return partial:true with continueFrom. The Salesforce worker resumes automatically. See governance and resume.

A response that landed records for extension nodes carries two id maps instead of one. The customIds key is the flat Salesforce id to NetSuite internal id map. The customIdsByNode key files the same ids under the node that wrote them, as {nodeKey: {sfId: nsId}}. That's what keeps two nodes over the same Salesforce record unambiguous: each one writes back its own document's id, instead of both taking whichever landed last. Writebacks prefer the by-node map and fall back to the flat one. Both keys are absent when a run landed no extension records at all.