FURNIDESK / DEVELOPER PORTAL
Furnidesk API · Developer Portal
Securely synchronize your products, fabric options, images and stock with your website or application.
https://work.furnidesk.com/api/v1
First connection
The Furnidesk platform administrator first enables your company and grants scopes. A company owner or manager creates a connection in API integrations. Use a separate connection for each source system. Create a time-limited key with only required scopes. The secret is shown once: store it in a server environment variable. Verify it with GET /me. No team account is created; actions are attributed to the connection.
curl "https://work.furnidesk.com/api/v1/me" \
-H "Authorization: Bearer $FURNIDESK_API_KEY"Authentication and keys
Send Authorization: Bearer on every request. A panel session does not authorize the API. Disabling the key, connection, company permission or subscription stops access. Company identity comes from the key. Effective scopes are the intersection of company grants and key permissions. Catalog read does not imply prices or costs. Keys last 1–365 days and can be revoked. Never embed the secret in browser JavaScript, mobile packages or public source. Custom-domain requests must use that company’s key.
| Scope | Access and behavior |
|---|---|
catalog:read | Read catalog |
catalog:write | Write catalog |
prices:read | Read prices |
prices:write | Write prices |
costs:read | Read costs |
costs:write | Write costs |
media:read | Read media |
media:write | Upload media |
stock:read | Read stock |
stock:write | Write physical stock |
Product upserts
POST /products/upsert updates a mapped external_id or creates a product. New products need a name and at least one real category. single is a direct product; bundle contains direct products with quantities. Multiple categories, one collection and one brand may be linked. V 1 does not write suppliers; existing supplier information is preserved. Send the GET record revision as expected_revision for updates. Omitted fields remain unchanged; explicitly nullable fields can be cleared with null. Sent category, media, component and fields arrays replace the entire list. Incomplete products can remain active:false. Activation follows existing sale validation. DELETE archives without changing historical documents.
{
"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"
}
}Identifiers, relationships and languages
external_id is unique within company + connection + resource; use a stable source identifier, not a mutable SKU. target_id can map an existing Furnidesk record initially, but an established mapping cannot move. References accept a Furnidesk UUID or an external_id object. Categories support parents and reject cycles. Brand/collection titles are shared; category titles and descriptions may be translated. Main-language content lives in name/description, other enabled languages in translations; missing content falls back to the main language. Examples assume Turkish main language with Turkish and English enabled.
{
"external_id": "sofas",
"name": "Koltuk",
"translations": {
"en": {
"name": "Sofas"
}
}
}Fabrics and selection fields
Import customization-types, then customization-groups and customization-options. Fabric types use pricing_scope:group: the swatch book carries the pricing rule and fabric samples use none. Other types may use pricing_scope:option. Rules are none, percent or fixed. Product fields define separate selection areas. A field can allow multiple books but selects one fabric from one book at sale time. Main fabric and arms use separate fields. A 30% book rule with coefficient 1 adds 30%; coefficient 0.3 adds 9% for that area. Explicit coefficients require prices:write. An empty fields list removes selection fields.
{
"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"
}
]
}Prices, costs and tax
mode:catalog rejects price and cost fields. mode:prices updates only existing mapped pricing while preserving names, categories and fabrics. combined supports both with the respective scopes; cost requires separate costs:write/read permissions. Send money as decimal strings such as 1000.00 in company currency; the API does not convert currencies. Product tax rates must be configured in company settings. Interpret price using tax_model: inclusive, exclusive, none or unconfigured. Existing quotation and sales snapshots keep their original prices.
{
"external_id": "sofa-100",
"expected_revision": 4,
"mode": "prices",
"currency": "EUR",
"price": "1150.00",
"market_price": "1300.00"
}Product and fabric stock
Stock requires an active stock package, enabled inventory module and stock scopes. Catalog-only transfers need no stock permissions. Managers assign writable locations to each connection. source_kind is product or option for direct products and fabrics/options. GET /inventory lists stock cards; /inventory/{id} uses the inventory item UUID, not the product UUID. Read location revisions in balances. POST set/in/out updates physical stock, preserves reservations and incoming quantities, and rejects physical below reserved or zero. Use a unique event_id, revision 0 for a new balance, and whole quantities for piece units.
{
"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"
}Image imports
POST /media accepts one multipart image with mandatory external_id and name. JPG/PNG/WebP up to 10 MB and 40 MP; catalog max 1500 px, option max 720 px. Smaller images are not enlarged; transparency and JPEG orientation are preserved; output is WebP. Poll GET /jobs/{id} after 202 until completed, then link the image UUID or external_id to a product, classification or fabric option. Images are processed sequentially in the company storage_slug folder. Completed versions are reused. Changed content/name/purpose needs a new external_id. V 1 does not download remote URLs or import company logos.
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]'Batch imports and two-way synchronization
POST /imports accepts 1–50 records. dry_run:true reports validation and rolls back all changes. Otherwise 202 queues a job with created/updated/unchanged/failed counts and per-record errors. Dependencies resolve over multiple passes, including categories submitted after products. Each record is atomic; failures do not cancel other valid records. For initial export, page GET lists with limit 1–100 and next_cursor. Save the first change_cursor, then read GET /changes. Persist next_cursor even for empty filtered pages. origin_connection_id prevents echo loops. Job reports are connection-private. Your connector chooses the business conflict-merging policy.
{
"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"
}
}
]
}Errors, limits and safe retries
All POST/DELETE requests require an 8–120 character Idempotency-Key. Repeat the same key and body after transient failures; changed bodies with the same key return 409. Read the current revision and resolve conflicts before retrying revision errors. Use bounded exponential backoff. Errors:401 credentials,403 permissions/module,404 missing record,409 conflict,422 validation,429 limits,503 busy database. Share request_id with support, never secrets. Default limit 120/key/minute and 5 times that company-wide. JSON max 1 MB,20 pending jobs and 100 MB staged images per connection. Storage authorization errors pause image processing until an operator fixes credentials and resumes it. Failed/paused jobs can be retried with POST /jobs/{id}/retry using an active key on the same connection.
Send a User-Agent identifying your application and version, for example FurnideskConnector/1.0. A non-JSON 403 or HTML response may originate from the network security layer before API validation. Check the response Content-Type and contact support.
{
"error": {
"code": "revision_conflict",
"message": "Read the current revision before updating.",
"details": {
"current_revision": 5
}
},
"request_id": "33333333-3333-4333-8333-333333333333"
}Versions and roadmap
V 1 covers products, categories, collections, brands, customization, media, optional prices/costs and stock. Orders, customers and account ledgers are future phases and have no active endpoints. Webhooks and ready-made connectors are also future additions; v1 uses polling. Compatible resources/scopes may be added under v1; existing keys never receive new scopes automatically. Breaking changes require a new major version. Documentation and OpenAPI are released alongside the application; static portal visits do not query the database.
Endpoint reference
Protocol field names remain the same in every language. The OpenAPI contract contains request fields, constraints, filters, examples and error responses.
| HTTP | Endpoint | Access and behavior |
|---|---|---|
| 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. |
Data models
Protocol field names remain the same in every language. The OpenAPI contract contains request fields, constraints, filters, examples and error responses.
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."
}