Externally Rated Usage Billing in NetSuite 2026.2: The Dedup Is Your Job
A retry created the second usage record.
The external rating platform posted a charge into NetSuite, the call timed out at the gateway before the response came back, and the client retried. The subscription line now carries the same period twice. Nobody notices until the rating run turns both records into charges, and at that point the cleanup is not a delete. NetSuite will not let you delete a usage charge after a rating run. You can only void it.[1]
That failure is why the architecture question in usage billing is worth arguing about, and NetSuite 2026.2 just moved the boundary.
2026.2 lets a third-party platform price the usage
The 2026.2 release notes add one item to SuiteBilling, and the wording is specific. For usage subscription line service types you can now select Use Usage Amount Only. With that option, "usage records can include externally calculated charge amounts from third-party mediation or rating platforms, instead of relying on NetSuite to calculate usage charges."[2]
Three details in that note matter more than the headline.
It works with the models you already sell on. Oracle lists standard usage billing, Commit Plus Overage and Prepaid subscription models as supported with usage amount records.[2] This is not a side path for flat consumption pricing. It lands inside commitment and prepaid balances too.
The dollar amounts reach revenue. "Usage-driven dollar amounts also update Advanced Revenue Management (ARM)."[2] An externally calculated number is not parked in a staging field for someone to review. It flows into invoicing, revenue allocation and revenue recognition. Anything wrong with it is wrong in the ledger, not in an integration log.
And the number arrives already priced. NetSuite is no longer the system that decides what the customer owes. It is the system that has to defend the figure later.
The usage record will not guard itself
The instinct of anyone who has built an integration in NetSuite before is to put a user event script on the record and validate on save. That does not work here.
Oracle's record reference for usage is blunt: "The usage record does not support client scripts, server scripts, or workflows."[3] The how-to page repeats the constraint from the other direction: "Usage records don't support user event scripts."[1] There is no beforeSubmit to reject a duplicate, no workflow to hold a suspicious amount for review, no client script to catch a quantity that arrived with six extra zeros.
The same reference page then says the record is "fully scriptable" and can be "created, updated, copied, deleted, and searched using SuiteScript."[3] Both statements are true, and the gap between them is the design constraint. You cannot hang logic on the usage record. You can freely write to it from outside.
So the validation moves to the door, because the room has no locks. Oracle builds it that way too: the SuiteBilling Enhancements SuiteApp creates usage records through a RESTlet of its own, individually or in bulk.[4] A RESTlet is a separate script record with its own deployment, which is exactly why the platform permits it.
SuiteScript cannot write the key you need
Here is the part that decides the integration, and it is easy to miss because it sits in a page about a different subject.
The deduplication primitive NetSuite gives you is the external ID. The SOAP reference states the case directly: "To prevent duplicate records, you should use external IDs and the upsert and upsertList operations to add records to NetSuite."[5] External IDs are unique within a record type and within certain record groups, so a second write carrying a key you have already used does not quietly become a second record.[5]
The same page also says, in a sentence about maintaining client ID relationships: "Note that SuiteScript does not support external IDs."[5] External IDs can be maintained "through CSV import, user event scripts, or web services"[5] and the usage record accepts no user event scripts.[1]
Follow that through and the write path chooses itself. If the idempotency key has to be an external ID, and SuiteScript cannot set one, and the one script type that could is forbidden on this record, then the create has to go through web services. Usage supports upsert and upsertList in SOAP,[6] and in REST the record ID is usage and an external ID can stand anywhere an internal ID can in the URL.[7][8] A PUT to that path creates the record if it is absent and updates it if it is present.[9]
A RESTlet can still do useful work in this flow, including everything the SuiteBilling Enhancements SuiteApp does with custom field mapping and bulk submission.[4] It just cannot be the thing that owns the key.
One constraint to settle before the second product line ships. "Although records of a particular type may be used in multiple integration scenarios, each record instance can only have a single external ID value. To maintain data integrity, only a single integrated application can set and update external ID values for each record type. External ID values for all records of a particular type must all be from the same external application."[5] Two metering platforms cannot both own external IDs on the usage record. One of them writes, and the others feed it.
Build a key that REST can actually read
Oracle uses this pattern in its own product. Field Service Management stores its idempotency key in the External ID field, with an fsm_ prefix, specifically to survive "syncs, retries, and unstable network conditions."[10] The idea below is the same one, applied to rated usage.
Two rules constrain the format. An external ID "can be any string containing letters, numbers, underscore (_), and hyphen (-)", and a pipe character is read by REST web services as a multi-select delimiter, which breaks parsing of the ID.[8] The obvious separator is the one you cannot use.
/**
* Deterministic external ID for one externally rated usage charge.
* Same source charge, same tariff version, same period => same key.
*/
function usageExternalId(payload) {
var required = [
'sourceSystem', 'sourceChargeId', 'tariffVersion',
'subscriptionLineId', 'periodStart', 'periodEnd'
];
var missing = required.filter(function (field) {
return !payload[field];
});
if (missing.length) {
throw new Error('rated usage rejected, missing: ' + missing.join(', '));
}
var key = [
payload.sourceSystem,
payload.sourceChargeId,
payload.tariffVersion,
payload.subscriptionLineId,
payload.periodStart,
payload.periodEnd
].join('_').replace(/[^A-Za-z0-9_-]/g, '-');
return 'rated_' + key;
}
The tariff version belongs in the key, not only in a field. A charge that records the amount it produced cannot be recomputed once the price book moves. A charge that records which version of the pricing logic produced it can be, and that is the difference between answering an audit question and apologising for it.
The sender then treats the key as the write itself. The body field names below are the ones Oracle uses in its own REST sample for this record.[7]
/**
* Send one rated usage charge to NetSuite. Safe to call again with the
* same payload: the external ID in the path makes the second call an
* update of the first record, not a second record.
*/
async function sendRatedUsage(account, token, payload) {
const externalId = usageExternalId(payload);
const url = 'https://' + account + '.suitetalk.api.netsuite.com'
+ '/services/rest/record/v1/usage/eid:' + externalId;
const response = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
usageSubscription: payload.subscriptionId,
usageSubscriptionLine: payload.subscriptionLineId,
usageQuantity: payload.quantity,
usageDate: payload.usageDate,
memo: payload.sourceSystem + ' ' + payload.tariffVersion
})
});
if (!response.ok) {
const detail = await response.text();
throw new Error('usage upsert ' + response.status + ' for '
+ externalId + ': ' + detail);
}
return externalId;
}
The key has to be minted from the payload on every attempt, which is why it is computed inside the send rather than passed in. A retry that generates a fresh key is not a retry. It is a second charge with better paperwork.
This is also where a backfill stops being the same problem. Replaying a month through this function is a long sequence of individual calls, and at that volume the shape of the job matters more than the shape of the request, with its own set of limits to respect.
After the rating run, the mistake is permanent
Usage billing runs behind the event. Oracle puts it plainly: "One time, commitment, and recurring charges often bill in advance, but usage charges bill in arrears. Usage and overage can't be billed until the item is used."[1] That lag is the window in which a bad record is still cheap to fix.
The rating run closes it. On commit plus overage lines, overage amounts are created only after a rating run, and usage that stays inside the commitment produces no charge at all, appearing instead on the subscription line commitment details subtab.[1] Before the run, a wrong record is a wrong record. After it, a wrong record is a charge that has moved into billing and, under 2026.2, into ARM.[2]
The correction is asymmetric: delete is gone, void is what remains.[1] So the reversal has to be designed into the integration rather than discovered during a close. The external platform needs a way to say that one charge replaces another, the replacement needs its own external ID so that it does not collide with the record it supersedes, and the reconciliation report has to show voided and replacement pairs rather than a net figure that hides both.
What the payload has to carry
A customer reference, a quantity and an amount are not enough to defend a number six months later. The minimum that makes an externally rated charge explainable inside NetSuite:
- the source system and its own charge ID, so the amount has an origin outside the ERP;
- the subscription and subscription line, without which the usage record has nothing to attach to;[6]
- the billing period covered, kept separate from the usage date;
- the tariff or price book version that produced the amount;
- the quantity, unit of measure and currency;
- the idempotency key, which is the external ID above;
- a reference to the charge being replaced, when the message is a correction.
Anything on that list you cannot supply is a question finance will eventually ask that engineering will have to answer by reading logs.
Where I would put the calculation
I would keep rating outside NetSuite and let 2026.2 do what it now advertises, on one condition: the external platform has to be able to replay a period and produce the same numbers.
If it can, NetSuite has no business ingesting raw product events. Event volume, out of order delivery and backfill are not ERP problems, and the usage record gives you no triggers to solve them with anyway. Let the specialised system carry the events and send NetSuite a rated result with its provenance attached.
If it cannot replay, keep the calculation in NetSuite even when the volume argues against it. An external number nobody can reproduce is not a billing integration. It is a figure you are asking an auditor to take on faith, and the general ledger is an expensive place to discover that.
Prove the replay before anything leaves Release Preview, against the same sandbox to production checklist you would use for any other financial path, with the integration authenticating the way it will in production rather than under a convenience login. That part of the stack is moving on its own schedule.
Sources
- Creating Usage Records
- Order Management, NetSuite 2026.2 Release Notes
- Usage, SuiteScript record reference
- Creating Usage Records Using the RESTlet
- External IDs Overview
- Usage, SOAP web services records
- Usage, REST web services
- Using External IDs in REST web services
- Using the Upsert Operation
- Idempotency in Field Service Management