# ILS Tracking API — Mobile Integration Guide

**Product:** Indian Logistics Services (ILS) — Order Tracking
**Audience:** Mobile app developers integrating order-tracking screens for a Shopify merchant using the ILS app.
**Version:** 1.0
**Status:** Stable
**Last updated:** 21 May 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 tracking settings](#81-get-tracking-settings)
   - [8.2 Login by tracking number](#82-login-by-tracking-number-awb_login)
   - [8.3 Login by order number](#83-login-by-order-number-awb_login_with_order)
   - [8.4 Get tracking details](#84-get-tracking-details-get_tracking_info)
9. [Recommended client flow](#9-recommended-client-flow)
10. [Code samples](#10-code-samples)
11. [Best practices](#11-best-practices)
12. [Versioning & change policy](#12-versioning--change-policy)
13. [FAQ & troubleshooting](#13-faq--troubleshooting)
14. [Support](#14-support)

---

## 1. Overview

The ILS Tracking API lets your mobile application show the same order-tracking
experience that the merchant's Shopify storefront does — including login by
order number or AWB, full shipment history from the carrier, line-item details
with product images, and the merchant's branding (theme colours, copy).

Every endpoint is **per-shop**, **authenticated**, and **read-only** from the
merchant's data. The API speaks JSON over HTTPS.

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

---

## 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 **Customer Tracking** feature
   (available on the *Advanced* and *Gold* plans, or any custom plan with the
   `customer_tracking` 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. There is no self-serve generation flow today.

### 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
   `customer_tracking` 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 |
| Storage    | Server-side: `settings.mobile_app_access_token` |

### Token rotation

If you suspect the token has been leaked (e.g. published in source control,
shared with the wrong party), email
[support@shopiapps.in](mailto:support@shopiapps.in)
requesting a rotation. The old token is revoked immediately on issue of a new
one.

### 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, a smoke test is one cURL command:

```bash
curl --location 'https://ils.shopiapps.in/prfiles/tracking/api/' \
  --header 'Auth-Token: YOUR_TOKEN_HERE' \
  --form 'shop=examplestore.myshopify.com' \
  --form 'action=get_tracking_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 format is non-empty.
2. The shop exists, is installed, and has an active payment status.
3. The shop's plan includes the `customer_tracking` 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 tracking details found.",
  "data": { ... endpoint-specific payload ... },

  "result": "success",
  "msg": "Order tracking details 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. Localised to the merchant's configured copy where applicable. 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`) 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`; email-phone mismatch on `awb_login_with_order` |
| 403  | `FORBIDDEN`       | App not installed for this shop |
| 403  | `PLAN_LIMIT`      | Customer-tracking feature not included in the shop's plan |
| 404  | `NOT_FOUND`       | Order / AWB / tracking record not present |
| 500  | `SERVER_ERROR`    | Unexpected server error — please retry, then contact support if it persists |

### Error response example

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

> The `message` for `NOT_FOUND` on `awb_login` / `awb_login_with_order` is
> taken from the merchant's `tracking_settings.error_messages` JSON when set,
> so the copy may vary per shop. Use `code` (not `message`) for branching.

---

## 8. Endpoint reference

### 8.1 Get tracking settings

> `POST /` with `action=get_tracking_settings`

Returns the merchant's per-shop tracking-page configuration: theme colours,
copy, date/time formats, login behaviour, and step labels. Call this once on
app launch (or when the user enters the tracking flow) and cache the response
for the session.

#### Request

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

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "OK",
  "data": {
    "track_enable": "1",
    "theme_color": "#0c4ca3",
    "font_color": "#000000",
    "button_font_color": "#ffffff",
    "additional_css": "",
    "date_formate": "F dS Y",
    "time_formate": "h:i:s a",
    "track_method": "0",
    "email_enable": "1",
    "email_required": "0",
    "login_page_description": "Enter your order details to track your shipment.",
    "login_button_text": "Track",
    "show_login_banner": "1",
    "login_banner_image_url": "https://ils.shopiapps.in/uploads/banners/123.jpg",
    "login_banner_position": "0",
    "view_content": ["orderdetail", "productlist", "trackingHistory", "trackingCompany"],
    "show_track_process_icon": "1",
    "track_process_steps": {
      "order": "Ordered",
      "ready_ship": "Ready To Ship",
      "transit": "In Transit",
      "out_delivery": "Out For Delivery",
      "delivered": "Delivered"
    },
    "show_promotion_banner": "0",
    "promotion_images": "",
    "show_track_detail_header": "0",
    "track_detail_header_content": "",
    "show_track_detail_footer": "0",
    "track_detail_footer_content": ""
  }
}
```

#### Field reference

| Field | Type | Meaning |
|-------|------|---------|
| `track_enable` | `"0"\|"1"` | Master switch. `0` → tracking screen disabled. |
| `theme_color`, `font_color`, `button_font_color` | hex | Apply to your screens for visual continuity with the storefront. |
| `additional_css` | string | Custom CSS injected on the web tracker. **Mobile clients should ignore this**. |
| `date_formate`, `time_formate` | PHP-style format | Use only as a hint — re-format dates from `shipment_data.history` with your platform's locale APIs for best results. |
| `track_method` | `"0"\|"1"\|"2"` | `0`: accept order number OR AWB. `1`: only order number. `2`: only AWB. |
| `email_enable` | `"0"\|"1"` | Show email/phone field on order-number login. |
| `email_required` | `"0"\|"1"` | Make email/phone mandatory when `email_enable=1`. |
| `login_page_description` | string | Display above the login form. |
| `login_button_text` | string | CTA label. |
| `show_login_banner`, `login_banner_image_url`, `login_banner_position` | flags + url | Optional banner on the login screen. Position `0`: left, `1`: right. |
| `view_content` | array | Subset of `orderdetail`, `productlist`, `trackingHistory`, `trackingCompany` — which sections to render after login. |
| `show_track_process_icon` | `"0"\|"1"` | Show the 5-step progress bar. |
| `track_process_steps` | object | Localised labels for steps 1-5. |
| `show_promotion_banner`, `promotion_images` | flag + array | Optional carousel of promo banners on the result screen. |
| `show_track_detail_header` / `_footer`, `track_detail_header_content` / `_footer_content` | flags + HTML | Optional merchant-controlled HTML around the result. |

---

### 8.2 Login by tracking number — `awb_login`

> `POST /` with `action=awb_login`

Verifies that an AWB (tracking) number belongs to the shop. Use this when the
user chose "Track by AWB" and entered a tracking number.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `awb_login` |
| `awb_no` | body | string | ✓ | Tracking number (max 64 chars) |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Tracking number successfully founds",
  "data": { "awb": "DLV1234567890" },
  "awb": "DLV1234567890",
  "result": "success",
  "msg": "Tracking number successfully founds"
}
```

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400  | `INVALID_INPUT` | `awb_no` missing or > 64 chars |
| 404  | `NOT_FOUND` | AWB not on file for this shop |

After a `200`, call **`get_tracking_info`** with the same `awb_no` to fetch the
full tracking payload.

---

### 8.3 Login by order number — `awb_login_with_order`

> `POST /` with `action=awb_login_with_order`

Resolves shipments for a given order number, optionally cross-checked against
an email **or** 10-digit phone number. Use this when the user chose "Track by
Order ID".

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `awb_login_with_order` |
| `order_name` | body | string | ✓ | Order number (max 64 chars) — Shopify display name (`#1042`) or plain numeric. |
| `order_email` | body | string | ✗ | Email **or** phone. Phones may be 10+ digits with `+`, `-`, or spaces; the API compares only the last 10 digits. **Required if `email_required=1` from `get_tracking_settings`.** |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "tracking number successfully founds",
  "data": {
    "awb": "DLV1234567890",
    "multiple_tracking": [
      {
        "number": "DLV1234567890",
        "lineItem": ["13241324", "13241325"],
        "company": "Delhivery",
        "displayStatus": "in_progress",
        "current_order_status": "Out for delivery"
      },
      {
        "number": "DLV1234567891",
        "lineItem": ["13241326"],
        "company": "Delhivery",
        "displayStatus": "delivered",
        "current_order_status": "Delivered"
      }
    ]
  },
  "awb": "DLV1234567890",
  "multiple_tracking": [ /* mirror of data.multiple_tracking */ ]
}
```

`multiple_tracking` lists **all** shipments for the order — useful when an
order ships in multiple parcels. `data.awb` is always the most recent
(primary) shipment.

#### `displayStatus` values

| Value | Meaning |
|-------|---------|
| `pending` | Not yet shipped |
| `in_progress` | In transit |
| `delivered` | Delivered |
| `failed` | Delivery failure / undelivered |
| `return` | Return-to-origin started |
| `cancelled` | Cancelled |
| `RTO delivered` | Returned and delivered back to origin |

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `order_name` missing / too long |
| 401 | `UNAUTHORIZED`  | `order_email` provided but neither email nor phone matches |
| 404 | `NOT_FOUND`     | Order not found OR order has no shipped packages yet |

---

### 8.4 Get tracking details — `get_tracking_info`

> `POST /` with `action=get_tracking_info`

Returns the complete tracking payload for a confirmed AWB: order summary
(customer, totals), shipment history from the carrier, and line items with
product images and handles.

This is the heaviest endpoint — it hits the carrier's tracking API live, plus
Shopify GraphQL for product details. Cache the response for a few minutes
client-side.

#### Request

| Field | Where | Type | Required | Description |
|-------|-------|------|---------:|-------------|
| `Auth-Token` | header | string | ✓ | |
| `shop` | body | string | ✓ | |
| `action` | body | string | ✓ | `get_tracking_info` |
| `awb_no` | body | string | ✓ | Tracking number returned by 8.2 or 8.3 |

#### Response (200)

```json
{
  "success": true,
  "code": "OK",
  "message": "Order tracking details found.",
  "data": {
    "process_step": "3",
    "order_data": {
      "order_name": "#1042",
      "customer_name": "Jane Doe",
      "shipping_address": "Flat 1, Main Street, Bengaluru, KA, 560001",
      "mobile": "9876543210",
      "email": "jane@example.com",
      "totalPrice": "1499.00",
      "total_tax": "228.30",
      "discount": "100.00",
      "shipping_charge": "0.00",
      "courier_name": "delhivery"
    },
    "shipment_data": {
      "pStatus": "3",
      "Origin": "BENGALURU",
      "Destination": "MUMBAI",
      "history": [
        { "date": "May 19th 2026", "time": "10:23:00 am", "status": "In Transit", "location": "Bengaluru Hub" },
        { "date": "May 18th 2026", "time": "08:11:00 pm", "status": "Picked up",  "location": "Bengaluru Warehouse" }
      ]
    },
    "lineitems_data": [
      {
        "title": "Cotton T-Shirt",
        "handle": "cotton-tshirt",
        "unit_price": "499.00",
        "total_qty": 2,
        "src": "https://cdn.shopify.com/.../tshirt.jpg"
      }
    ]
  },
  "processStep": "3",
  "order_data":    { /* legacy mirror */ },
  "shipment_data": { /* legacy mirror */ },
  "lineitems_data":[ /* legacy mirror */ ]
}
```

#### Field reference

| Path | Type | Meaning |
|------|------|---------|
| `data.process_step` | `"1"-"5"` | High-level step. `1` Ordered, `2` Ready to ship, `3` In transit, `4` Out for delivery, `5` Delivered. Use this to drive the progress bar. |
| `data.order_data.order_name` | string | Shopify display name, e.g. `#1042`. |
| `data.order_data.customer_name` | string | First + last name (falls back to shipping name). |
| `data.order_data.shipping_address` | string | Single-line shipping address (comma-joined). |
| `data.order_data.mobile`, `.email` | string | Contact info (best-effort; may be empty if the order was placed without one). |
| `data.order_data.totalPrice`, `.total_tax`, `.discount`, `.shipping_charge` | string (decimal) | Money values. **For partial shipments**, these are recalculated for the items in this AWB only. |
| `data.order_data.courier_name` | string | Internal courier code (e.g. `delhivery`, `bluedart`). Use `displayStatus` from 8.3 or your own mapping for a user-facing label. |
| `data.shipment_data.pStatus` | `"1"-"5"` | Mirror of `process_step`. |
| `data.shipment_data.Origin`, `.Destination` | string | Carrier-provided origin / destination hub. |
| `data.shipment_data.history` | array | List of scan events, **newest first**. Each event has `date`, `time`, `status`, `location`. Field shape is consistent across all couriers. |
| `data.lineitems_data[]` | array | Each line item in this AWB. `src` is the Shopify product image (falls back to a placeholder when unavailable). |

#### Errors

| HTTP | `code` | Cause |
|------|--------|-------|
| 400 | `INVALID_INPUT` | `awb_no` missing / too long |
| 404 | `NOT_FOUND` | AWB not on file, or carrier returned no scan events |

#### Supported couriers

The API returns a normalised shipment payload across all configured couriers.
Today's list:

> Bluedart · Delhivery · DTDC · Ecom Express · Smartr · Xpressbees · Ekart ·
> Amazon · Intargos · Shiprocket · Xpressbees Franchise · Xpressbees Ecom ·
> India Post · Borzo · Shree Maruti · TCI Express · Porter · FedEx · Pickrr

If the merchant has configured a courier that isn't in this list, the
endpoint returns `404 NOT_FOUND`. Reach out to support to request additional
courier coverage.

---

## 9. Recommended client flow

```
┌─────────────────────────┐
│ App launches            │
└────────────┬────────────┘
             │
             ▼
   ┌─────────────────────┐
   │ get_tracking_settings│  ─── if track_enable == "0", hide tracking entry point
   └─────────┬───────────┘
             │
             ▼
   ┌─────────────────────┐
   │ User enters         │
   │ AWB or order #      │
   └─────────┬───────────┘
             │
       ┌─────┴──────┐
       ▼            ▼
  awb_login   awb_login_with_order
       │            │
       └─────┬──────┘
             │  awb returned
             ▼
   ┌─────────────────────┐
   │ get_tracking_info   │  ◀── refresh on pull-to-reload
   └─────────┬───────────┘
             │
             ▼
       Render result
```

**Caching guidance:**

| Endpoint | Cache key | TTL suggestion |
|----------|-----------|----------------|
| `get_tracking_settings` | shop | 1 hour |
| `awb_login`, `awb_login_with_order` | shop + input | Do not cache |
| `get_tracking_info` | shop + awb | 2–5 minutes |

---

## 10. Code samples

### cURL

```bash
curl --location 'https://ils.shopiapps.in/prfiles/tracking/api/' \
  --header 'Auth-Token: YOUR_TOKEN' \
  --form 'shop=examplestore.myshopify.com' \
  --form 'action=get_tracking_info' \
  --form 'awb_no=DLV1234567890'
```

### JavaScript (fetch)

```js
async function getTrackingInfo(shop, awb, token) {
  const body = new URLSearchParams({
    shop,
    action: 'get_tracking_info',
    awb_no: awb,
  });
  const r = await fetch('https://ils.shopiapps.in/prfiles/tracking/api/', {
    method: 'POST',
    headers: { 'Auth-Token': token },
    body,
  });
  const json = await r.json();
  if (!json.success) throw new Error(json.code + ': ' + json.message);
  return json.data;
}
```

### Kotlin (OkHttp)

```kotlin
val client = OkHttpClient()
val body = FormBody.Builder()
    .add("shop", shop)
    .add("action", "get_tracking_info")
    .add("awb_no", awb)
    .build()

val req = Request.Builder()
    .url("https://ils.shopiapps.in/prfiles/tracking/api/")
    .header("Auth-Token", token)
    .post(body)
    .build()

client.newCall(req).execute().use { response ->
    val json = JSONObject(response.body!!.string())
    if (!json.getBoolean("success")) {
        throw IOException(json.getString("code") + ": " + json.getString("message"))
    }
    return json.getJSONObject("data")
}
```

### Swift (URLSession)

```swift
func getTrackingInfo(shop: String, awb: String, token: String) async throws -> [String: Any] {
    var req = URLRequest(url: URL(string: "https://ils.shopiapps.in/prfiles/tracking/api/")!)
    req.httpMethod = "POST"
    req.setValue(token, forHTTPHeaderField: "Auth-Token")
    req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
    let params = "shop=\(shop)&action=get_tracking_info&awb_no=\(awb)"
    req.httpBody = params.data(using: .utf8)

    let (data, _) = try await URLSession.shared.data(for: req)
    let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
    guard json["success"] as? Bool == true else {
        throw NSError(domain: "ILS", code: 0,
                      userInfo: [NSLocalizedDescriptionKey: json["message"] as? String ?? "Unknown error"])
    }
    return json["data"] as! [String: Any]
}
```

### PHP (server-to-server)

```php
$ch = curl_init('https://ils.shopiapps.in/prfiles/tracking/api/');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Auth-Token: ' . $token],
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query([
        'shop'   => $shop,
        'action' => 'get_tracking_info',
        'awb_no' => $awb,
    ]),
]);
$body = curl_exec($ch);
curl_close($ch);
$json = json_decode($body, true);
if (!$json['success']) {
    throw new RuntimeException($json['code'] . ': ' . $json['message']);
}
return $json['data'];
```

---

## 11. Best practices

- **Branch on `code`, display `message`.** `code` is stable; `message` may be
  localised or merchant-customised.
- **Always check `success`** before reading `data`. `data` is an empty array
  in most error responses.
- **Don't tight-loop tracking refreshes.** Couriers update scan events every
  few minutes at best — a 2–5 minute client-side cache is plenty.
- **Encode form bodies as `application/x-www-form-urlencoded`.** Multipart
  works but is unnecessary overhead.
- **Pin certificates** if your mobile threat model warrants it. ILS issues
  TLS certificates via Let's Encrypt — pin to the chain root, not the leaf.
- **Strip PII from logs.** `order_data.email`, `mobile`,
  `shipping_address`, and customer name are personal data — do not log them
  in production.
- **Show actionable errors.** Map `NOT_FOUND` to a "Couldn't find that order"
  state; `PLAN_LIMIT` to "Please contact the merchant"; `UNAUTHORIZED` to
  "Please re-authenticate".

---

## 12. Versioning & change policy

The API today is **v1**. Versioning rules:

- Additive changes (new optional response fields, new endpoints, new
  `code` values) ship without a version bump. Clients must tolerate unknown
  fields.
- Breaking changes (removed / renamed fields, changed types, removed
  endpoints) will trigger a new path prefix (`/v2/`) and a 6-month overlap
  window during which v1 keeps working.
- Legacy keys `result` and `msg` will be kept indefinitely on v1; new
  fields in v1 use `success` / `code` / `message` going forward.

Subscribe to release notes by emailing
[support@shopiapps.in](mailto:support@shopiapps.in) with subject *"ILS API
release notes"*.

---

## 13. FAQ & troubleshooting

**Q. I get `403 FORBIDDEN — App not installed!`.**
The shop domain you sent doesn't match an installed ILS app. Common causes:
typo in the shop domain (must end in `.myshopify.com`); the merchant
uninstalled the app; payment status lapsed. Check with the merchant.

**Q. I get `403 PLAN_LIMIT`.**
The merchant's plan does not include the Customer Tracking feature. Upgrade
to *Advanced* or *Gold*, or contact ILS for a custom plan.

**Q. I get `401 UNAUTHORIZED` even with a valid-looking token.**
Confirm: (a) `Auth-Token` header name spelled exactly (case is forgiving but
hyphen is required), (b) the token is for the same shop you're passing in
`shop`, (c) the token hasn't been rotated. If unsure, request a fresh token.

**Q. `get_tracking_info` returns `404` for an AWB that just shipped.**
Carriers can take 30–60 minutes to surface the first scan event. Show the
order as "Ready to ship" client-side and let the user retry.

**Q. `multiple_tracking` is empty on `awb_login_with_order`.**
The order has no shipped packages yet. The merchant has approved the order
but hasn't manifested the package. Suggest the user check back later.

**Q. Are there rate limits?**
There are no hard per-token rate limits today, but please stay under
60 requests / minute / shop. Excessive traffic that affects other merchants
may be throttled at the WAF.

**Q. Can I receive tracking updates via webhook?**
Webhook delivery of tracking events isn't on the public roadmap. Today the
recommended pattern is client-side polling with a 2–5 minute cadence.

**Q. Is there a sandbox?**
Not currently. Token issuance is per-shop, and you can request a test token
against a development store (`*-test.myshopify.com`) the same way as a
production token.

---

## 14. Support

- **Email**:   [support@shopiapps.in](mailto:support@shopiapps.in)
- **Subject prefix**: `[Mobile API]` for faster triage
- **Include**: shop domain, request `action`, full request body (redact the
  `Auth-Token`), the response JSON, and a timestamp (with timezone)

For verified security issues, include the prefix `[SECURITY]` — these are
triaged within one business day.

---

### Appendix A — Postman collection

A ready-to-import Postman v2.1 collection is published alongside this guide:

> [`ILS-Tracking-API.postman_collection.json`](./ILS-Tracking-API.postman_collection.json)

Import it, set the collection-level variables (`base_url`, `shop`,
`auth_token`, `awb_no`, `order_name`, `order_email`), and run the requests in
any order. Each request contains a basic test that asserts the response
envelope.

### Appendix B — Internal architecture (for ILS maintainers)

> The section below is **not** part of the public contract; it describes the
> server-side structure for ILS engineers maintaining the API.

```
prfiles/tracking/api/
├── index.php                            — thin router
├── bootstrap.php                        — config + auth + plan check
├── helpers/
│   ├── ResponseHelper.php               — JSON envelope + HTTP codes
│   ├── RequestValidator.php             — input sanitization + max-length
│   ├── AuthMiddleware.php               — shop / token / plan gate
│   ├── TrackingService.php              — settings + lookups + label maps
│   └── CourierTrackingResolver.php      — per-courier credential + dispatch
├── actions/
│   ├── get_tracking_settings.php
│   ├── awb_login.php
│   ├── awb_login_with_order.php
│   └── get_tracking_info.php
└── index_legacy.php                     — pre-refactor file (HTTP 410)
```

**Adding a new action**:

1. Create `actions/<name>.php`. It can rely on globals `$conn`, `$shop`,
   `$authContext` set by bootstrap.
2. Register the action in `$actionMap` inside `index.php`.
3. Always terminate via `ResponseHelper::success()` or
   `ResponseHelper::error()` — never `echo` directly.

**Adding a new courier**:

1. Add the column list to `CourierTrackingResolver::$credentialsMap`.
2. Add a private `resolveXxx()` method that loads credentials + calls the
   existing courier function in `prfiles/front_function.php`.
3. Add a `case` for the courier in the dispatch switch.
4. Add a display label to `TrackingService::$logistics`.
