FURNIDESK / DEVELOPER PORTAL

Furnidesk API · Developer Portal

Synchronisez vos produits, tissus, images et stocks en toute sécurité avec votre site ou application.

API v1REST · JSONOpenAPI 3.1

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

Première connexion

L’administrateur active l’API de votre entreprise. Le propriétaire ou responsable crée une connexion par système source et une clé limitée dans le panneau API. Le secret apparaît une seule fois : conservez-le dans une variable serveur et vérifiez GET /me. Aucun compte d’équipe n’est créé ; les opérations portent le nom de la connexion.

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

Authentification et clés

Authorization: Bearer est obligatoire. La session du panneau ne suffit pas. Les droits effectifs combinent ceux de l’entreprise et de la clé ; lire le catalogue ne donne pas accès aux prix/coûts. Durée 1–365 jours, révocation possible. Ne placez jamais la clé dans le navigateur. Une désactivation ou un abonnement expiré bloque l’accès. Le domaine personnalisé accepte uniquement la clé de son entreprise.

ScopeAccès et comportement
catalog:readLire catalogue
catalog:writeÉcrire catalogue
prices:readLire prix
prices:writeÉcrire prix
costs:readLire coûts
costs:writeÉcrire coûts
media:readLire images
media:writeImporter images
stock:readLire stock
stock:writeÉcrire stock physique

Créer et actualiser les produits

POST /products/upsert actualise external_id ou crée un produit. Nom et catégorie sont obligatoires à la création. single est un produit direct ; bundle regroupe des produits directs avec quantités. Plusieurs catégories, une collection et une marque possibles. expected_revision reprend revision du GET. Les champs omis restent inchangés ; les listes envoyées remplacent les listes complètes. active:false permet un produit incomplet. DELETE archive sans modifier les documents historiques. V 1 ne modifie pas les fournisseurs.

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

Identifiants, relations et langues

external_id est unique par entreprise + connexion + ressource. target_id permet la première association à un enregistrement existant. Les références acceptent UUID ou objet external_id. Les catégories ont des parents sans cycles. Les noms de marques/collections sont communs ; noms de catégories et descriptions sont traduisibles. name/description portent la langue principale, translations les autres langues actives. Une traduction manquante utilise la langue principale.

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

Tissus et zones de sélection

Importez types, groupes puis options. Pour les tissus, pricing_scope:group place la règle none/percent/fixed sur le nuancier et none sur l’échantillon. Les autres types peuvent utiliser option. Les fields du produit définissent des zones indépendantes ; une seule option parmi tous les nuanciers autorisés par zone. Tissu principal et accoudoirs sont distincts. 30% × coefficient 1 donne 30%, ×0.3 donne 9%. Les coefficients explicites nécessitent 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"
        }
    ]
}

Prix, coûts et taxes

catalog modifie les informations, prices uniquement les prix des associations existantes, combined les deux selon les permissions. Les coûts exigent costs:read/write. Montants en chaînes décimales dans la devise de l’entreprise ; aucune conversion. Les taxes doivent être configurées. tax_model vaut inclusive/exclusive/none/unconfigured. Les anciens devis et ventes conservent leurs prix.

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

Stocks de produits et tissus

Un forfait stock actif, un module activé et des droits stock sont requis. Les lieux autorisés sont associés à la connexion. source_kind:product/option lie produits ou tissus. /inventory/{id} utilise l’UUID de la fiche de stock ; balances contient les révisions. set/in/out change le physique, conserve réservations et arrivages, interdit physique sous réservations ou zéro. Nouvelle balance revision 0, event_id unique, quantités entières pour les pièces.

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

Importer des images

POST /media : un fichier multipart, external_id et name obligatoires. JPG/PNG/WebP jusqu’à10 MB/40 MP ;1500 px catalogue et 720 px options, sans agrandissement. Transparence et orientation JPEG conservées ; sortie WebP. Après 202, attendez completed via /jobs/{id} puis associez UUID/external_id. Les fichiers sont traités séquentiellement dans le dossier entreprise. Nouvelle external_id si contenu/nom/usage change. Aucun téléchargement URL ou logo entreprise en 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]'

Importation groupée et synchronisation

/imports accepte 1–50 lignes. dry_run:true valide sans sauvegarder ; sinon 202 et rapport created/updated/unchanged/failed via /jobs/{id}. Les dépendances se résolvent sur plusieurs passes, chaque ligne est atomique. Parcourez les listes avec limit 1–100 et next_cursor. Sauvegardez change_cursor puis interrogez /changes ; avancez next_cursor même sur une page filtrée vide. origin_connection_id évite les boucles. Les rapports restent privés à la connexion.

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

Erreurs, limites et reprises

POST/DELETE exigent Idempotency-Key 8–120 caractères. Réessayez avec le même contenu et la même clé. 409 impose de relire et résoudre le conflit. 401 accès,403 droits,404 absent,422 validation,429 limite,503 temporairement occupé. Limites :120/clé/minute, entreprise 5 fois plus ; JSON 1 MB,20 tâches ouvertes et 100 MB d’images par connexion. Une erreur d’autorisation stockage suspend les images jusqu’à réparation opérateur. POST /jobs/{id}/retry reprend une tâche échouée/suspendue avec une clé active de la même connexion.

Envoyez un User-Agent indiquant le nom et la version de votre application, par exemple FurnideskConnector/1.0. Une réponse 403 non JSON ou HTML peut provenir de la protection réseau avant la validation API. Vérifiez le Content-Type et contactez le support.

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

Versions et feuille de route

V 1 couvre catalogue, options, images, prix/coûts et stock facultatif. Commandes, clients et comptes seront ajoutés ultérieurement ; aucun endpoint actif pour ces ressources. Webhooks et connecteurs sont prévus. Les extensions compatibles restent en v1 ; aucune nouvelle permission automatique. Une rupture exige une nouvelle version majeure. Le portail statique ne consulte pas la base de données.

Référence des endpoints

Les noms techniques restent identiques dans toutes les langues. OpenAPI décrit les champs, contraintes, filtres, exemples et erreurs.

HTTPEndpointAccès et comportement
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.

Modèles de données

Les noms techniques restent identiques dans toutes les langues. OpenAPI décrit les champs, contraintes, filtres, exemples et erreurs.

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