# Create Orders/Trackings

This API allows you to create new orders, update existing orders, add shipment trackings, cancel trackings, cancel orders, and manage partial line item cancellations.

## Endpoint Details

* Method: `PUT`
* Path: `/v4/track/orders/`
* Full URL: `https://api.parcellab.com/v4/track/orders/`
* Authentication: Header `Authorization: Parcellab-API-Token <token>`

This endpoint is idempotent, meaning you can safely call it multiple times with the same data without creating duplicate orders. If an order exists, it will be updated; if not, it will be created.

<details>

<summary><i class="fa-key">:key:</i> Authentication</summary>

Use the `Authorization` header with this format:

`Authorization: Parcellab-API-Token <token>`

API tokens can be created at `https://app.parcellab.com/service/account/apitoken/`. For `PUT /v4/track/orders/`, the token must include at least the `write` scope. Make sure to use the API token (non-encoded version, not the encoded token).

</details>

<details>

<summary><i class="fa-bug">:bug:</i> Error Handling</summary>

The API returns standard HTTP status codes:

* `200 OK` - Order successfully updated
* `201 Created` - New order created
* `400 Bad Request` - Invalid request data
* `401 Unauthorized` - Missing or invalid authentication
* `403 Forbidden` - Insufficient permissions
* `404 Not Found` - Order not found (for GET requests)

**Validation Errors**

Payload validation errors reject the whole request with `400 Bad Request`. For example, an invalid email address returns:

```json
{
  "type": "client_error",
  "errors": [
    {
      "code": "invalid",
      "detail": "Enter a valid email address.",
      "attr": "recipient_email"
    }
  ]
}
```

A mutation payload validation error can look like this:

```json
{
  "type": "client_error",
  "errors": [
    {
      "detail": "Cannot cancel 3 of item ITEM-001. Only 2 available.",
      "attr": "mutations.0.line_items.0.quantity_to_cancel",
      "code": "invalid"
    }
  ]
}
```

The `attr` value identifies the invalid field. Nested payloads use dot notation with array indexes, such as `mutations.0.tracking` for the first mutation's `tracking` object.

If an `add_tracking` mutation tries to assign a `tracking_number` + `courier` pair that already belongs to another order in the same account, the request is also rejected with `400 Bad Request`:

```json
{
  "type": "client_error",
  "errors": [
    {
      "code": "invalid",
      "detail": "tracking_number + courier already belongs to order ORD-100045.",
      "attr": "mutations.0.tracking"
    }
  ]
}
```

To resolve this, check which order already owns the tracking pair. If the shipment belongs to that order, update the existing order. If the tracking was assigned to the wrong order, cancel the existing tracking assignment before adding it to the correct order.

**Mutation Results**

Accepted requests return `200 OK` or `201 Created`, but each mutation still includes its own `result`. Always check `mutations[].result.success`, `mutations[].result.message`, and `mutations[].result.warnings`. The top-level `errors[]` array is used when the request is rejected; `mutations[].result.errors` belongs to an accepted mutation result and is usually empty.

```json
{
  "mutations": [
    {
      "type": "add_tracking",
      "tracking": {
        "tracking_number": "794600000001",
        "courier": "fedex"
      },
      "result": {
        "success": true,
        "message": "",
        "errors": {},
        "warnings": [
          "Dropped unrecognized fields from tracking payload: enterprise_code"
        ]
      }
    },
    {
      "type": "change_line_item_quantity",
      "line_item_id": "LINE-404",
      "quantity": 0,
      "result": {
        "success": false,
        "message": "Line item LINE-404 not found",
        "errors": {},
        "warnings": []
      }
    }
  ]
}
```

Some successful mutation results have an empty `message`, especially when the mutation is accepted for asynchronous tracking processing.

</details>

## Example API Call

Use following samples as a starting point for the API interaction. Swap your token in the header (use your token in the same format) and set your `account` key in the payload.

