Create a sales order
Contents
- Overview
- ERP Context
- 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 theautoShipping: 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
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).
→ Full API Reference: Import Sales Order
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.
→ API Reference: List Customers
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"
}'→ API Reference: Create Customer
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"→ API Reference: List Projects
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"→ API Reference: List Payment Methods
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"→ API Reference: List Shipping Methods
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"→ API Reference: List Products
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"}
}'→ API Reference: Create Product
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
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"→ API Reference: Get Sales Order
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"}
}
}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"→ API Reference: List Sales Orders
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 (e.g., trying to release an already released order, or deleting a non-draft order) |
| 429 | Too Many Requests | Rate limit exceeded |
Note: Validation errors return
400 Bad Request, not422. The409 Conflicterror is primarily used in the V3 API.
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
- 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
Related Guides
- Create Customer (planned)
- Create Products (planned)
Updated 3 days ago