# Partially update a sales order

`PATCH /accounting/sales-orders/{id}`

Group: Accounting › Sales Orders · Operation ID: `updateSalesOrder`

Writes only the fields the request body carries. Everything omitted is left exactly as it was. Product lines are edited through three parallel arrays: `createProducts` adds new lines, `updateProducts` rewrites existing ones (matched by `salesOrderProductLineId`, omitted fields left unchanged), and `deleteProducts` removes them by id; `createSubscriptions` adds new subscriptions with their own lines. A new Commit with Overage line requires `allowance` (its Allowance & Overage terms, defaulting from the product catalog); other line types reject it, and `updateProducts` cannot change an existing line's allowance. While an order is pending approval the whole header and its lines are editable. Once approved only a small set of header fields may change, and `updateProducts` entries may carry nothing but `salesOrderProductLineId` and `tags`; any other field or product operation fails with `APPROVED_FIELD_LOCKED`. An unknown id fails with `NOT_FOUND`; a product line id that does not belong to the order fails with `INVALID_LINE`. Runs in a transaction, so a failure leaves the order untouched.

[Open in the interactive reference](https://docs.api.lensing.ai/?op=updateSalesOrder)

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Api-Version` | header | `"v1"` | yes | The API version this request targets, named by its public identifier. Required on every request; omitting it, or naming a version this company does not have, is answered with 400. This document describes version `v1`. |
| `Idempotency-Key` | header | string | no | An opaque caller-chosen string (1-255 characters) that makes this request safe to retry. Repeating the same key with an identical request body replays the original response instead of re-executing - but only once that original request succeeded; a failed attempt re-executes on retry. Repeating the key with a different body is answered with 422. A key still in flight is answered with 409 - as is a key whose original request was interrupted before it finished, for a few minutes afterwards, until that attempt is presumed dead. A completed response stays replayable for 24 hours. The key is scoped to the caller, not to the action: reusing it for a different action is answered with 409 or 422 rather than treated independently. On read actions this header is ignored. On write actions that cannot offer the guarantee it is answered with `400 IDEMPOTENCY_NOT_SUPPORTED` rather than silently ignored. |
| `id` | path | string | yes | UUID of the sales order to update. The selector, never itself changed. |

## Request body

Required, `application/json`: updateSalesOrderInput.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `billToAddressId` | string | no | UUID of the Address invoices for this order go to. An address that does not exist fails with `INVALID_ADDRESS`. Stays editable after the order is approved. |
| `createProducts` | array of object | no | Product lines to add to the order. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `createProducts[].allowance` | LineAllowance | no | The Allowance & Overage terms of a Commit with Overage line. Required for that line type and rejected on any other; omitted fields default from the product catalog. |
| `createProducts[].allowance.allowance` | string (decimal) | yes | Committed quantity per allowance term, in the catalog Allowance's unit of measure. |
| `createProducts[].allowance.allowanceTerm` | string | no | The period the allowance covers: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `orderTerm`. Defaults to the product catalog's Allowance term. |
| `createProducts[].allowance.maximumRolloverLimit` | string (decimal) | no | Cap on the quantity that may roll over. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. |
| `createProducts[].allowance.overage` | LineOverage | no | Discount and invoicing overrides for the line's overage. Omit it to take everything from the catalog Overage and its price-book entry. |
| `createProducts[].allowance.overage.discountAmount` | string (decimal) | no | Discount per unit off the overage price, in the order currency. Takes precedence over `discountPercentage` when both are sent; omit both to price the overage exactly as its price-book entry has it. |
| `createProducts[].allowance.overage.discountPercentage` | string (decimal) | no | Discount per unit off the overage price on a 0–100 scale, so `10` means ten percent. Used only when `discountAmount` is not given. |
| `createProducts[].allowance.overage.invoiceTiming` | string | no | Whether the overage invoices `advance` (at the start of its period) or `arrears` (after it). Defaults from the entity's active invoice policy for overage lines, then from the parent line. |
| `createProducts[].allowance.overage.lineInvoiceDay` | string | no | Which day of the period the overage invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy for overage lines, then from the parent line. |
| `createProducts[].allowance.rolloverDuration` | string (decimal) | no | How many allowance terms unused allowance rolls over for. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. |
| `createProducts[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. Used when no `unitPrice` is given, and then drives the other two. |
| `createProducts[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. The lowest precedence of the three — used only when neither `unitPrice` nor `discountAmount` is given. |
| `createProducts[].externalIdentifiers` | array of object | no | Identifiers this line carries in other systems, upserted by `fieldName`. |
| `createProducts[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. |
| `createProducts[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. |
| `createProducts[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. |
| `createProducts[].invoiceFrequency` | string | no | How often the line invoices — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Defaults to the product line's own setting. |
| `createProducts[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Defaults from the entity's active invoice policy for this line type. |
| `createProducts[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy. |
| `createProducts[].listPrice` | string (decimal) | no | Undiscounted price per unit, in the order currency. Defaults to the list price the order's price book holds for this product line, and is the figure the other pricing fields are reconciled against. |
| `createProducts[].priceModel` | string | no | How the line is priced: `flat`, `perUnit`, `tiered`, `volume`, `stairstep` or `rateCard`. Defaults to the product line's own model. `tiered`, `volume` and `stairstep` take their prices from `tiers` and `rateCard` from `rateCardEntries`, so for those the line-level discount fields are not reconciled. `thresholdBased` is rejected: it needs a base line plus a metered usage child, which this endpoint does not build. |
| `createProducts[].pricingTerm` | string | no | The period the price is quoted for: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `once`. Defaults to the product line's own term. |
| `createProducts[].productLineId` | string | yes | UUID of the ProductLine being added. It settles the product, the line type and the billing rules every omitted field defaults from. |
| `createProducts[].quantity` | string (decimal) | no | Number of units ordered. Required unless the line takes its quantity from somewhere else: usage-metered line types (PayAsYouGo, Usage, Billable*) meter it from usage records and tier-priced models (`tiered`, `volume`, `stairstep`, `thresholdBased`) derive it from the tier structure, so for those it defaults to zero. Anywhere else, omitting it fails the request. |
| `createProducts[].rateCardEntries` | array of object | no | Per-unit rates for a `rateCard`-priced line. Supplying any entry moves pricing onto the rate card, so the line-level discount fields stop being reconciled. |
| `createProducts[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. |
| `createProducts[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. |
| `createProducts[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. |
| `createProducts[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. |
| `createProducts[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. |
| `createProducts[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. |
| `createProducts[].serviceEndDate` | string (date) | no | Last day of the line's service period. The order's own service end is the latest across all its lines, and is left open on an evergreen order. |
| `createProducts[].serviceStartDate` | string (date) | no | First day of the line's service period. The order's own service start is the earliest across all its lines. |
| `createProducts[].subscriptionId` | string | no | UUID of a subscription already on this order to hang the line under. Omit it to add the line to the order itself as a one-time line. A subscription that is not on the order fails with `INVALID_SUBSCRIPTION`. |
| `createProducts[].tags` | array of object | no | Value-accounting tags to allocate this line by. Requires value-allocation tagging to be enabled for sales orders. |
| `createProducts[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. |
| `createProducts[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. |
| `createProducts[].tiers` | array of object | no | The pricing tiers of a `tiered`, `volume` or `stairstep` line. The ranges must be contiguous, and the last tier is left open-ended by omitting its `toQuantity`. |
| `createProducts[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. |
| `createProducts[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. |
| `createProducts[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. |
| `createProducts[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. |
| `createProducts[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. |
| `createProducts[].totalPrice` | string (decimal) | no | Total for the line, in the order currency. Persisted exactly as supplied rather than recomputed — omit it and the line's own pricing drives the total. |
| `createProducts[].unitOfMeasure` | string | no | The unit the line is priced and metered in. Defaults to the product line's own unit. |
| `createProducts[].unitPrice` | string (decimal) | no | Negotiated price per unit, in the order currency. The highest precedence of the three pricing inputs — supply it and `discountAmount` and `discountPercentage` are recomputed from it (discountAmount = listPrice − unitPrice). |
| `createSubscriptions` | array of object | no | New subscriptions to add to the order, each bringing its own product lines. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `createSubscriptions[].autoRenew` | boolean | no | Whether the subscription renews itself at the end of its term. |
| `createSubscriptions[].products` | array of ProductLine | yes | The recurring product lines of the new subscription. Every one must be a subscription-based product line, or the request fails with `INVALID_SUBSCRIPTION_PRODUCT`. |
| `createSubscriptions[].products[].allowance` | LineAllowance | no | The Allowance & Overage terms of a Commit with Overage line. Required for that line type and rejected on any other; omitted fields default from the product catalog. |
| `createSubscriptions[].products[].allowance.allowance` | string (decimal) | yes | Committed quantity per allowance term, in the catalog Allowance's unit of measure. |
| `createSubscriptions[].products[].allowance.allowanceTerm` | string | no | The period the allowance covers: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `orderTerm`. Defaults to the product catalog's Allowance term. |
| `createSubscriptions[].products[].allowance.maximumRolloverLimit` | string (decimal) | no | Cap on the quantity that may roll over. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. |
| `createSubscriptions[].products[].allowance.overage` | LineOverage | no | Discount and invoicing overrides for the line's overage. Omit it to take everything from the catalog Overage and its price-book entry. |
| `createSubscriptions[].products[].allowance.overage.discountAmount` | string (decimal) | no | Discount per unit off the overage price, in the order currency. Takes precedence over `discountPercentage` when both are sent; omit both to price the overage exactly as its price-book entry has it. |
| `createSubscriptions[].products[].allowance.overage.discountPercentage` | string (decimal) | no | Discount per unit off the overage price on a 0–100 scale, so `10` means ten percent. Used only when `discountAmount` is not given. |
| `createSubscriptions[].products[].allowance.overage.invoiceTiming` | string | no | Whether the overage invoices `advance` (at the start of its period) or `arrears` (after it). Defaults from the entity's active invoice policy for overage lines, then from the parent line. |
| `createSubscriptions[].products[].allowance.overage.lineInvoiceDay` | string | no | Which day of the period the overage invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy for overage lines, then from the parent line. |
| `createSubscriptions[].products[].allowance.rolloverDuration` | string (decimal) | no | How many allowance terms unused allowance rolls over for. Accepted only when the catalog Allowance has rollover enabled, and defaults to its setting. |
| `createSubscriptions[].products[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. Used when no `unitPrice` is given, and then drives the other two. |
| `createSubscriptions[].products[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. The lowest precedence of the three — used only when neither `unitPrice` nor `discountAmount` is given. |
| `createSubscriptions[].products[].externalIdentifiers` | array of ExternalIdentifier | no | Identifiers this line carries in other systems, upserted by `fieldName`. |
| `createSubscriptions[].products[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. |
| `createSubscriptions[].products[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. |
| `createSubscriptions[].products[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. |
| `createSubscriptions[].products[].invoiceFrequency` | string | no | How often the line invoices — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Defaults to the product line's own setting. |
| `createSubscriptions[].products[].invoiceFrequencyTemplateId` | string | no | UUID of the invoice frequency template. |
| `createSubscriptions[].products[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Defaults from the entity's active invoice policy for this line type. |
| `createSubscriptions[].products[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Defaults from the entity's active invoice policy. |
| `createSubscriptions[].products[].listPrice` | string (decimal) | no | Undiscounted price per unit, in the order currency. Defaults to the list price the order's price book holds for this product line, and is the figure the other pricing fields are reconciled against. |
| `createSubscriptions[].products[].priceModel` | string | no | How the line is priced: `flat`, `perUnit`, `tiered`, `volume`, `stairstep` or `rateCard`. Defaults to the product line's own model. `tiered`, `volume` and `stairstep` take their prices from `tiers` and `rateCard` from `rateCardEntries`, so for those the line-level discount fields are not reconciled. `thresholdBased` is rejected: it needs a base line plus a metered usage child, which this endpoint does not build. |
| `createSubscriptions[].products[].pricingTerm` | string | no | The period the price is quoted for: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually` or `once`. Defaults to the product line's own term. |
| `createSubscriptions[].products[].productLineId` | string | yes | UUID of the ProductLine being ordered. It settles the product, the line type and the billing rules every omitted field defaults from. It must exist, and it must not be threshold-based or prepaid: those need a base line plus a metered usage child under one product, which this endpoint does not build. |
| `createSubscriptions[].products[].quantity` | string (decimal) | no | Number of units ordered. Required unless the line takes its quantity from somewhere else: usage-metered line types (PayAsYouGo, Usage, Billable*) meter it from usage records and tier-priced models (`tiered`, `volume`, `stairstep`, `thresholdBased`) derive it from the tier structure, so for those it defaults to zero. Anywhere else, omitting it fails the request. |
| `createSubscriptions[].products[].rateCardEntries` | array of RateCardEntry | no | Per-unit rates for a `rateCard`-priced line. Supplying any entry moves pricing onto the rate card, so the line-level discount fields stop being reconciled. |
| `createSubscriptions[].products[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. |
| `createSubscriptions[].products[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. |
| `createSubscriptions[].products[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. |
| `createSubscriptions[].products[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. |
| `createSubscriptions[].products[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. |
| `createSubscriptions[].products[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. |
| `createSubscriptions[].products[].serviceEndDate` | string (date) | no | Last day of the line's service period. The order's own service end is the latest across all its lines, and is left open on an evergreen order. |
| `createSubscriptions[].products[].serviceStartDate` | string (date) | no | First day of the line's service period. The order's own service start is the earliest across all its lines. |
| `createSubscriptions[].products[].tags` | array of LineTag | no | Value-accounting tags to allocate this line by. Requires value-allocation tagging to be enabled for sales orders. |
| `createSubscriptions[].products[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. |
| `createSubscriptions[].products[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. |
| `createSubscriptions[].products[].tiers` | array of LineTier | no | The pricing tiers of a `tiered`, `volume` or `stairstep` line. The ranges must be contiguous, and the last tier is left open-ended by omitting its `toQuantity`. |
| `createSubscriptions[].products[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. |
| `createSubscriptions[].products[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. |
| `createSubscriptions[].products[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. |
| `createSubscriptions[].products[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. |
| `createSubscriptions[].products[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. |
| `createSubscriptions[].products[].totalPrice` | string (decimal) | no | Total for the line, in the order currency. Persisted exactly as supplied rather than recomputed — omit it and the line's own pricing drives the total. |
| `createSubscriptions[].products[].unitOfMeasure` | string | no | The unit the line is priced and metered in. Defaults to the product line's own unit. |
| `createSubscriptions[].products[].unitPrice` | string (decimal) | no | Negotiated price per unit, in the order currency. The highest precedence of the three pricing inputs — supply it and `discountAmount` and `discountPercentage` are recomputed from it (discountAmount = listPrice − unitPrice). |
| `createSubscriptions[].serviceEndDate` | string (date) | no | Last day the subscription is in service. Omit it on an evergreen subscription. |
| `createSubscriptions[].serviceStartDate` | string (date) | yes | First day the subscription is in service. |
| `createSubscriptions[].term` | string (decimal) | no | Intended length of a `termed` subscription in months. What actually bounds the subscription is `serviceStartDate` and `serviceEndDate`, so supply the end date to fix when it ends. |
| `createSubscriptions[].termType` | string | yes | `termed` for a subscription of fixed length, `evergreen` for an open-ended one that keeps renewing. |
| `deleteProducts` | array of string | no | UUIDs of product lines to remove from the order. Each must belong to it, or the request fails with `INVALID_LINE`. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `effectiveDate` | string (date) | no | Day the order becomes commercially effective. Stays editable after the order is approved. |
| `excludeFromTaxEngine` | boolean | no | Whether the order is kept out of the tax engine, so no tax is calculated for it. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `oneTimeDiscountAllocation` | string | no | How the one-time discount is spread across the order's invoices: `firstInvoice` puts all of it on the first, `alignToInvoiceFrequency` spreads it over the invoicing schedule, `custom` leaves the allocation to be set by hand. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `oneTimeDiscountAmount` | string (decimal) | no | A one-off discount on the order as a whole, in the order currency. Independent of the per-line discount fields. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `opportunityName` | string | no | Name of the opportunity the order came out of. Stays editable after the order is approved. |
| `paymentTermCode` | string | no | The order's payment term, named by its code. Stays editable after the order is approved. |
| `purchaseOrder` | string | no | The customer's own purchase-order number for this order. Stays editable after the order is approved. |
| `serviceEndDate` | string (date) | no | Last day of the order's service period. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `serviceStartDate` | string (date) | no | First day of the order's service period. Locked once the order is approved: sending it then fails with `APPROVED_FIELD_LOCKED`. |
| `signDate` | string (date) | no | Day the order was signed. Stays editable after the order is approved. |
| `soldToAddressId` | string | no | UUID of the Address of the contracting party. An address that does not exist fails with `INVALID_ADDRESS`. Stays editable after the order is approved. |
| `updateProducts` | array of object | no | Changes to product lines already on the order, each matched by its `salesOrderProductLineId`. Fields left out of an entry keep their current values. Once the order is approved an entry may carry only `salesOrderProductLineId` and `tags`; any other field then fails with `APPROVED_FIELD_LOCKED`. |
| `updateProducts[].discountAmount` | string (decimal) | no | Discount per unit off `listPrice`, in the order currency. This endpoint has no `unitPrice` field, so the discount is what drives it: unitPrice = listPrice − discountAmount. Takes precedence over `discountPercentage`. |
| `updateProducts[].discountPercentage` | string (decimal) | no | Discount per unit off `listPrice` on a 0–100 scale, so `10` means ten percent. Stored as a 0–1 fraction. Used only when `discountAmount` is absent. |
| `updateProducts[].externalIdentifiers` | array of object | no | Identifiers to set on the line, upserted by `fieldName`. Names not listed are left as they are — there is no way to remove one. |
| `updateProducts[].externalIdentifiers[].externalUrl` | string | no | Deep link to the record in the owning system, shown beside the id. |
| `updateProducts[].externalIdentifiers[].fieldName` | string | yes | Names the ExternalIdentifierConfiguration this id belongs to, as that configuration spells it. It must already be configured for the tenant; an unknown name is rejected. Identifiers are upserted by this name, so sending it again overwrites the stored value. |
| `updateProducts[].externalIdentifiers[].value` | string | yes | The identifier as the owning system spells it. |
| `updateProducts[].invoiceFrequency` | string | no | New invoicing frequency for the line — an InvoiceFrequency member: `monthly`, `biMonthly`, `quarterly`, `semiAnnually`, `annually`, `once`, `custom`, `template`, `milestone`, `allAtOnce` or `termEnd`. Omit it to leave the frequency as it stands. |
| `updateProducts[].invoiceTiming` | string | no | Whether the line invoices `advance` (at the start of the service period) or `arrears` (after it). Omit it to leave the timing as it stands. |
| `updateProducts[].lineInvoiceDay` | string | no | Which day of the period the line invoices on: `anniversary`, `first`, `last` or `specific`. Omit it to leave the day as it stands. |
| `updateProducts[].listPrice` | string (decimal) | no | New undiscounted price per unit, in the order currency. Changing it on its own still re-reconciles the line: the existing discount amount is held fixed and `unitPrice` and `discountPercentage` are re-derived against the new list price. |
| `updateProducts[].quantity` | string (decimal) | no | New number of units for the line. Omit it to leave the quantity as it stands. |
| `updateProducts[].rateCardEntries` | array of object | no | Replaces the line's rate-card rows: a row carrying an `id` is updated, one without is added, and any existing row whose id is not in the list is deleted. Omit the field entirely to leave the line's rows untouched. |
| `updateProducts[].rateCardEntries[].billableComponentId` | string | no | UUID of the BillableComponent this row meters. Optional — a row may price a unit of measure without naming a component — but a UUID that does not resolve rejects the whole request. |
| `updateProducts[].rateCardEntries[].conversion` | string (decimal) | no | Factor converting the billable unit into the reporting unit — 1000 when usage is metered per call but reported per thousand. |
| `updateProducts[].rateCardEntries[].description` | string | no | Free-text label for the row, carried through to the stored rate-card entry. |
| `updateProducts[].rateCardEntries[].id` | string | no | UUID of a rate-card row already on this line, to change it in place. Leave it out to add a new row. An id that is not on the line fails the request and nothing is applied. |
| `updateProducts[].rateCardEntries[].listPrice` | string (decimal) | no | Undiscounted rate per `unitOfMeasure`, in the order currency. Honoured only while the product line's `enforceListPrice` is false; otherwise the product line's own list price wins. |
| `updateProducts[].rateCardEntries[].unitOfMeasure` | string | yes | The unit this row is priced in. |
| `updateProducts[].rateCardEntries[].unitPrice` | string (decimal) | yes | Negotiated rate per `unitOfMeasure`, in the order currency. |
| `updateProducts[].salesOrderProductLineId` | string | yes | UUID of the order's product line to change. It must belong to this order, or the request fails with `INVALID_LINE`. |
| `updateProducts[].serviceEndDate` | string (date) | no | New last day of the line's service period. |
| `updateProducts[].serviceStartDate` | string (date) | no | New first day of the line's service period. |
| `updateProducts[].tags` | array of object | no | Replaces the line's value-accounting tags wholesale. Send `[]` to clear them; omit the field to leave them untouched. Lines that share one product (ramp deals) share their tags, so all of them must be sent the same set. Stays editable after the order is approved. |
| `updateProducts[].tags[].dimension` | string | yes | Name of the value-accounting dimension, as configured for the tenant. Resolved by name, not by id. |
| `updateProducts[].tags[].value` | string | yes | Name of the value within that dimension. The pair must resolve to an existing tag and the dimension must be enabled for sales orders; an unknown pair rejects the whole request. |
| `updateProducts[].tiers` | array of object | no | Replaces the line's pricing tiers wholesale. Omit the field to leave them untouched. |
| `updateProducts[].tiers[].fromQuantity` | string (decimal) | yes | Lower bound of the tier's quantity range, inclusive. |
| `updateProducts[].tiers[].listPrice` | string (decimal) | no | Undiscounted price for this tier, in the order currency. Defaults to `unitPrice`, i.e. no discount on the tier. |
| `updateProducts[].tiers[].priceModel` | string | no | How `unitPrice` applies within the tier: `perUnit` (the default) charges it for every unit in the range, `flat` charges it once for the whole tier. |
| `updateProducts[].tiers[].toQuantity` | string (decimal) | no | Upper bound of the tier's quantity range. Omit it on the last tier to leave the range open-ended. |
| `updateProducts[].tiers[].unitPrice` | string (decimal) | yes | Negotiated price for this tier, in the order currency. Charged per unit in the range when `priceModel` is `perUnit`, or once for the whole tier when it is `flat`. |
| `updateProducts[].totalPrice` | string (decimal) | no | New total for the line, in the order currency. Persisted exactly as supplied rather than recomputed. |

Example:

```json
{
  "billToAddressId": "5b8c31f6-04ad-472e-9f81-63a0d7e295cb",
  "createProducts": [
    {
      "allowance": {
        "allowance": "1000",
        "allowanceTerm": "monthly",
        "maximumRolloverLimit": "500",
        "overage": {
          "discountAmount": "1.50",
          "discountPercentage": "10",
          "invoiceTiming": "arrears",
          "lineInvoiceDay": "anniversary"
        },
        "rolloverDuration": "2"
      },
      "discountAmount": "120.00",
      "discountPercentage": "10",
      "externalIdentifiers": [
        {
          "externalUrl": "https://netsuite.example.com/app/accounting/transactions/salesord.nl?id=10042",
          "fieldName": "netsuiteInternalId",
          "value": "SO-10042"
        }
      ],
      "invoiceFrequency": "monthly",
      "invoiceTiming": "advance",
      "lineInvoiceDay": "anniversary",
      "listPrice": "1200.00",
      "priceModel": "perUnit",
      "pricingTerm": "annually",
      "productLineId": "6a1f2c94-3d58-4b17-9e02-7c8ab4d5f610",
      "quantity": "10",
      "rateCardEntries": [
        {
          "billableComponentId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
          "conversion": "1000",
          "description": "API calls beyond the included volume",
          "listPrice": "0.005",
          "unitOfMeasure": "apiCall",
          "unitPrice": "0.004"
        }
      ],
      "serviceEndDate": "2026-12-31",
      "serviceStartDate": "2026-01-01",
      "subscriptionId": "c8402e17-95bf-4a63-8d20-71ec3f6a0b48",
      "tags": [
        {
          "dimension": "Department",
          "value": "Engineering"
        }
      ],
      "tiers": [
        {
          "fromQuantity": "0",
          "listPrice": "15.00",
          "priceModel": "perUnit",
          "toQuantity": "100",
          "unitPrice": "12.50"
        }
      ],
      "totalPrice": "12960.00",
      "unitOfMeasure": "seat",
      "unitPrice": "1080.00"
    }
  ],
  "createSubscriptions": [
    {
      "autoRenew": true,
      "products": [
        {
          "allowance": {
            "allowance": "1000",
            "allowanceTerm": "monthly",
            "maximumRolloverLimit": "500",
            "overage": {
              "discountAmount": null,
              "discountPercentage": null,
              "invoiceTiming": null,
              "lineInvoiceDay": null
            },
            "rolloverDuration": "2"
          },
          "discountAmount": "120.00",
          "discountPercentage": "10",
          "externalIdentifiers": [
            {
              "externalUrl": null,
              "fieldName": null,
              "value": null
            }
          ],
          "invoiceFrequency": "monthly",
          "invoiceFrequencyTemplateId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
          "invoiceTiming": "advance",
          "lineInvoiceDay": "anniversary",
          "listPrice": "1200.00",
          "priceModel": "perUnit",
          "pricingTerm": "annually",
          "productLineId": "6a1f2c94-3d58-4b17-9e02-7c8ab4d5f610",
          "quantity": "10",
          "rateCardEntries": [
            {
              "billableComponentId": null,
              "conversion": null,
              "description": null,
              "listPrice": null,
              "unitOfMeasure": null,
              "unitPrice": null
            }
          ],
          "serviceEndDate": "2026-12-31",
          "serviceStartDate": "2026-01-01",
          "tags": [
            {
              "dimension": null,
              "value": null
            }
          ],
          "tiers": [
            {
              "fromQuantity": null,
              "listPrice": null,
              "priceModel": null,
              "toQuantity": null,
              "unitPrice": null
            }
          ],
          "totalPrice": "12960.00",
          "unitOfMeasure": "seat",
          "unitPrice": "1080.00"
        }
      ],
      "serviceEndDate": "2026-12-31",
      "serviceStartDate": "2026-01-01",
      "term": "12",
      "termType": "termed"
    }
  ],
  "deleteProducts": [
    "<deleteProducts>"
  ],
  "effectiveDate": "2026-02-01",
  "excludeFromTaxEngine": true,
  "oneTimeDiscountAllocation": "firstInvoice",
  "oneTimeDiscountAmount": "500.00",
  "opportunityName": "Contoso — 2026 renewal",
  "paymentTermCode": "NET30",
  "purchaseOrder": "PO-2026-0042",
  "serviceEndDate": "2026-12-31",
  "serviceStartDate": "2026-01-01",
  "signDate": "2026-01-10",
  "soldToAddressId": "5b8c31f6-04ad-472e-9f81-63a0d7e295cb",
  "updateProducts": [
    {
      "discountAmount": "120.00",
      "discountPercentage": "10",
      "externalIdentifiers": [
        {
          "externalUrl": "https://netsuite.example.com/app/accounting/transactions/salesord.nl?id=10042",
          "fieldName": "netsuiteInternalId",
          "value": "SO-10042"
        }
      ],
      "invoiceFrequency": "monthly",
      "invoiceTiming": "advance",
      "lineInvoiceDay": "anniversary",
      "listPrice": "1200.00",
      "quantity": "10",
      "rateCardEntries": [
        {
          "billableComponentId": "9f1c2d34-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
          "conversion": "1000",
          "description": "API calls beyond the included volume",
          "id": "3f7b9a52-0c1d-48e6-b934-a5d720e8146f",
          "listPrice": "0.005",
          "unitOfMeasure": "apiCall",
          "unitPrice": "0.004"
        }
      ],
      "salesOrderProductLineId": "a05e7d13-8f62-4b90-bc47-1e63d0592a8f",
      "serviceEndDate": "2026-12-31",
      "serviceStartDate": "2026-01-01",
      "tags": [
        {
          "dimension": "Department",
          "value": "Engineering"
        }
      ],
      "tiers": [
        {
          "fromQuantity": "0",
          "listPrice": "15.00",
          "priceModel": "perUnit",
          "toQuantity": "100",
          "unitPrice": "12.50"
        }
      ],
      "totalPrice": "12960.00"
    }
  ]
}
```

## Response

`200` Success

Under `data` (updateSalesOrderOutput):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `approvalStatus` | string | yes | The order's approval status after the update: `draft`, `pendingApproval`, `revise` or `approved`. |
| `id` | string | yes | UUID of the sales order that was updated. |

## Response headers

- `Idempotency-Replayed` — Present and `true` only when this response replays a previously stored one for the same Idempotency-Key, rather than reflecting a live execution. Absent on a live response - never sent as `false`.

## Errors

| Code | Status |
| --- | --- |
| `APPROVED_FIELD_LOCKED` | 400 |
| `INVALID_ADDRESS` | 400 |
| `INVALID_LINE` | 400 |
| `INVALID_SUBSCRIPTION` | 400 |
| `INVALID_SUBSCRIPTION_PRODUCT` | 400 |
| `VALIDATION_FAILED` | 400 |
| `NOT_FOUND` | 404 |
| `IDEMPOTENCY_KEY_CONFLICT` | 409 |
| `IDEMPOTENCY_PAYLOAD_MISMATCH` | 422 |

## Statuses

- `400` — Invalid request or business failure. Codes: APPROVED_FIELD_LOCKED, INVALID_ADDRESS, INVALID_LINE, INVALID_SUBSCRIPTION, INVALID_SUBSCRIPTION_PRODUCT, VALIDATION_FAILED
- `401` — Missing or invalid access token
- `403` — Not authorized
- `404` — Not found. Codes: NOT_FOUND
- `409` — The Idempotency-Key is still being processed by another request.
- `422` — The Idempotency-Key was already used with a different request payload.
- `429` — Too many requests
- `500` — Internal server error
