FURNIDESK / DEVELOPER PORTAL
Furnidesk API · Developer Portal
Sincronice productos, tejidos, imágenes y existencias de forma segura con su sitio o aplicación.
https://work.furnidesk.com/api/v1
Primera conexión
El administrador habilita la API de la empresa. El propietario o responsable crea una conexión por sistema de origen y una clave temporal. El secreto se muestra una vez: guárdelo en una variable del servidor y pruebe GET /me. No se crea un usuario de equipo; las operaciones se atribuyen a la conexión.
curl "https://work.furnidesk.com/api/v1/me" \
-H "Authorization: Bearer $FURNIDESK_API_KEY"Autenticación y claves
Authorization: Bearer es obligatorio; la sesión del panel no autoriza la API. Los permisos efectivos combinan empresa y clave; catálogo no incluye precios/costes. Duración 1–365 días, revocación disponible. Nunca incluya la clave en el navegador. Desactivar permisos o vencer la suscripción bloquea el acceso. Un dominio personalizado acepta solo claves de su empresa.
| Scope | Acceso y comportamiento |
|---|---|
catalog:read | Leer catálogo |
catalog:write | Escribir catálogo |
prices:read | Leer precios |
prices:write | Escribir precios |
costs:read | Leer costes |
costs:write | Escribir costes |
media:read | Leer imágenes |
media:write | Subir imágenes |
stock:read | Leer stock |
stock:write | Escribir stock físico |
Crear y actualizar productos
POST /products/upsert actualiza external_id o crea. Nombre y categoría obligatorios al crear. single es directo, bundle agrupa productos directos con cantidades. Varias categorías, una colección y una marca. expected_revision usa revision leído por GET. Campos omitidos se conservan; listas enviadas reemplazan listas completas. active:false permite productos incompletos. DELETE archiva sin cambiar documentos históricos. V 1 no modifica proveedores.
{
"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"
}
}Identificadores, relaciones e idiomas
external_id es único por empresa + conexión + recurso. target_id asocia inicialmente un registro existente. Referencias UUID u objeto external_id. Categorías con padres sin ciclos. Nombres de marcas/colecciones comunes; categorías y descripciones traducibles. name/description en idioma principal; translations en otros idiomas activos. Sin traducción se usa el principal.
{
"external_id": "sofas",
"name": "Koltuk",
"translations": {
"en": {
"name": "Sofas"
}
}
}Tejidos y campos de selección
Importe tipos, grupos y opciones. Tejidos usan pricing_scope:group con regla none/percent/fixed en el muestrario y none en la muestra. Otros tipos pueden usar option. fields define zonas independientes, una elección entre todos los muestrarios permitidos por zona. Tejido principal y brazos separados. 30% × coefficient 1 =30%, ×0.3 =9%. Coeficientes explícitos requieren 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"
}
]
}Precios, costes e impuestos
catalog cambia información; prices solo precios existentes; combined ambos con permisos separados. Costes requieren costs:read/write. Importes como cadenas decimales en moneda empresarial; sin conversión. Impuestos configurados en ajustes; tax_model inclusive/exclusive/none/unconfigured. Cotizaciones y ventas anteriores conservan sus precios.
{
"external_id": "sofa-100",
"expected_revision": 4,
"mode": "prices",
"currency": "EUR",
"price": "1150.00",
"market_price": "1300.00"
}Existencias de productos y tejidos
Paquete de stock activo, módulo habilitado y permisos stock requeridos. Ubicaciones de escritura asignadas a la conexión. source_kind:product/option para productos o tejidos. /inventory/{id} usa UUID de ficha de existencias; balances contiene revisiones. set/in/out cambia físico, preserva reservado y entrante, impide físico bajo reservado o cero. Saldo nuevo revision 0, event_id único y piezas enteras.
{
"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"
}Importar imágenes
POST /media recibe un archivo multipart con external_id/name. JPG/PNG/WebP hasta 10 MB/40 MP; catálogo 1500 px y opción 720 px, sin ampliar. Transparencia y orientación JPEG conservadas; resultado WebP. Tras 202 espere completed en /jobs/{id}, luego vincule UUID/external_id. Procesamiento secuencial en carpeta empresarial. Nueva external_id si cambia contenido/nombre/uso. Sin descarga URL ni logos empresariales 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]'Importación por lotes y sincronización
/imports acepta 1–50 registros. dry_run:true valida sin guardar; de otro modo 202 y reporte created/updated/unchanged/failed. Dependencias resueltas en varias pasadas; cada registro atómico. Listas GET con limit 1–100 y next_cursor. Guarde change_cursor y consulte /changes; avance next_cursor incluso en páginas filtradas vacías. origin_connection_id evita bucles. Reportes privados a la conexión.
{
"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"
}
}
]
}Errores, límites y reintentos
POST/DELETE requieren Idempotency-Key 8–120 caracteres. Repita misma clave y cuerpo tras errores temporales.409 exige releer y resolver conflicto.401 acceso,403 permisos,404 ausente,422 validación,429 límite,503 ocupado.120 solicitudes/clave/minuto y empresa 5 veces; JSON 1 MB,20 tareas abiertas y 100 MB imágenes por conexión. Errores de autorización de almacenamiento pausan imágenes hasta reparación del operador. /jobs/{id}/retry reinicia tareas fallidas/pausadas con clave activa de la misma conexión.
Envía un User-Agent con el nombre y la versión de tu aplicación, por ejemplo FurnideskConnector/1.0. Una respuesta 403 sin JSON o en HTML puede proceder de la seguridad de red antes de la validación de la API. Comprueba el Content-Type y contacta con soporte.
{
"error": {
"code": "revision_conflict",
"message": "Read the current revision before updating.",
"details": {
"current_revision": 5
}
},
"request_id": "33333333-3333-4333-8333-333333333333"
}Versiones y hoja de ruta
V 1 incluye catálogo, opciones, imágenes, precios/costes y stock opcional. Pedidos, clientes y cuentas son fases futuras sin endpoints activos. Webhooks y conectores previstos. Extensiones compatibles en v1; permisos nuevos no se otorgan automáticamente. Cambios incompatibles usan otra versión mayor. El portal estático no consulta la base de datos.
Referencia de endpoints
Los nombres técnicos son iguales en todos los idiomas. OpenAPI describe campos, restricciones, filtros, ejemplos y errores.
| HTTP | Endpoint | Acceso y comportamiento |
|---|---|---|
| GET | /api/v1/products | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/products/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| DELETE | /api/v1/products/{id} | catalog:write. Soft archive; existing quotations and sales remain unchanged. |
| POST | /api/v1/products/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/categories | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/categories/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/categories/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/collections | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/collections/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/collections/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/brands | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/brands/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/brands/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/customization-types | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/customization-types/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/customization-types/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/customization-groups | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/customization-groups/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/customization-groups/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/customization-options | catalog:read; prices mode requires prices:read. prices:read and costs:read independently expose pricing and cost fields. |
| GET | /api/v1/customization-options/{id} | catalog:read. Pricing and costs are included only with their separate read scopes. For price-only keys, use the list endpoint with mode=prices. |
| POST | /api/v1/customization-options/upsert | catalog:write and/or prices:write; costs:write separately. external_id is scoped to the current connection. |
| GET | /api/v1/media | media:read |
| POST | /api/v1/media | media:write. One file per request; serial worker. JPG/PNG/WebP <=10 MB /40 MP; catalog max1500px, option max720px, no upscaling. Immutable image versions: changing content/name/purpose requires a new external_id. |
| GET | /api/v1/media/{id} | media:read |
| GET | /api/v1/me | Any valid key. |
| GET | /api/v1/changes | Resource read scope required. origin_connection_id supports loop prevention. Always persist next_cursor, even for an empty filtered page. |
| POST | /api/v1/imports | Write scopes for every record. |
| GET | /api/v1/imports | Any valid key; jobs from another connection remain hidden. |
| GET | /api/v1/jobs | Any valid key; jobs from another connection remain hidden. |
| GET | /api/v1/jobs/{id} | Any valid key on the same connection. Per-record failures appear in report counts. |
| GET | /api/v1/imports/{id} | Any valid key on the same connection. Per-record failures appear in report counts. |
| POST | /api/v1/jobs/{id}/retry | Required write scopes; a replacement active key must belong to the same connection. Storage authorization failures also require an operator to resume storage after fixing credentials. |
| POST | /api/v1/imports/{id}/retry | Required write scopes; a replacement active key must belong to the same connection. Storage authorization failures also require an operator to resume storage after fixing credentials. |
| GET | /api/v1/inventory | stock:read; stock package and module must be active. Detail balances contain revisions. Pagination cursor is inventory item UUID. |
| POST | /api/v1/inventory | stock:write; company must have active stock package and enabled module. Location must be assigned to connection. |
| GET | /api/v1/inventory/{id} | stock:read; id is inventory item UUID, not product UUID. |
Modelos de datos
Los nombres técnicos son iguales en todos los idiomas. OpenAPI describe campos, restricciones, filtros, ejemplos y errores.
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."
}