Products
List products, services and special or assembled items, with prices and stock
The product catalog. Only active products are listed unless you ask for status=all or status=inactive.
Prices and stock are computed for you. A client with no price list restriction gets the base price and a price_lists array with the price of each of the seven price lists. A client restricted to one price list gets a single price: the one of its list, or the base price when the list price is zero. Stock is listed per store; for a client restricted to some stores, the totals add up those stores only.
Endpoints#
List: GET/api/v1/products One record: GET/api/v1/products/{id}
Example request#
List with a page size and a sort:
curl "https://votre-entreprise.evyooht.com/api/v1/products?per_page=5&sort=-name" \
-H "Authorization: Bearer evy_at_votre_jeton"$headers = @{ Authorization = "Bearer evy_at_votre_jeton" }
Invoke-RestMethod -Uri "https://votre-entreprise.evyooht.com/api/v1/products?per_page=5&sort=-name" -Headers $headersconst response = await fetch("https://votre-entreprise.evyooht.com/api/v1/products?per_page=5&sort=-name", {
headers: { Authorization: "Bearer evy_at_votre_jeton" },
});
const data = await response.json();
console.log(data);$ch = curl_init("https://votre-entreprise.evyooht.com/api/v1/products?per_page=5&sort=-name");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer evy_at_votre_jeton"],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);import requests
response = requests.get(
"https://votre-entreprise.evyooht.com/api/v1/products",
params={"per_page": 5, "sort": "-name"},
headers={"Authorization": "Bearer evy_at_votre_jeton"},
)
print(response.json())One record by its identifier:
curl "https://votre-entreprise.evyooht.com/api/v1/products/463" \
-H "Authorization: Bearer evy_at_votre_jeton"$headers = @{ Authorization = "Bearer evy_at_votre_jeton" }
Invoke-RestMethod -Uri "https://votre-entreprise.evyooht.com/api/v1/products/463" -Headers $headersconst response = await fetch("https://votre-entreprise.evyooht.com/api/v1/products/463", {
headers: { Authorization: "Bearer evy_at_votre_jeton" },
});
const data = await response.json();
console.log(data);$ch = curl_init("https://votre-entreprise.evyooht.com/api/v1/products/463");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer evy_at_votre_jeton"],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);import requests
response = requests.get(
"https://votre-entreprise.evyooht.com/api/v1/products/463",
headers={"Authorization": "Bearer evy_at_votre_jeton"},
)
print(response.json())Query parameters of the list#
All filters can be combined: they add up (AND). An unknown parameter or an invalid value is refused with a 400 error, never ignored.
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number, starting at 1. A page beyond the data is empty, not an error. At most 10000 pages: filter instead of paging deeper. Default: 1. |
per_page | integer | Results per page, from 1 to 100. Default: 25. |
sort | string | Sort fields, separated by commas, with - in front for descending order. Up to 3 fields. Sortable fields: id, name, code, updated_at. Default: name. Default: name. |
status | string | active (default), inactive, or all. When omitted, only active records are listed. Allowed values: active, inactive, all. |
type | string | Filter by type. Several values can be given, separated by commas. Allowed values: inventory, service, assembly, special. |
department_id | integer | Only products of this department. |
sub_department_id | integer | Only products of this sub-department. |
vendor_id | integer | Only records of this vendor. |
search | string | Text contained in the name, the code or the barcode. |
updated_since | string | Only records modified since this date (YYYY-MM-DD) or date and time (YYYY-MM-DDTHH:MM:SS). |
Response fields#
The list and the detail route return these fields. A field is null when the value is not recorded. A field marked needs permission is absent (not empty) unless your client holds that permission.
| Field | Type | Description |
|---|---|---|
id | integer | Unique identifier. Use it in the detail route. |
code | string| null | Product code (item code). |
barcode | string| null | Barcode, when one is recorded. |
name | string| null | Product name. |
description | string| null | Product description. |
type | "inventory"| "service" | "assembly" | "special" | null | Kind of item. inventory: stocked product. service: not stocked. assembly: built from other products. special: special item. |
status | "active"| "inactive" | null | active or inactive. |
size | string| null | Size, when recorded. |
attribute | string| null | Free attribute (color, variant…), when recorded. |
department_id | integer| null | Identifier of the department (see the departments resource). |
department | string| null | Name of the department. |
sub_department_id | integer| null | Identifier of the sub-department (see the sub-departments resource). |
sub_department | string| null | Name of the sub-department. |
unit | string| null | Main unit of sale (for example Box of 100). |
second_unit | string| null | Second unit of sale, when the product can be sold in two units. |
vendor_id | integer| null | Identifier of the usual vendor. 0 when none. |
reorder_point | number| null | Stock level under which the product should be reordered. |
currency | string| null | Currency of the product prices, when recorded. |
cost needs costs:read | number| null | Last purchase cost. Requires costs:read. |
average_cost needs costs:read | number| null | Weighted average cost. Requires costs:read. |
order_cost needs costs:read | number| null | Cost used on purchase orders. Requires costs:read. |
created_at | string(date-time, local) | null | When the record was created (local time, no time zone). |
updated_at | string(date-time, local) | null | When the record was last modified (local time, no time zone). Use it with the updated_since filter to synchronize. |
quantity_total | number| null | Quantity in stock. For a client restricted to some stores, the sum of those stores only. |
on_sales_order | number| null | Quantity reserved on open sales orders (same restriction as quantity_total). |
on_purchase_order | number| null | Quantity expected on open purchase orders (same restriction as quantity_total). |
price | number| null | Selling price applicable to your client. |
second_unit_price | number| null | Selling price of the second unit applicable to your client. |
price_lists | arrayof object | Price of each price list. Absent when your client is restricted to one price list. |
stock | arrayof object | Stock per store, limited to the stores your client may see. |
Example response: list#
{
"data": [
{
"id": 463,
"code": "PRD-0463",
"barcode": "6291041500213",
"name": "Thermal paper roll 57x18m",
"description": "Thermal paper roll for receipt printers",
"type": "inventory",
"status": "active",
"size": "",
"attribute": "",
"department_id": 3,
"department": "Office supplies",
"sub_department_id": 21,
"sub_department": "Paper",
"unit": "Box of 100",
"second_unit": "Box of 200",
"vendor_id": 12,
"reorder_point": 20,
"currency": "USD",
"cost": 90,
"average_cost": 92.5,
"order_cost": 90,
"created_at": "2026-03-15T14:30:00",
"updated_at": "2026-03-15T14:30:00",
"quantity_total": 12,
"on_sales_order": 4,
"on_purchase_order": 0,
"price": 125,
"second_unit_price": 250,
"price_lists": [
{
"price_list_id": 1,
"price": 125,
"second_unit_price": 250
},
{
"price_list_id": 2,
"price": 118,
"second_unit_price": 236
}
],
"stock": [
{
"store_id": 1,
"quantity": 8,
"on_sales_order": 3,
"on_purchase_order": 0
},
{
"store_id": 2,
"quantity": 4,
"on_sales_order": 1,
"on_purchase_order": 0
}
]
}
],
"meta": {
"page": 1,
"per_page": 25,
"total": 1,
"total_pages": 1
}
}Example response: one record#
{
"data": {
"id": 463,
"code": "PRD-0463",
"barcode": "6291041500213",
"name": "Thermal paper roll 57x18m",
"description": "Thermal paper roll for receipt printers",
"type": "inventory",
"status": "active",
"size": "",
"attribute": "",
"department_id": 3,
"department": "Office supplies",
"sub_department_id": 21,
"sub_department": "Paper",
"unit": "Box of 100",
"second_unit": "Box of 200",
"vendor_id": 12,
"reorder_point": 20,
"currency": "USD",
"cost": 90,
"average_cost": 92.5,
"order_cost": 90,
"created_at": "2026-03-15T14:30:00",
"updated_at": "2026-03-15T14:30:00",
"quantity_total": 12,
"on_sales_order": 4,
"on_purchase_order": 0,
"price": 125,
"second_unit_price": 250,
"price_lists": [
{
"price_list_id": 1,
"price": 125,
"second_unit_price": 250
},
{
"price_list_id": 2,
"price": 118,
"second_unit_price": 236
}
],
"stock": [
{
"store_id": 1,
"quantity": 8,
"on_sales_order": 3,
"on_purchase_order": 0
},
{
"store_id": 2,
"quantity": 4,
"on_sales_order": 1,
"on_purchase_order": 0
}
]
}
}Errors#
The error format and the full list of codes are described in Errors.
| Status | When |
|---|---|
400 | A query parameter is invalid or unknown. Every fault is reported at once in error.details. Nothing is ignored silently. |
401 | The token is missing, malformed, expired or revoked (missing_token or invalid_token). Ask for a new token. |
403 | Your token does not carry the permission this route needs (insufficient_scope), or the request was not made over HTTPS (https_required). |
429 | Too many requests this minute. Wait for the number of seconds in the Retry-After header. |