FURNIDESK / DEVELOPER PORTAL

Furnidesk API · Developer Portal

Ürünlerinizi, kumaş seçeneklerinizi, görsellerinizi ve stoklarınızı kendi web siteniz veya uygulamanızla güvenli biçimde eşitleyin.

API v1REST · JSONOpenAPI 3.1

https://work.furnidesk.com/api/v1

İlk bağlantı

Ana Furnidesk yöneticisi önce firmaya API izni verir ve kullanılabilecek kapsamları seçer. Firma sahibi veya sistem yöneticisi, firma panelindeki API entegrasyonları bölümünden bir bağlantı oluşturur. Her bağımsız kaynak sistem için ayrı bağlantı kullanın.

Bağlantının altında ihtiyacınız olan kapsamlarla süreli bir anahtar oluşturun. Anahtar yalnız oluşturulduğu anda gösterilir; sunucunuzun ortam değişkeninde saklayın. İlk doğrulama için GET /me çağrısını kullanın. API firma ekip üyesi oluşturmaz; işlemler bağlantı adıyla geçmişe kaydedilir.

curl "https://work.furnidesk.com/api/v1/me" \
  -H "Authorization: Bearer $FURNIDESK_API_KEY"

Yetkilendirme ve anahtarlar

Her istekte Authorization: Bearer anahtar başlığı zorunludur. Panel oturumu API yetkisi sağlamaz. Anahtar, bağlantı, firma izni veya abonelik kapatıldığında erişim kesilir. Firma kimliği anahtardan belirlenir; istemcinin gönderdiği firma kimliğiyle değiştirilmez.

Firma izni anahtarın erişebileceği üst sınırdır. Kapsamlar okuma/yazma olarak ayrıdır; ürün okuma izni fiyat veya maliyeti kendiliğinden açmaz. Anahtarlar 1–365 gün geçerlidir ve panelden iptal edilebilir. Anahtarı JavaScript tarayıcı koduna, mobil uygulama paketine veya herkese açık kaynak koduna koymayın. Custom domain üzerinden çağrıda anahtar o firmaya ait olmalıdır.

ScopeYetki ve davranış
catalog:readKatalog okuma
catalog:writeKatalog yazma
prices:readFiyat okuma
prices:writeFiyat yazma
costs:readMaliyet okuma
costs:writeMaliyet yazma
media:readGörsel okuma
media:writeGörsel yükleme
stock:readStok okuma
stock:writeFiziksel stok yazma

Ürün ekleme ve güncelleme

POST /products/upsert dış kimliği bulunan ürünü günceller; yoksa yeni ürün açar. Yeni üründe ad ve en az bir gerçek kategori gerekir. single direkt üründür; bundle başka direkt ürünleri adetleriyle bağlayan takımdır. Birden fazla kategori, tek koleksiyon ve tek marka bağlanabilir. V 1 tedarikçi yazımı içermez; var olan tedarikçi bilgisi korunur.

Güncellemede GET ile alınan revision değerini expected_revision olarak gönderin. Gönderilmeyen alanlar korunur; açıkça null kabul eden alanlar null ile temizlenir. Kategoriler, görseller, bileşenler ve fields listeleri gönderildiğinde bütün liste değiştirilir. Eksik ürünler active:false kaydedilebilir; aktifleştirme mevcut satış kontrollerine tabidir. DELETE /products/{id} ürünü arşivler, geçmiş satış belgelerini değiştirmez.

{
    "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"
    }
}

Kimlikler, ilişkiler ve diller

external_id, firma + bağlantı + kaynak türü içinde benzersizdir; SKU yerine kalıcı kaynak kimliğinizi kullanın. İlk bağlamada target_id ile mevcut Furnidesk kaydına eşleme yapılabilir. Eşleme başka kayda taşınamaz. İlişkiler Furnidesk UUID veya {external_id: kaynak-kimliği} ile gönderilebilir.

Kategoriler üst/alt kategori destekler ve döngü oluşturamaz. Koleksiyon ve markalarda tek ortak ad vardır; açıklamalar çevrilebilir. Kategorilerin adları çevrilebilir. Ana dil içeriği name/description alanlarında, etkin ek diller translations içinde tutulur. Ek dil içeriği yoksa uygulama ana dile döner. Örnekler tr/en dilleri etkin ve ana dili tr olan firma içindir.

{
    "external_id": "sofas",
    "name": "Koltuk",
    "translations": {
        "en": {
            "name": "Sofas"
        }
    }
}