For sample payloads at different stages of the order lifecycle, see below: [#typical-use-cases-for-oms-and-wms-integration](#typical-use-cases-for-oms-and-wms-integration "mention")

<details>

<summary><code>curl</code></summary>

```shellscript
curl --request PUT \
  --url https://api.parcellab.com/v4/track/orders/ \
  --header 'Authorization: Parcellab-API-Token MTYxMjE5NzpocTF1L3hvbnV5NWVtY25lbHlmYzQrNylwKnJva3N1dXV0YzhoKXVl' \
  --header 'content-type: application/json' \
  --data '{"account":1612197,"order_number":"ORD-2026-02-25-01","recipient_email":"customer@example.com","destination_country_iso3":"USA"}'
```

</details>

<details>

<summary>JavaScript <code>fetch</code></summary>

```javascript
const url = 'https://api.parcellab.com/v4/track/orders/';
const options = {
  method: 'PUT',
  headers: {
    'content-type': 'application/json',
    Authorization: 'Parcellab-API-Token MTYxMjE5NzpocTF1L3hvbnV5NWVtY25lbHlmYzQrNylwKnJva3N1dXV0YzhoKXVl'
  },
  body: '{"account":1612197,"order_number":"ORD-2026-02-25-01","recipient_email":"customer@example.com","destination_country_iso3":"USA"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

</details>

<details>

<summary>Python <code>requests</code></summary>

```python
import requests

url = "https://api.parcellab.com/v4/track/orders/"

payload = {
  "account": 1612197,
  "order_number": "ORD-2026-02-25-01",
  "recipient_email": "customer@example.com",
  "destination_country_iso3": "USA"
}
headers = {
  "content-type": "application/json",
  "Authorization": "Parcellab-API-Token MTYxMjE5NzpocTF1L3hvbnV5NWVtY25lbHlmYzQrNylwKnJva3N1dXV0YzhoKXVl"
}

response = requests.put(url, json=payload, headers=headers)

print(response.json())
```

</details>

## Best Practices

1. **Use Idempotent Updates**: The `PUT` endpoint is idempotent. Use it for both creation and updates to avoid duplicates.
2. **Partial Updates**: For scalar order fields, only send the fields you want to update. Lists such as `articles_order` represent the current order-level list you send and can replace the stored list.
3. **Order Identification**: Use either `order_number` + `account` or `external_id` to identify orders. When creating new orders, `external_id` is generated by parcelLab and returned in the response.
4. **Tracking Identification**: Trackings require either `tracking_number` + `courier` or `external_reference`.
5. **Use `change_line_item_quantity` for partial cancellations**: If only part of an order line should be cancelled or reduced, send a `change_line_item_quantity` mutation with the new absolute quantity. For full-order cancellation, use the `cancel_order` mutation instead.
6. **Keep line identifiers unique per logical line**: `line_item_id` should uniquely identify one logical order line. If the same SKU appears on multiple lines, do not reuse the same `line_item_id`; use the source system's stable line key so each line can be targeted independently.
7. **Keep order and tracking line identifiers aligned**: If you send shipment-level `tracking.articles`, the identifiers in those articles should match the corresponding item in `articles_order` by `line_item_id` or `order_item_id`. This alignment is required for later line-item changes to be reflected correctly on the order status page and in downstream payloads.
8. **Check Mutation Results**: An accepted request can still contain `mutations[].result.success: false` for individual mutation failures. Also check `mutations[].result.warnings` for accepted payload changes, such as dropped custom tracking fields.
9. **Validate Line Item Quantities**: Before cancelling, ensure the requested quantity doesn't exceed available quantity.
10. **Tags**: You can set tags on a tracking to identify custom attributes in filters and exports. For example: `tags: ["loyalty_customer", "customer_loyalty:gold", "international_order", "dropship"]`.

## Data Models

#### Order Schema

For upserts, send only the fields you want to set or update.

<details>

<summary>Recommended fields to send</summary>

<table><thead><tr><th width="201">Field</th><th width="112">Type</th><th width="141.5">When to send</th><th>Notes</th></tr></thead><tbody><tr><td><code>account</code></td><td>integer</td><td>Always</td><td>Required</td></tr><tr><td><code>order_number</code></td><td>string</td><td>Create + most updates</td><td>Primary external reference in most integrations.</td></tr><tr><td><code>destination_country_iso3</code></td><td>string</td><td>Create</td><td>Required on create.</td></tr><tr><td><code>recipient_email</code></td><td>string</td><td>Create</td><td>Required on create.</td></tr><tr><td><code>recipient_name</code></td><td>string</td><td>Recommended</td><td>Useful for notifications and lookup UX.</td></tr><tr><td><code>client_key</code></td><td>string</td><td>Recommended</td><td>Important for multi-brand/multi-shop accounts.</td></tr><tr><td><code>shipping_address</code></td><td>AddressSchema</td><td>Recommended</td><td>Strongly recommended if available.</td></tr><tr><td><code>billing_address</code></td><td>AddressSchema</td><td>Optional</td><td>Useful if billing and shipping differ.</td></tr><tr><td><code>language_iso2</code></td><td>string</td><td>Recommended</td><td>Enables localized communication.</td></tr><tr><td><code>timezone</code></td><td>string</td><td>Optional</td><td>Helps communication timing.</td></tr><tr><td><code>delivery_method</code></td><td>string</td><td>Optional</td><td>Must be configured in system.</td></tr><tr><td><code>articles_order</code></td><td>LineItemOrder[]</td><td>Recommended</td><td>Needed for line-item-level mutations and visibility.</td></tr><tr><td><code>external_reference</code></td><td>string</td><td>Optional</td><td>Retailer internal order reference.</td></tr><tr><td><code>customer_number</code></td><td>string</td><td>Optional</td><td>Retailer customer identifier.</td></tr><tr><td><code>invoice_number</code></td><td>string</td><td>Optional</td><td>Invoice reference.</td></tr><tr><td><code>payment_method</code></td><td>string</td><td>Optional</td><td>Payment method.</td></tr><tr><td><code>order_currency</code></td><td>string</td><td>Optional</td><td>ISO 4217</td></tr><tr><td><code>order_total_amount</code></td><td>decimal</td><td>Optional</td><td>Order total.</td></tr><tr><td><code>order_tax_amount</code></td><td>decimal</td><td>Optional</td><td>Tax amount.</td></tr><tr><td><code>order_net_amount</code></td><td>decimal</td><td>Optional</td><td>Net amount.</td></tr><tr><td><code>order_discount_amount</code></td><td>decimal</td><td>Optional</td><td>Discount amount.</td></tr><tr><td><code>order_date</code></td><td>datetime</td><td>Optional</td><td>Order creation timestamp.</td></tr><tr><td><code>channel</code></td><td>string</td><td>Optional</td><td>Sales channel.</td></tr><tr><td><code>announced_delivery_date_min</code></td><td>date</td><td>Optional</td><td>Earliest expected delivery.</td></tr><tr><td><code>announced_delivery_date_max</code></td><td>date</td><td>Optional</td><td>Latest expected delivery.</td></tr><tr><td><code>cancelled_date</code></td><td>datetime</td><td>Optional</td><td>Use when explicitly marking the full order as cancelled.</td></tr><tr><td><code>cancelled_reason</code></td><td>string</td><td>Optional</td><td>Optional full-order cancellation reason such as <code>customer</code>, <code>inventory</code>, <code>payment</code>, or <code>other</code>.</td></tr><tr><td><code>tags</code></td><td>string[]</td><td>Optional</td><td>For filtering/export.</td></tr><tr><td><code>additional_attributes</code></td><td>AdditionalAttribute[]</td><td>Optional</td><td>Custom attributes.</td></tr><tr><td><code>mutations</code></td><td>Mutation[]</td><td>Optional</td><td>Use for tracking and line-item changes.</td></tr></tbody></table>

</details>

<details>

<summary>Generated/returned by parcelLab</summary>

* `external_id` (UUID): generated by parcelLab on create.
* `mutations[].operation_id` (UUID): generated if not provided.
* `mutations[].result`: populated in responses with per-mutation processing status.

</details>

{% hint style="info" %}
Send `shipping_address` or `billing_address` only when you can provide a complete address object. Each address object requires `address_line`, `postal_code`, `city`, and `country_iso3`; blank strings and `null` values are not accepted for these fields. If a complete shipping address is not available, omit `shipping_address` and send order-level fields such as `destination_country_iso3`, `recipient_email`, `recipient_name`, and, when available, `recipient_postal_code` on the tracking instead.
{% endhint %}

#### Line Item Order Schema

Represents individual products in `articles_order`.

<details>

<summary>Recommended fields to send</summary>

| Field                   | Type                   | Notes                                                                          |
| ----------------------- | ---------------------- | ------------------------------------------------------------------------------ |
| `line_item_id`          | string                 | Required identifier for mutations. Must be unique per logical order line.      |
| `order_item_id`         | string                 | Recommended when available. Keep it stable across order and tracking payloads. |
| `quantity`              | integer                | Ordered/open quantity.                                                         |
| `sku`                   | string                 | Recommended                                                                    |
| `product_id`            | string                 | Recommended                                                                    |
| `variant_id`            | string                 | Recommended when variants exist.                                               |
| `article_name`          | string                 | Recommended for portal display.                                                |
| `article_category`      | string                 | Optional                                                                       |
| `article_store_url`     | string                 | Optional                                                                       |
| `article_image_url`     | string                 | Optional                                                                       |
| `unit_price`            | decimal                | Optional                                                                       |
| `tags`                  | string\[]              | Optional                                                                       |
| `additional_attributes` | AdditionalAttribute\[] | Optional                                                                       |

</details>

<details>

<summary>Generated/managed by parcelLab</summary>

* `shipped_quantity`
* `original_quantity`
* `status`
* `change_reason`
* `updated_at`

</details>

#### Tracking Schema

Represent an individual shipment, package or parcel used to fulfill (parts of) an order.

<details>

<summary>Recommended fields to send</summary>

| Field                         | Type            | Notes                                                                                                                   |
| ----------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `tracking_number` + `courier` | string + string | Primary tracking identifier pair.                                                                                       |
| `external_reference`          | string          | Alternative identifier where applicable.                                                                                |
| `articles`                    | LineItem\[]     | Recommended for shipment-level line-item mapping. Keep `line_item_id` or `order_item_id` aligned with `articles_order`. |
| `destination_country_iso3`    | string          | Recommended                                                                                                             |
| `recipient_postal_code`       | string          | Recommended when full address is not sent.                                                                              |
| `shipping_address`            | AddressSchema   | Recommended if tracking-level address differs.                                                                          |
| `courier_service_level`       | string          | Optional                                                                                                                |
| `warehouse`                   | string          | Optional                                                                                                                |
| `origin_country_iso3`         | string          | Optional                                                                                                                |
| `origin_postal_code`          | string          | Optional                                                                                                                |
| `announced_delivery_date`     | date            | Optional ETA communication.                                                                                             |
| `announced_send_date`         | date            | Optional                                                                                                                |
| `delivery_method`             | string          | Optional                                                                                                                |
| `shipping_cost_total`         | decimal         | Optional                                                                                                                |
| `shipping_weight_total`       | float           | Optional                                                                                                                |
| `shipping_weight_unit`        | string          | Optional                                                                                                                |
| `requires_signature`          | boolean         | Optional                                                                                                                |
| `is_return`                   | boolean         | Optional                                                                                                                |
| `flags`                       | string\[]       | Optional                                                                                                                |
| `tags`                        | string\[]       | Optional                                                                                                                |
| `additional_fields`           | object          | Optional custom payload.                                                                                                |

</details>

{% hint style="info" %}
Customer-defined tracking keys must be sent inside `tracking.additional_fields` as key-value pairs. Do not send custom keys such as `seller_organization_code` or `enterprise_code` at the top level of the `tracking` object.
{% endhint %}

<details>

<summary>Example custom field structure</summary>

```json
{
  "type": "add_tracking",
  "tracking": {
    "tracking_number": "T100001903",
    "courier": "fedex",
    "delivery_number": "PO_S100001903",
    "external_reference": "PO_S100001903",
    "warehouse": "RKC_VENDOR",
    "additional_fields": {
      "enterprise_code": "13",
      "seller_organization_code": "RKC_VENDOR"
    }
  }
}
```

</details>

<details>

<summary>Generated/managed by parcelLab</summary>

* `cancelled_date` can be set by parcelLab when a `cancel_tracking` mutation is processed.

</details>

<details>

<summary>Example tracking payload as JSON</summary>

```json
{
  "tracking_number": "1234567890",
  "courier": "dhl",
  "external_reference": "SHIP-2024-001",
  "recipient_postal_code": "10001",
  "destination_country_iso3": "USA",
  "language_iso2": "en",
  "timezone": "America/New_York",
  "courier_service_level": "express",
  "warehouse": "WH-US-EAST",
  "origin_country_iso3": "USA",
  "origin_postal_code": "07001",
  "announced_delivery_date": "2024-01-18",
  "announced_send_date": "2024-01-16",
  "shipping_weight_total": 1500,
  "shipping_weight_unit": "g",
  "requires_signature": true,
  "articles": [
    {
      "line_item_id": "ITEM-001",
      "product_id": "PROD-123",
      "article_name": "Blue T-Shirt",
      "quantity": 2,
      "sku": "TSHIRT-BLUE-M"
    }
  ],
  "tags": ["express-delivery", "high-value"],
  "additional_fields": {
    "special_instructions": "Handle with care"
  }
}
```

</details>

#### Additional Data

parcelLab in general allows you to send additional attributes with custom data. For full data-element definitions, refer to the following page.

{% content-ref url="/pages/-LQpTnZA2sICcHRiB8fQ" %}
[Data Model](/docs/developers/data-elements/data-model.md)
{% endcontent-ref %}

## Mutations

Mutations allow you to perform action-oriented changes on an order without replacing the full order payload. They can be used to add or cancel trackings, cancel an order, and change line items.

Supported mutation types are:

* `add_tracking`
* `cancel_tracking`
* `cancel_order`
* `change_line_item_quantity`
* `add_line_item`
* `replace_line_item`

<details>

<summary>Mutation validation and response behavior</summary>

**Request-Blocking Validation**

Some payload problems reject the whole request with `400 Bad Request`. Examples include malformed order fields, invalid email addresses, invalid country or timezone codes, unsupported mutation types, missing tracking identifiers, and tracking payloads that do not match the required shape.

Other problems are reported per mutation in the response. In those cases the request can still return `200 OK` or `201 Created`, but the affected mutation has `mutations[].result.success: false`. Examples include changing a line item that is not present on the order, failing a `current_quantity` guard, or trying to add a line item whose `line_item_id` already exists.

As a rule of thumb, request validation blocks malformed payloads and conflicts that can be detected before processing starts. Business-rule failures while applying a valid mutation are returned in that mutation's `result`.

**Warnings on Accepted Requests**

Non-fatal validation warnings are returned in `mutations[].result.warnings`. Current warning cases include:

* Unrecognized top-level fields inside an `add_tracking.tracking` payload are dropped. Send custom tracking data inside `tracking.additional_fields` instead.
* Updating an existing order with both `articles_order` and mutations can warn when the incoming `articles_order` list replaces the stored order-level list and omits existing line items.

{% hint style="info" %}
For accepted requests, inspect every `mutations[].result` entry. The HTTP status tells you whether the request payload was accepted; the mutation result tells you whether each requested action was applied or whether it produced warnings.
{% endhint %}

**Best-Effort Tracking Conflict Validation**

For `add_tracking`, parcelLab checks the `tracking_number` + `courier` pair when both values are present. If the same pair already belongs to a different order in the same account, the request is rejected with `400 Bad Request`.

This validation is intentionally scoped:

* It only runs for `add_tracking` mutations.
* It only runs when the mutation contains both `tracking_number` and `courier`.
* It checks for conflicts within the same account.
* It allows the same pair when it resolves to the same order.

The check is best effort and eventually consistent. It catches common integration mistakes at request time, but it does not replace downstream uniqueness checks. If the latest tracking data is not available during request validation, the request can still be accepted and later processing may still prevent the duplicate tracking from being persisted. A `2xx` response means parcelLab accepted the order API request for processing; use the order response, later status lookups, and configured status updates to confirm the final tracking state.

Example rejected response:

```json
{
  "type": "client_error",
  "errors": [
    {
      "code": "invalid",
      "detail": "tracking_number + courier already belongs to order ORD-100045.",
      "attr": "mutations.0.tracking"
    }
  ]
}
```

</details>

<details>

<summary><code>AddOrUpdateTrackingMutation</code></summary>

Adds or updates a tracking of an order. Update an existing tracking by running this mutation and using the same values for keys `courier` and `tracking_number`, as the combined value of those fields are idempotent.

```json
{
  "type": "add_tracking",
  "tracking": {
    // TrackingSchema fields
  }
}
```

</details>

<details>

<summary><code>CancelTrackingMutation</code></summary>

Cancel a tracking by identifying it by its `courier` and `tracking_number`.

```json
{
  "type": "cancel_tracking",
  "tracking": {
    "tracking_number": "1234567890",
    "courier": "dhl"
    // Only identifiers needed
  }
}
```

</details>

<details>

<summary><code>ChangeLineItemQuantityMutation</code></summary>

Cancel or reduce the open quantity of an order line item. Send the new absolute quantity for the line item. Quantities already assigned to a tracking are considered fulfilled and cannot be cancelled anymore. When `current_quantity` is provided, parcelLab compares it with the stored quantity and fails the mutation if the values differ.

This is a line-item mutation. Use it when a specific line should be reduced or cancelled by setting its `quantity` to `0`.

{% hint style="warning" %}
`change_line_item_quantity` targets one logical order line. Make sure the target line has a unique `line_item_id` in `articles_order`. If you also send shipment-level `tracking.articles`, keep `line_item_id` or `order_item_id` aligned between the order and tracking payloads so later line-item changes can be mapped correctly on the order status page.
{% endhint %}

```json
{
  "type": "change_line_item_quantity",
  "line_item_id": "ITEM-001",
  "quantity": 0,
  "current_quantity": 1,
  "reason": "customer_changed_mind"
}
```

If all order lines are changed to `quantity: 0`, downstream portals may end up displaying the order as cancelled because no active items remain. For an explicit full-order cancellation, use the `cancel_order` mutation instead of relying only on line-item mutations.

</details>

<details>

<summary><code>CancelOrderMutation</code></summary>

Cancel an entire order. The server sets `order_status` to `Cancelled` and generates `cancelled_date` when it is not provided. If the payload also contains conflicting top-level `order_status`, `cancelled_date`, or `cancelled_reason` fields, the mutation values take precedence.

```json
{
  "type": "cancel_order",
  "cancelled_reason": "customer",
  "cancelled_date": "2026-03-27T10:15:00Z"
}
```

Both `cancelled_reason` and `cancelled_date` are optional. Valid values for `cancelled_reason` are `customer`, `inventory`, `payment`, and `other`.

{% hint style="info" %}
`cancel_order` can be combined with other mutations in the same request. For example, sending `cancel_order` together with `add_tracking` will process both the tracking update and the order cancellation.
{% endhint %}

If shipment records already exist and should also be marked as cancelled, send `cancel_tracking` mutations for those trackings in the same request.

</details>

<details>

<summary><code>AddLineItemMutation</code></summary>

Adds a line item to an order.

```json
{
  "type": "add_line_item",
  "line_item": {
    "line_item_id": "ITEM-NEW",
    "product_id": "PROD-999",
    "article_name": "Green Hat",
    "quantity": 1,
    "unit_price": "19.99"
  }
}
```

</details>

<details>

<summary><code>ReplaceLineItemMutation</code></summary>

Replaces a line item with another line item. To cancel a line item, replace it with a `quantity: 0` line item.

```json
{
  "type": "replace_line_item",
  "old_line_item_id": "ITEM-001",
  "new_line_item": {
    "line_item_id": "ITEM-NEW2",
    "product_id": "PROD-888",
    "article_name": "Red Hat",
    "quantity": 1,
    "unit_price": "19.99"
  },
  "reason": "size_exchange"
}
```

</details>

## Typical Use Cases for OMS and WMS Integration

You can use the Order API to update parcelLab about all updates on the order from placement to cancellation or fulfillment.

### Integration Flow

1. **Order Placement**: Create the order with all known information
2. **Cancellations**: Handle partial cancellations with `change_line_item_quantity` or full-order cancellations with `cancel_order`
3. **Fulfillment**: Add trackings as items ship
4. **Delivery**: System automatically updates based on carrier events (outside of this API)

### Sample Mutations

<details>

<summary><i class="fa-file-circle-plus">:file-circle-plus:</i> Create a New Order</summary>

**Request:**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "ORD-2024-001",
  "recipient_email": "customer@example.com",
  "recipient_name": "John Doe",
  "destination_country_iso3": "USA",
  "language_iso2": "en",
  "order_total_amount": "129.99",
  "order_currency": "USD",
  "order_date": "2024-01-15T10:30:00Z",
  "shipping_address": {
    "first_name": "John",
    "last_name": "Doe",
    "address_line": "123 Main St",
    "city": "New York",
    "postal_code": "10001",
    "country_iso3": "USA",
    "region_code": "NY"
  },
  "articles_order": [
    {
      "line_item_id": "ITEM-001",
      "product_id": "PROD-123",
      "article_name": "Blue T-Shirt",
      "quantity": 2,
      "unit_price": "29.99"
    },
    {
      "line_item_id": "ITEM-002",
      "product_id": "PROD-456",
      "article_name": "Black Jeans",
      "quantity": 1,
      "unit_price": "69.99"
    }
  ]
}
```

**Response:**

```json
{
  "external_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "order_number": "ORD-2024-001"
  // ... all order fields ...
}
```

</details>

<details>

<summary><i class="fa-file-pen">:file-pen:</i> Update an Existing Order</summary>

Only send the fields you want to update.

**Request:**

```
PUT /v4/track/orders/
```

```json
{
  "order_number": "ORD-2024-001",
  "account": 1234,
  "customer_number": "CUST-98765",
  "payment_method": "credit_card"
}
```

</details>

<details>

<summary><i class="fa-box-circle-check">:box-circle-check:</i> Add Tracking to an Order</summary>

**Request:**

```
PUT /v4/track/orders/
```

```json
{
  "order_number": "ORD-2024-001",
  "account": 1234,
  "mutations": [
    {
      "type": "add_tracking",
      "tracking": {
        "tracking_number": "1234567890",
        "courier": "dhl",
        "warehouse": "WH-01",
        "announced_delivery_date": "2024-01-18",
        "articles": [
          {
            "line_item_id": "ITEM-001",
            "quantity": 2
          }
        ]
      }
    }
  ]
}
```

</details>

<details>

<summary><i class="fa-file-circle-xmark">:file-circle-xmark:</i> Cancel a Tracking</summary>

**Request:**

```
PUT /v4/track/orders/
```

```json
{
  "order_number": "ORD-2024-001",
  "account": 1234,
  "mutations": [
    {
      "type": "cancel_tracking",
      "tracking": {
        "tracking_number": "1234567890",
        "courier": "dhl"
      }
    }
  ]
}
```

</details>

<details>

<summary><i class="fa-ban">:ban:</i> Cancel an Order</summary>

**Request:**

```
PUT /v4/track/orders/
```

```json
{
  "order_number": "ORD-2024-001",
  "account": 1234,
  "mutations": [
    {
      "type": "cancel_order",
      "cancelled_reason": "customer"
    }
  ]
}
```

**Notes on Order Cancellation:**

* `cancelled_date` is generated server-side when not provided
* The mutation sets `order_status` to `Cancelled`
* If the payload contains conflicting top-level cancellation fields, the mutation values take precedence
* To also cancel existing shipment records, include `cancel_tracking` mutations in the same request

</details>

<details>

<summary><i class="fa-cart-circle-xmark">:cart-circle-xmark:</i> Cancel Line Items (Partial Cancellation)</summary>

This allows cancelling specific quantities of line items at the order level.

**Request:**

```
PUT /v4/track/orders/
```

```json
{
  "order_number": "ORD-2024-001",
  "account": 1234,
  "mutations": [
    {
      "type": "change_line_item_quantity",
      "line_item_id": "ITEM-001",
      "quantity": 0,
      "current_quantity": 1,
      "reason": "customer_changed_mind"
    }
  ]
}
```

**Notes on Line Item Cancellation:**

* Cancellations are tracked at the order level in `articles_order`
* The system validates quantity constraints (including shipped quantity guards)
* When line items are cancelled, updates are sent to all trackings in the order
* Line item mutation metadata is stored on the item (for example `status`, `original_quantity`, `change_reason`, `updated_at`)

</details>

<details>

<summary><i class="fa-conveyor-belt-boxes">:conveyor-belt-boxes:</i> Complex Multi-Operation Update</summary>

You can combine multiple operations in a single request.

**Request:**

```
PUT /v4/track/orders/
```

```json
{
  "order_number": "ORD-2024-001",
  "account": 1234,
  "customer_number": "CUST-UPDATE",  // Update order field
  "mutations": [
    {
      "type": "add_tracking",
      "tracking": {
        "tracking_number": "9876543210",
        "courier": "ups",
        "articles": [
          {
            "line_item_id": "ITEM-002",
            "quantity": 1
          }
        ]
      }
    },
    {
      "type": "change_line_item_quantity",
      "line_item_id": "ITEM-001",
      "quantity": 0,
      "current_quantity": 1,
      "reason": "customer_changed_mind"
    }
  ]
}
```

</details>

<details>

<summary><i class="fa-boxes-packing">:boxes-packing:</i> OMS Mixed Status Example (2 Cancelled, 3 Shipped, 1 Remaining)</summary>

**Scenario:**

* `order_number`: `OMS-100045`
* Line `LINE-1` was ordered with quantity `6`
* OMS then cancels `2`
* FedEx ships `3`
* `1` remains open/backordered for later shipment

**Step 1: Create the order (minimal but valid create payload):**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "OMS-100045",
  "destination_country_iso3": "USA",
  "recipient_email": "customer@example.com",
  "articles_order": [
    {
      "line_item_id": "LINE-1",
      "sku": "SKU-RED-42",
      "quantity": 6
    },
    {
      "line_item_id": "LINE-2",
      "sku": "SKU-BLUE-10",
      "quantity": 1
    },
    {
      "line_item_id": "LINE-3",
      "sku": "SKU-GREEN-7",
      "quantity": 1
    }
  ]
}
```

**Step 2: OMS partial cancellation (6**→**4) using `change_line_item_quantity`:**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "OMS-100045",
  "mutations": [
    {
      "type": "change_line_item_quantity",
      "line_item_id": "LINE-1",
      "quantity": 4,
      "current_quantity": 6,
      "reason": "oms_partial_cancel"
    }
  ]
}
```

