Create a sales order
Contents
- Overview
- ERP Context
- Order Lifecycle
- At a Glance
- Prerequisites
- Before You Start
- Workflow
- Step-by-Step Guide
- Auto Shipping Control
- Price Matching with setTotalAmount
- Discount Handling
- Tax Settings
- Data Source Hierarchy
- Preventing Duplicates
- Error Handling
- Related Resources
Overview
This guide shows you how to create a sales order via the Xentral API. You'll learn which data you need beforehand, how to find it, and how the creation process works.
Use Cases:
- Import orders from an online shop (Shopify, WooCommerce, etc.) into Xentral
- Automatically create B2B portal orders
- Synchronize marketplace orders (Amazon, eBay, Kaufland)
- Custom order app for sales representatives
New to the Xentral API? Read first:
- Authentication - Create API Token
- Rate Limiting - Request limits
- Help Center: Order Management - General Xentral sales order documentation
ERP Context
Important for developers without ERP experience: A sales order in Xentral is not simply an "order" as you know it from a shop. It's the starting point for many downstream processes.
What can happen when you create a sales order?
In Xentral, a sales order can potentially trigger:
- Stock Reservations - Inventory is reserved (actual bookings happen on the delivery note)
- Invoice Creation - Via auto dispatch or manual trigger (not automatic on order creation)
- Delivery Note Creation - Via auto dispatch, not automatic on order creation
- Statistics - Revenue, customer history, product statistics
- Auto Dispatch - Not automatic! Triggered via API, UI, or cron job
Why is this important?
Important: The import endpoint creates the sales order and immediately releases it. If auto dispatch is enabled (via project settings or cron job), processing may start immediately. Use the
autoShipping: falseparameter if you need to do additional steps before fulfillment.
Understanding this helps you:
- Know which data must be present (customer, project, payment method, etc.)
- Know which data overrides default values
- Handle errors appropriately
Order Lifecycle
A sales order in Xentral follows a defined lifecycle. Understanding these statuses is essential for building reliable integrations — especially when deciding how to handle errors, cancellations, and retries.
Status Overview
| API Status (V1/V2) | API Status (V3) | Xentral UI (DE) | Xentral UI (EN) | Description |
|---|---|---|---|---|
created | draft | Angelegt | Created | Draft order. No document number assigned yet. Can be edited and deleted freely. |
released | released | Freigegeben | Released | Order is active and has a document number. Stock reservations are active. Ready for dispatch. Cannot be deleted, only canceled. |
completed | completed | Abgeschlossen | Completed | Fully processed — delivery note and/or invoice have been created. No further changes. Can still be canceled. |
canceled | cancelled | Storniert | Canceled | Order is canceled. Remains in the system for traceability. Documents created earlier (invoices, delivery notes) are not affected. |
Note on the V1 import endpoint: The import endpoint (
POST /api/v1/salesOrders/actions/import) creates orders directly inreleasedstatus — it skips thecreated(draft) phase entirely. The order immediately receives a document number. If you need a draft state where orders can be reviewed before release, use the V3 API.Note on spelling: V1/V2 uses
canceled(single l), V3 usescancelled(double l). Make sure your integration handles the correct spelling for the API version you use.
Status Transitions
┌─────────────┐ Release ┌─────────────┐ Dispatch ┌─────────────┐
│ created │ ────────▶ │ released │ ────────▶ │ completed │
│ (Angelegt) │ │(Freigegeben)│ │(Abgeschloss.)│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
Delete│ Cancel │ Cancel │ Cancel
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────────────────────────────────────────────┐
│ DELETED │ │ canceled (Storniert) │
│(permanent)│ │ Cancellation can be undone in the UI. │
└──────────┘ └──────────────────────────────────────────────────┘
Key rules:
- Forward only. Once released, an order cannot go back to
created. Once completed, it cannot go back toreleased. - Cancel from any active status. Orders in status
created,released, orcompletedcan be canceled. (Tested: cancel onreleasedreturns204.) - Canceled is final via API. A canceled order cannot be canceled again (
409 Conflict). However, cancellation can be undone in the Xentral UI (Action menu > "Storno aufheben"). - Delete only from draft. Only orders in
created(draft) status can be deleted. Released or completed orders return409 Conflictwith:"Only Sales Order with status draft can be deleted." - Canceling a draft deletes it. When you cancel a draft order (status
created), Xentral automatically deletes it instead of marking it as canceled.
The Order Status Indicator (Ampel)
Before an order can be dispatched, all status indicator lights ("Ampeln") must be green. The indicator checks:
| Indicator | What it checks |
|---|---|
| Payment | Has payment been received? Payment methods configured as "Rechnung" (invoice) or similar automatically pass this check. Prepayment methods (Vorkasse) block until payment is recorded. |
| Stock | Are all ordered items available in the warehouse? |
| Address | Is the shipping address complete and valid? |
| Credit limit | Is the customer within their credit limit? |
| Delivery block | Is there a manual delivery block on the order? |
Tip: If dispatch fails, check the order status indicator in the Xentral UI to see which light is red. See Help Center: Using the order status indicator.
Controlling the payment indicator via API: The payment check is controlled by the payment method type. If your payment method behaves like "Rechnung" (invoice), the payment indicator is automatically green. For prepayment types (PayPal, credit card, etc.), you can either:
- Record a payment in Xentral before dispatching
- Configure the payment method to behave like "Rechnung" in your project settings
- Set
"vorab als bezahlt markieren"(mark as pre-paid) on the order — this can also be passed from the shop
Cancel vs. Delete
| Cancel | Delete | |
|---|---|---|
| Allowed from | created, released, completed | Only created (draft) |
| Result | Status changes to canceled. Order stays in system. | Order is permanently removed. |
| Document number | Kept (gap in number sequence) | Freed up (no number was assigned yet) |
| Existing documents | Delivery notes and invoices created earlier are not affected | N/A (no documents exist for drafts) |
| Reversible? | Yes, in Xentral UI ("Storno aufheben") | No |
| API use case | Order was released or known to external systems | Order was created by mistake, never released |
Important: The V1 import endpoint creates orders directly in
releasedstatus. These orders cannot be deleted — they can only be canceled.
Cancel an Order
curl -X POST "https://{instance}.xentral.biz/api/v1/salesOrders/{orderId}/actions/cancel" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json"Response: 204 No Content — The order is now canceled.
Error responses:
If the order is already canceled:
{
"type": "https://api.xentral.biz/problems/conflict",
"title": "Sales order cannot be cancelled.",
"messages": ["SalesOrder with id 6 could not be processed. Transition to storniert is not valid for this orders current status"]
}Status: 409 Conflict
Delete a Draft Order
Required Scope:
salesOrder:delete
curl -X DELETE "https://{instance}.xentral.biz/api/v1/salesOrders/{orderId}" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Response: 204 No Content — The order is permanently removed.
Error response (if order is not in draft status):
{
"type": "https://api.xentral.biz/problems/conflict",
"title": "Sales order cannot be deleted.",
"messages": ["SalesOrder with id 8 could not be processed. Only Sales Order with status draft can be deleted."]
}Status: 409 Conflict
Dispatch an Order
Dispatching moves an order from released to completed. During dispatch, items are booked out of the warehouse and documents (delivery note, invoice) are created.
Required Scope:
salesOrder:updatePrerequisite: All order status indicator lights must be green. See The Order Status Indicator.
curl -X POST "https://{instance}.xentral.biz/api/v1/salesOrders/{orderId}/actions/dispatch" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"createDocuments": "deliveryNote"}'Document creation options:
"deliveryNote"— Create delivery note only"invoice"— Create invoice only"deliveryNoteAndInvoice"— Create both
Response: 204 No Content — The order is now completed.
Common dispatch errors:
| HTTP | Error | Cause |
|---|---|---|
| 400 | "Sales order needs to be in status released. Dispatching rejected." | Order is not in released status (e.g., already completed or canceled) |
| 400 | "Check payment not passed. Dispatching rejected" | Payment indicator is red. Configure payment method as "Rechnung" type or record payment first. |
| 400 | "Check stock not passed. Dispatching rejected" | Stock indicator is red. Not enough inventory for one or more positions. |
| 422 | "Unknown error while trying to dispatch the sales order." | Generic error — typically a missing configuration (warehouse, dispatch center, or project settings). Check the order status indicator in the UI. |
Note: Dispatch is not automatic by default. It must be triggered via:
- API:
POST /api/v1/salesOrders/{id}/actions/dispatch- Xentral UI: Action menu > "Auto dispatch: hand over to dispatch center"
- Cron job: Process starter
autoversand_plus(fully automated) orautoversand_manuell(semi-automated via Delivery handover tab)The
autoShippingparameter on the order controls whether the cron job will pick up the order. SettingautoShipping: falseprevents automatic dispatch even if the cron job is running.
Completing an Order Without Dispatch
An order can also be marked as completed manually without going through the dispatch process. This is useful for service orders or orders that don't require warehouse operations.
In the Xentral UI: Action menu > "Mark as completed"
Note: There is no dedicated API endpoint for manually completing an order. Orders are completed through the dispatch process or manually in the UI.
Filter Orders by Status
curl -s "https://{instance}.xentral.biz/api/v1/salesOrders?page[number]=1&page[size]=10&filter[0][key]=status&filter[0][op]=equals&filter[0][value]=released" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Valid filter values: created, released, completed, canceled
Document Status vs. Order Status
The statuses above (created, released, completed, canceled) are the active order statuses. These are the only statuses that current Xentral processes set on sales orders.
About the sent status on sales orders:
The sent status on sales orders is set by the Send Sales Order V3 endpoint. When you call this endpoint, the order status transitions to sent. While this status is not part of the V2 OpenAPI specification, it is actively used in the V3 API workflow.
Note: If you encounter
senton older sales orders, it may also be legacy data from a previous system version. For new integrations using V3, the Send endpoint is the standard way to mark an order as sent.
Documents have their own status lifecycle. Business documents created from the order (delivery note, invoice) do actively use the sent status:
| Document Status | German | Description |
|---|---|---|
created | Erstellt | Document has been generated but not yet sent to the customer. |
sent | Versendet | Document has been transmitted to the customer — via the Send endpoint, email in auto-dispatch, or printed at the packing station. |
Note: Automatic processes like logistics (auto-dispatch, packing station printing) can also set the
sentstatus on documents. For a full overview of document statuses and transitions, see the Business Documents Lifecycle guide (planned).
V1 Import vs. V3 Create
| Aspect | V1 Import | V3 Create |
|---|---|---|
| Endpoint | POST /api/v1/salesOrders/actions/import | POST /api/v3/salesOrders |
| Initial status | released (immediately, with document number) | draft (no document number yet) |
| Separate release step | No (automatic) | Yes (PATCH /api/v3/salesOrders/{id}/actions/release) |
| Can be deleted | No (already released) | Yes (while in draft) |
| Use case | Shop imports where orders are already confirmed | Workflows where orders need review before release |
At a Glance
| Endpoint | POST /api/v1/salesOrders/actions/import |
| Method | POST |
| Auth | Bearer Token (Documentation) |
| Required Scope | salesOrder:create |
curl -X POST "https://{instance}.xentral.biz/api/v1/salesOrders/actions/import" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-01-28",
"customer": {"id": "4"},
"project": {"id": "1"},
"financials": {
"paymentMethod": {"id": "8"},
"currency": "EUR"
},
"delivery": {
"shippingMethod": {"id": "1"},
"autoShipping": false
},
"positions": [{
"product": {"id": "1"},
"quantity": 2,
"price": {"amount": "19.99", "currency": "EUR"}
}]
}'Response: 201 Created - The new order ID is in the Location header (e.g., /api/v1/salesOrders/2).
Prerequisites
- Xentral account with API access
- Personal Access Token (PAT) with scope
salesOrder:create(Guide) - At least one customer in the system (or permission to create)
- At least one project in the system (projects cannot be created via API!)
- At least one payment method configured (can be listed via API, but cannot be created via API)
- At least one shipping method configured (can be listed and created via API)
- At least one product in the system (or permission to create)
Before You Start
Decision 1: One-time buyers or regular customers?
B2C / One-time buyers (e.g., Amazon, eBay):
- Customers will likely only order once
- You can specify all addresses directly in the order
- Customer master data doesn't need to be perfectly maintained
- Recommendation: Check if customer exists, if not → create, pass addresses in order
B2B / Regular customers:
- Same customers order regularly
- Clean master data is important (delivery addresses, payment terms)
- Recommendation: Store customer mapping in your own database/middleware
- Maintain addresses in customer master data, not in every order
Decision 2: Prices from Xentral or from the shop?
Option A: Xentral calculates the price
- You don't specify a price in the position
- Xentral uses the stored sales price (possibly customer-specific)
- Advantage: Consistent prices
- Disadvantage: Possible deviations from shop
Option B: Use shop price (recommended)
- You explicitly specify the price in the position
- The shop price is taken 1:1
- Advantage: No price deviations
- Disadvantage: You must always provide the price
Important: If the customer purchased something with a confirmed price in the shop, that price MUST be used. Otherwise, payment matching (Zahlungseingang) will fail and DATEV amounts will be incorrect.
Decision 3: Cache payment methods and shipping methods?
Check once (recommended for fixed integrations):
- Payment methods and shipping methods rarely change
- Query IDs once and store in your integration
- Use stored ID for each order
- Fewer API calls, faster processing
- Could be refreshed once a day
Check every time (for dynamic scenarios):
- If payment methods/shipping methods change frequently
- If you serve different Xentral instances
- More API calls, potential rate limit issues, slower performance
Best Practice: Store payment methods and shipping methods in a mapping table:
{"paypal": "8", "dhl": "1"}. This saves 2 API calls per order.
Workflow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 1. Check │ ──▶ │ 2. Find │ ──▶ │ 3. Payment/ │ ──▶ │ 4. Check │ ──▶ │ 5. Create │
│ Customer │ │ Project │ │ Shipping │ │ Products │ │ Order │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Not found? │ │ Cache! │ │ Not found? │
│ Create! │ │ │ │ Create! │
└─────────────┘ └─────────────┘ └─────────────┘
Step-by-Step Guide
Step 1: Check/Find Customer
Before creating an order, you need the customer ID. The API is completely ID-based.
Search customer (V2 API):
Required Scope:
customer:read
curl -s "https://{instance}.xentral.biz/api/v2/customers?filter[0][key]=name&filter[0][op]=equals&filter[0][value]=Max%20Mustermann" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Note: The V2 filter syntax requires
key,op, andvalueparameters. For customers, only theequalsoperator is supported for thenamefield.
Response:
{
"data": [{
"id": "4",
"number": "10000",
"customerType": "person",
"name": "Max Mustermann",
"firstname": "Max",
"lastname": "Mustermann"
}],
"extra": {"page": {"number": 1, "size": 10}, "totalCount": 1}
}Step 2: Create Customer (if not found)
If the customer doesn't exist, you need to create them first.
Request (V2 API):
Required Scope:
customer:create
curl -s -X POST "https://{instance}.xentral.biz/api/v2/customers" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"customerType": "person",
"firstname": "Max",
"lastname": "Mustermann"
}'Response: 201 Created - No body, customer ID is in the Location header.
Note: For
customerType: "person",firstnameandlastnameare required. ForcustomerType: "company",nameis required.
Add addresses separately (V2):
Required Scope:
customer:create
curl -s -X POST "https://{instance}.xentral.biz/api/v2/customers/{customerId}/addresses" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"type": "deliveryaddress",
"name": "Max Mustermann",
"street": "Musterstraße 1",
"zip": "10115",
"city": "Berlin",
"country": "DE"
}'API Reference: Create Customer Address
Address types:
masterdata(master data),billingaddress(invoice),deliveryaddress(shipping)
Step 3: Find Project
Projects control important process settings (warehouse processes, number ranges, etc.). You cannot create projects via API - they must exist in Xentral.
List projects (V1):
Note: This endpoint has no scope requirements.
curl -s "https://{instance}.xentral.biz/api/v1/projects" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Response:
{
"data": [{
"id": "1",
"name": "Standard Project",
"keyName": "STANDARD",
"currency": "EUR",
"normalTaxRate": 19,
"reducedTaxRate": 7
}],
"extra": {"page": {"number": 1, "size": 10}, "totalCount": 1}
}Why specify the project? In the UI, the project is automatically pulled from the customer. Via API it's intentionally different: Depending on the sales channel (Shopify vs. eBay vs. B2B portal), you might need different logistics processes → different projects.
Step 4: Find Payment Method
List payment methods (V1):
Note: This endpoint has no scope requirements.
curl -s "https://{instance}.xentral.biz/api/v1/paymentMethods" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Response:
{
"data": [
{"id": "8", "type": "paypal", "designation": "Paypal"},
{"id": "9", "type": "bar", "designation": "Cash"},
{"id": "10", "type": "lastschrift", "designation": "Direct Debit"}
],
"extra": {"page": {"number": 1, "size": 10}, "totalCount": 14}
}Why specify this? The same customer can pay with PayPal once, by invoice another time. This information comes from the shop - Xentral can't guess it.
Step 5: Find Shipping Method
List shipping methods (V1):
Note: This endpoint has no scope requirements.
curl -s "https://{instance}.xentral.biz/api/v1/shippingMethods" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Response:
{
"data": [
{"id": "1", "designation": "DHL", "type": "DHL"},
{"id": "3", "designation": "DPD", "type": "DPD"},
{"id": "6", "designation": "GLS", "type": "gls"}
],
"extra": {"page": {"number": 1, "size": 10}, "totalCount": 11}
}Step 6: Check Product
Before creating the order, ensure all products exist in Xentral.
Search product (V2 API):
Required Scope:
product:read
curl -s "https://{instance}.xentral.biz/api/v2/products?filter[0][key]=number&filter[0][op]=equals&filter[0][value]=1000039" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Search alternatives:
- By product number:
filter[0][key]=number&filter[0][op]=equals&filter[0][value]=1000039 - By name (contains):
filter[0][key]=name&filter[0][op]=contains&filter[0][value]=Kaffee - By EAN:
filter[0][key]=ean&filter[0][op]=equals&filter[0][value]=4260123456789
If product not found:
Required Scope:
product:create
curl -s -X POST "https://{instance}.xentral.biz/api/v2/products" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"number": "NEW-001",
"name": "New Product",
"project": {"id": "1"},
"salesPrice": {"amount": "19.99", "currency": "EUR"}
}'Best Practice: Like with customers - store the mapping between shop product number and Xentral ID in your middleware. This saves API calls per order.
Step 7: Create Order
Now you have all IDs and can create the order.
With explicit price and external order number (recommended):
curl -s -X POST "https://{instance}.xentral.biz/api/v1/salesOrders/actions/import" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-01-28",
"externalOrderNumber": "SHOP-12345",
"customer": {"id": "4"},
"project": {"id": "1"},
"financials": {
"paymentMethod": {"id": "8"},
"currency": "EUR"
},
"delivery": {
"shippingMethod": {"id": "1"},
"autoShipping": false
},
"positions": [{
"product": {"id": "1"},
"quantity": 2,
"price": {"amount": "19.99", "currency": "EUR"}
}]
}'API Reference: Import Sales Order
Important:
- Without a price, Xentral calculates from product master data - if no price exists there, import fails!
- The order ID is returned in the
Locationresponse header- The order is immediately in status
released(see Order Lifecycle)
Response: 201 Created
The order ID is in the Location header:
Location: https://{instance}.xentral.biz/api/v1/salesOrders/2
Step 8: Validate Order (optional)
Required Scope:
salesOrder:read
curl -s "https://{instance}.xentral.biz/api/v1/salesOrders/2" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"Response (shortened):
{
"data": {
"id": "2",
"documentNumber": "200002",
"externalOrderNumber": "SHOP-12345",
"date": "2026-01-28",
"status": "released",
"customer": {"id": "4", "number": "10000"},
"netSales": {"amount": "39.98", "currency": "EUR"},
"total": {"amount": "47.58", "currency": "EUR"}
}
}Tip: Check the
statusfield to confirm the order was created successfully. After import, it should bereleased. See Order Lifecycle for all possible statuses and transitions.
Auto Shipping Control
The import endpoint creates the order and immediately releases it. If you need to do additional processing before fulfillment, disable auto shipping:
{
"delivery": {
"shippingMethod": {"id": "1"},
"autoShipping": false
}
}Multi-step workflow example:
- Create the sales order with
autoShipping: false - Do calculations, determine correct warehouse or shipping method, prepare purchase orders...
- Update the order via PATCH to set final values
- Activate auto shipping via API or let cron job pick it up
Control document creation:
{
"autoCreateDocuments": "deliveryNote"
}Allowed values:
"deliveryNote"- Create delivery note only"invoice"- Create invoice only"deliveryNote+invoice"- Create both
Note: If you want to prevent automatic document creation, simply omit this parameter entirely. The field is optional and defaults to no automatic creation.
Price Matching with setTotalAmount
When using auto dispatch, Xentral needs to match calculated prices with external prices for payment reconciliation and DATEV.
The problem:
- Your shop calculates with gross prices
- Xentral calculates with net prices
- Different rounding can cause cent differences
- Payment matching (Zahlungseingang) will fail if amounts don't match
The solution - setTotalAmount:
{
"date": "2026-01-28",
"customer": {"id": "4"},
"project": {"id": "1"},
"financials": {
"paymentMethod": {"id": "8"},
"currency": "EUR"
},
"positions": [...],
"setTotalAmount": {
"isActive": true,
"maximumDifferenceToCalculatedSum": 0.05,
"totalGrossAmountFromExternal": 47.58
}
}Important:
setTotalAmountonly sets the total amount for the order, not for individual positions. It's used for:
- Payment matching (Zahlungseingang)
- Correct amounts for DATEV export
Fields:
| Field | Type | Description |
|---|---|---|
isActive | boolean | Enables the setTotalAmount logic. If set to false, this entire block is ignored. |
maximumDifferenceToCalculatedSum | float | Maximum allowed difference in EUR (e.g., 0.05 = 5 cents) |
totalGrossAmountFromExternal | float | Gross total from the shop |
Discount Handling
Xentral supports two types of discounts:
1. Line Item Discount
Apply a percentage discount to a specific position:
"positions": [{
"product": {"id": "1"},
"quantity": 2,
"price": {"amount": "19.99", "currency": "EUR"},
"discount": 0.15
}]This applies 15% discount to this line item only.
2. Discount Position (Order-level)
For order-wide discounts, use discountPositions:
{
"positions": [
{"product": {"id": "1"}, "quantity": 2, "price": {"amount": "19.99", "currency": "EUR"}},
{"product": {"id": "2"}, "quantity": 1, "price": {"amount": "29.99", "currency": "EUR"}}
],
"discountPositions": [{
"product": {"id": "99"},
"discount": 0.10
}]
}Note: Discount products must be configured in Xentral as discount articles. They cannot be added as regular positions.
For detailed information on how Xentral handles discounts, see the Help Center.
Tax Settings
By default, VAT settings are taken from the product. You can override this per position.
Note: If you define tax as
normalorreduced, Xentral applies the tax rates configured in the Lieferschwellen (delivery thresholds) and Steuersätze (tax rates) modules. Make sure these are properly configured in your Xentral instance.
Using VAT category:
"positions": [{
"product": {"id": "1"},
"quantity": 2,
"tax": {
"vatCategory": "reduced"
}
}]Categories: normal, reduced, taxfree
Using custom rate:
"positions": [{
"product": {"id": "1"},
"quantity": 2,
"tax": {
"rate": 7.0,
"taxText": "7% VAT"
}
}]Use case: When selling in different countries with different tax rules, or when the same product has different taxation depending on context (e.g., food for takeaway vs. dine-in).
Data Source Hierarchy
Xentral uses a fallback system. Values in your request always win:
Addresses
- Request (if provided) → used
- Customer (if nothing in request) → Billing/Shipping address from master data
- Default → Master data address
Important for delivery addresses: Xentral only uses the delivery address marked as "default". If no address has this flag, none is used - even if the customer has 10 delivery addresses!
Payment Terms
- Request →
paymentTermsin request - Customer → Payment terms in master data
- Payment Method → Default of payment method
- System Settings → System-wide defaults
Preventing Duplicates
In integrations, an order may be sent multiple times (network errors, retry logic, etc.). Use the external order number to prevent duplicates.
externalOrderNumber
{
"externalOrderNumber": "SHOP-12345",
"date": "2026-01-28",
"customer": {"id": "4"},
...
}Benefits:
- Unique mapping between shop order and Xentral order
- You can check before import if the order already exists
- Easy troubleshooting: "Which Xentral order belongs to shop order X?"
Check before import:
curl -s "https://{instance}.xentral.biz/api/v1/salesOrders?filter[0][key]=externalOrderNumber&filter[0][op]=equals&filter[0][value]=SHOP-12345" \
-H "Authorization: Bearer {token}" \
-H "Accept: application/json"If totalCount > 0 → Order already exists, don't create again.
Best Practice for High Volume
For many orders per day (>100):
| Strategy | Recommendation |
|---|---|
| Customer IDs | Cache in middleware/database |
| Product IDs | Cache in middleware/database |
| Payment/Shipping methods | Query once, store statically, refresh daily |
| Duplicate check | Always set externalOrderNumber |
This reduces API calls per order from ~6 to 1-2.
Error Handling
| Status | Meaning | Common Cause |
|---|---|---|
| 400 | Bad Request | Invalid JSON or validation error (missing required fields) |
| 401 | Unauthorized | Invalid or expired token |
| 403 | Forbidden | Missing required scope (e.g., salesOrder:create) |
| 404 | Not Found | Invalid URL or resource ID doesn't exist |
| 409 | Conflict | Action not allowed on resource in current state (see Order Lifecycle) |
| 429 | Too Many Requests | Rate limit exceeded |
Note: Validation errors return
400 Bad Request, not422. The409 Conflicterror occurs when performing an action that is not valid for the current order status — for example, deleting a released order or canceling an already canceled order.
Rate Limiting Best Practice:
Monitor the X-RateLimit-Remaining header to avoid hitting limits:
| Remaining | Recommended Action |
|---|---|
| 100-50 | Full speed (no delay) |
| 50-25 | Add a small delay (e.g., 50ms) between requests |
| < 25 | Add a larger delay (e.g., 200ms) between requests |
Note: These are recommendations. Adjust delay values based on your specific rate limit and use case.
This way you'll never hit the rate limit while maximizing throughput.
Related Resources
API Documentation
- salesorderimport - Import/Create Sales Order
- salesorderlist - List Sales Orders
- salesorderview - View Sales Order
- salesorderactionscancel - Cancel Sales Order
- salesorderactionsdispatch - Dispatch Sales Order
- customerlistv2 - List Customers
- customercreatev2 - Create Customer
- customeraddresscreatev2 - Create Customer Address
- projectlist - List Projects
- paymentmethodlist - List Payment Methods
- shippingmethodlist - List Shipping Methods
- productlistv2 - List Products
- productcreatev2 - Create Product
- Authentication
- Rate Limiting
Help Center
- Introduction to Order Management - Order lifecycle, statuses, and creation methods
- Steps for manual sales order processing - Cancel, copy, dispatch, and more
- Transferring sales orders to logistics (Auto-shipping) - Dispatch center and automation
- Using the order status indicator - Traffic light system for dispatch readiness
- Sales Price Determination
- Discount Handling
Related Guides
- G2: Create Customer - Create customers for orders
- G3: Create Product - Create products for order positions
Updated 9 days ago