Kumaşlar ve seçim alanları

Önce customization-types, ardından customization-groups, sonra customization-options aktarılır. Kumaş tipinde pricing_scope:group seçin: kartela fiyat kuralını taşır, kumaş örneklerinin fiyat kuralı none olur. Diğer seçeneklerde pricing_scope:option kullanıldığında fark seçenek üzerinde tanımlanır. Fiyat farkı none, percent veya fixed olabilir.

Üründeki fields alanı her seçim bölgesini tanımlar. Bir alana birden fazla kartela bağlanabilir fakat satışta o alan için yalnız tek karteladan tek kumaş seçilir. Ana kumaş ve kollar için ayrı alanlar kullanın. %30 kartela farkında coefficient:1 toplamda %30, coefficient:0.3 o bölge için %9 fark üretir. Katsayı yazımı prices:write gerektirir. fields boş liste ile tüm seçim alanları kaldırılır.

{
    "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"
        }
    ]
}

Fiyat, maliyet ve vergi

mode:catalog ürün bilgilerini günceller ve fiyat/maliyet alanlarını kabul etmez. mode:prices yalnız mevcut eşlenmiş kaydın fiyatlarını değiştirir; ad, kategori ve kumaş bağlantıları korunur. mode:combined iki alanı birlikte değiştirir, ilgili kapsamlar ayrı ayrı gerekir. Maliyet için ayrıca costs:write ve görüntüleme için costs:read gerekir.

Parasal değerleri ondalık metin olarak gönderin: 1000.00. Para birimi firma katalog para birimiyle aynı olmalıdır; bu API kur çevrimi yapmaz. Ürün KDV oranı firmanın satış ayarlarında tanımlı olmalıdır. price alanını dönen tax_model ile yorumlayın: inclusive vergi dahil, exclusive hariç, none vergisiz, unconfigured henüz tanımlanmamış modeldir. Fiyat güncellemesi eski teklif/satışların kayıtlı fiyatlarını değiştirmez.

{
    "external_id": "sofa-100",
    "expected_revision": 4,
    "mode": "prices",
    "currency": "EUR",
    "price": "1150.00",
    "market_price": "1300.00"
}

Ürün ve kumaş stokları

Stok aktarımı için etkin stok paketi, açık stok modülü ve stock:read/stock:write yetkileri gerekir. Stoksuz katalog aktarımı bu yetkileri gerektirmez. Bağlantıda stok yazılabilecek konumları firma yöneticisi seçer; farklı konuma yazım reddedilir. Direkt ürün ve kumaş/seçenek stokları source_kind:product veya option ile bağlanır.

GET /inventory stok kartlarını listeler; /inventory/{id} içindeki id ürün UUID’si değil stok kartı UUID’sidir. Detay balances alanında konum revision değerini alın. POST /inventory action:set/in/out fiziksel miktarı değiştirir; ayrılmış ve yoldaki miktarları korur. Fiziksel stok eksiye veya ayrılmış miktarın altına düşürülemez. Her olay için yeni event_id, ilk bakiye için revision:0 kullanın; adet birimi tam sayı olmalıdır.

{
    "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"
}

Görsellerin aktarımı

POST /media multipart/form-data ile bir fotoğraf alır. external_id ve name zorunludur. JPG, PNG veya WebP; en fazla 10 MB ve 40 megapiksel kabul edilir. purpose:catalog uzun kenarı 1500 piksele, purpose:option 720 piksele sınırlar. Küçük görseller büyütülmez; şeffaflık ve JPEG yönü korunur. Sonuç WebP olur.

202 yanıtındaki job.id için GET /jobs/{id} ile completed durumunu bekleyin. Fotoğrafı sonra UUID veya dış kimliğiyle ürüne, kategoriye, markaya, koleksiyona veya kumaş seçeneğine bağlayın. Dosyalar firma storage_slug dizininde okunabilir adla, tek tek ve sırayla işlenir. Aynı görüntü sürümü yeniden yüklenmez; içeriği/adı/amacı değişen görsel yeni external_id kullanmalıdır. Bu sürüm uzaktan URL indirmesi veya firma logosu aktarımı içermez.

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]'

Toplu aktarım ve çift yönlü eşitleme

