Creates a sale. The sale status comes from the **Default status for new sales** setting in the system settings, and the status determines whether products and materials are deducted immediately (see [Changing the Sale Status](https://developers.controlata.com/hc/api-docs/articles/api-orders-update-status)).

```
v1/orders/add.php
```

## Parameters

| Field | Type | Required | Description |
|---|---|---|---|
| num | string | Yes | Sale number without the prefix |
| products | array | products or materials | Product lines |
| materials | array | products or materials | Material lines. Accepted when the **Enable ordering materials** setting is on |
| customer_id | int | customer_id or customer_name | ID of an existing customer |
| customer_name | string | customer_id or customer_name | Customer name. If there is none, a customer is created from the customer_ prefixed fields |
| date_placed | string | No | Sale date. Default today |
| date_shipped | string | No | Shipping date |
| products_storage_id | int | No | Products storage location. Default from settings |
| materials_storage_id | int | No | Materials storage location. Default from settings |
| production | int | No | 1 to fulfill the sale by made-to-order production. Default 0 |
| delivery_price | float | No | Delivery cost. Default 0 |
| discount | float | No | Discount. Default 0 |
| notes | string | No | Notes |

The **Default fulfillment method for new sales** setting does not affect the API: without production, the sale is fulfilled from stock.

## Sale lines

The sale's lines are passed as two arrays: products for products and materials for materials. At least one line in one of the arrays is required.

| Field | Type | Required | Description |
|---|---|---|---|
| id | int | id or sku | Product or material ID |
| sku | string | id or sku | SKU. A product is also matched by its alternative SKUs |
| amount | float | Yes | Quantity |
| total | float | Yes | Line total (not the price per unit) |

If an item is not found, the line is skipped and a message is appended to the sale notes, for example:

```
Product with SKU "P099", amount 2, total 5000 not found in Controlata, check sku.
```

If no line is found, the sale is not created.

Materials in sales are accepted only if **Enable ordering materials** is on under **Settings → General**. Otherwise a request with materials is rejected.

## Customer

A customer is specified in one of two ways:

* **customer_id**: the ID of an existing customer, e.g. from [v1/customers/add](https://developers.controlata.com/hc/api-docs/articles/api-customers-add);
* **customer_name**: the customer's name. If there is no customer with that name, Controlata creates one together with the sale from the fields below.

| Field | Type | Description |
|---|---|---|
| customer_name | string | Name |
| customer_email | string | Email |
| customer_phone | string | Phone |
| customer_address_real | string | Delivery address |
| customer_address_legal | string | Legal address |
| customer_agreement | string | Agreement |
| customer_manager_name | string | Manager |
| customer_manager_post | string | Manager's position |
| customer_notes | string | Customer notes |

If both are passed, customer_id is used and customer_* are ignored. If a customer with that name already exists, the sale is linked to it and customer_* are not updated.

## Sale number

Controlata adds a prefix from the **Sale number prefix** setting in the API connection. With the prefix "A-", the number "1001" you pass is saved as "A-1001" and returned as "A-1001".

## Example request

```
{
    "num": "1001",
    "date_placed": "2026-10-06",
    "customer_name": "John Smith",
    "customer_phone": "+1 917 555-0145",
    "customer_email": "john@example.com",
    "customer_address_real": "5 Forest St, Apt 12, New York",
    "products": [
        {
            "sku": "P001",
            "amount": 2,
            "total": 26000
        },
        {
            "id": 816,
            "amount": 1,
            "total": 4500
        }
    ],
    "delivery_price": 500,
    "discount": 1000,
    "notes": "Order from the website"
}
```

## Example response

```
{
    "success": true,
    "order_id": 43727,
    "customer_id": 512
}
```

customer_id holds the sale's customer ID: the one found by customer_id or created from customer_name.

## Errors

| Error | Reason |
|---|---|
| No num in input | num is missing or empty |
| Products is not an array | products is not an array (same for materials) |
| No products or materials in input | No line was passed |
| Materials in orders are disabled in company settings | Materials were passed, but the **Enable ordering materials** setting is off |
| Storage ID is not set | An empty storage location was passed |
| Storage ID not found | Storage location not found or deleted |
| Date is not a valid date in format YYYY-MM-DD | Invalid sale or shipping date |
| Invalid delivery_price value. Must be a number, 0 or greater | Delivery cost is not a number or below 0 |
| Invalid discount value. Must be a number, 0 or greater | Discount is not a number or below 0 |
| Line 0 of products must be an object | The line is not an object (instead of 0, the line number) |
| SKU or id not set for product 0 | The line has neither id nor sku |
| Amount not set for product with SKU "P001" | The line has no amount |
| Total not set for product with SKU "P001" | The line has no total |
| Amount must be greater than 0 for product with SKU "P001" | Quantity is not a number or not above 0 after rounding |
| Total must be a number for product with SKU "P001" | The line total is not a number |
| All products have wrong SKU or id | No line was found. If materials were passed, the text is "All products and materials have wrong SKU or id" |
| No customer_name or customer_id in input | No customer was passed |
| Customer not found | The customer_id customer was not found or is deleted |
| Customer name must not be empty | Empty customer_name |
| Customer name must be a string | customer_name is not a string |

In line error texts, product becomes material for material lines, and instead of SKU "P001" there is id 816 if the line was passed by id.