**Step 3: First FedEx shipment (`3` units) using `add_tracking`:**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "OMS-100045",
  "mutations": [
    {
      "type": "add_tracking",
      "tracking": {
        "tracking_number": "794600000001",
        "courier": "fedex",
        "articles": [
          {
            "line_item_id": "LINE-1",
            "quantity": 3
          }
        ]
      }
    }
  ]
}
```

**Step 4: Remaining quantity (`1`) is implicit.**

No additional mutation is required to represent "not yet shipped/backordered". At this point:

* Ordered/open quantity for `LINE-1` is `4`
* Shipped quantity recorded across trackings is `3`
* Remaining not-yet-shipped quantity is `1`

**Step 5a (later): If the remaining `1` ships, send another `add_tracking`:**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "OMS-100045",
  "mutations": [
    {
      "type": "add_tracking",
      "tracking": {
        "tracking_number": "794600000002",
        "courier": "fedex",
        "articles": [
          {
            "line_item_id": "LINE-1",
            "quantity": 1
          }
        ]
      }
    }
  ]
}
```

**Step 5b (alternative): If the remaining `1` is cancelled instead of shipped:**

```json
PUT /v4/track/orders/
{
  "account": 1234,
  "order_number": "OMS-100045",
  "mutations": [
    {
      "type": "change_line_item_quantity",
      "line_item_id": "LINE-1",
      "quantity": 3,
      "current_quantity": 4,
      "reason": "oms_cancel_remaining"
    }
  ]
}
```

