> For the complete documentation index, see [llms.txt](https://developer.esw.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.esw.com/returns-api/grp-returns-api/error-handling/errors-and-common-causes.md).

# Errors and Common Causes

### 401

`401` responses use `ProblemDetails`. The response is documented on 9 of the 14 endpoints. For the 5 endpoints that don't document it, auth failures may surface as `403` or `500`.

```json
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Bearer token is missing or invalid.",
  "instance": "/{identifier}/outbound-orders"
}
```

#### Common Causes

| Cause                                 | Fix                                                     |
| ------------------------------------- | ------------------------------------------------------- |
| Missing `Authorization` header        | Add `Authorization: Bearer <token>` to every request    |
| Expired JWT                           | Refresh the token and retry once                        |
| Malformed or invalid token            | Re-authenticate to obtain a valid token                 |
| Token not scoped for returns platform | Confirm the token was issued with the GRP Returns scope |

#### Token Refresh Pattern

{% code expandable="true" %}

```javascript
async function callWithRefresh(url, options, getToken, refreshToken) {
  let token = await getToken();
  let response = await fetch(url, {
    ...options,
    headers: { ...options.headers, Authorization: `Bearer ${token}` },
  });

  if (response.status === 401) {
    token = await refreshToken();
    response = await fetch(url, {
      ...options,
      headers: { ...options.headers, Authorization: `Bearer ${token}` },
    });
  }

  return response;
}
```

{% endcode %}

> Apply token refresh logic universally — not just on endpoints that explicitly document `401`. Auth failures on `POST /return-orders`, `GET /configuration/countries`, and `GET /orders/{ref}` may still occur as undocumented `401`s or mis-coded `403`s.

***

### 403

`403` responses use `ProblemDetails` and are documented on 12 of 14 endpoints. It covers both permission and tenant-scoping failures.

#### Common Causes

| Cause                                                          | Fix                                                       |
| -------------------------------------------------------------- | --------------------------------------------------------- |
| `{identifier}` in path does not match the token's tenant scope | Use the correct `identifier` for the authenticated tenant |
| Token lacks the required role or permission for this operation | Request elevated permissions from the ESW platform team   |
| Retailer not configured for the requested feature              | Confirm the feature is activated for the tenant           |
| Attempting to access another tenant's data                     | Never cross-reference identifiers across tenants          |

```javascript
if (response.status === 403) {
  const err = await parseGrpError(response);
  // err.schema === "ProblemDetails"
  console.error(`[403] Forbidden on ${endpoint}: ${err.title} — ${err.detail}`);
  // Do NOT retry — permissions issue, not transient
  throw new ForbiddenError(err.detail ?? "Access denied");
}
```

***

### Bad Request&#x20;

`400` is returned when field-level validation fails. The `errors` object maps each failing field name to one or more descriptive error strings. **Always iterate the dictionary** — multiple fields can fail in the same request.

{% code expandable="true" %}

```javascript
if (response.status === 400) {
  const err = await parseGrpError(response);
  // err.schema === "ValidationProblemDetails"

  const fieldErrors = err.errors ?? {};
  const allMessages = [];

  for (const [field, messages] of Object.entries(fieldErrors)) {
    for (const msg of messages) {
      console.error(`  [${field}] ${msg}`);
      allMessages.push({ field, message: msg });
    }
  }

  // Handle structural 400 where errors dict is empty
  if (allMessages.length === 0) {
    console.error(`[400] ${err.title}: ${err.detail}`);
  }

  throw new ValidationError(allMessages, err.title);
}
```

{% endcode %}

#### When `errors` Is Empty or Absent

If the body is missing entirely, is malformed JSON, or fails a top-level structural check before field validation runs, the response may carry `title` and `detail` with an empty `errors` object:

```json
{
  "title": "Bad Request",
  "status": 400,
  "detail": "The request body is required.",
  "errors": {}
}
```

Always check both `errors` entries and the top-level `detail` field.

***

### 400 & 422

#### Create Outbound Order — `POST /{identifier}/outbound-orders`

**Schema:** `CreateOutboundOrderWebRequest`

All fields are optional in the schema — no top-level `required` array. The API validates business rules, so `400` reflects semantic failures more than structural ones.

| Field / Rule                                              | `errors` Key                                         | Fix                                                         |
| --------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------- |
| `orderReference` missing or empty                         | `"orderReference"`                                   | Provide a non-empty order reference                         |
| `orderItems` contains items with no `productCode`         | `"orderItems[0].productCode"`                        | Each item needs a product code                              |
| `orderItems[].quantity` is zero or negative               | `"orderItems[0].quantity"`                           | Quantity must be a positive integer                         |
| `orderItems[].weight.weightUnit` invalid                  | `"orderItems[0].weight.weightUnit"`                  | Use `"Kg"` or `"Lb"` — **note mixed case** (see Section 14) |
| `orderItems[].shippingInformation.shippingStatus` invalid | `"orderItems[0].shippingInformation.shippingStatus"` | Use `"Unshipped"`, `"Shipped"`, or `"Cancelled"`            |
| Unknown fields sent                                       | top-level                                            | `additionalProperties: false` — strip unknown fields        |

> **`canOverwrite` controls 409 behaviour:** When `canOverwrite: true`, re-creating an existing order will overwrite it. When `canOverwrite: false` (default), re-creating returns `409`. Set this explicitly to avoid surprises.

#### Update Outbound Order — `PUT /{identifier}/outbound-orders/{orderReference}`

**Schema:** `UpdateOutboundOrderWebRequest`

Same field validation rules as Create. The `orderReference` is taken from the path — do not include it in the body.

#### Submit Outbound Shipment — `POST /{identifier}/outbound-shipments`

**Schema:** `SubmitOutboundShipmentWebRequest`

| Field / Rule                                | `errors` Key                           | Fix                                       |
| ------------------------------------------- | -------------------------------------- | ----------------------------------------- |
| `orderReference` missing                    | `"orderReference"`                     | Required to link the shipment to an order |
| `shippingReference` missing                 | `"shippingReference"`                  | Required for tracking                     |
| `shippingDate` not ISO 8601                 | `"shippingDate"`                       | Use `"2024-03-15T10:30:00Z"` format       |
| `shippingItems[].weight.weightUnit` invalid | `"shippingItems[0].weight.weightUnit"` | Use `"Kg"` or `"Lb"`                      |

#### Label Request — `POST /{identifier}/label-requests`

**Schema:** `LabelRequestWebRequest`

| Field / Rule                        | `errors` Key                  | Fix                                      |
| ----------------------------------- | ----------------------------- | ---------------------------------------- |
| `outboundOrderReference` missing    | `"outboundOrderReference"`    | Required — links the label to an order   |
| `shippingReference` missing         | `"shippingReference"`         | Required for label generation            |
| `shopperAddress.countryIso` invalid | `"shopperAddress.countryIso"` | ISO 3166-1 alpha-2 (e.g. `"IE"`, `"US"`) |
| `orderItems[].productCode` missing  | `"orderItems[0].productCode"` | Each item needs a product code           |

> **`POST /label-requests` returns `200`, not `201`** on success. This deviates from REST convention for creation — do not treat an absent `201` as a failure.

#### Create Return Order — `POST /{identifier}/orders/{orderReference}/return-orders`

**Schema:** `CreateReturnOrderWebRequest`

| Field / Rule                                 | `errors` Key                   | Fix                                                                     |
| -------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------- |
| `returnItems` missing or empty               | `"returnItems"`                | At least one return item is required                                    |
| `returnItems[].productCode` missing          | `"returnItems[0].productCode"` | Required to identify the item                                           |
| `returnItems[].reasonCode` missing           | `"returnItems[0].reasonCode"`  | Required for processing                                                 |
| `returnMethod` invalid enum                  | `"returnMethod"`               | Use `"Postal"`, `"PUDO"`, or `"Collections"`                            |
| `returnType` invalid enum                    | `"returnType"`                 | Use `"Normal"`, `"Undeliverable"`, `"BlindReturn"`, or `"EUCoolingOff"` |
| `paidBy` invalid enum                        | `"paidBy"`                     | Use `"Unpaid"`, `"Retailer"`, or `"Shopper"`                            |
| `dropOffLocation.type` invalid when provided | `"dropOffLocation.type"`       | Use `"Store"` or `"Pudo"`                                               |
| `weight.weightUnit` invalid                  | `"weight.weightUnit"`          | Use `"Kg"` or `"Lb"`                                                    |

#### Update Return Order — `PUT /{identifier}/orders/{orderReference}/return-orders/{returnOrderNumber}`

**Schema:** `UpdateReturnOrderWebRequest`

| Field / Rule                             | `errors` Key                        | Fix                                                                                                                |
| ---------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `status` invalid enum                    | `"status"`                          | Use one of the 11 `ReturnOrderStatus` values                                                                       |
| `returnItems[].returnItemStatus` invalid | `"returnItems[0].returnItemStatus"` | Only `"Accepted"` or `"Rejected"` are valid here (`UpdateReturnItemStatus` — not the full `ReturnItemStatus` enum) |
| `weight.weightUnit` invalid              | `"weight.weightUnit"`               | Use `"Kg"` or `"Lb"`                                                                                               |

> **Two separate item status enums:** `ReturnItemStatus` (5 values — full lifecycle) is used for read/response. `UpdateReturnItemStatus` (2 values — `"Accepted"`, `"Rejected"` only) is used when updating items. Sending `"InProgress"`, `"RejectedNotReturned"`, or `"Cancelled"` on a PUT will produce a `400`.

#### Search Return Methods — `POST /{identifier}/return-methods/{orderReference}`

**Schema:** `SearchReturnMethodsWebRequest`

| Field / Rule                                | `errors` Key       | Fix                                          |
| ------------------------------------------- | ------------------ | -------------------------------------------- |
| `returnItems` missing or empty              | `"returnItems"`    | At least one item required to search methods |
| `returnItems[].productCode` or `id` missing | `"returnItems[0]"` | At least one item identifier is required     |

#### List Return Orders — `GET /{identifier}/return-orders`

**Query parameters:**

| Parameter    | Notes                                                      | Fix                                                                     |
| ------------ | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| `status`     | Must be a valid `ReturnOrderStatus` enum value if provided | Use one of the 11 documented values                                     |
| `startUtc`   | ISO 8601 UTC date-time string                              | Use `"2024-01-01T00:00:00Z"` format                                     |
| `endUtc`     | ISO 8601 UTC date-time string                              | Must be after `startUtc`                                                |
| `returnType` | Must be a valid `ReturnType` enum value                    | Use `"Normal"`, `"Undeliverable"`, `"BlindReturn"`, or `"EUCoolingOff"` |

#### Tracking Events — `POST /{identifier}/tracking-events`

**Schema:** `GetTrackingEventsWebRequest`

| Field / Rule                            | `errors` Key          | Fix                                                 |
| --------------------------------------- | --------------------- | --------------------------------------------------- |
| `trackingReference` missing             | `"trackingReference"` | Required — must be a valid int64 tracking reference |
| `trackingReference` not a valid integer | `"trackingReference"` | Field type is `int64` — do not send a string        |

#### Get Order — `GET /{identifier}/orders/{orderReference}`

| Parameter                      | Notes                                                      |
| ------------------------------ | ---------------------------------------------------------- |
| `orderItems` (query, optional) | Filters items in the response — validate as a string value |

#### Location Search — `GET /location/`

**Required query parameters:**

| Parameter               | Type             | Notes                                          |
| ----------------------- | ---------------- | ---------------------------------------------- |
| `identifier`            | string           | Tenant identifier — query param here, not path |
| `CountryCode`           | string           | ISO 3166-1 alpha-2                             |
| `PostalCode`            | string           | Postal/zip code                                |
| `RouteType`             | `RouteType` enum | `"Inbound"` or `"Outbound"`                    |
| `CarrierServiceRouteId` | string           | Carrier route ID — must be pre-configured      |

**Optional query parameters:**

| Parameter          | Type    | Notes                       |
| ------------------ | ------- | --------------------------- |
| `City`             | string  | City name to narrow results |
| `Longitude`        | number  | Decimal coordinate          |
| `Latitude`         | number  | Decimal coordinate          |
| `MaxDistanceInKms` | integer | Max radius in kilometres    |
| `MaxLocations`     | integer | Cap on results returned     |

Missing any required parameter produces a `400`. See Section 11 for `422` which is also documented on this endpoint.

***

### 404

`404` uses `ProblemDetails` across all endpoints that document it. A `404` is never a transient error — do not retry without resolving the underlying data issue.

#### Endpoint-Specific 404 Meanings

| Endpoint                                     | Spec Description                                                | What It Means                                                                                      |
| -------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `POST /{id}/outbound-orders`                 | `"Not Found"`                                                   | The `identifier` is not a registered tenant, or a referenced entity was not found during creation  |
| `PUT /{id}/outbound-orders/{ref}`            | `"Outbound Order does not exist"`                               | The `orderReference` does not exist under this `identifier`                                        |
| `GET /{id}/return-orders/{num}/{docType}`    | `"Not Found"`                                                   | No document found for the return order number and document type combination                        |
| `GET /{id}/configuration/countries/{iso}`    | `"Not Found"`                                                   | No country configuration exists for the given `countryIso`                                         |
| `GET /{id}/orders/{ref}`                     | `"Not Found"`                                                   | No outbound order found for `orderReference` under this `identifier`                               |
| `POST /{id}/return-methods/{ref}`            | `"Not Found"`                                                   | No order found for `orderReference`, or no return methods configured for the country               |
| `PUT /{id}/orders/{ref}/return-orders/{num}` | `"Return Order can't be found"`                                 | The `returnOrderNumber` does not exist under the order                                             |
| `GET /{id}/orders/{ref}/return-orders/{num}` | `"Return Order can't be found"`                                 | Same — return order not found                                                                      |
| `GET /{id}/return-orders`                    | `"Could not find any return orders based on provided criteria"` | No return orders match the filter criteria — **treat this as an empty result, not a hard failure** |
| `POST /{id}/orders/{ref}/return-orders`      | `"Tenant Code doesn't exist"`                                   | The `identifier` is not a registered tenant in the GRP platform                                    |
| `POST /{id}/tracking-events`                 | `"Tracking doesn't exist"`                                      | The `trackingReference` value has no associated tracking data                                      |

> **Important:** `404` on `POST /{id}/outbound-orders` (a creation endpoint) is unusual. It indicates the `identifier` is unknown or a dependency could not be found — not that the order being created already exists. Distinguish this from `409 Conflict` which means the order already exists.

> **`GET /{id}/return-orders` returning `404`:** The spec describes this as *"Could not find any return orders based on provided criteria"* — treat it as a successful empty result set, not a terminal error. It is equivalent to `200` with an empty array on many other APIs.

javascript

```javascript
if (response.status === 404) {
  const err = await parseGrpError(response);

  // Special case: list endpoint uses 404 for empty results
  if (endpoint.includes("/return-orders") && !endpoint.match(/\/return-orders\/[^/]+/)) {
    console.info("No return orders found matching criteria.");
    return [];
  }

  console.warn(`[404] Not found: ${err.detail ?? err.title}`);
  return null;
}
```

***

### &#x20;409

`409` uses `ProblemDetails` and is documented on three endpoints.

#### Endpoint-Specific 409 Meanings

| Endpoint                                | Spec Description                                                                          | Resolution                                                                                                                             |
| --------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /{id}/label-requests`             | `"A request for this label with order reference and shipping reference already occurred"` | A label has already been issued for this combination — do not resend. Check if the existing label is still valid.                      |
| `POST /{id}/outbound-orders`            | `"The Outbound Order already exists. (If replaceOrderPermitted is false)"`                | Set `canOverwrite: true` in the request body if you intend to update an existing order, or use `PUT` instead.                          |
| `POST /{id}/orders/{ref}/return-orders` | `"Conflict"`                                                                              | A return order for this outbound order already exists in a non-terminal state. Check existing return orders before creating a new one. |

{% code expandable="true" %}

```javascript
if (response.status === 409) {
  const err = await parseGrpError(response);

  if (endpoint.includes("label-requests")) {
    // Label already exists — safe to treat as idempotent if reference matches
    console.warn(`[409] Label already requested: ${err.detail}`);
    return { alreadyExists: true };
  }

  if (endpoint.includes("outbound-orders") && method === "POST") {
    // Order exists — switch to PUT or set canOverwrite: true
    console.warn(`[409] Outbound order already exists. Use PUT to update or set canOverwrite: true.`);
    throw new ConflictError("Order already exists", err.detail);
  }

  // Return order conflict — check existing return orders first
  console.error(`[409] Conflict on ${endpoint}: ${err.detail}`);
  throw new ConflictError(err.detail ?? "Conflict");
}
```

{% endcode %}

***

### 422

`422` is documented **exclusively** on `GET /location/` and uses the `ValidationProblemDetails` schema — the same as `400`. The distinction between `400` and `422` on this endpoint reflects the difference between missing/malformed parameters (`400`) and semantically invalid but structurally valid parameters (`422`).

#### Likely 422 Causes on `GET /location/`

* `RouteType` value is structurally a valid string but semantically not supported for the given carrier/country
* `CarrierServiceRouteId` exists as a string but references a route that has no configured locations
* Coordinates (`Longitude`/`Latitude`) are valid numbers but outside a supported geographic range
* `MaxDistanceInKms` is a valid integer but exceeds the platform's maximum allowed radius

javascript

{% code expandable="true" %}

```javascript
// Handle both 400 and 422 identically for the location endpoint
if (response.status === 400 || response.status === 422) {
  const err = await parseGrpError(response);
  const fieldErrors = err.errors ?? {};
  const messages = Object.entries(fieldErrors).flatMap(([f, msgs]) =>
    msgs.map((m) => ({ field: f, message: m }))
  );

  if (response.status === 422) {
    console.error(`[422] Unprocessable location query:`, messages);
    throw new UnprocessableError(messages, err.detail);
  }

  throw new ValidationError(messages, err.title);
}
```

{% endcode %}

***

### 500

`500` uses `ProblemDetails` on all endpoints **except** `GET /{identifier}/return-orders/{returnOrderNumber}/{documentType}`, which uses `BaseResponse`.

#### Standard 500 — `ProblemDetails`

{% code expandable="true" %}

```javascript
if (response.status === 500) {
  const err = await parseGrpError(response);

  if (err.schema === "BaseResponse") {
    // Document retrieval endpoint only
    console.error(`[500] Document error: ${err.userMessage} — ${err.details}`);
    throw new ServerError(500, err.userMessage);
  }

  // All other endpoints
  console.error(`[500] Server error: ${err.title} — ${err.detail}`);
  throw new ServerError(500, err.detail ?? err.title);
}
```

{% endcode %}

#### Retry with Exponential Backoff

{% code expandable="true" %}

```javascript
async function fetchWithRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fn();
    if (response.status !== 500) return response;

    const wait = Math.min(1000 * 2 ** i, 30_000) + Math.random() * 500;
    console.warn(`[500] Attempt ${i + 1}/${maxRetries}. Retrying in ${Math.round(wait)}ms.`);
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new ServerError(500);
}
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://developer.esw.com/returns-api/grp-returns-api/error-handling/errors-and-common-causes.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
