# ILS Return-Request API — Mobile Integration Guide

**Product:** Indian Logistics Services (ILS) — Customer Returns & Exchanges
**Audience:** Mobile app developers integrating the return/exchange flow for a Shopify merchant using the ILS app.
**Version:** 1.0
**Status:** Stable
**Last updated:** 02 June 2026
**Contact:** [support@shopiapps.in](mailto:support@shopiapps.in)

---

## Table of contents

1. [Overview](#1-overview)
2. [Prerequisites](#2-prerequisites)
3. [Getting your API token](#3-getting-your-api-token)
4. [Quick start](#4-quick-start)
5. [Authentication](#5-authentication)
6. [Response envelope](#6-response-envelope)
7. [HTTP status codes & error codes](#7-http-status-codes--error-codes)
8. [Endpoint reference](#8-endpoint-reference)
   - [8.1 Get return settings](#81-get-return-settings)
   - [8.2 Get order list](#82-get-order-list)
   - [8.3 Get order details](#83-get-order-details)
   - [8.4 Get customer shipping addresses](#84-get-customer-shipping-addresses)
   - [8.5 Edit shipping address](#85-edit-shipping-address)
   - [8.6 Edit phone number](#86-edit-phone-number)
   - [8.7 Cancel order](#87-cancel-order)
   - [8.8 Get exchange variants](#88-get-exchange-variants)
   - [8.9 Get exchange products](#89-get-exchange-products)
   - [8.10 Submit return request](#810-submit-return-request)
   - [8.11 Reschedule pickup](#811-reschedule-pickup)
9. [Recommended client flow](#9-recommended-client-flow)
10. [Encrypted identifiers](#10-encrypted-identifiers)
11. [Versioning & change policy](#11-versioning--change-policy)
12. [FAQ & troubleshooting](#12-faq--troubleshooting)
13. [Support](#13-support)

---

## 1. Overview

The ILS Return-Request API exposes the merchant's return/exchange workflow to a
mobile app — the same flow that runs on the storefront proxy page. Customers
can browse their recent orders, request a return or exchange (with optional
proof image, return reason, refund mode, replacement variant), edit the
shipping address before fulfilment, cancel an order, or reschedule a declined
pickup.

Every endpoint is **per-shop**, **authenticated**, and operates only on data
the customer is allowed to see (verified by email or phone). The API speaks
JSON over HTTPS.

| | |
|--|--|
| **Base URL** | `https://ils.shopiapps.in/prfiles/return-request/api/` |
| **Protocol** | HTTPS only |
| **Format**   | JSON response, `application/x-www-form-urlencoded` request (or `multipart/form-data` for return submissions with proof images) |
| **Method**   | `POST` (single endpoint, RPC-style; `action` field selects the operation) |
| **Auth**     | `Auth-Token` header + `shop` body field |

---

## 2. Prerequisites

Before calling any endpoint, the merchant store must satisfy three conditions.
If any of these fails, the API returns `403 FORBIDDEN` or `403 PLAN_LIMIT`.

1. The **ILS app** must be installed in the merchant's Shopify store
   (`app.app_status = 'installed'`).
2. The merchant's billing must be active
   (`app.payment_status` is `free` or `accepted`).
3. The merchant's plan must include the **Return Request** feature
   (available on the *Advanced* and *Gold* plans, or any custom plan with the
   `return_request` flag enabled).

> Tip: Plan details are visible to the merchant under
> **ILS admin → Plans & Pricing**.

---

## 3. Getting your API token

The mobile-app access token (`Auth-Token`) is **per-shop** and is issued by the
ILS team. It is the **same token used by the Tracking API** — one token gives
access to both products, scoped to the same merchant.

### How to request a token

1. The merchant (or their integrator) emails
   [support@shopiapps.in](mailto:support@shopiapps.in)
   with the subject **"Mobile API token request"** and the body containing:
   - **Shop domain** — e.g. `examplestore.myshopify.com`
   - **Mobile app name / platform** — e.g. `Acme Shop iOS`, `Acme Shop Android`
   - **Contact email**
2. ILS support verifies the shop has an active plan that includes
   `return_request` and provisions the token.
3. The token is delivered out-of-band (encrypted email or shared secret store).

### Token characteristics

| Property | Value |
|----------|-------|
| Format     | Hex string, ~96 characters |
| Lifetime   | Long-lived (no expiry); rotate on demand |
| Scope      | Single Shopify shop, both Tracking & Return APIs |
| Storage    | Server-side: `settings.mobile_app_access_token` |

### Storing the token in your app

- **Do not** hard-code the token in your client binary. Bundling secrets in
  shipped APKs / IPAs is trivial to reverse-engineer.
- **Do not** commit the token to source control.
- Use the platform's secure storage:
  - **iOS** — Keychain Services
  - **Android** — EncryptedSharedPreferences / Keystore
- Treat the token as you would treat a password.

---

## 4. Quick start

Once you have a token, smoke-test it by loading the return settings:

```bash
curl --location 'https://ils.shopiapps.in/prfiles/return-request/api/' \
  --header 'Auth-Token: YOUR_TOKEN_HERE' \
  --data-urlencode 'shop=examplestore.myshopify.com' \
  --data-urlencode 'action=get_return_settings'
```

A `200` response with `"success": true` and a populated `data` object confirms
the integration is wired up correctly.

---

## 5. Authentication

Every request must include:

| Where  | Name         | Description |
|--------|--------------|-------------|
| Header | `Auth-Token` | Long-lived per-shop token from §3 |
| Body   | `shop`       | Shopify shop domain — exactly as `*.myshopify.com` |

The server verifies, in order:

1. The token is non-empty.
2. The shop exists, is installed, and has an active payment status.
3. The shop's plan includes the `return_request` feature.
4. The supplied `Auth-Token` matches the token stored for that shop.

A failure on any check terminates the request with the matching error code from
[§7](#7-http-status-codes--error-codes).

> **CORS**: `Access-Control-Allow-Origin: *` is set, and the API responds to
> `OPTIONS` preflight requests. The endpoints can be safely called from a web
> wrapper, mobile WebView, or React Native bridge.

---

## 6. Response envelope

All responses share the same JSON shape — both success and error.

```json
{
  "success": true,
  "code": "OK",
  "message": "Order found.",
  "data": { ... endpoint-specific payload ... },

  "result": "success",
  "msg": "Order found."
}
```

| Field | Type | Notes |
|-------|------|-------|
| `success` | `bool` | `true` for any 2xx response, `false` otherwise. **Prefer this** for branching in client code. |
| `code` | `string` | Stable machine code (`OK`, `INVALID_INPUT`, ...). See [§7](#7-http-status-codes--error-codes). Safe to switch on. |
| `message` | `string` | Human-readable message. Safe to display in UI. |
| `data` | `object` \| `array` | Endpoint-specific payload. See [§8](#8-endpoint-reference). |
| `result` | `string` | **Legacy v1 alias** of `success` — values: `"success"` / `"fail"`. Kept for backward compatibility; prefer `success`. |
| `msg` | `string` | **Legacy v1 alias** of `message`. |

> Newer integrations should rely on `success` + `code` + `data`. The legacy
> keys (`result`, `msg`) and any top-level mirrors of `data` will continue to
> be sent for the foreseeable future but will not be expanded with new fields.

---

## 7. HTTP status codes & error codes

| HTTP | `code`            | When you'll see it |
|-----:|-------------------|--------------------|
| 200  | `OK`              | Successful response |
| 400  | `INVALID_INPUT`   | Missing or malformed parameters; unknown `action` |
| 401  | `UNAUTHORIZED`    | Missing / invalid `Auth-Token` |
| 403  | `FORBIDDEN`       | App not installed for this shop |
| 403  | `PLAN_LIMIT`      | Return-request feature not included in the shop's plan |
| 404  | `NOT_FOUND`       | Order / customer / product / address not found |
| 409  | `CONFLICT`        | State conflict — order already cancelled, item already returned, fulfilment not started yet |
| 500  | `SERVER_ERROR`    | Unexpected server error — please retry, then contact support if it persists |

### Order-state priority for write actions

Every write action (`return_requests_submit`, `cancel_order`,
`edit_customer_shipping_address`, `edit_customer_phone_number`,
`reschedule_pickup`) runs a priority-ordered precondition check before
touching anything. The **first** failing rule wins — clients always get the
most-actionable error.

| Priority | Rule | HTTP | `code` | `message` |
|---------:|------|-----:|--------|-----------|
| 1 | Order doesn't exist          | 404 | `NOT_FOUND` | "Sorry we can't find your order." |
| 2 | Order was cancelled in Shopify | 409 | `CONFLICT`  | "Order has been cancelled." (or "Order has already been cancelled." on `cancel_order`) |
| 3 | Order was closed/archived    | 409 | `CONFLICT`  | "Order has been closed." |
| 4 | Order has shipped (AWB set)  | 409 | `CONFLICT`  | "Order has been shipped — this action is no longer available." (wording is action-specific) |
| 5 | Order not fulfilled yet      | 409 | `CONFLICT`  | "Order not fulfilled yet!" *(only enforced where fulfilment is a precondition, i.e. `return_requests_submit`)* |
| 6 | Action-specific conflict     | 409 | `CONFLICT`  | e.g. "This item has already returned!" on `return_requests_submit` |

Each endpoint's reference below lists only the rules it enforces. A cancelled
order, for example, will never produce "Order not fulfilled yet!" — the
cancellation always wins.

### Error response example

```json
{
  "success": false,
  "code": "NOT_FOUND",
  "message": "Sorry we can't find your order.",
  "data": [],
  "result": "fail",
  "msg": "Sorry we can't find your order."
}
```

---

## 8. Endpoint reference

All endpoints are `POST` to the base URL and select the operation with the
`action` body field.

### 8.1 Get return settings

> `POST /` with `action=get_return_settings`

Returns the merchant's per-shop return-page configuration: theme colours, copy,
banner, reason list, refund modes, and behaviour flags. Call this on app
launch / when the user enters the return flow.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | API token |
| `shop` | body | string | ✓ | `*.myshopify.com` |
| `action` | body | string | ✓ | `get_return_settings` |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Return page settings loaded.",
  "data": {
    "login_page_heading": "RETURN & EXCHNAGE ORDERS",
    "login_page_description": "Let's start process! Please enter your details to show orders and customizations of it.",
    "show_login_banner": "1",
    "login_banner_image_url": "https://ils.shopiapps.in/return-images/examplestore_myshopify_com/banner.jpg?x=1717300000",
    "login_banner_position": "1",
    "heading_font_color": "#222222",
    "text_color": "#000000",
    "text_size": "13px",
    "button_font_color": "#ffffff",
    "button_color": "#0c4ca3",
    "custom_css": "",
    "contact_mail": "support@examplestore.com",
    "return_page_heading": "Return Order Request",
    "return_page_text_size": "12px",
    "return_button_text": "Return",
    "show_return_page_top_content": "0",
    "return_page_top_content": "",
    "show_return_page_bottom_content": "0",
    "return_page_bottom_content": "",
    "return_reasons": ["Wrong size", "Damaged on arrival", "Changed my mind"],
    "enable_return_option": "1",
    "return_options_list": "refund,exchange,store_credit",
    "return_options_text": { "refund": "Refund", "exchange": "Exchange", "store_credit": "Store credit" },
    "refund_modes": ["upi", "bank_transfer"],
    "selected_replace_option": "1",
    "exchange_summary": "Exchanges are dispatched within 3-5 working days.",
    "enable_customer_note": "1",
    "enable_return_proof_image": "1",
    "allow_partial_return": "1"
  }
}
```

#### Field reference (selected)

| Field | Meaning |
|-------|---------|
| `login_page_heading` / `login_page_description` | Copy for the order-lookup screen. |
| `show_login_banner` + `login_banner_image_url` + `login_banner_position` | Optional banner on the login screen. Position `0`: left, `1`: right. |
| `return_reasons` | Reason list to populate the dropdown when the user requests a return. |
| `enable_return_option` + `return_options_list` + `return_options_text` | What return *types* the merchant offers (refund, exchange, store credit). |
| `refund_modes` | Refund payout methods to show on the form (UPI, bank, ...). |
| `selected_replace_option` | `0`: variant only, `1`: any product. |
| `enable_customer_note` | Show the customer-note textarea. |
| `enable_return_proof_image` | Allow attaching a proof image (`multipart/form-data` in §8.10). |
| `allow_partial_return` | Allow returning fewer items than were ordered. |

---

### 8.2 Get order list

> `POST /` with `action=get_order_list`

Lists the customer's orders from the last 60 days. Cursor-paginated via
Shopify's GraphQL `after:` parameter.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `get_order_list` |
| `email` | body | string | ✓ | Customer email |
| `next_data` | body | string | ✗ | Cursor returned by a previous call. Empty / omitted on the first page. |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Orders found.",
  "data": {
    "<encrypted_o_id>": {
      "name": "#1042",
      "total_price": "₹1499.00",
      "line_item": {
        "title": "Cotton T-Shirt",
        "src": "https://cdn.shopify.com/.../tshirt.jpg",
        "vendor": "Acme",
        "variant": { "title": "M / Blue" },
        "sku": "TS-M-BLUE"
      },
      "created_date": "21 May, 2026 14:03:22",
      "status_name": "Fulfilled",
      "payment_mode": "Pre-paid"
    }
  },
  "next_data": "eyJsYXN0X2lkIjoxMjM0fQ=="
}
```

The key under `data` is the **encrypted order id** — pass it as `order_id` to
[`order_details`](#83-get-order-details), [`cancel_order`](#87-cancel-order),
etc.

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `email` missing or > 254 chars |
| 404 | `NOT_FOUND`     | No orders in the last 60 days for this email |

---

### 8.3 Get order details

> `POST /` with `action=order_details`

Returns the full order payload needed by the return-request screen — delivery
address, courier + AWB, per-line-item returnable flag, days remaining, existing
returns/exchanges, plus flags telling the UI whether the customer can still
edit the address / phone / cancel the order.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `order_details` |
| `order_name` | body | string | ✓ | Shopify order number — display name (`#1042`) or plain numeric. Max 128 chars. |
| `email` | body | string | ✓ | Email **or** 10-digit phone. Phones are compared on the last 10 digits. |

#### Response (200, abridged)

```json
{
  "success": true,
  "code": "OK",
  "message": "Order found.",
  "data": {
    "order_id": "<encrypted>",
    "order_name": "#1042",
    "order_date": "21 May, 2026",
    "customer_id": "<encrypted>",
    "delivery_address": {
      "name": "Jane Doe",
      "address1": "Flat 1, Main Street",
      "address2": "",
      "city": "Bengaluru",
      "province": "Karnataka",
      "provinceCode": "KA",
      "zip": "560001",
      "phone": "9876543210"
    },
    "order_shipment_data": {
      "courier_service": "Delhivery",
      "awb": "DLV1234567890",
      "show_shipment": 1,
      "track_status": 4,
      "order_status_list": ["Ordered", "Packed", "Shipped", "Delivered"]
    },
    "show_return_btn": true,
    "allow_edit_phone": false,
    "allow_edit_address": false,
    "allow_cancel_order": false,
    "line_items": {
      "<encrypted_l_id>": {
        "title": "Cotton T-Shirt",
        "handle": "cotton-tshirt",
        "vendor": "Acme",
        "price": "998.00",
        "unit_price": "499.00",
        "total_qty": 2,
        "variant_id": "<encrypted>",
        "src": "https://cdn.shopify.com/.../tshirt.jpg",
        "item_id": "<encrypted>",
        "product_id": "<encrypted>",
        "is_fulfilled": true,
        "fulfill_qty": 2,
        "remain_days": 7,
        "unfulfilled_qty": 0,
        "return": "true",
        "msg": "<strong>7</strong> Days remain to return",
        "remain_return_qty": 2,
        "item_tracking_url": "",
        "re_schedule_pickup": false
      }
    }
  }
}
```

#### Field reference (selected)

| Path | Meaning |
|------|---------|
| `data.order_shipment_data.track_status` | Tracking step int. `1` ordered, `2` packed, `3` shipped, `4` delivered, `5` RTO, `6` cancelled, `7` NDR. |
| `data.order_shipment_data.show_shipment` | `0`: order ships via another app (no progress bar). `1`: shipped via ILS. |
| `data.show_return_btn` | `true` if at least one line item is currently returnable. |
| `data.allow_edit_phone` / `allow_edit_address` / `allow_cancel_order` | Drive the optional action buttons on the screen. |
| `data.line_items.<key>.return` | `"true"` if the customer can submit a return for this line. |
| `data.line_items.<key>.remain_days` | Days remaining in the return window. |
| `data.line_items.<key>.msg` | Human-readable explanation when `return = "false"` (e.g., "Not Shipped yet", "Return days limit exceeded."). May contain HTML `<strong>` for the days remaining. |
| `data.line_items.<key>.return_data[]` | Existing return rows for this line item (if any), each with `id`, `status`, `quantity`, `return_reason`, `exchange_product_details`, etc. |
| `data.line_items.<key>.re_schedule_pickup` | `true` if the customer can re-schedule a previously declined / cancelled pickup. |

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `order_name` or `email` missing |
| 404 | `NOT_FOUND`     | No order matches `order_name` + `email`/phone for this shop |
| 409 | `CONFLICT`      | Order has been cancelled |

---

### 8.4 Get customer shipping addresses

> `POST /` with `action=get_customer_shipping_addresses`

Lists all addresses on file for the customer (Shopify `customer.addresses[]`).
Used to populate the "change shipping address" dropdown.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `get_customer_shipping_addresses` |
| `customer_id` | body | string | ✓ | Encrypted customer id from [`order_details`](#83-get-order-details). |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Shipping addresses found.",
  "data": [
    {
      "location_id": "<encrypted>",
      "first_name": "Jane",
      "last_name": "Doe",
      "company": "",
      "address1": "Flat 1, Main Street",
      "address2": "",
      "city": "Bengaluru",
      "province": "Karnataka",
      "country": "India",
      "zip": "560001",
      "phone": "9876543210",
      "name": "Jane Doe",
      "province_code": "KA",
      "country_code": "IN",
      "country_name": "India"
    }
  ]
}
```

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `customer_id` missing or undecryptable |
| 404 | `NOT_FOUND`     | Customer not found OR has zero saved addresses |

---

### 8.5 Edit shipping address

> `POST /` with `action=edit_customer_shipping_address`

Replaces the order's shipping address with one of the customer's saved
addresses (selected by `location_id`).

> Only callable while `data.allow_edit_address == true` from
> [`order_details`](#83-get-order-details). Once the order is fulfilled or has
> an AWB assigned, this returns `409 CONFLICT`.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `edit_customer_shipping_address` |
| `order_id` | body | string | ✓ | Encrypted |
| `customer_id` | body | string | ✓ | Encrypted |
| `location_id` | body | string | ✓ | Encrypted — id from [`get_customer_shipping_addresses`](#84-get-customer-shipping-addresses) |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Shipping address changed successfully.",
  "data": { /* the address object that was applied */ }
}
```

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | Any of `order_id`, `customer_id`, `location_id` missing or undecryptable |
| 404 | `NOT_FOUND`     | Order not found, customer not found, or the chosen `location_id` is not in the customer's address book |
| 409 | `CONFLICT`      | Priority-ordered: **cancelled** → "Order has been cancelled." &middot; **shipped** → "Order has been shipped — the address can no longer be edited." |
| 500 | `SERVER_ERROR`  | Shopify rejected the order-update mutation |

---

### 8.6 Edit phone number

> `POST /` with `action=edit_customer_phone_number`

Updates the phone number on the order's shipping address. Same eligibility
window as §8.5.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `edit_customer_phone_number` |
| `order_id` | body | string | ✓ | Encrypted |
| `phone_number` | body | string | ✓ | Max 32 chars |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Phone number changed successfully.",
  "data": "9876543210"
}
```

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `order_id` or `phone_number` missing |
| 404 | `NOT_FOUND`     | Order not found for this shop |
| 409 | `CONFLICT`      | Priority-ordered: **cancelled** → "Order has been cancelled." &middot; **shipped** → "Order has been shipped — the phone number can no longer be edited." |
| 500 | `SERVER_ERROR`  | Shopify rejected the order-update mutation |

---

### 8.7 Cancel order

> `POST /` with `action=cancel_order`

Cancels the Shopify order on the customer's behalf with
`reason = CUSTOMER, refund = true, restock = true, notifyCustomer = true`.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `cancel_order` |
| `order_id` | body | string | ✓ | Encrypted |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Order cancelled successfully.",
  "data": []
}
```

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `order_id` missing or undecryptable |
| 404 | `NOT_FOUND` | Order not found for this shop |
| 409 | `CONFLICT` | Priority-ordered: order **already cancelled** → "Order has already been cancelled." &middot; order **shipped** (AWB on file) → "Order has been shipped — it can no longer be cancelled." &middot; otherwise Shopify userError text (`<br />`-joined). |

---

### 8.8 Get exchange variants

> `POST /` with `action=get_variant`

Lists the variants of a single product the customer is exchanging *into*.
Honours the shop's "hide out-of-stock variants" and "only same-price exchange"
flags.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `get_variant` |
| `order_id` | body | string | ✓ | Encrypted |
| `product_id` | body | string | ✓ | Encrypted — product the customer is exchanging into |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Variants found.",
  "data": [
    { "id": "<encrypted>", "title": "M / Blue", "price": "499.00", "compare_at_price": null }
  ]
}
```

---

### 8.9 Get exchange products

> `POST /` with `action=get_products`

Paginated product search for the "exchange with another product" flow.
Title-prefix search; respects `only_same_price_product_display`.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `get_products` |
| `order_id` | body | string | ✓ | Encrypted |
| `product_id` | body | string | ✓ | Encrypted (anchor item — used for same-price filtering) |
| `search` | body | string (JSON) | ✗ | `{"type":"next\|previous","cursor":"...","search_pro":"..."}` |

#### Response (200, abridged)

```json
{
  "success": true,
  "code": "OK",
  "message": "Products have been fetched successfully.",
  "data": {
    "products": [
      {
        "cursor": "eyJsYXN0X2lkIjox...",
        "id": "<encrypted>",
        "title": "Cotton T-Shirt",
        "handle": "cotton-tshirt",
        "product_img": "https://cdn.shopify.com/.../tshirt.jpg",
        "options": [
          { "id": "<encrypted>", "name": "Size", "values": ["S","M","L"] }
        ],
        "variants": {
          "M / Blue": {
            "id": "<encrypted>",
            "title": "M / Blue",
            "price": "499.00",
            "compare_price": "0",
            "qty": "12",
            "inventoryPolicy": "DENY",
            "image": "https://cdn.shopify.com/.../tshirt-m-blue.jpg",
            "price_symbol": "₹"
          }
        }
      }
    ],
    "hasNextPage": true,
    "hasPreviousPage": false
  }
}
```

To page forward: re-call with
`search={"type":"next","cursor":"<last item's cursor>","search_pro":""}`.

---

### 8.10 Submit return request

> `POST /` with `action=return_requests_submit`

Creates one `reverse_pickup` row per line item the customer is returning or
exchanging. Optional proof images are accepted as multipart files keyed by
`img_<lineitem_id>` (where `<lineitem_id>` is the **decrypted** id — encrypted
to the client, decrypted server-side; clients should send the raw form-key as
the server expects it).

> ⚠ **Multipart only when images are attached.** When no images are uploaded,
> a plain `application/x-www-form-urlencoded` body is fine.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `return_requests_submit` |
| `order_id` | body | string | ✓ | Encrypted |
| `product_data` | body | string (JSON) | ✓ | Array of items. See schema below. |
| `return_address` | body | string (JSON) | ✗ | Customer's pickup address as an object — base64-encoded server-side. |
| `img_<lineitem_id>` | file | file | ✗ | JPG/JPEG/PNG. Images > 500 KB are resized to 600px wide. |

##### `product_data[]` schema

```json
[
  {
    "item_id":             "<encrypted lineitem id>",
    "quantity":            1,
    "reason":              "Wrong size",
    "customer_note":       "Please send a size L instead",
    "return_option":       "exchange",
    "replace_variant_id":  "<encrypted variant id>",
    "replace_product_id":  "<encrypted product id>",
    "exchange_quantity":   1,
    "refund_mode":         "",
    "refund_mode_details": {}
  }
]
```

For a pure refund, set `return_option = "refund"`, leave the `replace_*` fields
empty, and supply `refund_mode` + `refund_mode_details` (e.g.
`{ "upi_id": "abc@bank" }`).

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Return request submitted successfully.",
  "data": []
}
```

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `order_id` or `product_data` missing |
| 404 | `NOT_FOUND`     | Order not found for this shop |
| 409 | `CONFLICT`      | Priority-ordered: **cancelled** → "Order has been cancelled." &middot; **closed** → "Order has been closed." &middot; **not fulfilled** → "Order not fulfilled yet!" &middot; **per-item** already-returned conflicts surface only after all order-level checks pass. |

---

### 8.11 Reschedule pickup

> `POST /` with `action=reschedule_pickup`

Resets a previously declined or cancelled return request back to "pending"
and fires the customer + admin confirmation mails.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `reschedule_pickup` |
| `id` | body | string | ✓ | Encrypted `reverse_pickup.id` (from `return_data[].id` in [§8.3](#83-get-order-details)) |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Order has been re-scheduled successfully.",
  "data": []
}
```

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `id` missing or undecryptable |
| 404 | `NOT_FOUND` | Return request id not found for this shop |
| 409 | `CONFLICT`  | Order on the return request has been cancelled — pickup cannot be rescheduled. |

---

## 9. Recommended client flow

```
[App launch]
  │
  ▼
get_return_settings ─▶ theme the screen, build reason/refund dropdowns
  │
  ▼
[Customer enters email]
  │
  ▼
get_order_list  ─────▶ render the order picker (cards)
  │                       │
  │                       ▼
  │                  [Customer picks one]
  │                       │
  ▼                       ▼
order_details  ─────────▶ show line items
  │                       │
  │                       ├─ allow_cancel_order        → cancel_order
  │                       ├─ allow_edit_phone           → edit_customer_phone_number
  │                       ├─ allow_edit_address         → get_customer_shipping_addresses
  │                       │                                  + edit_customer_shipping_address
  │                       │
  │                       └─ Per line item:
  │                            ├─ exchange w/ same product variants → get_variant
  │                            ├─ exchange w/ another product       → get_products
  │                            └─ re-schedule previously declined   → reschedule_pickup
  ▼
return_requests_submit (one call per customer "Submit" action)
```

---

## 10. Encrypted identifiers

Every primary identifier exposed to the client is encrypted with the shop's
key. **Never** treat these as opaque IDs you can substring or pattern-match
against — round-trip them: receive from the API, store, send back as-is.

| Field | Comes from | Sent back to |
|-------|------------|--------------|
| `order_id` / `<encrypted_o_id>` keys in `get_order_list` | `order_details`, `cancel_order`, `edit_customer_*`, `return_requests_submit`, `get_variant`, `get_products` |
| `customer_id` | `get_customer_shipping_addresses`, `edit_customer_shipping_address` |
| `location_id` (address id) | `edit_customer_shipping_address` |
| `item_id` (line item) | `return_requests_submit.product_data[].item_id`; also used in `img_<id>` multipart key **decrypted** server-side |
| `product_id` | `get_variant`, `get_products`, `return_requests_submit.product_data[].replace_product_id` |
| `variant_id` | `return_requests_submit.product_data[].replace_variant_id` |
| `id` (return request) | `reschedule_pickup.id` |

---

## 11. Versioning & change policy

- **No breaking field renames** are planned within the v1 line.
- New fields will be **added** to existing response objects — your JSON
  parser must ignore unknown keys.
- The legacy `result` / `msg` keys (and `data` mirrors that some endpoints
  echo at the top level) are kept indefinitely. New endpoints will not
  expand them.
- Deprecations will be announced via the support email at least 90 days
  before removal.

---

## 12. FAQ & troubleshooting

**Q. I'm getting `401 UNAUTHORIZED` even with a fresh token.**
Check the casing — the header is `Auth-Token`, not `Authorization`. Some HTTP
clients lowercase headers; the API matches case-insensitively, but proxies in
front of it may not.

**Q. `403 PLAN_LIMIT` — what now?**
The shop's plan doesn't include `return_request`. The merchant must upgrade
under **ILS admin → Plans & Pricing** before the API will respond.

**Q. `return_requests_submit` succeeds but the customer didn't get the email.**
Email/SMS is dispatched via the merchant's configured sender. Confirm the
sender is verified under **ILS admin → Templates** and that the customer
has a valid email on the order.

**Q. The IDs I get from the API are very long and look random — is that ok?**
Yes. All primary IDs are encrypted per-shop. Send them back verbatim; never
URL-decode, trim, or hash them.

**Q. Can I call this from a browser (JS)?**
Yes — CORS is wide-open (`*`). But never embed the `Auth-Token` in client-side
JavaScript shipped to the public web. Use a server-side proxy.

---

## 13. Support

- **Email**: [support@shopiapps.in](mailto:support@shopiapps.in)
- **Postman collection**: `documentation/api/return/ILS-Return-Request-API.postman_collection.json`
- **Source**: `prfiles/return-request/api/`
- **Sibling API**: see `documentation/api/tracking/tracking.md` for the
  shipment-tracking endpoints that pair naturally with this API.