</details>

<details>

<summary><i class="fa-cart-circle-exclamation">:cart-circle-exclamation:</i> Other Minimal Mutation Payloads (Same <code>order_number</code> Pattern)</summary>

**Add a line item:**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "OMS-100045",
  "mutations": [
    {
      "type": "add_line_item",
      "line_item": {
        "line_item_id": "LINE-4",
        "sku": "SKU-BLACK-9",
        "quantity": 1
      }
    }
  ]
}
```

**Replace a line item (substitution):**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "OMS-100045",
  "mutations": [
    {
      "type": "replace_line_item",
      "old_line_item_id": "LINE-2",
      "new_line_item": {
        "line_item_id": "LINE-2B",
        "sku": "SKU-BLUE-10-V2",
        "quantity": 1
      },
      "reason": "substitution"
    }
  ]
}
```

**Cancel an existing tracking (e.g., label voided):**

```
PUT /v4/track/orders/
```

```json
{
  "account": 1234,
  "order_number": "OMS-100045",
  "mutations": [
    {
      "type": "cancel_tracking",
      "tracking": {
        "tracking_number": "794600000001",
        "courier": "fedex"
      }
    }
  ]
}
```

</details>

## FAQs

<details>

<summary>Where do I get an API token?</summary>

Create it in the parcelLab App:

