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

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

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:

  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 (e.g., trying to release an already released order, or deleting a non-draft order)
429Too Many RequestsRate limit exceeded

Note: Validation errors return 400 Bad Request, not 422. The 409 Conflict error is primarily used in the V3 API.

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

  • Create Customer (planned)
  • Create Products (planned)