Create a sales order

Contents


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:


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: false parameter 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
createddraftAngelegtCreatedDraft order. No document number assigned yet. Can be edited and deleted freely.
releasedreleasedFreigegebenReleasedOrder is active and has a document number. Stock reservations are active. Ready for dispatch. Cannot be deleted, only canceled.
completedcompletedAbgeschlossenCompletedFully processed — delivery note and/or invoice have been created. No further changes. Can still be canceled.
canceledcancelledStorniertCanceledOrder 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 in released status — it skips the created (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 uses cancelled (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 to released.
  • Cancel from any active status. Orders in status created, released, or completed can be canceled. (Tested: cancel on released returns 204.)
  • 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 return 409 Conflict with: "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:

IndicatorWhat it checks
PaymentHas payment been received? Payment methods configured as "Rechnung" (invoice) or similar automatically pass this check. Prepayment methods (Vorkasse) block until payment is recorded.
StockAre all ordered items available in the warehouse?
AddressIs the shipping address complete and valid?
Credit limitIs the customer within their credit limit?
Delivery blockIs 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

CancelDelete
Allowed fromcreated, released, completedOnly created (draft)
ResultStatus changes to canceled. Order stays in system.Order is permanently removed.
Document numberKept (gap in number sequence)Freed up (no number was assigned yet)
Existing documentsDelivery notes and invoices created earlier are not affectedN/A (no documents exist for drafts)
Reversible?Yes, in Xentral UI ("Storno aufheben")No
API use caseOrder was released or known to external systemsOrder was created by mistake, never released

Important: The V1 import endpoint creates orders directly in released status. 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"

API Reference: Cancel Sales Order

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:update

Prerequisite: 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"}'

API Reference: Dispatch Sales Order

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:

HTTPErrorCause
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) or autoversand_manuell (semi-automated via Delivery handover tab)

The autoShipping parameter on the order controls whether the cron job will pick up the order. Setting autoShipping: false prevents 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"

API Reference: List Sales Orders

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 sent on 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 StatusGermanDescription
createdErstelltDocument has been generated but not yet sent to the customer.
sentVersendetDocument 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 sent status on documents. For a full overview of document statuses and transitions, see the Business Documents Lifecycle guide (planned).

V1 Import vs. V3 Create

AspectV1 ImportV3 Create
EndpointPOST /api/v1/salesOrders/actions/importPOST /api/v3/salesOrders
Initial statusreleased (immediately, with document number)draft (no document number yet)
Separate release stepNo (automatic)Yes (PATCH /api/v3/salesOrders/{id}/actions/release)
Can be deletedNo (already released)Yes (while in draft)
Use caseShop imports where orders are already confirmedWorkflows where orders need review before release

At a Glance

EndpointPOST /api/v1/salesOrders/actions/import
MethodPOST
AuthBearer Token (Documentation)
Required ScopesalesOrder: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, and value parameters. For customers, only the equals operator is supported for the name field.

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", firstname and lastname are required. For customerType: "company", name is 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 Location response 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"

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"}
  }
}

Tip: Check the status field to confirm the order was created successfully. After import, it should be released. 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:

  1. Create the sales order with autoShipping: false
  2. Do calculations, determine correct warehouse or shipping method, prepare purchase orders...
  3. Update the order via PATCH to set final values
  4. 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: setTotalAmount only sets the total amount for the order, not for individual positions. It's used for:

  • Payment matching (Zahlungseingang)
  • Correct amounts for DATEV export

Fields:

FieldTypeDescription
isActivebooleanEnables the setTotalAmount logic. If set to false, this entire block is ignored.
maximumDifferenceToCalculatedSumfloatMaximum allowed difference in EUR (e.g., 0.05 = 5 cents)
totalGrossAmountFromExternalfloatGross 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 normal or reduced, 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

  1. Request (if provided) → used
  2. Customer (if nothing in request) → Billing/Shipping address from master data
  3. 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

  1. RequestpaymentTerms in request
  2. Customer → Payment terms in master data
  3. Payment Method → Default of payment method
  4. 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):

StrategyRecommendation
Customer IDsCache in middleware/database
Product IDsCache in middleware/database
Payment/Shipping methodsQuery once, store statically, refresh daily
Duplicate checkAlways set externalOrderNumber

This reduces API calls per order from ~6 to 1-2.


Error Handling

StatusMeaningCommon Cause
400Bad RequestInvalid JSON or validation error (missing required fields)
401UnauthorizedInvalid or expired token
403ForbiddenMissing required scope (e.g., salesOrder:create)
404Not FoundInvalid URL or resource ID doesn't exist
409ConflictAction not allowed on resource in current state (see Order Lifecycle)
429Too Many RequestsRate limit exceeded

Note: Validation errors return 400 Bad Request, not 422. The 409 Conflict error 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:

RemainingRecommended Action
100-50Full speed (no delay)
50-25Add a small delay (e.g., 50ms) between requests
< 25Add 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

Help Center

Related Guides