Skip to main content

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​

EnvironmentBase URL
Test / sandboxhttps://api-1.test.logitrail.com
Productionhttps://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:

HeaderValue
AuthorizationBearer <access_token>
X-Logitrail-Merchant-IdYour Logitrail merchant id
Idempotency-KeyA 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​

FieldRequiredMeaning
source_skuyesSKU of the product whose stock is split. Must be one of your products.
target.skuyesSKU of the product that receives the stock. Must differ from source_sku.
target.namenoName for a newly created target. Defaults to the source product's name.
target.name_translationsnoTranslated names for a newly created target, keyed by language code (for example fi).
filters.best_beforenoOnly units with exactly this best-before date (YYYY-MM-DD). Units without a date never match.
filters.locationnoOnly units on the shelf with this location code (for example AF-18-2-1).
filters.max_quantitynoMove 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.name and target.name_translations are 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_created in 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​

StatusMeaningRetry?
200The split was done, and moved_quantity says how many units moved.—
200, moved_quantity: 0No 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_KEYThe Idempotency-Key header is missing or invalid.Fix the request first.
400 TARGET_SAME_AS_SOURCEThe target SKU equals the source SKU.Fix the request first.
400 SPLIT_TOO_LARGEMore than 5000 units would be moved. Nothing was changed.Narrow the request with filters first.
400 VALIDATION_ERRORInvalid request body. validation_errors in the response lists each problem.Fix the request first.
404 SOURCE_PRODUCT_NOT_FOUNDYou have no product with the source SKU.Fix the SKU first.
409 SPLIT_IN_PROGRESSAnother 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_CHANGEDThe 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_REUSEDThe key was already used with a different body.No — use a new key.
409 TARGET_PRODUCT_INACTIVEA product with the target SKU exists but is not active.No — use another SKU or reactivate the product.
409 PHANTOM_STOCK_SUSPECTEDThe stock figures for the product do not add up. Nothing was changed.No — retrying gives the same answer. Contact Logitrail support.
500Unexpected 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.processed webhook 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_id in 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:

  1. Ask Logitrail for M2M credentials (client_id / client_secret) that include the products:manage scope.

  2. 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
  3. Send Authorization: Bearer <access_token> and X-Logitrail-Merchant-Id: <merchant id> on every request.

9.2 Request mapping​

LegacyNew
source_product.merchants_idsource_sku
target_product.merchants_id and other target_product.* product fieldstarget.sku, target.name, target.name_translations
best_beforefilters.best_before
location (internal location id)filters.location (location code)
max_countfilters.max_quantity
op_namedropped
(none)Idempotency-Key header, required

9.3 Response mapping​

LegacyNew
success: false, reason: no_articles_found200 with moved_quantity: 0
source_product / target_product full product JSONsource_product_id, source_sku, target_product_id, target_sku. Fetch the full product from the Products API if you need it.
inbound_shipment full JSONinbound_shipment_id
incident.idincident_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-Key is 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_PROGRESS instead of waiting.

9.5 Cutover checklist​

  1. Get test credentials with the products:manage scope.
  2. Validate your integration against the test base URL.
  3. Switch production to the new endpoint.
  4. Confirm with Logitrail that production runs on the new endpoint; the legacy route is turned off after that.