# Send orders to SparkLayer

URL: https://docs.sparklayer.io/developers/api/purchasing/send-orders

Build Purchasing API orders that show correctly: packages for shipments, returns and cancellations, statuses, totals, currencies, customers and safe re-runs.

> **For AI assistants:** these facts apply to every SparkLayer API call.
>
> - **Base URLs:** Live `https://app.sparklayer.io`, test `https://test.app.sparklayer.io`. Each environment has its own data and its own API keys.
> - **Access:** API access needs the Pro or Enterprise plan. Create credentials (Site ID, Client ID, Client Secret) in the SparkLayer Dashboard at Settings > API.
> - **Access token:** `POST {base}/api/auth/token` with a `Site-Id` header and a JSON body (not form-encoded): `{"grant_type":"client_credentials","client_id":"…","client_secret":"…"}`. It returns `access_token`, valid for 3,600 seconds. There is no refresh token: cache the token and request a new one shortly before it expires.
> - **Every request:** `Authorization: Bearer <access_token>` and `Site-Id: <site id>`, plus `Content-Type: application/json` when there is a body. Set a `User-Agent` that names your integration.
> - **Errors:** RFC 7807 problem details: `title`, `status`, `detail` and an optional `errors[]` of `{ code, message, property }`. Some error responses have no body. Retry `5xx`, `429` (honouring `Retry-After`) and concurrent-update `409`s with exponential backoff; on `401`, get a new token once and retry; don't retry other `4xx` without changing the request.
> - **Pagination:** Most list endpoints return everything. `GET /api/v2/price-lists` pages with `page` and `page_size` (up to 250) until `pagination.current_page` equals `total_pages`. `GET /api/v1/purchases` uses `limit` (up to 500) and `offset`: stop when a page has fewer than `limit` results.
> - **Ground rules:** Use only endpoints, fields and SDK methods that the docs or OpenAPI specs define. Prefer `GET /api/v2/price-lists` over the deprecated v1, and the Customers API (`/api/v2/customers`) over the legacy `DELETE /api/v1/customers` in the Core API. Products are matched by SKU. Keep credentials in environment variables or a secrets manager, never in code.
> - **Read the docs as Markdown:** Add `.md` to any page URL. Index of every page: https://docs.sparklayer.io/llms.txt. Every API operation, compactly: https://docs.sparklayer.io/llms-api.txt. The developer guides in full: https://docs.sparklayer.io/developers/llms.txt.
> - **OpenAPI specs:** `customers`, `core`, `ordering`, `pricing`, `purchasing`, `stock`, `files`, `sync-log`: https://docs.sparklayer.io/openapi/<api>.yaml (also `.json`). Ignite: https://docs.sparklayer.io/openapi/ignite.yaml.
> - **MCP:** Search and read these docs from your assistant with the docs MCP server at https://docs.sparklayer.io/mcp.

