FURNIDESK / DEVELOPER PORTAL
Furnidesk API · Developer Portal
Безопасно синхронизируйте товары, ткани, изображения и остатки с вашим сайтом или приложением.
https://work.furnidesk.com/api/v1
Первое подключение
Администратор платформы включает API компании. Владелец или менеджер создаёт подключение для каждой исходной системы и временный ключ в панели интеграций. Секрет показывается один раз: храните его в переменной сервера и проверьте GET /me. Участник команды не создаётся; операции записываются от имени подключения.
curl "https://work.furnidesk.com/api/v1/me" \
-H "Authorization: Bearer $FURNIDESK_API_KEY"Авторизация и ключи
Authorization: Bearer обязателен. Сессия панели не авторизует API. Эффективные права — пересечение разрешений компании и ключа; каталог не включает цены/себестоимость. Срок1–365 дней, возможен отзыв. Не включайте секрет в код браузера. Отключение или истечение подписки блокирует доступ. Свой домен принимает только ключ своей компании.
| Scope | Доступ и поведение |
|---|---|
catalog:read | Читать каталог |
catalog:write | Изменять каталог |
prices:read | Читать цены |
prices:write | Изменять цены |
costs:read | Читать себестоимость |
costs:write | Изменять себестоимость |
media:read | Читать изображения |
media:write | Загружать изображения |
stock:read | Читать остатки |
stock:write | Изменять физический остаток |
Создание и обновление товаров
POST /products/upsert обновляет external_id либо создаёт товар. При создании нужны название и категория. single — отдельный товар, bundle — комплект отдельных товаров с количеством. Несколько категорий, одна коллекция и марка. expected_revision берётся из revision GET. Пропущенные поля сохраняются, переданные списки заменяются целиком. Неполные товары active:false. DELETE архивирует без изменения старых документов. Поставщики в v1 не изменяются.
{
"external_id": "sofa-100",
"expected_revision": 0,
"mode": "combined",
"name": "Solo Sofa",
"kind": "single",
"active": false,
"category_ids": [
{
"external_id": "sofas"
}
],
"price": "1000.00",
"currency": "EUR",
"unit": "piece",
"details": {
"width": "220.00",
"height": "85.00",
"depth": "95.00"
}
}Идентификаторы, связи и языки
external_id уникален для компании + подключения + ресурса. target_id связывает существующую запись впервые. Ссылки — UUID или объект external_id. Категории имеют родителей без циклов. Названия марок/коллекций общие; категории и описания переводятся. name/description — основной язык, translations — остальные активные. При отсутствии перевода используется основной язык.
{
"external_id": "sofas",
"name": "Koltuk",
"translations": {
"en": {
"name": "Sofas"
}
}
}Ткани и поля выбора
Импортируйте типы, группы, варианты. Для тканей pricing_scope:group задаёт none/percent/fixed на коллекции образцов, отдельный образец none. Другие типы могут использовать option. fields определяет независимые зоны с одним выбором из всех разрешённых коллекций на зону. Основная ткань и подлокотники отдельны.30% × coefficient 1 =30%, ×0.3 =9%. Явные коэффициенты требуют prices:write.
{
"external_id": "sofa-100",
"expected_revision": 3,
"fields": [
{
"external_id": "main-fabric",
"name": "Ana kumaş",
"type_id": {
"external_id": "fabric"
},
"group_ids": [
{
"external_id": "fabric-a"
},
{
"external_id": "fabric-b"
}
],
"coefficient": "1"
},
{
"external_id": "arm-fabric",
"name": "Kollar",
"type_id": {
"external_id": "fabric"
},
"group_ids": [
{
"external_id": "fabric-a"
}
],
"coefficient": "0.3"
}
]
}Цены, себестоимость и налоги
catalog меняет сведения, prices только существующие цены, combined оба набора с разными правами. Себестоимость требует costs:read/write. Деньги — десятичные строки в валюте компании без конвертации. Налоги должны быть настроены. tax_model inclusive/exclusive/none/unconfigured. Старые предложения и продажи сохраняют цены.
{
"external_id": "sofa-100",
"expected_revision": 4,
"mode": "prices",
"currency": "EUR",
"price": "1150.00",
"market_price": "1300.00"
}Остатки товаров и тканей
Нужны активный складской пакет, включённый модуль и права stock. Места записи назначаются подключению. source_kind:product/option для товаров или тканей. /inventory/{id} использует UUID складской карточки; balances содержит ревизии. set/in/out меняет физический остаток, сохраняет резерв и ожидаемое поступление, запрещает остаток ниже резерва или нуля. Новый баланс revision 0, уникальный event_id, целое количество для штук.
{
"event_id": "11111111-1111-4111-8111-111111111111",
"source_kind": "product",
"source_id": {
"external_id": "sofa-100"
},
"location_id": "22222222-2222-4222-8222-222222222222",
"action": "set",
"quantity": "5",
"revision": 0,
"note": "Website initial physical balance"
}Импорт изображений
POST /media принимает одно multipart-изображение с external_id/name. JPG/PNG/WebP до10 MB/40 MP; каталог1500 px, варианты720 px, без увеличения. Прозрачность и ориентация JPEG сохраняются; результат WebP. После202 дождитесь completed через /jobs/{id}, затем свяжите UUID/external_id. Обработка последовательная в папке компании. Новый контент/имя/назначение требует новой external_id. Загрузка URL и логотипов компаний в v1 отсутствует.
curl -X POST "https://work.furnidesk.com/api/v1/media" \
-H "Authorization: Bearer $FURNIDESK_API_KEY" \
-H "Idempotency-Key: image-sofa-100-v1" \
-F 'external_id=sofa-photo-100-v1' -F 'name=Solo Sofa Beige' \
-F 'purpose=catalog' -F '[email protected]'Пакетный импорт и синхронизация
/imports принимает1–50 записей. dry_run:true проверяет без сохранения; иначе202 и отчёт created/updated/unchanged/failed. Зависимости разрешаются за несколько проходов, каждая запись атомарна. GET-списки limit 1–100/next_cursor. Сохраните change_cursor и читайте /changes; продвигайте next_cursor даже для пустой отфильтрованной страницы. origin_connection_id предотвращает циклы. Отчёты доступны только тому же подключению.
{
"dry_run": true,
"records": [
{
"resource": "products",
"body": {
"external_id": "new-chair",
"name": "Chair",
"active": false,
"category_ids": [
{
"external_id": "new-chairs"
}
]
}
},
{
"resource": "categories",
"body": {
"external_id": "new-chairs",
"name": "Chairs"
}
}
]
}Ошибки, лимиты и повторы
POST/DELETE требуют Idempotency-Key 8–120 символов. Повторяйте одинаковые ключ/тело после временных ошибок.409 требует перечитать и разрешить конфликт.401 доступ,403 права,404 отсутствует,422 проверка,429 лимит,503 занято.120 запросов/ключ/минуту, компания5 раз больше; JSON 1 MB,20 открытых задач,100 MB изображений на подключение. Ошибка прав хранилища приостанавливает изображения до исправления оператором. /jobs/{id}/retry повторяет неудачную/приостановленную задачу активным ключом того же подключения.
Отправляйте User-Agent с названием и версией приложения, например FurnideskConnector/1.0. Ответ 403 без JSON или ответ HTML может поступить от сетевой защиты до проверки API. Проверьте Content-Type и обратитесь в поддержку.
{
"error": {
"code": "revision_conflict",
"message": "Read the current revision before updating.",
"details": {
"current_revision": 5
}
},
"request_id": "33333333-3333-4333-8333-333333333333"
}Версии и план развития
V 1 включает каталог, варианты, изображения, цены/себестоимость и необязательный склад. Заказы, клиенты и счета — будущие этапы без активных endpoints. Webhooks и готовые коннекторы планируются. Совместимые расширения в v1 без автоматических новых прав. Несовместимые изменения требуют новой основной версии. Статический портал не обращается к базе данных.
Справочник endpoints
Технические имена полей одинаковы во всех языках. OpenAPI описывает поля, ограничения, фильтры, примеры и ошибки.
| HTTP | Endpoint | Доступ и поведение |
|---|---|---|
| GET | /api/v1/products | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/products/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| DELETE | /api/v1/products/{id} | catalog:write. Soft archive; existing quotations and sales remain unchanged. |
| POST | /api/v1/products/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/categories | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/categories/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/categories/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/collections | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/collections/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/collections/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/brands | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/brands/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/brands/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/customization-types | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/customization-types/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/customization-types/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/customization-groups | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/customization-groups/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/customization-groups/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/customization-options | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/customization-options/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/customization-options/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/media | media:read |
| POST | /api/v1/media | media:write. One file per request; serial worker. JPG/PNG/WebP <=10 MB /40 MP; catalog max1500px, option max720px, no upscaling. Immutable image versions: changing content/name/purpose requires a new external_id. |
| GET | /api/v1/media/{id} | media:read |
| GET | /api/v1/me | Any valid key. |
| GET | /api/v1/changes | Resource read scope required. origin_connection_id supports loop prevention. Always persist next_cursor, even for an empty filtered page. |
| POST | /api/v1/imports | Write scopes for every record. |
| GET | /api/v1/imports | Any valid key; jobs from another connection remain hidden. |
| GET | /api/v1/jobs | Any valid key; jobs from another connection remain hidden. |
| GET | /api/v1/jobs/{id} | Any valid key on the same connection. Per-record failures appear in report counts. |
| GET | /api/v1/imports/{id} | Any valid key on the same connection. Per-record failures appear in report counts. |
| POST | /api/v1/jobs/{id}/retry | Required write scopes; a replacement active key must belong to the same connection. Storage authorization failures also require an operator to resume storage after fixing credentials. |
| POST | /api/v1/imports/{id}/retry | Required write scopes; a replacement active key must belong to the same connection. Storage authorization failures also require an operator to resume storage after fixing credentials. |
| GET | /api/v1/inventory | stock:read; stock package and module must be active. Detail balances contain revisions. Pagination cursor is inventory item UUID. |
| POST | /api/v1/inventory | stock:write; company must have active stock package and enabled module. Location must be assigned to connection. |
| GET | /api/v1/inventory/{id} | stock:read; id is inventory item UUID, not product UUID. |
Модели данных
Технические имена полей одинаковы во всех языках. OpenAPI описывает поля, ограничения, фильтры, примеры и ошибки.
Reference
{
"oneOf": [
{
"type": "string",
"format": "uuid"
},
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": ""
}
],
"description": "Furnidesk UUID or external ID in the current company + connection + resource namespace."
}SelectionField
{
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"name": {
"type": "string"
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"type_id": {
"$ref": "#/components/schemas/Reference"
},
"group_ids": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Reference"
},
"maxItems": 100
},
"coefficient": {
"type": "string",
"pattern": "^\\d+(\\.\\d{1,4})?$",
"description": "0 < coefficient <= 100. Explicit coefficients require prices:write. 1 × 30% = 30%; 0.3 × 30% = 9%."
}
},
"required": [
"name",
"type_id"
],
"additionalProperties": false,
"description": "Supply id or external_id. Each field permits one option across its groups; main fabric and arms are separate fields."
}Error
{
"type": "object",
"required": [
"error",
"request_id"
],
"properties": {
"request_id": {
"type": "string",
"format": "uuid"
},
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"details": {
"type": [
"object",
"array"
]
},
"retry_after": {
"type": "integer"
}
}
}
}
}Envelope
{
"type": "object",
"description": "Record fields are filtered by read scopes. Decimal values are strings. All responses include request_id; successful writes can include replayed:true.",
"properties": {
"request_id": {
"type": "string",
"format": "uuid"
},
"outcome": {
"enum": [
"created",
"updated",
"unchanged"
]
},
"record": {
"type": "object"
},
"records": {
"type": "array",
"items": {
"type": "object"
}
},
"job": {
"type": "object"
},
"principal": {
"type": "object"
},
"report": {
"type": "object"
},
"next_cursor": {
"type": [
"string",
"null"
]
},
"has_more": {
"type": "boolean"
},
"replayed": {
"type": "boolean"
}
}
}products-upsert
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"target_id": {
"type": "string",
"format": "uuid"
},
"expected_revision": {
"type": "integer",
"minimum": 0,
"description": "Required for updates; read record.revision first. Use 0 or omit for a new record."
},
"mode": {
"enum": [
"catalog",
"prices",
"combined"
],
"default": "catalog"
},
"currency": {
"type": "string",
"description": "Must equal company currency when provided."
},
"name": {
"type": "string",
"maxLength": 200
},
"sku": {
"type": "string",
"maxLength": 100
},
"kind": {
"enum": [
"single",
"bundle"
]
},
"active": {
"type": "boolean",
"default": false
},
"category_ids": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Reference"
},
"minItems": 1,
"maxItems": 50
},
"collection_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
},
"brand_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"details": {
"type": "object",
"properties": {
"description": {
"type": "string",
"maxLength": 4000
},
"width": {
"type": [
"string",
"null"
],
"description": "Non-negative decimal; cm for width/height/depth, kg for weight, m³ for packaged volume."
},
"height": {
"type": [
"string",
"null"
],
"description": "Non-negative decimal; cm for width/height/depth, kg for weight, m³ for packaged volume."
},
"depth": {
"type": [
"string",
"null"
],
"description": "Non-negative decimal; cm for width/height/depth, kg for weight, m³ for packaged volume."
},
"weight": {
"type": [
"string",
"null"
],
"description": "Non-negative decimal; cm for width/height/depth, kg for weight, m³ for packaged volume."
},
"volume": {
"type": [
"string",
"null"
],
"description": "Non-negative decimal; cm for width/height/depth, kg for weight, m³ for packaged volume."
}
},
"required": [],
"additionalProperties": false,
"description": ""
},
"unit": {
"enum": [
"piece",
"metre",
"sqm",
"cbm"
]
},
"components": {
"type": "array",
"items": {
"type": "object",
"properties": {
"product_id": {
"$ref": "#/components/schemas/Reference"
},
"quantity": {
"type": "integer",
"minimum": 1,
"maximum": 10000
}
},
"required": [
"product_id",
"quantity"
],
"additionalProperties": false,
"description": ""
},
"maxItems": 100
},
"media_ids": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Reference"
},
"maxItems": 50
},
"cover_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
},
"fields": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SelectionField"
},
"maxItems": 100
},
"price": {
"type": "string",
"pattern": "^\\d{1,16}(\\.\\d{1,2})?$",
"description": "Non-negative decimal string. Money uses the company currency; no currency conversion."
},
"market_price": {
"oneOf": [
{
"type": "string",
"pattern": "^\\d{1,16}(\\.\\d{1,2})?$",
"description": "Non-negative decimal string. Money uses the company currency; no currency conversion."
},
{
"type": "null"
}
]
},
"cost": {
"oneOf": [
{
"type": "string",
"pattern": "^\\d{1,16}(\\.\\d{1,2})?$",
"description": "Non-negative decimal string. Money uses the company currency; no currency conversion."
},
{
"type": "null"
}
]
},
"tax_rate": {
"oneOf": [
{
"type": "string",
"pattern": "^\\d{1,16}(\\.\\d{1,2})?$",
"description": "Non-negative decimal string. Money uses the company currency; no currency conversion."
},
{
"type": "null"
}
]
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": "Partial update. New records also require name and relevant category/type/group references. catalog mode excludes price/cost; prices mode changes only price fields on existing records; combined allows both with separate scopes."
}categories-upsert
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"target_id": {
"type": "string",
"format": "uuid"
},
"expected_revision": {
"type": "integer",
"minimum": 0,
"description": "Required for updates; read record.revision first. Use 0 or omit for a new record."
},
"mode": {
"enum": [
"catalog",
"prices",
"combined"
],
"default": "catalog"
},
"currency": {
"type": "string",
"description": "Must equal company currency when provided."
},
"name": {
"type": "string"
},
"parent_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"description": {
"type": "string"
},
"image_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": "Partial update. New records also require name and relevant category/type/group references. catalog mode excludes price/cost; prices mode changes only price fields on existing records; combined allows both with separate scopes."
}collections-upsert
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"target_id": {
"type": "string",
"format": "uuid"
},
"expected_revision": {
"type": "integer",
"minimum": 0,
"description": "Required for updates; read record.revision first. Use 0 or omit for a new record."
},
"mode": {
"enum": [
"catalog",
"prices",
"combined"
],
"default": "catalog"
},
"currency": {
"type": "string",
"description": "Must equal company currency when provided."
},
"name": {
"type": "string"
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"description": {
"type": "string"
},
"image_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": "Partial update. New records also require name and relevant category/type/group references. catalog mode excludes price/cost; prices mode changes only price fields on existing records; combined allows both with separate scopes."
}brands-upsert
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"target_id": {
"type": "string",
"format": "uuid"
},
"expected_revision": {
"type": "integer",
"minimum": 0,
"description": "Required for updates; read record.revision first. Use 0 or omit for a new record."
},
"mode": {
"enum": [
"catalog",
"prices",
"combined"
],
"default": "catalog"
},
"currency": {
"type": "string",
"description": "Must equal company currency when provided."
},
"name": {
"type": "string"
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"description": {
"type": "string"
},
"image_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": "Partial update. New records also require name and relevant category/type/group references. catalog mode excludes price/cost; prices mode changes only price fields on existing records; combined allows both with separate scopes."
}customization-types-upsert
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"target_id": {
"type": "string",
"format": "uuid"
},
"expected_revision": {
"type": "integer",
"minimum": 0,
"description": "Required for updates; read record.revision first. Use 0 or omit for a new record."
},
"mode": {
"enum": [
"catalog",
"prices",
"combined"
],
"default": "catalog"
},
"currency": {
"type": "string",
"description": "Must equal company currency when provided."
},
"name": {
"type": "string"
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"pricing_scope": {
"enum": [
"group",
"option"
]
},
"archived": {
"type": "boolean"
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": "Partial update. New records also require name and relevant category/type/group references. catalog mode excludes price/cost; prices mode changes only price fields on existing records; combined allows both with separate scopes."
}customization-groups-upsert
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"target_id": {
"type": "string",
"format": "uuid"
},
"expected_revision": {
"type": "integer",
"minimum": 0,
"description": "Required for updates; read record.revision first. Use 0 or omit for a new record."
},
"mode": {
"enum": [
"catalog",
"prices",
"combined"
],
"default": "catalog"
},
"currency": {
"type": "string",
"description": "Must equal company currency when provided."
},
"name": {
"type": "string"
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"type_id": {
"$ref": "#/components/schemas/Reference"
},
"active": {
"type": "boolean"
},
"photo_enabled": {
"type": "boolean"
},
"pricing_mode": {
"enum": [
"none",
"percent",
"fixed"
]
},
"pricing_value": {
"type": "string",
"pattern": "^\\d{1,16}(\\.\\d{1,2})?$",
"description": "Non-negative decimal string. Money uses the company currency; no currency conversion."
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": "Partial update. New records also require name and relevant category/type/group references. catalog mode excludes price/cost; prices mode changes only price fields on existing records; combined allows both with separate scopes."
}customization-options-upsert
{
"type": "object",
"properties": {
"external_id": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"target_id": {
"type": "string",
"format": "uuid"
},
"expected_revision": {
"type": "integer",
"minimum": 0,
"description": "Required for updates; read record.revision first. Use 0 or omit for a new record."
},
"mode": {
"enum": [
"catalog",
"prices",
"combined"
],
"default": "catalog"
},
"currency": {
"type": "string",
"description": "Must equal company currency when provided."
},
"name": {
"type": "string"
},
"translations": {
"type": "object",
"description": "Enabled language codes only; main name/description fields use the company base language. Category names are translated; collection/brand names are shared. Omitted fields are preserved.",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"group_id": {
"$ref": "#/components/schemas/Reference"
},
"code": {
"type": "string"
},
"active": {
"type": "boolean"
},
"media_id": {
"oneOf": [
{
"$ref": "#/components/schemas/Reference"
},
{
"type": "null"
}
]
},
"pricing_mode": {
"enum": [
"none",
"percent",
"fixed"
]
},
"pricing_value": {
"type": "string",
"pattern": "^\\d{1,16}(\\.\\d{1,2})?$",
"description": "Non-negative decimal string. Money uses the company currency; no currency conversion."
}
},
"required": [
"external_id"
],
"additionalProperties": false,
"description": "Partial update. New records also require name and relevant category/type/group references. catalog mode excludes price/cost; prices mode changes only price fields on existing records; combined allows both with separate scopes."
}StockWrite
{
"type": "object",
"properties": {
"event_id": {
"type": "string",
"format": "uuid"
},
"source_kind": {
"enum": [
"product",
"option"
]
},
"source_id": {
"$ref": "#/components/schemas/Reference"
},
"location_id": {
"type": "string",
"format": "uuid"
},
"action": {
"enum": [
"set",
"in",
"out"
]
},
"quantity": {
"type": "string",
"pattern": "^\\d{1,9}(\\.\\d{1,3})?$"
},
"revision": {
"type": "integer",
"minimum": 0
},
"note": {
"type": "string",
"minLength": 1,
"maxLength": 1000
}
},
"required": [
"event_id",
"source_kind",
"source_id",
"location_id",
"action",
"quantity",
"revision",
"note"
],
"additionalProperties": false,
"description": "Updates physical stock only in an assigned location. Preserves reserved and incoming stock. Cannot reduce physical below reserved or zero. Pieces must be whole numbers. A new balance uses revision 0. Read inventory/{item_id} balances for subsequent revisions."
}Import
{
"type": "object",
"properties": {
"dry_run": {
"type": "boolean",
"default": false
},
"records": {
"type": "array",
"minItems": 1,
"maxItems": 50,
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"resource": {
"const": "products"
},
"body": {
"$ref": "#/components/schemas/products-upsert"
}
},
"required": [
"resource",
"body"
],
"additionalProperties": false,
"description": ""
},
{
"type": "object",
"properties": {
"resource": {
"const": "categories"
},
"body": {
"$ref": "#/components/schemas/categories-upsert"
}
},
"required": [
"resource",
"body"
],
"additionalProperties": false,
"description": ""
},
{
"type": "object",
"properties": {
"resource": {
"const": "collections"
},
"body": {
"$ref": "#/components/schemas/collections-upsert"
}
},
"required": [
"resource",
"body"
],
"additionalProperties": false,
"description": ""
},
{
"type": "object",
"properties": {
"resource": {
"const": "brands"
},
"body": {
"$ref": "#/components/schemas/brands-upsert"
}
},
"required": [
"resource",
"body"
],
"additionalProperties": false,
"description": ""
},
{
"type": "object",
"properties": {
"resource": {
"const": "customization-types"
},
"body": {
"$ref": "#/components/schemas/customization-types-upsert"
}
},
"required": [
"resource",
"body"
],
"additionalProperties": false,
"description": ""
},
{
"type": "object",
"properties": {
"resource": {
"const": "customization-groups"
},
"body": {
"$ref": "#/components/schemas/customization-groups-upsert"
}
},
"required": [
"resource",
"body"
],
"additionalProperties": false,
"description": ""
},
{
"type": "object",
"properties": {
"resource": {
"const": "customization-options"
},
"body": {
"$ref": "#/components/schemas/customization-options-upsert"
}
},
"required": [
"resource",
"body"
],
"additionalProperties": false,
"description": ""
}
]
}
}
},
"required": [
"records"
],
"additionalProperties": false,
"description": "Dry run returns a synchronous report and rolls back all changes. Otherwise 202 creates a job. Records resolve dependencies in multiple passes; each record is atomic; failures do not cancel other records."
}