POST /imports en fazla 50 kayıt alır. dry_run:true doğrulama raporu verir ve değişiklikleri kaydetmez. Normal aktarım 202 ile iş oluşturur; raporda created, updated, unchanged ve failed sayıları ile kayıt başına nedenler bulunur. Bağımlı kayıtlar birden fazla geçişte çözülür; kategori üründen sonra gönderilebilir. Her kayıt kendi içinde atomiktir, bir kayıt hatası diğer geçerli kayıtları durdurmaz.

İlk dışa aktarımda GET liste endpointlerini limit:1–100 ve dönen next_cursor ile tamamlayın. İlk listenin change_cursor değerini saklayıp GET /changes ile sonradan oluşan değişiklikleri okuyun. Boş filtrelenmiş sayfada da next_cursor ilerletilir. origin_connection_id kendi yazımlarınızı tanıyıp döngüyü engellemenizi sağlar. RLS ve anahtar kapsamları firma dışı veriyi engeller. İş raporları yalnız aynı bağlantıda görünür; gerçek iki yönlü birleştirme politikasını bağlantı uygulamanız seçmelidir.

{
    "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"
            }
        }
    ]
}

Hatalar, limitler ve güvenli tekrar

POST ve DELETE çağrılarında 8–120 karakterli Idempotency-Key zorunludur. Aynı anahtarı aynı gövdeyle tekrar gönderin; tamamlanan işlem tekrarlanmaz. Anahtar farklı gövdede kullanılırsa 409 döner. Güncel revision olmadan güncelleme yapılmaz; 409 durumunda kaydı tekrar okuyup çakışmayı çözün. Geçici bağlantı hatalarında aynı idempotency anahtarıyla sınırlı, artan bekleme uygulayın.

401 geçersiz erişim; 403 yetersiz yetki/modül; 404 eksik kayıt; 409 çakışma; 422 doğrulama; 429 limit; 503 geçici veritabanı yoğunluğu anlamına gelir. Yanıtta request_id, hata kodu, açıklama ve gerektiğinde details bulunur. Destek için anahtar yerine request_id paylaşın. Varsayılan anahtar limiti dakikada 120, toplam firma limiti bunun 5 katıdır. JSON sınırı 1 MB; bağlantı başına en fazla 20 bekleyen iş ve 100 MB görsel kuyruğu vardır. Depolama yetki hatasında görsel aktarımı durur; yetkiler düzeltildikten sonra operatör devam ettirir. Başarısız/duraklayan iş /jobs/{id}/retry ile aynı bağlantının etkin anahtarıyla yeniden başlatılabilir.

İstemcinizin uygulama adı ve sürümünü belirten bir User-Agent gönderin; örnek: FurnideskConnector/1.0. JSON dışı 403 veya HTML yanıtı, API doğrulamasından önce ağ güvenlik katmanından gelebilir. Yanıtın Content-Type değerini kontrol edin ve durumu destek ekibine bildirin.

{
    "error": {
        "code": "revision_conflict",
        "message": "Read the current revision before updating.",
        "details": {
            "current_revision": 5
        }
    },
    "request_id": "33333333-3333-4333-8333-333333333333"
}

Sürüm politikası ve yol haritası

API v1 ürün, kategori, koleksiyon, marka, özelleştirme, görsel, fiyat/maliyet ve isteğe bağlı stok entegrasyonlarını kapsar. Sipariş, müşteri ve cari hesap entegrasyonları sonraki fazlardır; şu an bu kaynaklar için açık endpoint yoktur. Webhook ve hazır platform bağlayıcıları da sonraki faz kapsamındadır; v1 değişiklik akışı sorgulanarak çalışır.

Uyumlu yeni kaynaklar ve kapsamlar v1 altında eklenebilir; mevcut anahtarlara yeni yetkiler kendiliğinden verilmez. Davranışı bozan değişiklikler yeni ana sürümde yayınlanır. OpenAPI dosyası uygulama sürümüyle birlikte üretilir; bu portal statik yayınlandığından ziyaretçiler için veritabanı sorgusu oluşturmaz.

Endpoint referansı

Aşağıdaki teknik sözleşme tüm dillerde aynı alan adlarını kullanır. OpenAPI dosyasında istek alanları, zorunluluklar, örnekler, filtreler ve hata yanıtları bulunur.

HTTPEndpointYetki ve davranış
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.

Veri modelleri

Aşağıdaki teknik sözleşme tüm dillerde aynı alan adlarını kullanır. OpenAPI dosyasında istek alanları, zorunluluklar, örnekler, filtreler ve hata yanıtları bulunur.

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."
}