This guide explains how to build an order for [Create or update a purchase](https://docs.sparklayer.io/developers/api/purchasing/create-update-purchase.md) (`PUT /api/v1/purchases/{lookupBy}/{identifier}`), so that it shows correctly in My Account and in Pulse. It covers orders taken outside SparkLayer, such as phone orders keyed into your ERP, and one-off imports of past orders. The rules come from how SparkLayer's own Shopify, BigCommerce and Wix integrations send orders, and how the Purchasing API stores them.

Orders you send here aren't sent to your eCommerce platform, and nothing syncs them afterwards: SparkLayer shows them as you last sent them.

## How an order is put together

An order is a purchase of type `order`. Its line items live in **packages**: each package is one thing that happened to some of the items, such as a shipment, a cancellation or a return. SparkLayer works out the order's status and total from its packages.

An order and its packages:

1. **Order**: Identifiers, customer, dates, currency and addresses
2. **Packages**: One per shipment, cancellation or return, plus a processing package for anything still to ship
3. **Line items**: The SKUs and quantities in each package, with their amounts

| Part | What it holds |
| --- | --- |
| The order | `type`, `purchase_identifiers`, `customer_identifiers`, `dates`, `currency_code`, `billing_address`, `payment_method`, `po_number`, `custom_fields`, `accounting_files` |
| Each package | `type`, `identifiers`, `dates`, `line_items`, `shipping_method`, `shipping_address`, `shipments` (carrier and tracking), `accounting_files` |
| Each line item | `type`, `identifiers` (such as `sku`), `name_parent`, `name_child`, `quantity`, `line_total`, `custom_attributes` |

`packages`, `accounting_files` and `custom_fields` are required on the order, and `accounting_files` and `shipments` on each package. Send an empty array (`[]`) when there's nothing to put in them.

## Identify the order

`purchase_identifiers` holds the order's ID in each system. The `lookupBy` in the URL says which one `identifier` is.

| Identifier | Use it for |
| --- | --- |
| `internal` | Your ID for the order, such as the sales order number in your ERP. Use this as `lookupBy` for orders from your own systems. |
| `visible` | The order number customers see in My Account. Often the same as `internal`. |
| `platform` | The order's ID on your eCommerce platform, if it has one. |
| `sparklayer` | SparkLayer's ID (a UUID). Leave it out for orders from your own systems: SparkLayer generates one when it creates the order. Only send it for an order placed through SparkLayer, as the SparkLayer cart ID, so the order replaces the cart. |

- **The body must contain the ID in the URL.** For `PUT /api/v1/purchases/internal/SO-10452`, the body needs `"purchase_identifiers": { "internal": "SO-10452" }`. A missing or different value is rejected with a `400`.
- **Lookups are exact and case-sensitive.** `so-10452` doesn't find `SO-10452`.
- **Keep IDs to 64 characters**, and the `po_number` too.
- **SparkLayer doesn't stop two orders sharing an ID.** If 2 orders have the same `internal` ID, a lookup by it finds either of them. Give every order its own ID.
- **Never generate a new `sparklayer` ID each time you send an order.** A lookup by a new ID never finds the existing order, so every request creates another copy.

### Orders that are already in SparkLayer

Orders placed through SparkLayer, and orders your eCommerce platform syncs to SparkLayer (such as [Shopify orders marked for import](https://docs.sparklayer.io/help/guides/show-imported-orders.md)), are already there. If you send the same order from your ERP as well, it shows twice. Before importing an order you also have on your platform, check with [Get a purchase](https://docs.sparklayer.io/developers/api/purchasing/get-purchase.md), with `lookupBy=platform` and the platform's order ID, and skip it if it's found.

## Choose the customer

`customer_identifiers` says who the order belongs to.

| Identifier | Who |
| --- | --- |
| `sparklayer` | Required. The SparkLayer ID of the customer account the order belongs to. For [company users](https://docs.sparklayer.io/help/customers/company-users.md), this is the main account, not the person who ordered. |
| `sparklayer_child` | The company user who placed the order, if it wasn't the main account. |
| `sparklayer_impersonator` | The sales rep who placed the order for the customer, if one did. |

My Account lists an order for the account in `customer_identifiers.sparklayer`, and its company users see it too.

To find a customer's SparkLayer ID, look them up with [Get a customer](https://docs.sparklayer.io/developers/api/customers/get-a-customer.md) in the Customers API, by their ID on your eCommerce platform or by their email address. Use the `id` in the response:

```http title="Find a customer's SparkLayer ID"
GET /api/v2/customers/platform/{platform-customer-id}
GET /api/v2/customers/email/{url-encoded-email}
```

The Purchasing API doesn't check that the customer exists. An order for an ID SparkLayer doesn't know is stored, but no customer sees it in My Account and it drops out of sales reps' figures in Pulse. Make sure every customer is in SparkLayer before you send their orders, including customers who no longer buy from you. On Shopify, a customer syncs once they have the `b2b` tag: see [Tag existing customers in Shopify](https://docs.sparklayer.io/help/platforms/shopify/install.md#tag-your-customers-in-shopify). For a past customer who shouldn't be able to sign in, create them with [Create or update a customer](https://docs.sparklayer.io/developers/api/customers/create-or-update-a-customer.md) and `status` set to `inactive`: their orders count in Pulse, and you can make them `active` later.

## Packages

Each package has a `type`, and the dates that go with it. **SparkLayer works out a package's status from its dates, not its `type`**, so always set the date that matches the type. A `cancelled` package with no `cancelled_at`, for example, counts as still processing, and its items count towards the total.

| Package `type` | Dates to set | Counts as |
| --- | --- | --- |
| `processing` | None | Processing: ordered, not yet shipped |
| `shipment` | `shipped_at` | Shipped |
| `cancelled` | `cancelled_at` | Cancelled. Left out of the total |
| `return` | `return_started_at`, and `return_completed_at` once the return is complete | Returned. Taken off the total |
| `incoming` | None | Only for purchases with no `placed_at`. Orders always have one, so never send `incoming` for an order |

Give dates in RFC 3339 format, with a time zone offset or `Z` for UTC: `2024-03-18T14:00:00Z`.

### Rules for packages

1. **Every unit ordered is in exactly one `processing`, `shipment` or `cancelled` package.** Add up those packages and you get the quantities ordered.
2. **Returned units stay in their shipment.** List them again in a `return` package. SparkLayer subtracts returns from what was shipped, so a return package with no shipment package is rejected.
3. **Send one package for each real event:** each shipment, each cancellation and each return, with its own date.
4. **Put the shipping method on one package only.** Use the first shipment, or the `processing` package if nothing has shipped. Set `shipping_method` to `null` on every other package: each package with a shipping method adds its shipping cost to the total. Shipping on a cancelled or returned package is ignored.
5. **Amounts are for the package's quantity**, not the whole order line. If 6 of 10 units ship, the shipment's `line_total` is for 6 units: see [Line items and amounts](#line-items-and-amounts).
6. **Give each package a `sparklayer_package_id` that never changes.** It's a UUID you generate, unique within the order. Send the same ID for the same package every time, for example a version 5 UUID made from your order number and shipment number. SparkLayer treats a package with a new ID as a new package.

`identifiers.internal` and `identifiers.visible` on a package are your references for it, such as a delivery note number. `shipments` holds the carrier and tracking details: `carrier`, `carrier_id` (the tracking number) and `tracking_url`.

### A part-shipped order

10 units of `HB-200` were ordered. 6 have shipped and 4 are still to ship, so the order has a `shipment` package for 6 and a `processing` package for 4. The order's status is `part_shipped`. Amounts are left out to keep the example short.

```json title="packages (excerpt)"
[
  {
    "type": "shipment",
    "identifiers": { "sparklayer_package_id": "0b7c1f4e-5a2d-5c8e-9f31-6d2a7e4b8c10", "internal": "SO-10452-1" },
    "dates": { "shipped_at": "2024-03-18T14:00:00Z" },
    "line_items": [{ "identifiers": { "sku": "HB-200" }, "name_parent": "Handlebar grip", "quantity": 6, "line_total": { "…": "…" } }],
    "shipping_method": { "name": "Standard delivery", "line_total": { "…": "…" } },
    "shipments": [{ "carrier": "DPD", "carrier_id": "15501234567890", "tracking_url": null }],
    "accounting_files": []
  },
  {
    "type": "processing",
    "identifiers": { "sparklayer_package_id": "6f1d2c3b-8e4a-5b7c-a1d2-3e4f5a6b7c8d" },
    "line_items": [{ "identifiers": { "sku": "HB-200" }, "name_parent": "Handlebar grip", "quantity": 4, "line_total": { "…": "…" } }],
    "shipping_method": null,
    "shipments": [],
    "accounting_files": []
  }
]
```

When the other 4 ship, send the whole order again with a second `shipment` package for 4 and no `processing` package. The status becomes `shipped`.

### Cancellations

- **Whole order cancelled before anything shipped:** one `cancelled` package with every unit, dated when it was cancelled. The status is `cancelled` and the total is zero.
- **Some units cancelled:** take them out of the `processing` package and put them in a `cancelled` package with its `cancelled_at`. If the rest is still to ship, the status stays `processing`. If the rest has shipped, it's `varied`: see [Order status](#order-status).

### Returns and refunds

- **Whole order returned:** keep the shipment packages, and add a `return` package with every unit that shipped. When the units returned equal the units shipped, the status is `returned`.
- **Some units returned:** keep the shipment packages, and add a `return` package with the units that came back. The status is `part_returned`.
- **A refund with no items returned or cancelled**, such as a goodwill credit, isn't a package. Record the money with a [transaction](#payments-and-the-account-balance).

SparkLayer compares the total quantity shipped with the total quantity returned across the whole order, not SKU by SKU.

## Order status

SparkLayer calculates the order's status, `calculated_fulfilment_status`, from the statuses of its packages every time you send the order. It's read-only, so don't send it. There are no rules about which status can follow which: send the packages as they are now, and the status follows.

| The order's packages are | Status |
| --- | --- |
| All processing | `processing` |
| All shipped | `shipped` |
| Shipped and processing | `part_shipped` |
| All cancelled | `cancelled` |
| Processing and cancelled | `processing` |
| Shipped and returned, every shipped unit returned | `returned` |
| Shipped and returned, some shipped units returned | `part_returned` |
| Any other mix, such as shipped and cancelled | `varied` |
| No `placed_at` on the purchase | `incoming` |

## Line items and amounts

Each line item needs `identifiers`, `name_parent`, `quantity` and `line_total`.

- **`type`**: `product` for a product in your catalogue, or `custom` for anything else, such as a service or a fee.
- **`identifiers.sku`**: the SKU, up to 128 characters. SparkLayer matches order lines to your products by SKU, for example in Pulse's product figures.
- **`identifiers.sparklayer_variant_id`**: the variant's ID on your eCommerce platform, if you know it.
- **`name_parent`** and **`name_child`**: the product and variant names. They're kept as sent, so the order still reads correctly if the product is renamed or deleted later.
- **`custom_attributes`**: extra details for the line, as `key` and `value` pairs, such as engraving text.

Every amount is a money object: a `number` as a string and a 3-letter `currency`, such as `{ "number": "72.00", "currency": "EUR" }`.

| `line_total` field | What to send |
| --- | --- |
| `total_net` | Required. The line's price before tax, after discounts, for the package's quantity. |
| `discount_net` | Required. The discount already taken off `total_net`, for the customer's information. Use `"0.00"` for no discount. |
| `total_tax` | The tax on the line. |
| `total_gross` | `total_net` plus `total_tax`. |
| `total_net_base`, `discount_net_base`, `total_tax_base`, `total_gross_base` | The same amounts in your store's base currency: see [Currencies](#currencies). |

- **Every `line_total` needs a value** on an order. Only quotes can have a `null` line total.
- **`total_net`, `discount_net` and tax-line amounts must be in the order's `currency_code`.** A different currency is rejected.
- **Split amounts across packages by quantity.** For a line of 10 at `120.00` net, a shipment of 6 has `total_net` `72.00` and a processing package of 4 has `48.00`.
- **Leave out `tax_rate`.** It's deprecated: send the tax amount in `total_tax`, or as `tax_lines`.

The shipping method has the same `line_total` fields, plus a `name` and an optional `sku`.

### How SparkLayer adds up the total

`calculated_total` is the order's gross total. SparkLayer calculates it, so don't send it. It adds up:

- The net and tax of every line in `processing` and `shipment` packages.
- Minus the net and tax of every line in `return` packages.
- The net and tax of the shipping method on every package that isn't cancelled or returned.
- Lines in `cancelled` packages count for nothing.

If you send `tax_lines` on the order itself, SparkLayer uses their sum for the tax instead of the line-by-line tax, minus the tax on cancelled and returned lines. Leave them out unless your system gives you order-level tax lines (for example several tax rates) that you want customers to see.

## Currencies

Send the order's currency in `currency_code`. It's required.

Pulse reports in your store's **base currency**. Set it once, before your first order, with [Update the store currency](https://docs.sparklayer.io/developers/api/purchasing/update-store-currency.md), or send it with every order in `currency_code_base`.

- **Orders in the base currency:** you don't need to send any `_base` amounts. SparkLayer copies them from the order's amounts.
- **Orders in another currency:** send every `_base` amount yourself, converted at the rate on the order's date: `total_net_base`, `discount_net_base`, `total_tax_base` and `total_gross_base` on every line item and shipping method, and `amount_base` on every tax line.

If an order in another currency is missing a `_base` amount, SparkLayer converts its total at today's exchange rate instead, and Pulse leaves the order out of its revenue figures. `calculated_total_usd` always uses today's rate.

SparkLayer recalculates totals every time you send an order. If you set the store currency after sending orders, send them again to fill in their base totals.

## Dates

| Field | What to send |
| --- | --- |
| `dates.placed_at` | Required for orders: when the order was placed. Pulse and order statistics use this date, so imported orders appear on their real dates. |
| `dates.payment_due_at` | When payment is due, if you have it. |
| `shipping_requested_date` | The delivery date the customer asked for, if any. |
| `revision_date` | When the order last changed in your system. SparkLayer rejects an update with an older `revision_date` than the one it holds (`409`), so a delayed or repeated request can't overwrite newer data. |

`dates.created_at` and `dates.updated_at` are set by SparkLayer and can't be sent: they're the dates the order was stored. Sending a read-only field, such as `created_at` or `calculated_fulfilment_status`, is rejected with a `400`.

## Payments and the account balance

### The account balance

Each customer has an account balance: what they owe you, shown in the **Financials** section of [My Account](https://docs.sparklayer.io/help/storefront/interfaces/my-account.md) when you use [credit limits and pay on account](https://docs.sparklayer.io/help/ordering/credit-net-terms-and-invoicing.md).

- **Creating an order adds its total to the customer's balance**, dated `placed_at`. Updating it later adjusts the balance by the change in total.
- **A transaction with `apply_to_balance: true` takes its amount off the balance**, dated `transaction_date`.

Without an opening balance, the balance is every order and payment added together, however old. When a customer has an [opening balance](https://docs.sparklayer.io/help/ordering/payment-methods.md#payments---account-balance) with a balance date, only orders and payments dated on or after that date count.

Before you import past orders for a customer who pays on account, give them an opening balance dated after the newest order you'll import, so old orders don't change what they owe. On any platform, set it with [Adjust a customer's account balance](https://docs.sparklayer.io/developers/api/customers/adjust-a-customers-account-balance.md) and `type` set to `opening_balance`:

```json title="POST /api/v2/customers/platform/{platform-customer-id}/credit"
{
  "type": "opening_balance",
  "amount": 1250.00,
  "date": "2026-11-01T00:00:00Z"
}
```

### Payment status

SparkLayer calculates `payment_status` (`unpaid`, `part_paid` or `paid`) and `total_paid` from transactions, for orders placed on or after 6 March 2026. For earlier orders, it doesn't: send `payment_status` and `total_paid` yourself if you want them shown, and SparkLayer keeps them as sent. Pulse only counts orders placed on or after 6 March 2026 as amounts owed.

### Transactions

Record payments and refunds with [Create or update a purchase transaction](https://docs.sparklayer.io/developers/api/purchasing/create-or-update-a-purchase-transaction.md), after the order exists. Send:

| Field | What to send |
| --- | --- |
| `platform_id` | The transaction's ID in your payment or accounting system. Sending the same `platform_id` again for the order updates that transaction. |
| `type` | What it is, such as `payment` or `refund`. SparkLayer stores it as text. |
| `amount` | The amount as a string: positive for a payment, negative for a refund. |
| `currency_code` | The 3-letter currency code. |
| `transaction_date` | When the payment was made. |
| `apply_to_balance` | `true` to count it towards the payment status and the account balance, `false` to store it for information only. |
| `full_transaction` | The transaction as your system holds it, as a JSON string. Remove card numbers and other sensitive data first. |

See [Purchase transactions](https://docs.sparklayer.io/developers/api/purchasing.md#purchase-transactions) for what `apply_to_balance` does.

## Send an order again

Sending an order with the same `lookupBy` and `identifier` updates it rather than creating a copy, so an import can be run as often as you like.

- **Send the whole order every time.** The request replaces everything stored: a package you leave out is removed.
- **A few values are kept if you leave them out:** the `sparklayer` ID, `payment_status`, `total_paid` and `dates.created_at`.
- **Keep every ID the same:** the identifier you look up by and each `sparklayer_package_id`.
- **Send `revision_date`**, so an older version can't replace a newer one.

[Delete a purchase](https://docs.sparklayer.io/developers/api/purchasing/delete-purchase.md) removes an order from My Account and Pulse. Sending it again with the same identifier brings it back.

## Errors

| Status | Cause | Fix |
| --- | --- | --- |
| `400` | A required field is missing or invalid. The error names the field. | Correct the field and send the order again. |
| `400` | The ID in the URL isn't in `purchase_identifiers`, or doesn't match. | Put the same ID in the body. |
| `400` | "Line item total cannot be null for orders". | Give every line item a `line_total`. |
| `400` | "Invalid package status". | Use `processing`, `shipment`, `cancelled` or `return` packages, and set `dates.placed_at`. |
| `400` | A read-only field was sent, such as `created_at`. | Leave out read-only and calculated fields. |
| `400` | The order's total is over 99,999,999.9999. | Check the amounts. |
| `400` or `500` | A return package with no shipment package. | Keep returned units in their shipment package too. |
| `403` | The order was found, but the `sparklayer` ID in the body is different. | Leave `sparklayer` out, or send the stored one. |
| `409` | "A newer revision already exists": `revision_date` is older than the stored one. | Nothing to do: SparkLayer already has a newer version. |
| `409` | "Duplicate SparkLayer ID": the `sparklayer` ID belongs to another order. | Leave `sparklayer` out for orders from your own systems. |
| `500` | An ID or `po_number` over 64 characters. | Shorten it. |

See [Errors](https://docs.sparklayer.io/developers/errors.md) for the error format and when to retry.

## A complete order

Order `SO-10452` was placed on 14 March 2024, in euros, by a customer of a store whose base currency is pounds (1 EUR = 0.85 GBP on the order date). The customer ordered 10 handlebar grips and 4 seats, with 15.00 shipping, all with 20% VAT:

- On 18 March, 6 grips and the 4 seats shipped.
- On 25 March, the other 4 grips shipped.
- On 2 April, the customer started a return of 1 seat, which was completed on 9 April.

That's 2 `shipment` packages and a `return` package. 14 units shipped and 1 came back, so the status is `part_returned`. The total is 402.00 ordered, less the 60.00 seat returned: 342.00 EUR, or 290.70 GBP.

```http title="Request"
PUT /api/v1/purchases/internal/SO-10452
```

```json title="Body"
{
  "type": "order",
  "purchase_identifiers": {
    "internal": "SO-10452",
    "visible": "SO-10452"
  },
  "customer_identifiers": {
    "sparklayer": "4217ba4f-7618-4e3b-b1af-9055639b18d1"
  },
  "dates": {
    "placed_at": "2024-03-14T09:30:00Z",
    "payment_due_at": "2024-04-13T00:00:00Z"
  },
  "revision_date": "2024-04-09T16:00:00Z",
  "currency_code": "EUR",
  "currency_code_base": "GBP",
  "payment_method": "paymentByInvoice",
  "po_number": "PO-7781",
  "customer_reference": null,
  "billing_address": {
    "company": "Harbour Cycles Ltd",
    "address_line1": "12 Quay Street",
    "city": "Cork",
    "postal_code": "T12 X2Y3",
    "country_code": "IE"
  },
  "custom_fields": [],
  "accounting_files": [],
  "packages": [
    {
      "type": "shipment",
      "identifiers": {
        "sparklayer_package_id": "0b7c1f4e-5a2d-5c8e-9f31-6d2a7e4b8c10",
        "internal": "SO-10452-1",
        "visible": "SO-10452-1"
      },
      "dates": { "shipped_at": "2024-03-18T14:00:00Z" },
      "shipping_address": {
        "company": "Harbour Cycles Ltd",
        "address_line1": "12 Quay Street",
        "city": "Cork",
        "postal_code": "T12 X2Y3",
        "country_code": "IE"
      },
      "shipping_method": {
        "name": "Standard delivery",
        "line_total": {
          "total_net": { "number": "15.00", "currency": "EUR" },
          "discount_net": { "number": "0.00", "currency": "EUR" },
          "total_tax": { "number": "3.00", "currency": "EUR" },
          "total_gross": { "number": "18.00", "currency": "EUR" },
          "total_net_base": { "number": "12.75", "currency": "GBP" },
          "discount_net_base": { "number": "0.00", "currency": "GBP" },
          "total_tax_base": { "number": "2.55", "currency": "GBP" },
          "total_gross_base": { "number": "15.30", "currency": "GBP" }
        }
      },
      "shipments": [
        { "carrier": "DPD", "carrier_id": "15501234567890", "tracking_url": null }
      ],
      "accounting_files": [],
      "line_items": [
        {
          "type": "product",
          "identifiers": { "sku": "HB-200" },
          "name_parent": "Handlebar grip",
          "name_child": "Black",
          "quantity": 6,
          "line_total": {
            "total_net": { "number": "72.00", "currency": "EUR" },
            "discount_net": { "number": "18.00", "currency": "EUR" },
            "total_tax": { "number": "14.40", "currency": "EUR" },
            "total_gross": { "number": "86.40", "currency": "EUR" },
            "total_net_base": { "number": "61.20", "currency": "GBP" },
            "discount_net_base": { "number": "15.30", "currency": "GBP" },
            "total_tax_base": { "number": "12.24", "currency": "GBP" },
            "total_gross_base": { "number": "73.44", "currency": "GBP" }
          }
        },
        {
          "type": "product",
          "identifiers": { "sku": "SD-110" },
          "name_parent": "Comfort seat",
          "name_child": null,
          "quantity": 4,
          "line_total": {
            "total_net": { "number": "200.00", "currency": "EUR" },
            "discount_net": { "number": "0.00", "currency": "EUR" },
            "total_tax": { "number": "40.00", "currency": "EUR" },
            "total_gross": { "number": "240.00", "currency": "EUR" },
            "total_net_base": { "number": "170.00", "currency": "GBP" },
            "discount_net_base": { "number": "0.00", "currency": "GBP" },
            "total_tax_base": { "number": "34.00", "currency": "GBP" },
            "total_gross_base": { "number": "204.00", "currency": "GBP" }
          }
        }
      ]
    },
    {
      "type": "shipment",
      "identifiers": {
        "sparklayer_package_id": "9a4e8c2d-1b3f-5d6e-8a7b-2c4d6e8f0a1b",
        "internal": "SO-10452-2",
        "visible": "SO-10452-2"
      },
      "dates": { "shipped_at": "2024-03-25T11:15:00Z" },
      "shipping_address": {
        "company": "Harbour Cycles Ltd",
        "address_line1": "12 Quay Street",
        "city": "Cork",
        "postal_code": "T12 X2Y3",
        "country_code": "IE"
      },
      "shipping_method": null,
      "shipments": [
        { "carrier": "DPD", "carrier_id": "15501234567999", "tracking_url": null }
      ],
      "accounting_files": [],
      "line_items": [
        {
          "type": "product",
          "identifiers": { "sku": "HB-200" },
          "name_parent": "Handlebar grip",
          "name_child": "Black",
          "quantity": 4,
          "line_total": {
            "total_net": { "number": "48.00", "currency": "EUR" },
            "discount_net": { "number": "12.00", "currency": "EUR" },
            "total_tax": { "number": "9.60", "currency": "EUR" },
            "total_gross": { "number": "57.60", "currency": "EUR" },
            "total_net_base": { "number": "40.80", "currency": "GBP" },
            "discount_net_base": { "number": "10.20", "currency": "GBP" },
            "total_tax_base": { "number": "8.16", "currency": "GBP" },
            "total_gross_base": { "number": "48.96", "currency": "GBP" }
          }
        }
      ]
    },
    {
      "type": "return",
      "identifiers": {
        "sparklayer_package_id": "3c5e7a9b-2d4f-5a6c-9e8d-7f1a3b5c7d9e",
        "internal": "RMA-2231",
        "visible": "RMA-2231"
      },
      "dates": {
        "return_started_at": "2024-04-02T10:00:00Z",
        "return_completed_at": "2024-04-09T16:00:00Z"
      },
      "shipping_address": null,
      "shipping_method": null,
      "shipments": [],
      "accounting_files": [],
      "line_items": [
        {
          "type": "product",
          "identifiers": { "sku": "SD-110" },
          "name_parent": "Comfort seat",
          "name_child": null,
          "quantity": 1,
          "line_total": {
            "total_net": { "number": "50.00", "currency": "EUR" },
            "discount_net": { "number": "0.00", "currency": "EUR" },
            "total_tax": { "number": "10.00", "currency": "EUR" },
            "total_gross": { "number": "60.00", "currency": "EUR" },
            "total_net_base": { "number": "42.50", "currency": "GBP" },
            "discount_net_base": { "number": "0.00", "currency": "GBP" },
            "total_tax_base": { "number": "8.50", "currency": "GBP" },
            "total_gross_base": { "number": "51.00", "currency": "GBP" }
          }
        }
      ]
    }
  ]
}
```

The response is the stored order, with `calculated_fulfilment_status` of `part_returned`, `calculated_total` of `342.00` EUR and `calculated_total_base` of `290.70` GBP.

## Import order history

To load past orders once, so customers see them in My Account and your team sees them in Pulse:

1. **Start in the test environment.** Create an API key in [Test mode](https://docs.sparklayer.io/developers/authentication.md#environments), and import a few orders there first.
2. **Set the store base currency** with [Update the store currency](https://docs.sparklayer.io/developers/api/purchasing/update-store-currency.md), if it isn't set.
3. **Make sure every customer is in SparkLayer**, and map each customer in your export to their SparkLayer ID: see [Choose the customer](#choose-the-customer).
4. **Set opening balances** for customers who pay on account, dated after the newest order you'll import: see [The account balance](#the-account-balance).
5. **Leave out orders that are already in SparkLayer**: see [Orders that are already in SparkLayer](#orders-that-are-already-in-sparklayer).
6. **Build each order** from your order, shipment, return and credit note records, with the rules above. Send them one at a time, looked up by `internal` ID, and log every failure with its error.
7. **Run it again** for the orders that failed, once you've fixed them. Orders that went through are updated, not copied.
8. **Check the results** in My Account, signed in as a customer, and in Pulse. Pulse picks up new orders within about 4 hours.

There's a ready-made prompt for this in [Build with AI](https://docs.sparklayer.io/developers/build-with-ai.md#order-history), and a guide for merchants in [Import order history](https://docs.sparklayer.io/help/guides/import-order-history.md).

## Next steps

- [Purchasing API](https://docs.sparklayer.io/developers/api/purchasing.md): Identifiers, purchase types, transactions and every endpoint
- [Create or update a purchase](https://docs.sparklayer.io/developers/api/purchasing/create-update-purchase.md): The full request and response schema
- [Errors](https://docs.sparklayer.io/developers/errors.md): The error format, and when to retry
