POST · PUT · Import · Upsert
Import API
Import data into the store. Since v2.1.0, RESTful HTTP methods have been separated: POST for new records, PUT for updates.
POST /Import/JSON/{entity} # INSERT
PUT /Import/JSON/{entity} # UPDATE
POST = INSERT
Create new records
- Creates only new records
- ID = 0 or no ID → automatic ID assignment
- ID > 0 that already exists →
409 Conflict
- Example: Create a new category
POST /Import/JSON/categories
{"data": [{"categories_id": 0, ...}]}
PUT = UPDATE
Update existing records
- Updates only existing records
- The ID must exist; otherwise →
404 Not Found
- No new records allowed
- Example: Change category description
PUT /Import/JSON/categories
{"data": [{"categories_id": 142, ...}]}
Migration from v2.0.x: The previous POST behavior (upsert) is no longer supported. Split your imports into INSERT (POST) and UPDATE (PUT).
| Entity |
POST (INSERT) |
PUT (UPDATE) |
Scope |
| Products |
POST /Import/JSON/products |
PUT /Import/JSON/products |
products:write |
| Categories |
POST /Import/JSON/categories |
PUT /Import/JSON/categories |
categories:write |
| Customers |
POST /Import/JSON/customers |
PUT /Import/JSON/customers |
customers:write |
| Manufacturers |
POST /Import/JSON/manufacturers |
PUT /Import/JSON/manufacturers |
manufacturers:write |
| News Articles |
POST /Import/JSON/newsdesks |
PUT /Import/JSON/newsdesks |
news:write |
| News Categories |
POST /Import/JSON/newsdeskcats |
PUT /Import/JSON/newsdeskcats |
newscategories:write |
| FAQ |
POST /Import/JSON/faq |
PUT /Import/JSON/faq |
faq:write |
| FAQ Categories |
POST /Import/JSON/faqcats |
PUT /Import/JSON/faqcats |
faqcategories:write |
| SEO History |
POST /Import/JSON/seohistory |
PUT /Import/JSON/seohistory |
seohistory:write |
| Contracts |
POST /Import/JSON/contracts |
PUT /Import/JSON/contracts |
contracts:write |
| Newsletter Subscribers |
POST /Import/JSON/newsletter_subscribers |
PUT /Import/JSON/newsletter_subscribers |
newsletter:write |
| Newsletter Campaigns |
POST /Import/JSON/newsletters |
PUT /Import/JSON/newsletters |
newsletters:write |
| Tickets v2.60.0 |
POST /Import/JSON/tickets |
PUT /Import/JSON/tickets |
tickets:write |
| Orders NEW v2.20.0 |
POST /Import/JSON/orders |
PUT /Import/JSON/orders |
orders:write |
| Media NEW v2.16.0 | POST /Import/JSON/media | PUT /Import/JSON/media/{id} | media:write |
| Slider NEW v2.22.0 |
POST /Import/JSON/slider |
PUT /Import/JSON/slider |
slider:write |
| Projects (xoCRM) NEW v2.35.0 |
POST /Import/JSON/projects |
PUT /Import/JSON/projects |
projects:write |
| Project Tasks NEW v2.35.0 |
POST /Import/JSON/project_tasks |
PUT /Import/JSON/project_tasks |
project_tasks:write |
| Appointments (xoCRM) NEW v2.44.0 |
POST /Import/JSON/appointments |
PUT /Import/JSON/appointments |
appointments:write |
| Attributes & Option Values NEW v2.96.0 |
POST /Import/JSON/products_options |
PUT /Import/JSON/products_options |
products:write |
| Delivery Time Profiles NEW v2.94.0 |
POST /Import/JSON/shipping_profiles |
PUT /Import/JSON/shipping_profiles |
products:write |
| Ticket Departments NEW v2.103.0 |
POST /Import/JSON/ticket_departments |
PUT /Import/JSON/ticket_departments |
tickets:write |
| Settings NEW v2.108.0 |
– |
PUT /Import/JSON/configuration |
configuration:write |
| CSV/XLS Importer NEW v2.109.0 |
– |
PUT /Import/JSON/porters |
porters:write |
| Ticket Claims NEW v2.118.0 |
– |
PUT /Import/JSON/ticket_claims |
tickets:write |
New in v2.57.0: POST/PUT /Import/JSON/customers now manages customer discounts via the field discounts[] (percentage discount per category; category_id: 0 together with subcategories = entire product range; optional flags subcategories, qpb, option, special). “replace-on-present” semantics: If the field is present, the customer’s entire discount history is replaced; an empty array [] removes all discounts; if the field is missing, the discounts remain unchanged. Additionally, customers_id_extern (WaWi customer number) and customers_group_id (which is validated against existing customer groups) are writable, and PUT can address the customer even without an email address via customers_id or customers_id_extern (pure discount/master data maintenance)—POST requests still require an email address and a physical address.
New in v2.57.0: POST/PUT /Import/JSON/products writes customer-specific prices via the new field customers_prices[]: Customer via customers_id or customers_id_extern (WaWi customer number), partial upsert (only columns included in the request are written, e.g., customers_price, tiered pricing products_price1..12 + products_price1..12_qty, products_qty_blocks, products_min_qty, products_max_qty), delete via "_action": "delete", full sync via customers_prices_replace: true (when combined with an empty customers_prices: [], all customer prices for the product are removed). The Products export provides customers_prices with round-trip capability.
New in v2.24.0: PUT /Import/JSON/tickets can now send not only internal comments but also public replies to customers. Two optional Boolean flags per item: is_public_reply: true makes the history entry public (visible in the frontend), notify_customer: true additionally sends an email to the customer using the $mail_ticket_update_with_content template (similar to the backend reply path). Both flags must be strictly Boolean true — all other values are ignored. Starting with v2.25.0: For is_public_reply: true, the admin signature stored in the backend is automatically appended (history entry + email) — Opt-out via append_signature: false. Therefore, do not include a separate signature in ticket_comments. admin_id > 0 is required for public replies. The default behavior (without flags) remains internal + no email — backward compatible with all v2.11.0+ clients. Starting with v2.60.0: POST/PUT /Import/JSON/tickets also accepts attachments[] (name + content_base64) — Ticket attachments are now not only readable but also writable. The files are attached to the history entry: without ticket_comments in the same request, they will be rejected with an error message.
New in v2.20.0: POST/PUT /Import/JSON/orders enables the creation and updating of orders. Delegated to the generic service \\Xonic\\Order\\OrderBuilder, which can also be reused by Marketplace importers. Inventory is posted via xo_stock_change (best effort).
New in v2.17.0: PUT /Import/JSON/products can manage product-specific special offers via specials[]: multiple customer groups, net special price, or discount_percent, time period, status, offer description, and deletion per group.
{
"type": "categories",
"data": [
{
"categories_id": 142,
"parent_id": 110,
"status": 1,
"languages": {
"de": {
"categories_name": "Produktattribute",
"categories_description": "<p>Beschreibung als HTML-String...</p>"
}
}
}
]
}
{
"success": true,
"api_version": "2.1.0",
"type": "categories",
"stats": {
"total": 2,
"inserted": 1,
"updated": 1,
"failed": 0
},
"errors": [],
"timestamp": "2026-01-27 14:00:00",
"records": [
{"index": 1, "categories_id": 144, "status": "inserted"},
{"index": 2, "categories_id": 142, "status": "updated"}
]
}
409 Conflict (POST)
ID already exists:
{"success":false,"error":{"code":"ALREADY_EXISTS"}}
404 Not Found (PUT)
ID does not exist:
{"success":false,"error":{"code":"NOT_FOUND"}}
Additional fields for a product are written exclusively via the extra_fields field—not as a field at the top level of the product. A key such as google_product_category directly next to products_model is ignored (and reported in the response as UNKNOWN_FIELD ).
Starting with v2.51.0, any of the following formats are accepted—either using the field ID or the field name (case and spaces are ignored):
{
"type": "products",
"data": [{
"products_model": "ABC-123",
"extra_fields": {
"Google Produktkategorie (xoPort)": "1234",
"Google Zustand (xoPort)": "new",
"Google Verfügbarkeit (xoPort)": "in stock"
}
// ... alternativ nach Feld-ID:
// "extra_fields": {"7": "1234", "8": "new", "9": "in stock"}
// ... oder als Liste:
// "extra_fields": [{"products_extra_fields_id": 7, "products_extra_fields_value": "1234"}]
}]
}
The structure returned by the JSON export (an object keyed by field ID) can also be imported again without any changes.
Google Shopping Fields
The fields in the Google feed are standard additional fields with fixed IDs:
| Field ID |
Field Name (Default) |
Google Feed |
7 |
Google Product Category (xoPort) |
g:google_product_category |
8 |
Google Status (xoPort) |
g:condition |
9 |
Google Availability (xoPort) |
g:availability |
Note: An empty string ( "" ) clears the field; " null " means "not included" (the field remains unchanged). A field name that does not exist in the store is reported as a " EXTRA_FIELD" warning in the response instead of being silently discarded.
Product tabs (e.g., “Product Description,” “Data Sheet,” “Installation Instructions”) are maintained via the tabs field—a flat array of tab objects. A nested format such as {"de": […]} is not supported and is ignored without an error message (no warning, no tabs).
{
"type": "products",
"data": [{
"products_model": "ABC-123",
"tabs": [
{
"tab_name": "Leistungsbeschreibung",
"tab_content": "<h3>Leistungsbeschreibung</h3><p>...</p>",
"language_id": 2,
"sort_order": 0
},
{
"tab_name": "Service description",
"tab_content": "<h3>Service description</h3><p>...</p>",
"language_id": 1,
"sort_order": 0
}
]
}]
}
| Field |
Required |
Description |
tab_name |
✓ (when creating a new entry) |
Tab title as it appears on the product. If " tab_name " is omitted, the entry is skipped. |
tab_content |
– |
HTML content of the tab. |
language_id |
– |
Language of the entry (1 = English, 2 = German). If not specified, the store’s default language is used. |
sort_order |
– |
Sort order of the tab within the product (default 0). |
tab_id |
– |
For updates only: updates tab_name/tab_content for an existing tab in the specified language. |
Behavior in Detail
- New creation: Any entry without `
tab_id ` creates a new tab with its own ` tab_id ` —one per language. Multilingual tabs are therefore sent as one entry per language (see example above); the frontend displays only the tabs for the active language, filtered by language.
tab_id Update: Using ` tab_id ` updates the existing tab in the specified language—but only if the language entry already exists. A missing language version of an existing tab cannot be created retroactively via the API (to do so, send a new tab without ` `).
- No “Delete-on-absent”: Tabs not included in the request remain unchanged. Tabs are deleted in the backend at the product level.
- Export round trip:
GET /products returns the tabs as an object keyed by tab_id (including tab_name, tab_content, language_id, and sort_order).
Note: The field is called tab_name/tab_content —not tabs_title/tabs_content. Entries with incorrect spelling or a mixed-language structure will be rejected without comment.
Stock, safety stock, and reorder point for each warehouse are maintained in the product import exclusively via the field storages —a flat array of warehouse objects, each with storages_id (alias warehouse_id). Since v2.45.0, this is a partial update: a partial payload such as {"storages_id": 3, "products_reorder_level": 10} changes only the reorder point for this warehouse and does not set the remaining warehouse columns to 0.
This requires the extended inventory system to be enabled (EXTENDED_STORAGE_SYSTEM); otherwise, the field is ignored and the response includes the warning “ STORAGE_SYSTEM_DISABLED.” All fields, their semantics, and potential pitfalls are described in the “Inventory & Reorder Points per Warehouse ” section of the Products reference.
Note: ` products_reorder_level ` and ` products_safe_quantity ` at the top level of the product refer to the main warehouse (the ` products` table), not a warehouse from ` storages `—and they can only be imported starting with v2.63.0; prior to that, they were discarded as unknown fields. `storages_id: 1 ` is already the first additional warehouse, not the main warehouse. A warehouse entry without storages_id is skipped without a warning —the import then reports success even though nothing was written.
Start Import
Create an OAuth2 client with ":write" scopes for the desired resources.
Create an OAuth2 client