* Open <https://app.parcellab.com/service/account/apitoken/>
* Create a token with at least `read` and `write` scope
* Use the **non-encoded** token value in your API calls
  * If the UI shows both an “encoded” and “non-encoded” value, pick the **non-encoded** one
* Send it via this header: `Authorization: Parcellab-API-Token <token>`

</details>

<details>

<summary>Where do I find my Account ID?</summary>

Two common ways:

**In the parcelLab App (recommended)**

* Open <https://app.parcellab.com/service/account/account/>
* Select the account you want to use
* Copy the 7-digit number shown under `ID`

**From an encoded token (optional)**

* If you have an *encoded* token, it can be Base64-decoded into the format:\
  `<account_id>:<token>`

</details>

<details>

<summary>Why am I getting a <code>401</code> Unauthorized error when requesting with my token?</summary>

This is almost always one of these:

* Wrong header format. It must be exactly:\
  `Authorization: Parcellab-API-Token <token>`
* Using `Bearer` instead of `Parcellab-API-Token`
* Copy/paste issues (extra whitespace, surrounding quotes)
* Token scope missing `write` (required for this endpoint)
* Account mismatch: The token belongs to a different account than the `account` you send in the payload

</details>

<details>

<summary>I sent a tracking update with <code>courier</code> and <code>tracking_number</code> and got a positive response, but the tracking record does not show up. What should I check?</summary>

