Suite Utils
Back to Blog
Ecosystem InsightsSep 24, 2026 • 6 min read

Add Zero-Value Line Items to NetSuite Orders via API

Learn to Add Zero-Value Line Items to NetSuite Orders via API Without Overwriting Originals.

Arav SharmaArav SharmaCore SuiteScript & Integration Engineer
Add Zero-Value Line Items to NetSuite Orders via API
On this page

When Shopify orders sync to NetSuite, you often need to attach marketing inserts, free samples, or promotional materials that don't exist on the e-commerce side. These zero-value line items must be appended to existing sales orders without disturbing the original line data. The SuiteTalk REST API handles this through the Add or Update operation with a null lineId, but the behavior trips up developers who expect a standard PATCH or POST.

Why Standard Update Fails

A straight PATCH to /record/v1/salesOrder/{id} requires every sublist line to carry its lineId. If you omit it, the API treats the payload as a full replacement and wipes existing lines. The lineId is mandatory for Update because NetSuite must know exactly which line you're modifying. Add or Update relaxes this: when lineId is null or absent, NetSuite interprets the line as an append.

The permission that actually trips people up is REST Web Services under Setup > Company > Enable Features > SuiteCloud. Without it, the endpoint returns 403 even with valid TBA tokens.

Prerequisites in NetSuite

  1. Enable REST Web Services Setup > Company > Enable Features > SuiteCloud > REST Web Services

  2. Enable Token-Based Authentication Same page, TBA checkbox. Create an Integration record at Setup > Integration > Manage Integrations > New to get Consumer Key/Secret.

  3. Generate Token User menu > Settings > Access Tokens > New. Assign the integration, role (must have Sales Order edit permission), and note Token ID/Secret.

  4. Expose Internal IDs Home > Set Preferences > Show Internal IDs. You'll need the sales order id and each item's item internal ID. NetSuite Connector documentation notes these IDs are permanent and visible in record URLs.

The Add or Update Payload

Endpoint: POST https://{accountId}.suitetalk.api.netsuite.com/services/rest/record/v1/salesOrder

Headers:

Authorization: Bearer <access_token>
Content-Type: application/json
Prefer: return=representation

Body, note the null lineId on the new line:

{
  "id": "10452",
  "item": {
    "items": [
      {
        "lineId": null,
        "item": { "id": "487" },
        "quantity": 1,
        "rate": 0,
        "amount": 0,
        "description": "Marketing Insert - Summer Catalog",
        "custcol_is_promo_item": true
      }
    ]
  }
}

Key fields:

  • id, sales order internal ID (not tranid/order number)
  • item.items, the sublist array; existing lines can be omitted entirely
  • lineId: null, signals append; omit the property entirely for same effect
  • rate: 0 and amount: 0, zero-value enforcement
  • custcol_is_promo_item, custom column field (optional but recommended for reporting)

The response returns the full updated order with the new line's assigned lineId.

Worked Example: Append Two Promo Items

/**
 * @NApiVersion 2.1
 * @NScriptType Restlet
 */
define(['N/record', 'N/log'], (record, log) => {
    const post = (context) => {
        const { orderId, promoItems } = context; // promoItems: [{ itemId, qty, description }]
        const so = record.load({ type: record.Type.SALES_ORDER, id: orderId, isDynamic: true });

        promoItems.forEach((p) => {
            so.selectNewLine({ sublistId: 'item' });
            so.setCurrentSublistValue({ sublistId: 'item', fieldId: 'item', value: p.itemId });
            so.setCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity', value: p.qty || 1 });
            so.setCurrentSublistValue({ sublistId: 'item', fieldId: 'rate', value: 0 });
            so.setCurrentSublistValue({ sublistId: 'item', fieldId: 'description', value: p.description });
            so.setCurrentSublistValue({ sublistId: 'item', fieldId: 'custcol_is_promo_item', value: true });
            so.commitLine({ sublistId: 'item' });
        });

        const savedId = so.save({ enableSourcing: true, ignoreMandatoryFields: true });
        return { success: true, orderId: savedId };
    };
    return { post };
});

