Product Split: moving stock into another SKU
Audience: developers integrating a merchant's ERP or inventory system with Logitrail's Product Inventory API, who need to move part of a product's warehouse stock under a different SKU, and developers migrating from the legacy product_split endpoint.
This guide explains what a split does, which stock it moves, how to call it safely, and how to move over from the legacy endpoint. It assumes you already have Logitrail API credentials — see Authentication if you don't. The endpoint reference (every field, type and response schema) is in the Product Inventory API reference.
1. Base URL and versioning
| Environment | Base URL |
|---|---|
| Test / sandbox | https://api-1.test.logitrail.com |
| Production | https://api-1.logitrail.com |
The endpoint is POST /v1/products/split.
2. Authentication
Every request needs the standard headers (OAuth 2.0 client-credentials bearer token + merchant id), plus the Idempotency-Key header described in section 6:
| Header | Value |
|---|---|
Authorization | Bearer <access_token> |
X-Logitrail-Merchant-Id | Your Logitrail merchant id |
Idempotency-Key | A key you choose per intended split (required) |
The client needs the products:manage scope. See Authentication for how to obtain credentials and tokens.
3. What a split does
A split moves units of one product (the source) to another product (the target). The units stay on the same shelves and keep their best-before dates; only the product they belong to changes.
The typical use case is stock that is close to expiry. Say product 41473 has units with several best-before dates and you want to sell the January 2027 units separately, at a discount, under their own SKU 41473-2027-01. A split moves exactly those units.
Worked example — move all units with best-before 2027-01-15 into a new SKU:
POST /v1/products/split
Authorization: Bearer <access_token>
X-Logitrail-Merchant-Id: <merchant id>
Idempotency-Key: 41473:2027-01-15:2026-10-17
Content-Type: application/json
{
"source_sku": "41473",
"target": {
"sku": "41473-2027-01",
"name": "Example product, best before January 2027",
"name_translations": { "fi": "Esimerkkituote, parasta ennen tammikuu 2027" }
},
"filters": { "best_before": "2027-01-15" }
}
{
"moved_quantity": 100,
"source_product_id": "…",
"source_sku": "41473",
"target_product_id": "…",
"target_sku": "41473-2027-01",
"target_created": true,
"inbound_shipment_id": "…",
"incident_id": "…"
}
4. Which stock is moved
Only units that are on the shelf and free are moved. Units that are reserved for an order, being picked, not yet shelved, written off, or blocked are never moved.
Because of this, a split can move fewer units than the product's total quantity, and the response tells you exactly how many moved (moved_quantity).
5. The request
| Field | Required | Meaning |
|---|---|---|
source_sku | yes | SKU of the product whose stock is split. Must be one of your products. |
target.sku | yes | SKU of the product that receives the stock. Must differ from source_sku. |
target.name | no | Name for a newly created target. Defaults to the source product's name. |
target.name_translations | no | Translated names for a newly created target, keyed by language code (for example fi). |
filters.best_before | no | Only units with exactly this best-before date (YYYY-MM-DD). Units without a date never match. |
filters.location | no | Only units on the shelf with this location code (for example AF-18-2-1). |
filters.max_quantity | no | Move at most this many units, lowest unit id first (roughly the order they were received). |
All filters are optional and combine with AND. Without filters, every available unit of the source product is moved.
A single split moves at most 5000 units. If more would be moved the request is refused with 400 SPLIT_TOO_LARGE and nothing changes; narrow it with filters or filters.max_quantity and repeat the split for the rest.
The target product
- If you already have a product with the target SKU, it receives the stock and is not modified —
target.nameandtarget.name_translationsare ignored. - If a product with the target SKU exists but is not active, the request is refused with 409
TARGET_PRODUCT_INACTIVE. - Otherwise the target is created as a copy of the source product, including its GTIN, with the name fields you supplied applied.
target_createdin the response tells you which case happened.
6. Retrying safely: the Idempotency-Key header
The Idempotency-Key header is required. A split moves physical stock records, so a network timeout followed by a blind retry must not move the stock twice.
- Choose a stable key per intended split — for example
<source sku>:<best-before>:<run date>. Use up to 64 visible ASCII characters, no spaces. - Send the same key on every retry of that split. A repeat with the same key and the same body returns the first result unchanged, with the response header
Idempotent-Replayed: true, and moves nothing. - Use a new key for a new split. Reusing a key with a different body is refused with 409.
- Keys are scoped to your merchant account and remembered for 30 days.
7. Responses and errors
| Status | Meaning | Retry? |
|---|---|---|
| 200 | The split was done, and moved_quantity says how many units moved. | — |
200, moved_quantity: 0 | No unit matched your filters. Nothing was created or changed; target_product_id, inbound_shipment_id and incident_id are null. | Not needed. |
400 INVALID_IDEMPOTENCY_KEY | The Idempotency-Key header is missing or invalid. | Fix the request first. |
400 TARGET_SAME_AS_SOURCE | The target SKU equals the source SKU. | Fix the request first. |
400 SPLIT_TOO_LARGE | More than 5000 units would be moved. Nothing was changed. | Narrow the request with filters first. |
400 VALIDATION_ERROR | Invalid request body. validation_errors in the response lists each problem. | Fix the request first. |
404 SOURCE_PRODUCT_NOT_FOUND | You have no product with the source SKU. | Fix the SKU first. |
409 SPLIT_IN_PROGRESS | Another split of the same source product or into the same target SKU is running (or a request with the same key is still being processed). | Yes — retry after a few seconds. |
409 STOCK_CHANGED | The stock changed (for example an order reserved a unit) while the split was running. Nothing was moved. | Yes — retry with the same key. |
409 IDEMPOTENCY_KEY_REUSED | The key was already used with a different body. | No — use a new key. |
409 TARGET_PRODUCT_INACTIVE | A product with the target SKU exists but is not active. | No — use another SKU or reactivate the product. |
409 PHANTOM_STOCK_SUSPECTED | The stock figures for the product do not add up. Nothing was changed. | No — retrying gives the same answer. Contact Logitrail support. |
| 500 | Unexpected error. If it happens right after the split was committed, retry with the same key: you get the first result back (after a short wait if you see SPLIT_IN_PROGRESS). | Yes — same key. |
8. What you will see afterwards
- A processed inbound shipment named "Product Split" appears in your inbound shipment list for the target product. It is created by the system, is already processed, and is not invoiced. No
inbound_shipment.processedwebhook is sent for it. - Inventory change webhooks fire for both the source and the target product.
- Units keep their batch number (if any) and best-before date in the target product.
- The units leaving the source product are recorded as an incident on the source product (
incident_idin the response).
9. Migrating from the legacy endpoint
The legacy POST /2015-01-01/products/product_split endpoint is being retired. The new endpoint validates stock eligibility more strictly. Logitrail will confirm the legacy route's switch-off date with you separately.
9.1 Authentication changes
The legacy endpoint used session authentication. The new endpoint uses OAuth 2.0 client credentials plus the merchant header:
-
Ask Logitrail for M2M credentials (
client_id/client_secret) that include theproducts:managescope. -
Fetch a token. The token endpoint is the same for test and production; see Authentication for the details of the flow:
POST https://idp.logitrail.com/realms/logitrail/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<your-client-id>&client_secret=<your-client-secret>&scope=products:manage -
Send
Authorization: Bearer <access_token>andX-Logitrail-Merchant-Id: <merchant id>on every request.
9.2 Request mapping
| Legacy | New |
|---|---|
source_product.merchants_id | source_sku |
target_product.merchants_id and other target_product.* product fields | target.sku, target.name, target.name_translations |
best_before | filters.best_before |
location (internal location id) | filters.location (location code) |
max_count | filters.max_quantity |
op_name | dropped |
| (none) | Idempotency-Key header, required |
9.3 Response mapping
| Legacy | New |
|---|---|
success: false, reason: no_articles_found | 200 with moved_quantity: 0 |
source_product / target_product full product JSON | source_product_id, source_sku, target_product_id, target_sku. Fetch the full product from the Products API if you need it. |
inbound_shipment full JSON | inbound_shipment_id |
incident.id | incident_id |
9.4 Behaviour differences to plan for
- The new endpoint may move fewer units than the legacy one did for the same request, because stock that is not on the shelf and free is excluded.
- An existing target product is no longer updated with the supplied product fields.
- Repeating a call with the same
Idempotency-Keyis a no-op that returns the first result. - A split already running on the same source SKU, or into the same target SKU, returns 409
SPLIT_IN_PROGRESSinstead of waiting.
9.5 Cutover checklist
- Get test credentials with the
products:managescope. - Validate your integration against the test base URL.
- Switch production to the new endpoint.
- Confirm with Logitrail that production runs on the new endpoint; the legacy route is turned off after that.