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_PARAMSand the error text lists the writable names. Read-only fields that appear in product-row responses —priceAccount,ownerId,storeIdand others — are accepted and ignored, so an object read viaGETcan 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 newideven when the previous identifier is sent in the body. To edit a row and keep itsid, usePATCH /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
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
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
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
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
{
"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:
{
"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.