Deploy as a RESTlet (Setup > Scripting > Scripts > New > RESTlet) and call it with a lightweight JSON payload. This bypasses the REST API's lineId complexity entirely and runs server-side with full sourcing.

CSV Import as a Debugging Sandbox

Before coding, model the behavior in CSV Import (Setup > Import/Export > Import CSV Records):

  1. Record Type: Sales Order
  2. Mapping: Internal ID → id, Item → item, Quantity → quantity, Rate → rate, Line ID → leave blank
  3. Import Type: Add or Update
  4. Run a test file with one existing order ID and a new line, verify the line appends without deleting existing lines.

This mirrors the API's Add or Update logic exactly and takes minutes to validate.

Common Failure Modes

SymptomCauseFix
400 "Line ID is required"Used PATCH or Update operationSwitch to POST with Add or Update; ensure lineId is null/omitted
403 ForbiddenTBA token missing REST Web Services scopeRe-generate token with a role that has Access REST Web Services permission
Existing lines disappearSent full item array without original linesOmit existing lines; only send new lines with null lineId
Rate reverts to item base pricerate omitted or sourcing overwritesExplicitly set rate: 0 and amount: 0; disable "Use Item Price" on the item record if persistent

When to Use a UserEvent Script Instead

If the zero-value items follow predictable rules, e.g., "add catalog to every order over $100" or "include return label for international shipments", a beforeSubmit UserEvent script on Sales Order eliminates the external call entirely:

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/record', 'N/search', 'N/log'], (record, search, log) => {
    const beforeSubmit = (ctx) => {
        if (ctx.type !== ctx.UserEventType.CREATE && ctx.type !== ctx.UserEventType.EDIT) return;
        const so = ctx.newRecord;
        const total = so.getValue({ fieldId: 'total' });
        const isInternational = so.getValue({ fieldId: 'shipcountry' }) !== 'US';

        if (total >= 100 && !hasPromoLine(so, '487')) {
            appendPromoLine(so, '487', 'Summer Catalog');
        }
        if (isInternational && !hasPromoLine(so, '512')) {
            appendPromoLine(so, '512', 'International Return Label');
        }
    };

    const hasPromoLine = (rec, itemId) => {
        const lines = rec.getLineCount({ sublistId: 'item' });
        for (let i = 0; i < lines; i++) {
            if (rec.getSublistValue({ sublistId: 'item', fieldId: 'item', line: i }) === itemId) return true;
        }
        return false;
    };

    const appendPromoLine = (rec, itemId, desc) => {
        rec.selectNewLine({ sublistId: 'item' });
        rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'item', value: itemId });
        rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity', value: 1 });
        rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'rate', value: 0 });
        rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'description', value: desc });
        rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'custcol_is_promo_item', value: true });
        rec.commitLine({ sublistId: 'item' });
    };

    return { beforeSubmit };
});

Deploy on Sales Order, beforeSubmit, and the logic runs automatically, no middleware, no sync lag. System Notes will capture each scripted line addition for audit trail.

Validation Step

After any method, open the sales order in the UI and confirm:

  1. Original Shopify lines remain intact with correct quantities and rates
  2. New lines show Rate 0.00, Amount 0.00
  3. custcol_is_promo_item is checked (add to form if not visible)
  4. Order total unchanged, critical for revenue recognition and commission calc

Run a saved search (Transaction > Sales Order, filter: custcol_is_promo_item = T) to audit all promo lines across the account.


About the author

Put these ideas to work.

Suite Utils builds small NetSuite tools that fix the specific thing breaking your day. Each one runs as a native SuiteScript SuiteApp inside your account. No sales call, no onboarding.

Browse the Tools

Enjoyed this one?

Get NetSuite tips like this in your inbox. No spam. Practical guides only.

Keep reading