Check the response body first, especially `mutations[].result`.

* If `mutations[].result.success` is `false`, use the `message` field to identify why the mutation was not applied.
* If `mutations[].result.warnings` contains entries, review them before retrying. For example, custom tracking fields sent outside `tracking.additional_fields` can be dropped from the accepted payload.
* If the response looks positive but the tracking still does not appear, check whether the same `tracking_number` + `courier` already exists on another order in the same account.

parcelLab validates duplicate `tracking_number` + `courier` assignments for `add_tracking` requests when possible. This check is best effort and eventually consistent: a duplicate can be blocked immediately with `400 Bad Request`, or the request can be accepted before later processing prevents the duplicate tracking from being persisted.

</details>

<details>

<summary>Why did my request fail with <code>400 Bad Request</code>?</summary>

A `400 Bad Request` means the payload failed request-blocking validation and the request was not accepted for processing. The response includes an `errors[]` list with a `detail` message and an `attr` path pointing to the invalid field.

Common causes include invalid email, country, currency, language, or timezone values; missing tracking identifiers; unsupported mutation types; malformed address or line-item payloads; and `tracking_number` + `courier` pairs that already belong to another order in the same account.

Fix the field identified by `attr` and retry the request. If the error points to `mutations.0.tracking`, it refers to the `tracking` object in the first mutation.

</details>

<details>

<summary>I sent data and got a <code>2xx</code> response, but cannot see my data in the app?</summary>

Check these in order:

* You’re in the right account in the app (account switcher in the top-left)
* The `account` in your payload matches the account you’re viewing
* You’re looking at the right area and filters (orders, timeframe, status, etc.)
* For mutation requests, check `mutations[].result.success`, `mutations[].result.message`, and `mutations[].result.warnings` in the response.

If everything matches and it still doesn’t show up, wait a few minutes. Under times of heavy load ingestion can be deferred in favor of critical events.

</details>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.parcellab.com/docs/developers/orders/create-orders-trackings.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
