FURNIDESK / DEVELOPER PORTAL

Furnidesk API · Developer Portal

Безопасно синхронизируйте товары, ткани, изображения и остатки с вашим сайтом или приложением.

API v1REST · JSONOpenAPI 3.1

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 описывает поля, ограничения, фильтры, примеры и ошибки.

HTTPEndpointДоступ и поведение
GET/api/v1/productscatalog: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/upsertcatalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection.
GET/api/v1/categoriescatalog: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/upsertcatalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection.
GET/api/v1/collectionscatalog: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/upsertcatalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection.
GET/api/v1/brandscatalog: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/upsertcatalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection.
GET/api/v1/customization-typescatalog: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/upsertcatalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection.
GET/api/v1/customization-groupscatalog: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/upsertcatalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection.
GET/api/v1/customization-optionscatalog: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/upsertcatalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection.
GET/api/v1/mediamedia:read
POST/api/v1/mediamedia: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/meAny valid key.
GET/api/v1/changesResource read scope required. origin_connection_id supports loop prevention. Always persist next_cursor, even for an empty filtered page.
POST/api/v1/importsWrite scopes for every record.
GET/api/v1/importsAny valid key; jobs from another connection remain hidden.
GET/api/v1/jobsAny 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}/retryRequired 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}/retryRequired 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/inventorystock:read; stock package and module must be active. Detail balances contain revisions. Pagination cursor is inventory item UUID.
POST/api/v1/inventorystock: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."
}