Skip to main content
Preview — not official documentation

This page is a preview. It describes documentation, APIs, or plans that have not shipped and are not officially published. Content may change or be withdrawn without notice before anything here becomes final.

We're sharing it early so the developer community can comment on it — if you have feedback, reach out to your Logitrail integration contact.

Order Return API — Integration Guide

Audience: developers integrating a merchant's order/ERP system with Logitrail's Order Return API.

This guide explains how to register returns with Logitrail, track their progress, tell Logitrail how a return should be handled once it arrives at the warehouse, and (optionally) request a prepaid return label for the customer. It assumes you already have Logitrail API credentials; if you don't, contact your Logitrail integration contact.


1. Base URL and versioning

EnvironmentBase URL
Test / sandboxhttps://api-1.test.logitrail.com
Productionhttps://api-1.logitrail.com

All endpoints in this guide are under the /v1/order-returns path. We recommend integrating against the test environment first.


2. Authentication

The API uses OAuth 2.0 with the client credentials grant. Logitrail issues each merchant a client_id / client_secret pair scoped to the permissions your integration needs.

2.1 Getting an access token

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>

The response is a standard OAuth token response:

{
"access_token": "eyJhbGciOi...",
"expires_in": 300,
"token_type": "Bearer"
}

Cache the token and request a new one shortly before it expires — do not request a fresh token on every API call.

2.2 Calling the API

Every request needs two headers:

HeaderValue
AuthorizationBearer <access_token>
X-Logitrail-Merchant-IdYour Logitrail merchant id

The scopes your credentials carry determine what you can do:

ScopeGrants
order_returns:readList and read returns, poll return-label status
order_returns:manageCreate, cancel, set the merchant decision, attach services, request return labels

If a request is missing a required header or scope, it is rejected with 401 Unauthorized or 403 Forbidden before reaching any business logic.


3. Core concepts

3.1 What an order return is

An order return represents a customer sending goods back to Logitrail's warehouse — whether or not the exact contents are known yet. A return can be created with full detail (which order it belongs to, which items, in what quantity) or as a bare placeholder that gets filled in once the parcel physically arrives and Logitrail's warehouse identifies it. This lets you register a return the moment a customer requests one, even before you know exactly what's coming back.

Two return types exist:

TypeMeaning
customer_returnThe customer initiated the return (the common case).
agent_returnA carrier or pickup point initiated the return because the original recipient never picked up the shipment.

3.2 Lifecycle and statuses

A return moves through the following statuses:

StatusMeaning
preinfo_receivedThe return has been registered with Logitrail but has not physically arrived yet. This is the initial status.
pending_merchant_decisionThe return has arrived at the Logitrail warehouse and is waiting for you to tell Logitrail how to handle it (see §3.3).
acceptedYou have set the merchant decision. The return is queued for processing.
processingLogitrail is actively processing the return per your decision.
processedProcessing is complete — this is a terminal status.
deletedThe return was cancelled before it reached the warehouse.

You'll also occasionally see the legacy statuses pending, resend_pending, and restocking_pending on older returns — treat them as informational; new integrations should key off the statuses above.

A return can only be cancelled while it is preinfo_received (or the legacy pending) — i.e. before it has physically arrived. Once Logitrail's warehouse has received it, cancellation is no longer possible.

The diagram below shows the overall lifecycle. What happens during processing depends on the whole-return decision code — see §3.3 for the four paths.

