# ZoShop — Sell Corner API

REST API for the **Sell Corner** (customer classifieds) feature of the ZoShop Flutter app.

- **Audience:** Flutter / mobile developers integrating Sell Corner.
- **Scope:** only the 11 Sell Corner endpoints. Other app endpoints (login/OTP, shops, banners …) are not covered here.
- **Last updated:** 2026-11-12

## Contents

1. [Overview](#1-overview)
2. [Conventions](#2-conventions)
3. [Objects](#3-objects)
4. [Payment flow](#4-payment-flow)
5. [Endpoints](#5-endpoints)
6. [Item states and visibility](#6-item-states-and-visibility)
7. [Limits and rules](#7-limits-and-rules)

---

## 1. Overview

A logged-in customer can post an item for sale. The listing is **not public** until the seller pays a fixed fee through Razorpay. After payment it is public for a fixed period.

| Rule | Value |
|---|---|
| Listing fee | ₹100 (10000 paise), fixed, set by the server |
| Public duration | 30 days, counted from the moment of payment |
| Images per item | 0 to 5 |
| Image size / type | up to 1 MB each (the decoded image); jpg, png or webp; sent as **base64**, one image per request |
| Renewal | Not supported. After expiry the seller posts a new item |

Life of an item:

1. Seller posts the item (text only) → `payment_status = "pending"`, not visible to others.
2. Seller uploads the photos, **one request per photo, as base64** (optional, up to 5).
3. Seller pays ₹100 via Razorpay Checkout → `payment_status = "paid"`, `expires_at` = payment time + 30 days.
4. The item is public until `expires_at`, unless the seller **unpublishes** it.
5. After `expires_at` it disappears from the public list. It stays in the seller's own list.

The seller can **edit** the item and **publish / unpublish** it at any time. None of these actions change the payment state or the expiry date.

Items added by the ZoShop team look exactly like any other item in the app; they simply have no seller account behind them. The team can also take any item down: a taken-down item is hidden from the public list and details like an expired one, the seller cannot bring it back, and the API does not tell the app about it.

---

## 2. Conventions

### 2.1 Base URL

| Environment | Base URL |
|---|---|
| Production | `https://admin.zoshoponline.com/api` |
| Local (Laragon) | `http://admin.zoshop-admin-web.test/api` |

All paths below are relative to the base URL, e.g. `GET /sell-items` means `GET https://admin.zoshoponline.com/api/sell-items`.

### 2.2 Headers

| Header | Required | Notes |
|---|---|---|
| `Accept: application/json` | **Always** | Without it, errors such as "not logged in" or "validation failed" come back as HTTP redirects instead of JSON 401 / 422. |
| `X-App-Version` | Always | The app version, as for every other app endpoint. If it is missing the server answers `403` (`type: "force_update"`); if it is below the minimum supported version it answers `426`. |
| `Authorization: Bearer <token>` | Endpoints marked **Login** | The token from the OTP login (`POST /verify-otp`). |
| `Content-Type` | Requests with a body | `application/json`. Images are sent as base64 text inside the JSON body, never as file uploads. |

### 2.3 Authentication

| Marker | Meaning |
|---|---|
| **Public** | No token needed. |
| **Login** | Customer bearer token required. Missing or invalid token → `401`. |
| **Login + owner** | Token required **and** the item must belong to the logged-in customer. Someone else's item (or an unknown uuid) → `404`, so other people's items are never revealed. |

### 2.4 Formats

- **Item identifier:** `uuid` (36-character UUID). It is used in every URL. The numeric database id is never exposed, except for image ids.
- **Timestamps:** ISO 8601 in **UTC**, e.g. `2026-12-12T10:15:30.000000Z`. Convert to the user's local time on the device.
- **Money:** `price` is a **string with 2 decimals** in rupees (`"5000.00"`) or `null`. Razorpay `amount` is an **integer in paise** (`10000`).
- **Phone numbers:** exactly 10 digits, no `+91`, no spaces (`"9876543210"`).
- **Image URLs:** absolute, public, no authentication needed.
- **Success envelope:** every successful response is `{ "data": ... }`.

### 2.5 Errors

| Status | When | Body |
|---|---|---|
| `401` | Missing / invalid token on a **Login** endpoint | `{ "message": "Unauthenticated." }` |
| `403` / `426` | App version check failed (see `X-App-Version`) | `{ "status": false, "type": "force_update", "message": "..." }` |
| `404` | Unknown uuid / image id, item not visible (public endpoints), or not your item (owner endpoints) | `{ "message": "..." }` |
| `409` | Pay endpoint only: the item is already paid, or is not available | `{ "message": "This item has already been paid for." }` or `{ "message": "This item is not available." }` |
| `422` | Validation failed, or payment verification failed | see below |
| `5xx` | Server or Razorpay problem | `{ "message": "..." }` — safe to retry later |

Validation errors (`422`):

```json
{
  "message": "The contact number field must be 10 digits.",
  "errors": {
    "contact_number": ["The contact number field must be 10 digits."],
    "images.0": ["The images.0 field must be an image."]
  }
}
```

Show messages from `errors` next to the matching fields. For `404` / `5xx` rely on the **status code**; the text of `message` is informational and may change.

### 2.6 Pagination

Only `GET /sell-items` returns a window of a larger list. It uses `limit` and `offset`.

| Query | Default | Notes |
|---|---|---|
| `limit` | 20 | How many items to return. Maximum 50; larger values are capped at 50. |
| `offset` | 0 | How many items to skip from the start of the list. |

Invalid values never cause an error: a `limit` that is missing, below 1 or not a whole number becomes 20; an `offset` that is missing, negative or not a whole number becomes 0. The response always shows the values that were actually used.

The response keeps the `data` array and adds `meta`:

```json
{
  "data": [ /* items */ ],
  "meta": { "limit": 20, "offset": 40, "total": 87 }
}
```

- `total` is the number of items that are public right now, across all windows.
- Next window: `offset = offset + limit`. There is nothing more to load when `offset + limit >= total` (or when `data` has fewer than `limit` items).
- Items are ordered newest payment first; items with the same payment time are ordered by id, so the order is stable.
- If an item goes live, is unpublished or expires between two requests, the window can shift by one: you may see an item twice or miss one. De-duplicate by `uuid`, and restart from `offset = 0` when the user pulls to refresh.
---

## 3. Objects

### 3.1 Image

| Field | Type | Description |
|---|---|---|
| `id` | integer | Image id. Needed only to delete the image. |
| `url` | string | Absolute public URL of the image. |

### 3.2 Item (public view)

Returned by the public endpoints (`GET /sell-items`, `GET /sell-items/{uuid}`).

| Field | Type | Description |
|---|---|---|
| `uuid` | string | Item identifier used in URLs. |
| `item_name` | string | Name of the item. |
| `description` | string | Description of the item. |
| `location` | string | Free-text location, e.g. `"Paltan Bazaar, Guwahati"`. |
| `seller_name` | string | Seller's display name. |
| `contact_number` | string | Seller's contact number (10 digits). Visible to everyone. |
| `price` | string \| null | Price in rupees with 2 decimals, or `null` if not set. |
| `images` | Image[] | Photos in display order. May be empty. |
| `expires_at` | string \| null | When the listing stops being public. `null` until paid. |
| `created_at` | string | When the item was added. |

```json
{
  "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
  "item_name": "Second-hand bike",
  "description": "Well maintained, single owner.",
  "location": "Paltan Bazaar, Guwahati",
  "seller_name": "Ravi",
  "contact_number": "9876543210",
  "price": "5000.00",
  "images": [
    { "id": 12, "url": "https://admin.zoshoponline.com/sell-corner/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/Qm3xV1.jpg" }
  ],
  "expires_at": "2026-12-12T10:15:30.000000Z",
  "created_at": "2026-11-12T10:10:02.000000Z"
}
```

### 3.3 My item (owner view)

Returned by all **Login** endpoints. It is the public view **plus** three fields:

| Field | Type | Description |
|---|---|---|
| `payment_status` | string | `"pending"` (not paid yet) or `"paid"`. |
| `is_published` | boolean | The seller's own publish switch. `true` by default. |
| `paid_at` | string \| null | When the payment was recorded. `null` until paid. |

```json
{
  "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
  "item_name": "Second-hand bike",
  "description": "Well maintained, single owner.",
  "location": "Paltan Bazaar, Guwahati",
  "seller_name": "Ravi",
  "contact_number": "9876543210",
  "price": "5000.00",
  "images": [],
  "expires_at": null,
  "created_at": "2026-11-12T10:10:02.000000Z",
  "payment_status": "pending",
  "is_published": true,
  "paid_at": null
}
```

### 3.4 Order (from the Pay endpoint)

| Field | Type | Description |
|---|---|---|
| `order_id` | string | Razorpay order id (`order_...`). Pass to Checkout. |
| `amount` | integer | Amount in **paise** (`10000` = ₹100). Pass to Checkout. |
| `currency` | string | Always `"INR"`. |
| `key` | string | Razorpay **public** key id (`rzp_live_...` / `rzp_test_...`). Pass to Checkout. |
| `sell_item_uuid` | string | The item being paid for. |

---

## 4. Payment flow

```
App                                   ZoShop API                         Razorpay
 |  POST /sell-items  ───────────────►  creates item (pending), text only
 |  ◄── 201 { uuid, payment_status: "pending" }
 |
 |  POST /sell-items/{uuid}/images  ─►  stores ONE photo (base64)      repeat 0–5 times
 |  ◄── 200 { images: [ ... ] }
 |
 |  POST /sell-items/{uuid}/pay  ────►  creates (or reuses) Razorpay order
 |  ◄── { order_id, amount, currency, key }
 |
 |  open Razorpay Checkout (key, order_id, amount, currency) ───────────────►
 |  ◄── onSuccess { payment_id, order_id, signature } ─────────────────────── user pays
 |
 |  POST /sell-items/{uuid}/payment-success  ─►  verifies signature, marks paid
 |  ◄── 200 { payment_status: "paid", expires_at: +30 days }
 |                                                        ▲
 |                                  Razorpay also notifies the server directly ──┘
```

Points to know:

1. **Photos come before payment but are not required by the server.** Upload them one by one right after creating the item; the server does not block payment when there are none. Photos can also be added or removed later, in any state.
2. **The server confirms the payment in two independent ways**: your `payment-success` call and a direct notification from Razorpay to the server. Whichever arrives first marks the item as paid; the other changes nothing. Your call is for speed, not the only source of truth.
3. **`payment-success` is safe to call more than once**, with the same body. A repeat returns `200` with the current item and does not extend the expiry.
4. **If the app is closed or loses network after the user paid**, the item still becomes `paid` through Razorpay's direct notification, usually within seconds. When the app reopens, refresh `GET /my-sell-items` and look at `payment_status`.
5. **If the user cancels or the payment fails in Checkout**, nothing is charged and the item stays `pending`. Call `Pay` again; it returns the **same** `order_id`, so the user can retry.
6. **Never call `payment-success` unless Checkout reported success**, and always send the three values exactly as Razorpay gave them. Invented or edited values are rejected with `422`.
7. The amount is decided by the server. Always use `amount` from the Pay response, never a hard-coded value.

Mapping for the `razorpay_flutter` plugin (`PaymentSuccessResponse`):

| Plugin field | Send as |
|---|---|
| `paymentId` | `razorpay_payment_id` |
| `orderId` | `razorpay_order_id` |
| `signature` | `razorpay_signature` |

Checkout options come from the Pay response: `key`, `order_id`, `amount`, `currency`.
---

## 5. Endpoints

| # | Method | Path | Auth | Purpose |
|---|---|---|---|---|
| 1 | GET | `/sell-items` | Public | List public items (paginated) |
| 2 | GET | `/sell-items/{uuid}` | Public | Item details |
| 3 | GET | `/my-sell-items` | Login | Seller's own items, every state |
| 4 | POST | `/sell-items` | Login | Post a new item (text only) |
| 5 | PUT / PATCH | `/sell-items/{uuid}` | Login + owner | Edit an item |
| 6 | POST | `/sell-items/{uuid}/images` | Login + owner | Add **one** image (base64) |
| 7 | DELETE | `/sell-items/{uuid}/images/{imageId}` | Login + owner | Remove an image |
| 8 | POST | `/sell-items/{uuid}/publish` | Login + owner | Show the item publicly again |
| 9 | POST | `/sell-items/{uuid}/unpublish` | Login + owner | Hide the item from the public list |
| 10 | POST | `/sell-items/{uuid}/pay` | Login + owner | Create the Razorpay order (₹100) |
| 11 | POST | `/sell-items/{uuid}/payment-success` | Login + owner | Report a successful payment |

---

### 5.1 List public items

`GET /sell-items` — **Public**

Returns items that are **paid, published and not expired**, newest payment first. Pending, unpublished and expired items never appear here.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer | No | How many items to return. Default 20, maximum 50. |
| `offset` | integer | No | How many items to skip. Default 0. |

Invalid values fall back to the defaults; see [Pagination](#26-pagination).

**Request**

```bash
curl "https://admin.zoshoponline.com/api/sell-items?limit=20&offset=0" \
  -H "Accept: application/json" \
  -H "X-App-Version: 1.0.0"
```

**Response `200`** — array of Item (public view) plus `meta`.

```json
{
  "data": [
    {
      "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
      "item_name": "Second-hand bike",
      "description": "Well maintained, single owner.",
      "location": "Paltan Bazaar, Guwahati",
      "seller_name": "Ravi",
      "contact_number": "9876543210",
      "price": "5000.00",
      "images": [
        { "id": 12, "url": "https://admin.zoshoponline.com/sell-corner/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/Qm3xV1.jpg" }
      ],
      "expires_at": "2026-12-12T10:15:30.000000Z",
      "created_at": "2026-11-12T10:10:02.000000Z"
    }
  ],
  "meta": { "limit": 20, "offset": 0, "total": 1 }
}
```

An empty list (or an `offset` past the end) is `200` with `"data": []`; `meta.total` still shows how many items exist.
---

### 5.2 Get item details

`GET /sell-items/{uuid}` — **Public**

| Path parameter | Description |
|---|---|
| `uuid` | The item's uuid. |

```bash
curl "https://admin.zoshoponline.com/api/sell-items/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47" \
  -H "Accept: application/json" -H "X-App-Version: 1.0.0"
```

**Response `200`**

```json
{
  "data": {
    "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
    "item_name": "Second-hand bike",
    "description": "Well maintained, single owner.",
    "location": "Paltan Bazaar, Guwahati",
    "seller_name": "Ravi",
    "contact_number": "9876543210",
    "price": "5000.00",
    "images": [
      { "id": 12, "url": "https://admin.zoshoponline.com/sell-corner/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/Qm3xV1.jpg" }
    ],
    "expires_at": "2026-12-12T10:15:30.000000Z",
    "created_at": "2026-11-12T10:10:02.000000Z"
  }
}
```

**Errors**

| Status | Reason |
|---|---|
| `404` | Unknown uuid, **or** the item is not public (pending, unpublished, expired, taken down by the team, or the seller's account was deleted). Sellers should use [`GET /my-sell-items`](#53-list-my-items) to see their own items in any state. |

---

### 5.3 List my items

`GET /my-sell-items` — **Login**

Returns **all** items of the logged-in customer, newest first, in every state (pending, paid, unpublished, expired). Not paginated.

```bash
curl "https://admin.zoshoponline.com/api/my-sell-items" \
  -H "Accept: application/json" -H "X-App-Version: 1.0.0" \
  -H "Authorization: Bearer <token>"
```

**Response `200`** — array of My item.

```json
{
  "data": [
    {
      "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
      "item_name": "Second-hand bike",
      "description": "Well maintained, single owner.",
      "location": "Paltan Bazaar, Guwahati",
      "seller_name": "Ravi",
      "contact_number": "9876543210",
      "price": "5000.00",
      "images": [],
      "expires_at": "2026-12-12T10:15:30.000000Z",
      "created_at": "2026-11-12T10:10:02.000000Z",
      "payment_status": "paid",
      "is_published": true,
      "paid_at": "2026-11-12T10:15:30.000000Z"
    }
  ]
}
```

To decide what to show for each item, see the [state table](#6-item-states-and-visibility).

**Errors:** `401` if not logged in.

---

### 5.4 Post a new item

`POST /sell-items` — **Login**

Creates the item with `payment_status = "pending"`. It is **not public** until it has been paid for (see [Pay](#510-pay-create-the-razorpay-order)).

The body is JSON and contains **text fields only**. Photos are added afterwards, one request per photo, with [5.6 Add an image](#56-add-an-image). An `images` field sent here is ignored.

**Body fields**

| Field | Type | Required | Rules |
|---|---|---|---|
| `item_name` | string | Yes | Max 255 characters. |
| `location` | string | Yes | Max 255 characters. Free text. |
| `seller_name` | string | Yes | Max 255 characters. |
| `description` | string | Yes | Max 5000 characters. |
| `contact_number` | string | Yes | Exactly 10 digits. |
| `price` | number | No | `0` to `99999999.99`. Omit or send `null` for "no price". |

Fields not listed here (e.g. `payment_status`, `expires_at`, `is_published`) are ignored.

**Request**

```bash
curl -X POST "https://admin.zoshoponline.com/api/sell-items" \
  -H "Accept: application/json" -H "Content-Type: application/json" -H "X-App-Version: 1.0.0" \
  -H "Authorization: Bearer <token>" \
  -d '{
        "item_name": "Second-hand bike",
        "location": "Paltan Bazaar, Guwahati",
        "seller_name": "Ravi",
        "description": "Well maintained, single owner.",
        "contact_number": "9876543210",
        "price": 5000
      }'
```

**Response `201`** — My item

```json
{
  "data": {
    "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
    "item_name": "Second-hand bike",
    "description": "Well maintained, single owner.",
    "location": "Paltan Bazaar, Guwahati",
    "seller_name": "Ravi",
    "contact_number": "9876543210",
    "price": "5000.00",
    "images": [],
    "expires_at": null,
    "created_at": "2026-11-12T10:10:02.000000Z",
    "payment_status": "pending",
    "is_published": true,
    "paid_at": null
  }
}
```

Keep the returned `uuid`. Next steps: upload the photos ([5.6](#56-add-an-image)), then [Pay](#510-pay-create-the-razorpay-order).

**Errors**

| Status | Reason |
|---|---|
| `401` | Not logged in. |
| `422` | Validation failed (missing field, `contact_number` not 10 digits, …). Nothing is saved. |
---

### 5.5 Edit an item

`PUT /sell-items/{uuid}` or `PATCH /sell-items/{uuid}` — **Login + owner**

Send JSON with **only the fields you want to change**. Works in any state (pending, paid, unpublished, expired). It **never** changes `payment_status`, `paid_at`, `expires_at` or `is_published`; those fields are ignored if sent.

**Body fields** (all optional; same rules as when posting)

| Field | Type | Rules |
|---|---|---|
| `item_name` | string | Not empty, max 255. |
| `location` | string | Not empty, max 255. |
| `seller_name` | string | Not empty, max 255. |
| `description` | string | Not empty, max 5000. |
| `contact_number` | string | Exactly 10 digits. |
| `price` | number \| null | `0` to `99999999.99`. Send `null` to remove the price. |

Images are managed with endpoints [5.6](#56-add-an-image) and [5.7](#57-remove-an-image).

```bash
curl -X PATCH "https://admin.zoshoponline.com/api/sell-items/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47" \
  -H "Accept: application/json" -H "Content-Type: application/json" -H "X-App-Version: 1.0.0" \
  -H "Authorization: Bearer <token>" \
  -d '{ "price": 4500, "description": "Price reduced." }'
```

**Response `200`** — My item (updated).

**Errors**

| Status | Reason |
|---|---|
| `401` | Not logged in. |
| `404` | Unknown uuid or not your item. |
| `422` | Validation failed. |

---

### 5.6 Add an image

`POST /sell-items/{uuid}/images` — **Login + owner**

Adds **one** photo to an existing item. Call it once per photo (up to 5 per item). The image is sent as **base64 text** inside a JSON body — not as a file upload.

**Body fields**

| Field | Type | Required | Description |
|---|---|---|---|
| `image` | string | Yes | The image as base64. Either the plain base64 string, or a data URI such as `data:image/jpeg;base64,/9j/4AAQ…`. |

**Image rules**

| Rule | Value |
|---|---|
| Size | At most **1 MB (1,048,576 bytes) of the decoded image**. The base64 text is about 1.37× longer, so a 1 MB photo is a request of roughly 1.4 MB. Compress on the device **before** encoding. |
| Type | JPEG, PNG or WebP. The type is detected from the image data itself; the `data:image/...` prefix is not trusted. SVG, GIF, PDF and everything else are rejected. |
| Count | At most 5 images per item. |
| Encoding | Standard base64 with no URL-safe characters. Line breaks inside the string are tolerated. |

Dart example:

```dart
final bytes = await file.readAsBytes();               // already compressed to <= 1 MB
final body  = jsonEncode({'image': base64Encode(bytes)});
```

**Request**

```bash
curl -X POST "https://admin.zoshoponline.com/api/sell-items/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/images" \
  -H "Accept: application/json" -H "Content-Type: application/json" -H "X-App-Version: 1.0.0" \
  -H "Authorization: Bearer <token>" \
  -d "{\"image\": \"$(base64 -w0 bike1.jpg)\"}"
```

(`base64 -w0` is the GNU/Linux form; on macOS use `base64 -i bike1.jpg`.)

**Response `200`** — My item, with the **full updated `images` array** (the new image is last). You can use it as the current state of the item.

```json
{
  "data": {
    "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
    "item_name": "Second-hand bike",
    "description": "Well maintained, single owner.",
    "location": "Paltan Bazaar, Guwahati",
    "seller_name": "Ravi",
    "contact_number": "9876543210",
    "price": "5000.00",
    "images": [
      { "id": 12, "url": "https://admin.zoshoponline.com/sell-corner/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/Qm3xV1.jpg" }
    ],
    "expires_at": null,
    "created_at": "2026-11-12T10:10:02.000000Z",
    "payment_status": "pending",
    "is_published": true,
    "paid_at": null
  }
}
```

Uploading several photos: send them one after another (or one at a time per user action). If photo 3 of 5 fails, photos 1 and 2 are already saved — retry only the failed one.

**Errors**

| Status | Reason |
|---|---|
| `401` | Not logged in. |
| `404` | Unknown uuid or not your item. |
| `422` | Validation failed. The message is under `errors.image`: `"The image field is required."` · `"The image must be a valid base64 string."` · `"The image must not be larger than 1 MB."` · `"The image must be a jpg, png or webp file."` · `"An item can have at most 5 images."`. Nothing is saved. |
| `413` | The web server rejected the request body as too large before the API saw it. Should not happen for images within the limit; report it to the backend team. |
---

### 5.7 Remove an image

`DELETE /sell-items/{uuid}/images/{imageId}` — **Login + owner**

| Path parameter | Description |
|---|---|
| `uuid` | The item's uuid. |
| `imageId` | The `id` of the image, from the item's `images` array. |

```bash
curl -X DELETE "https://admin.zoshoponline.com/api/sell-items/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/images/13" \
  -H "Accept: application/json" -H "X-App-Version: 1.0.0" \
  -H "Authorization: Bearer <token>"
```

**Response `200`** — My item, with the remaining `images`.

**Errors**

| Status | Reason |
|---|---|
| `401` | Not logged in. |
| `404` | Unknown uuid, not your item, or the image does not belong to this item. |

---

### 5.8 Publish

`POST /sell-items/{uuid}/publish` — **Login + owner** — no body.

Sets `is_published = true`. If the item is paid and not expired, it appears in the public list again. Safe to call repeatedly. Does **not** change the expiry date.

**Response `200`** — My item.

**Errors:** `401`, `404`.

---

### 5.9 Unpublish

`POST /sell-items/{uuid}/unpublish` — **Login + owner** — no body.

Sets `is_published = false`. The item immediately disappears from the public list and its public details return `404`. It stays in the seller's `GET /my-sell-items`. Safe to call repeatedly. The **expiry date keeps running** — unpublishing does not pause or extend it.

**Response `200`** — My item.

**Errors:** `401`, `404`.

---

### 5.10 Pay (create the Razorpay order)

`POST /sell-items/{uuid}/pay` — **Login + owner** — no body.

Prepares the ₹100 payment and returns what Razorpay Checkout needs. Call it every time the user taps "Pay" — if an order already exists for the item it is **returned again** (same `order_id`) instead of creating a new one, so retrying is safe and never double-charges.

```bash
curl -X POST "https://admin.zoshoponline.com/api/sell-items/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/pay" \
  -H "Accept: application/json" -H "X-App-Version: 1.0.0" \
  -H "Authorization: Bearer <token>"
```

**Response `200`** — Order

```json
{
  "data": {
    "order_id": "order_Pq7Xr1Lm9Ab2Cd",
    "amount": 10000,
    "currency": "INR",
    "key": "rzp_live_xxxxxxxxxxxxxx",
    "sell_item_uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47"
  }
}
```

Open Razorpay Checkout with `key`, `order_id`, `amount` and `currency` exactly as returned.

**Errors**

| Status | Reason |
|---|---|
| `401` | Not logged in. |
| `404` | Unknown uuid or not your item. |
| `409` | The item is already paid (do not open Checkout; refresh the item instead), **or** it is not available (`"This item is not available."`) because the team has taken it down — do not charge the user. |
| `5xx` | Razorpay could not be reached. Try again. |

---

### 5.11 Payment success

`POST /sell-items/{uuid}/payment-success` — **Login + owner**

Call this from the Razorpay `onSuccess` callback. The server checks that the payment really belongs to this item's order (using Razorpay's signature), then marks the item `paid` and sets `expires_at` to 30 days from now.

**Idempotent:** calling it again — or after Razorpay's own notification already marked the item paid — returns `200` with the unchanged item. The expiry is **not** extended.

**Body fields** (JSON; copy the values from Razorpay's success response without changing them)

| Field | Type | Required | Description |
|---|---|---|---|
| `razorpay_payment_id` | string | Yes | Payment id (`pay_...`). |
| `razorpay_order_id` | string | Yes | Order id (`order_...`); must be the one returned by Pay for this item. |
| `razorpay_signature` | string | Yes | Signature from Checkout. |

```bash
curl -X POST "https://admin.zoshoponline.com/api/sell-items/9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47/payment-success" \
  -H "Accept: application/json" -H "Content-Type: application/json" -H "X-App-Version: 1.0.0" \
  -H "Authorization: Bearer <token>" \
  -d '{
        "razorpay_payment_id": "pay_Qa1Bc2De3Fg4Hi",
        "razorpay_order_id": "order_Pq7Xr1Lm9Ab2Cd",
        "razorpay_signature": "9ef4dffbfd84f1318f6739a3ce19f9d85851857ae648f114332d8401e0949a3d"
      }'
```

**Response `200`** — My item, now paid

```json
{
  "data": {
    "uuid": "9f3c2b64-7d1e-4c52-a3f9-0e8b6a1d2c47",
    "item_name": "Second-hand bike",
    "description": "Well maintained, single owner.",
    "location": "Paltan Bazaar, Guwahati",
    "seller_name": "Ravi",
    "contact_number": "9876543210",
    "price": "5000.00",
    "images": [],
    "expires_at": "2026-12-12T10:15:30.000000Z",
    "created_at": "2026-11-12T10:10:02.000000Z",
    "payment_status": "paid",
    "is_published": true,
    "paid_at": "2026-11-12T10:15:30.000000Z"
  }
}
```

**Errors**

| Status | Reason |
|---|---|
| `401` | Not logged in. |
| `404` | Unknown uuid or not your item. |
| `422` | A field is missing; **or** `razorpay_order_id` is not this item's order (`errors.razorpay_order_id`); **or** the signature did not verify (`errors.razorpay_signature`: `"Payment verification failed."`). The item is left unchanged. |

If you get a `422` after Razorpay reported success, do not retry with different values. Refresh `GET /my-sell-items`: if the payment really went through, the item will already show `payment_status: "paid"` because the server is also notified by Razorpay directly.

---

## 6. Item states and visibility

The values come from `GET /my-sell-items`.

| `payment_status` | `is_published` | `expires_at` | State | Public list / details | What the seller can do |
|---|---|---|---|---|---|
| `pending` | any | `null` | Waiting for payment | Hidden | Pay, edit, manage images |
| `paid` | `true` | in the future | **Live** | **Visible** | Edit, manage images, unpublish |
| `paid` | `false` | in the future | Live but hidden by the seller | Hidden | Edit, manage images, publish |
| `paid` | any | in the past | Expired | Hidden | Edit only (cannot be renewed or paid again) |

An item is public only when all three are true: `payment_status = "paid"`, `is_published = true` and `expires_at` is in the future. This is enforced by the server — the app never has to filter. The team can also take an item down; it then behaves as if it were not public, whatever the fields above say, and nothing in the API responses reports it. A payment that was already made for such an item is still recorded.

Tip for the "My items" screen: derive the badge on the device — `pending` → "Payment pending" (show a Pay button); `paid` and `expires_at` in the past → "Expired"; `paid` and `is_published = false` → "Hidden"; otherwise "Live until {expires_at}".

---

## 7. Limits and rules

| Rule | Value |
|---|---|
| Listing fee | ₹100, always `amount: 10000` paise, decided by the server |
| Public period | 30 days from payment |
| Images per item | 5 |
| Image file | jpg / png / webp, max 1 MB **decoded**, sent as base64 (≈ 1.4 MB of text), one image per request |
| `item_name`, `location`, `seller_name` | Max 255 characters |
| `description` | Max 5000 characters |
| `contact_number` | Exactly 10 digits |
| `price` | `0` to `99999999.99`, optional |
| List window | `limit` default 20, max 50; `offset` default 0 |
| Not available | Deleting an item, renewing an expired item, searching / filtering the public list |
| Privacy | `contact_number` is shown to everyone who can see the item |
