For AI agents: markdown of this page — /docs-content-en/entities/invoices/products-set.md documentation index — /llms.txt

Set invoice products

PUT /v1/invoices/:id/products

Sets the invoice product rows. Fully replaces the current list — pass all the rows you need.

Field names are validated. A field name that is not in the writable set is not dropped silently: the request is rejected with 400 INVALID_PARAMS and the error text lists the writable names. Read-only fields that appear in product-row responses — priceAccount, ownerId, storeId and others — are accepted and ignored, so an object read via GET can be sent back as is.

Row identifiers do not carry over. A row whose fields are unchanged keeps its id. A modified one comes back with a new id even when the previous identifier is sent in the body. To edit a row and keep its id, use PATCH /v1/invoices/:id/products/:rowId.

Request fields (body)

Parameter Type Required Description
Parameteritems Typearray Requiredyes DescriptionArray of product rows
Parameteritems[].productId Typenumber Requiredno DescriptionCatalog product ID. Catalog: GET /v1/products. With neither productId nor productName the row is created with no product and an empty name
Parameteritems[].productName Typestring Requiredno DescriptionRow name — for a free-text row with no catalog product
Parameteritems[].price Typenumber Requiredno DescriptionPrice per unit. Without it the row is created with a zero price
Parameteritems[].quantity Typenumber Requiredno DescriptionQuantity. Without it the row is created with a quantity of 1
Parameteritems[].discountTypeId Typenumber Requiredno DescriptionHow the discount is set: 1 — as an amount in discount, 2 — as a percentage in discountRate. Defaults to 2
Parameteritems[].discount Typenumber Requiredno DescriptionDiscount amount. Applied only when discountTypeId is 1
Parameteritems[].discountRate Typenumber Requiredno DescriptionDiscount percentage. Applied only when discountTypeId is 2
Parameteritems[].taxRate Typenumber Requiredno DescriptionTax rate (%)
Parameteritems[].taxIncluded Typeboolean Requiredno DescriptionTax included in the price

Examples

curl — personal key

Terminal
curl -X PUT "https://vibecode.bitrix24.com/v1/invoices/58/products" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "productId": 1, "price": 25000, "quantity": 2 },
      { "productId": 5, "price": 5000, "quantity": 1, "discountTypeId": 1, "discount": 500 }
    ]
  }'

curl — OAuth application

Terminal
curl -X PUT "https://vibecode.bitrix24.com/v1/invoices/58/products" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "productId": 1, "price": 25000, "quantity": 2 },
      { "productId": 5, "price": 5000, "quantity": 1, "discountTypeId": 1, "discount": 500 }
    ]
  }'

JavaScript — personal key

javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/invoices/58/products', {
  method: 'PUT',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: [
      { productId: 1, price: 25000, quantity: 2 },
      { productId: 5, price: 5000, quantity: 1, discountTypeId: 1, discount: 500 },
    ],
  }),
})

const { success, data } = await res.json()

JavaScript — OAuth application

javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/invoices/58/products', {
  method: 'PUT',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: [
      { productId: 1, price: 25000, quantity: 2 },
      { productId: 5, price: 5000, quantity: 1, discountTypeId: 1, discount: 500 },
    ],
  }),
})

const { success, data } = await res.json()

Response fields

Field Type Description
Fieldsuccess Typeboolean DescriptionAlways true on success
Fielddata Typearray DescriptionArray of the resulting product rows
Fielddata[].productId Typenumber DescriptionCatalog product ID
Fielddata[].productName Typestring DescriptionRow name
Fielddata[].price Typenumber DescriptionPrice per unit
Fielddata[].quantity Typenumber DescriptionQuantity
Fielddata[].discount Typenumber DescriptionDiscount amount
Fielddata[].discountTypeId Typenumber DescriptionHow the discount is set: 1 — as an amount, 2 — as a percentage
Fielddata[].taxRate Typenumber | null DescriptionTax rate (%)
Fielddata[].taxIncluded Typeboolean DescriptionTax included in the price

Response example

JSON
{
  "success": true,
  "data": [
    {
      "productId": 1,
      "productName": "Server equipment",
      "price": 25000,
      "quantity": 2,
      "discount": 0,
      "discountTypeId": 2,
      "taxRate": null,
      "taxIncluded": false
    },
    {
      "productId": 5,
      "productName": "Installation and setup",
      "price": 5000,
      "quantity": 1,
      "discount": 500,
      "discountTypeId": 1,
      "taxRate": null,
      "taxIncluded": false
    }
  ]
}

Error response example

400 — invalid format:

JSON
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "items must be an array"
  }
}

Errors

HTTP Code Description
HTTP400 CodeINVALID_PARAMS DescriptionThe body contains a field name that is not among the writable fields — see Product fields
HTTP400 CodeINVALID_PARAMS Descriptionitems is not an array
HTTP400 CodeINVALID_PARAMS DescriptionAn entry in the items array carries no writable product-row field — an empty object, or read-only fields only. The error text lists the writable names
HTTP404 CodeENTITY_NOT_FOUND DescriptionInvoice not found
HTTP403 CodeSCOPE_DENIED DescriptionThe API key does not have the crm scope
HTTP401 CodeTOKEN_MISSING DescriptionThe API key has no configured tokens

Full list of common API errors — Errors.

Known specifics

Full replacement: PUT replaces the entire product list. To add a row — first fetch the current ones (GET), add the new one to the array, and send everything (PUT). Fetch the list in full: without limit, GET returns only the first page, so pass limit=5000 or page through with offset until meta.hasMore is false. Sending back an incomplete list erases everything it omits. An empty items array erases every row of the invoice.

An amount discount requires discountTypeId: 1. The discount type defaults to 2, a percentage, and in that mode the discount field is not stored: the row comes back with discount: 0 and no error is raised. To set a discount as an amount, send discountTypeId: 1 together with discount; to set it as a percentage, send discountRate with type 2.

See also