(Legacy statuses pending, resend_pending, restocking_pending are omitted from the diagram — they're informational only, see above.)

3.3 The merchant decision

Once a return arrives at the warehouse (status pending_merchant_decision), you must tell Logitrail what to do with it by calling _merchant-decision. The decision has two levels:

  1. A whole-return code — one of four processing paths:

    CodeWhat happens
    resend_as_it_isThe parcel is not opened; it is re-shipped to the customer against the original order, unchanged. Requires the return to have a linked original order.
    destroyThe return is destroyed. No items are shelved or resent.
    to_shelfThe items are inspected and shelved back into your stock.
    resend_as_newSome items are shelved and/or destroyed, and a new order is created for the items you want resent to the customer.
  2. A per-line decision — only applies to to_shelf and resend_as_new, and is optional: any line with no explicit decision defaults to the whole-return code's implied disposition, so you only need to supply items for lines you want to handle differently.

    Per-line codeMeaning
    to_shelfThis line goes back into stock. Optionally, you can redirect it to a different product than the one originally returned (e.g. the item is restocked as a B-class/outlet-grade variant) by supplying restock_as_product. Default for every line under a whole-return to_shelf decision.
    resendThis line is included in the new order created by a whole-return resend_as_new decision. Only legal when the whole-return code is resend_as_new. Default for every line under a whole-return resend_as_new decision.
    destroyThis line is destroyed. Never a default — only applies when you set it explicitly.

    For example, under a whole-return to_shelf decision you can omit items entirely (every line defaults to to_shelf), or supply items only for the lines that should be destroy-d instead — everything else still defaults to to_shelf. Same idea under resend_as_new: omit items for "resend everything," or list only the lines that should go to_shelf/destroy instead of the default resend.

    Note: you never create the inbound shipment yourself. Once a to_shelf or resend_as_new decision is released for processing, Logitrail's system automatically creates an inbound shipment from the return's to_shelf line items and links it back via inbound_shipment (see §4.1). Likewise, resend_as_new automatically creates the new order it resends to, linked via resend_order. Both are purely system-driven side effects of processing — you only ever set the decision, never the resulting shipment or order.

Recording a decision does not immediately start processing — it transitions the return to accepted and queues it for Logitrail's warehouse to release for processing when ready (see §3.2). Until release happens, you can send the decision again (whole-return code, per-line, or both) to replace what was previously recorded — each call fully replaces the prior decision rather than merging with it, so a call that omits a previously-decided line resets that line rather than leaving it untouched. Once the return has been released for processing, the decision is locked and further attempts to change it are rejected.


4. Data reference

4.1 Order return object

FieldTypeDescription
idstringTechnical Logitrail id (24-character hex string).
namestring | nullFree-form name for the return.
typestringcustomer_return or agent_return.
statusstringSee §3.2.
remarksstring | nullFree-form remarks.
original_order{ id } | nullThe order this return belongs to, if known.
merchant_referencestring | nullYour own reference for this return.
handling_instructionsstring | nullSpecial handling instructions for Logitrail's warehouse.
created_at / updated_atstring (ISO 8601)Timestamps.
itemsarrayLine items — see §4.2. Omitted/empty when the contents aren't known yet.
inbound_shipment{ id } | nullReference to the inbound shipment. System-created, not something you create or supply — Logitrail automatically creates it from the return's to_shelf line items once processing starts (to_shelf / resend_as_new decisions only). Null until then.
merchant_decision{ code } | nullThe whole-return decision, once set — see §3.3.
warehouse_location{ id } | nullThe warehouse location code the return arrived at, once received.
resend_order{ id } | nullThe new order created by a resend_as_new decision, once created.
tracking_codesarray of string | nullCarrier tracking codes for the return shipment, one entry per physical box (a return parcel can be multi-box). Set by Logitrail when the return label is created through the Logitrail API, or by you when the label is created independently. Settable at creation time (§5.1); look up a return by one of its codes via the tracking_code list filter (§5.2).
servicesarrayReserved for future use — always empty today. See the note in §5.7 for how to track attached services in the meantime.

4.2 Line item object

FieldTypeDescription
idstring | nullTechnical Logitrail id of this line item. Use this to identify the line when calling _merchant-decision or the per-item decision endpoint. null only for legacy items written before per-item identifiers existed.
product.idstringThe product being returned.
quantitynumberQuantity of this line.
original_order{ id } | nullOverrides the return's original_order for this specific line, for returns spanning multiple orders.
merchant_decisionobject | nullThis line's per-line decision, once set — see §3.3. Shape: { code, restock_as_product: { id } | null }.
servicesarrayReserved for future use — always empty today. See the note in §5.8.

5. Endpoints

5.1 Create a return

POST /v1/order-returns
Scope: order_returns:manage

Request body:

{
"order_return": {
"name": "Customer requested return",
"type": "customer_return",
"original_order": { "id": "64b8f0c2e1b2c3d4e5f67890" },
"merchant_reference": "RMA-2026-00042",
"items": [
{ "product": { "id": "64b8f0c2e1b2c3d4e5f67891" }, "quantity": 1 }
]
}
}

Every field is optional — you can create a bare placeholder return ({ "order_return": {} }) when you only know a return is coming but not yet what it contains. Logitrail's warehouse fills in the details once the parcel is identified on arrival.

Response — 201 Created:

{
"order_return": {
"id": "64c1a2b3c4d5e6f7a8b9c0d1",
"status": "preinfo_received",
"type": "customer_return",
"...": "full order return object, see §4.1"
}
}

400 Bad Request if a referenced order or product is invalid, with error_code one of ORIGINAL_ORDER_NOT_FOUND, ORIGINAL_ORDER_MERCHANT_MISMATCH, PRODUCT_NOT_FOUND, or PRODUCT_MERCHANT_MISMATCH.

5.2 List returns

GET /v1/order-returns
Scope: order_returns:read

Query parameters (all optional):

ParameterDescription
statusFilter by status. Accepts a single value or a comma-separated list; matches any of the given statuses.
typeFilter by return type.
qFree-text search (minimum 2 characters) over the name, your reference, or (for a 24-character hex phrase) the return id.
original_order_idRestrict to returns linked to a specific order.
tracking_codeRestrict to the return carrying this carrier tracking code.
offsetPagination offset. Default 0.
limitPage size. Default 100, maximum 500.

Response — 200 OK:

{
"order_returns": [ { "...": "order return summary object, see §4.1" } ],
"total": 137
}

List items omit the items and services collections — fetch the full detail via GET /v1/order-returns/{id} when you need line-item detail.

5.3 Get a return

GET /v1/order-returns/{orderReturnId}
Scope: order_returns:read

Response — 200 OK: { "order_return": { "...": "see §4.1" } }.

404 Not Found if the return doesn't exist or doesn't belong to your merchant account.

5.4 Set the merchant decision

POST /v1/order-returns/{orderReturnId}/_merchant-decision
Scope: order_returns:manage

This is the endpoint that tells Logitrail how to handle a return once it has arrived (status pending_merchant_decision). See §3.3 for the decision model. Setting a decision records it and transitions the return to accepted; Logitrail's warehouse then releases it for processing (moving it to processing) once ready to act on it. From your side there is nothing further to call — the decision is the only step you take.

Example — resend_as_it_is or destroy (no per-line decisions):

{
"merchant_decision": {
"code": "resend_as_it_is"
}
}

Example — to_shelf, uniform (every line defaults to to_shelf; items omitted entirely):

{
"merchant_decision": {
"code": "to_shelf"
}
}

Example — to_shelf, one line remapped (only the exceptional line needs an entry — the return's other lines still default to to_shelf):

{
"merchant_decision": {
"code": "to_shelf",
"items": [
{
"id": "64c1a2b3c4d5e6f7a8b9c0d3",
"code": "to_shelf",
"restock_as_product": { "id": "64c1a2b3c4d5e6f7a8b9c0d9" }
}
]
}
}

Example — resend_as_new (mixing an explicit to_shelf line with a line left to default to resend):

{
"merchant_decision": {
"code": "resend_as_new",
"items": [
{ "id": "64c1a2b3c4d5e6f7a8b9c0d2", "code": "to_shelf" }
]
}
}

Response — 200 OK: { "order_return": { "...": "the updated return" } }.

Rejections:

  • 400 Bad Request — the per-line decisions don't satisfy the whole-return code's rules. error_code is one of:

    error_codeMeaning
    ORDER_RETURN_DECISION_ITEMS_NOT_ALLOWEDPer-line items supplied alongside resend_as_it_is or destroy, which don't take any.
    ORDER_RETURN_DECISION_REQUIRES_ORIGINAL_ORDERresend_as_it_is was used on a return with no linked original order.
    ORDER_RETURN_DECISION_UNKNOWN_ITEMA per-line decision referenced an item id that isn't on the return.
    ORDER_RETURN_DECISION_RESEND_NOT_ALLOWEDA per-line resend was used under a whole-return code other than resend_as_new.
    ORDER_RETURN_DECISION_RESTOCK_AS_PRODUCT_NOT_ALLOWEDrestock_as_product was set on a per-line decision other than to_shelf.
    ORDER_RETURN_DECISION_RESTOCK_AS_PRODUCT_NOT_FOUND / ..._MERCHANT_MISMATCHrestock_as_product doesn't exist, or isn't one of your products.
  • 404 Not Found — return doesn't exist.

  • 409 Conflict — the return isn't in a decidable status (e.g. it has already been released for processing, or hasn't arrived yet). Sending the same or a different decision while still accepted is not a conflict — it replaces what was recorded (see §3.3).

5.5 Set a single line item's decision

POST /v1/order-returns/{orderReturnId}/items/{itemId}/merchant_decision
Scope: order_returns:manage

A targeted alternative to including items in _merchant-decision: use this to set or change one line's decision individually, without resending the whole batch. The whole-return decision must already be set first, and only whole-codes that accept per-line decisions (to_shelf, resend_as_new) allow it. Like the batch endpoint, this may be called again any time before release to replace the previously recorded decision for this line.

{
"merchant_decision": {
"code": "to_shelf",
"restock_as_product": { "id": "64c1a2b3c4d5e6f7a8b9c0d9" }
}
}

Response — 200 OK: { "order_return": { "...": "the updated return" } }.

Rejections:

  • 400 Bad Request — same error_code vocabulary as _merchant-decision: ORDER_RETURN_DECISION_ITEMS_NOT_ALLOWED (whole-return code doesn't accept per-line decisions), ORDER_RETURN_DECISION_RESEND_NOT_ALLOWED, ORDER_RETURN_DECISION_RESTOCK_AS_PRODUCT_NOT_ALLOWED, ORDER_RETURN_DECISION_RESTOCK_AS_PRODUCT_NOT_FOUND / ..._MERCHANT_MISMATCH.
  • 404 Not Found — return or item doesn't exist.
  • 409 Conflict — no whole-return decision has been set yet, or the return is no longer in accepted status (already released for processing).

5.6 Cancel a return

POST /v1/order-returns/{orderReturnId}/_cancel
Scope: order_returns:manage

Only possible while the return hasn't arrived at the warehouse yet (see §3.2). Response — 200 OK: { "order_return": { "...": "now status: deleted" } }. 409 Conflict if the return is past the cancellable lifecycle.

5.7 Attach a service to the whole return

POST /v1/order-returns/{orderReturnId}/extra_services
Scope: order_returns:manage

Attaches an extra service (e.g. inspection, repackaging, disposal handling) that applies to the return as a whole.

{
"service_attachment": {
"service": { "code": "INSPECTION" },
"quantity": 1,
"merchant_remarks": "Please check for water damage"
}
}

Identify the service by id or code — whichever you have on hand. Response — 201 Created, carrying the newly created attachment (with its own id, status, and timestamps). 403 Forbidden if the service isn't available to your merchant account; 409 Conflict if the service is disabled in the catalog.

Note: there is currently no endpoint to list which services are already attached to a return — the 201 response above is the only place the attachment's id is returned. If you need to reference it later (e.g. to check completion status through another channel), record it on your side at attach time.

5.8 Attach a service to a line item

POST /v1/order-returns/{orderReturnId}/items/{itemId}/extra_services
Scope: order_returns:manage

Same request/response shape as §5.7, scoped to a single line item instead of the whole return. The same note about there being no list-back endpoint applies here too.

5.9 Request a return label

POST /v1/order-returns/{orderReturnId}/_request-label?wait_seconds=10
Scope: order_returns:manage

Requests a prepaid return-label PDF for the customer to use when sending the parcel back. Requires the return to have a resolvable original order. Generation happens asynchronously; the endpoint waits briefly (wait_seconds, default and maximum 10) for a fast result:

  • If generation finishes within the wait window: 200 OK with the final state inline.
  • Otherwise: 202 Accepted — poll GET .../return-label for completion.

Calling this again while a label is already pending/processing returns the existing request's state rather than starting duplicate work.

Response body (both 200 and 202):

{
"order_return_id": "64c1a2b3c4d5e6f7a8b9c0d1",
"return_label": {
"status": "pending",
"requested_at": "2026-07-17T12:00:00.000Z",
"updated_at": null,
"attachment": null,
"error_code": null,
"error": null
}
}

409 Conflict if the return has no resolvable original order.

5.10 Get return-label status

GET /v1/order-returns/{orderReturnId}/return-label
Scope: order_returns:read

Response — 200 OK: { "return_label": { "...": "same shape as §5.9" } }.

return_label.status is one of:

StatusMeaning
pendingRequested, not yet started.
processingGeneration in progress.
completeDone — attachment.url is a download link for the label PDF.
failedGeneration could not be completed. error carries a message you can surface to the customer; error_code is a stable machine-readable code.

404 Not Found if the return doesn't exist or no label has ever been requested for it.

5.11 List a return's attachments

GET /v1/order-returns/{orderReturnId}/attachments
Scope: order_returns:read

Returns the files (photos, PDFs — e.g. damage evidence photographed at intake, or a return document) Logitrail's warehouse has attached to the return. There is currently no endpoint for you to attach a file yourself; attachments on this return are added by Logitrail during warehouse processing.

Response — 200 OK:

{
"attachments": [
{
"id": "64c1a2b3c4d5e6f7a8b9c0d5",
"purpose": "damage_evidence",
"remarks": "Crushed corner, photographed at intake",
"file_name": "damage-photo-1.jpg",
"mime": "image/jpeg",
"uploaded_at": "2026-07-17T12:05:00.000Z",
"download_link": {
"url": "https://...",
"valid_until": "2026-07-17T12:10:00.000Z"
}
}
]
}

download_link.url is short-lived (~5 minutes) and re-minted on every call — don't cache it.

404 Not Found if the return doesn't exist or doesn't belong to your merchant account.


6. Error responses

Every non-2xx response uses the same shape:

{
"message": "Human-readable description of what went wrong.",
"error_code": "stable_machine_readable_code",
"details": { "...": "optional structured context" }
}

Treat error_code as the stable value to branch on programmatically; message is for logs and humans, and may be reworded over time.

HTTP statusTypical cause
400The request body failed validation, or a business rule was violated (e.g. an inconsistent merchant decision).
401Missing or invalid bearer token, or missing X-Logitrail-Merchant-Id header.
403Your credentials don't carry a required scope, or a referenced resource (e.g. a service) isn't available to your merchant account.
404The return (or a resource it references) doesn't exist, or doesn't belong to your merchant account.
409The request conflicts with the return's current state (e.g. trying to cancel a return that has already arrived, or set a decision twice).

7. A typical integration flow

  1. Customer requests a return. Call POST /v1/order-returns with whatever you already know — the original order and items if the customer specified them, or just a name/reference if not.
  2. (Optional) Send the customer a return label. Call POST /_request-label, then poll GET .../return-label until status is complete, and hand the attachment.url to the customer.
  3. Track arrival. Poll GET /v1/order-returns/{id} (or list with a status filter) until status becomes pending_merchant_decision — this means the parcel has physically reached Logitrail's warehouse.
  4. Set the merchant decision. Call POST /_merchant-decision with the whole-return code and, optionally for to_shelf / resend_as_new, per-line decisions for any lines that shouldn't follow the whole-code default.
  5. Track processing. Continue polling GET /v1/order-returns/{id}status moves to accepted, then processing, then processed once Logitrail has finished executing your decision. If the decision created a new order (resend_as_new) or restocked items via an inbound shipment (to_shelf / resend_as_new), the corresponding resend_order / inbound_shipment reference appears on the return.

8. Webhooks

Logitrail does not currently offer webhooks or any other push-notification mechanism for order return events — there is no way to register a callback URL and be notified automatically when a return's status changes, a decision is required, or processing completes. Polling is the only option today; see the polling steps in §7.

If real-time notifications become important for your integration, contact your Logitrail integration contact — this is expected to change in a future revision of this API.


Questions

For API credentials, scope requests, or anything not covered here, contact your Logitrail integration contact.