---
title: Marketplace API
tags: [api, marketplace, reference]
---

# Marketplace API

> **Last updated:** 2026-05-31 — request-template delivery mode and payment rail validation updated.

All marketplace endpoints live under `/api/marketplace/*`. The router is composed of several files mounted from `app.ts`:

- New controller-pattern routes: [`backend/src/services/marketplace/controllerRoutes.ts`](../../backend/src/services/marketplace/controllerRoutes.ts) (`marketplaceControllerRouter`)
- Legacy + delivery-code routes: [`backend/src/services/marketplace/routes.ts`](../../backend/src/services/marketplace/routes.ts) (`marketplaceRouter`)
- Request templates: [`requestTemplateRoutes.ts`](../../backend/src/services/marketplace/requestTemplateRoutes.ts) (`/api/marketplace/request-templates/*`)
- Shop settings: [`shopSettingsRoutes.ts`](../../backend/src/services/marketplace/shopSettingsRoutes.ts) (`/api/marketplace/shop/*`)
- Reviews: [`reviewRoutes.ts`](../../backend/src/services/marketplace/reviewRoutes.ts) (`/api/marketplace/reviews/*`)

Core models: [[PurchaseRequest]], [[SellerOffer]], [[Category]], [[RequestTemplate]], [[ShopSettings]], [[Review]]. Real-time updates are emitted on the `request-<id>`, `seller-<id>`, `buyer-<id>`, and `sellers`/`buyers` rooms — see [[Socket Events]].

## Categories

### GET /api/marketplace/categories

**Description:** Flat list of all categories.
**Auth required:** No
**Response 200:** `{ success: true, data: [Category, ...] }`

### GET /api/marketplace/categories/tree

**Description:** Nested tree based on `parentId`.
**Auth required:** No

### GET /api/marketplace/categories/:id

**Description:** Single category by id.
**Auth required:** No
**Errors:** `404` not found.

## Sellers

### GET /api/marketplace/sellers

**Description:** Public seller directory (users with `role: 'seller'`). Includes aggregated stats (completed orders, ratings).
**Auth required:** No

### GET /api/marketplace/request-templates/sellers

**Description:** Sellers who currently expose at least one active [[RequestTemplate]] (used by the shop discovery page).
**Auth required:** No

### GET /api/marketplace/request-templates/sellers/:sellerId

**Description:** Public shop profile for a seller, including their templates and shop settings.
**Auth required:** No

## Purchase Requests

The buyer-facing CRUD plus seller-side workflow endpoints. Model: [[PurchaseRequest]].

### POST /api/marketplace/purchase-requests

**Description:** Create a purchase request (new controller path).
**Auth required:** Bearer JWT (buyer)
**Request body:**
```ts
{
  title: string;
  description: string;
  categoryId: string;
  productLink?: string;
  size?: string;
  color?: string;
  quantity?: number;          // default 1
  budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" | "USDT" | "USDC" };
  urgency?: "low" | "medium" | "high" | "urgent";
  deliveryInfo?: {
    deliveryType: "physical" | "online";
    addressId?: string;       // when physical
    email?: string;           // when online
  };
  preferredSellerIds?: string[];
  attachments?: string[];     // URLs from [[File API]]
}
```
**Response 201:** `{ success, data: { purchaseRequest } }`
**Side effects:** Emits `new-purchase-request` to the `sellers` room (broadcast).
**Source:** `marketplaceController.createPurchaseRequest`

### POST /api/marketplace/purchase-requests/bulk

**Description:** Bulk creation (used by the template checkout flow).
**Auth required:** Bearer JWT
**Request body:** `{ items: Array<PurchaseRequestInput> }`

### GET /api/marketplace/purchase-requests

**Description:** List requests with filters. Buyers see their own; sellers see ones routed to them; admins see all.
**Auth required:** Bearer JWT
**Query params:** `status`, `categoryId`, `urgency`, `search`, `page`, `limit`, `sortBy`, `sortOrder`

> **Note:** Use query params on this endpoint for filtering/searching. The separate search and stats endpoints documented in earlier versions do not exist — see below.

### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/purchase-requests/search

This endpoint does not exist. Use query params (`search`, `status`, `categoryId`, etc.) on `GET /api/marketplace/purchase-requests` instead.

### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/purchase-requests/stats

This endpoint does not exist in the backend.

### GET /api/marketplace/purchase-requests/my

**Description:** Shortcut for the caller's own purchase requests.
**Auth required:** Bearer JWT

### GET /api/marketplace/purchase-requests/:id

**Description:** Single request with populated category and selected offer.
**Auth required:** No (public read for shareable links)
**Errors:** `404` not found.

### PATCH /api/marketplace/purchase-requests/:id

**Description:** Buyer edits draft / pending request fields.
**Auth required:** Bearer JWT (owner)

> ⚠️ **KNOWN BUG:** The frontend sends `PUT` but the backend registers `PATCH`. Requests from clients using `PUT` will receive `404`. Use `PATCH`.

### PATCH /api/marketplace/purchase-requests/:id/status

**Description:** Transition the request status (`draft` → `pending` → `payment` → `processing` → `delivery` → `delivered` → `seller_paid` → `completed`, or `cancelled`).
**Auth required:** Bearer JWT (owner or admin)
**Request body:** `{ status: string }`
**Side effects:** Emits `purchase-request-update` to `request-<id>`.

### DELETE /api/marketplace/purchase-requests/:id

**Description:** Cancel/delete a request that has no committed payment.
**Auth required:** Bearer JWT (owner)

### GET /api/marketplace/purchase-requests/:id/workflow-steps

**Description:** Returns the ordered workflow steps + current pointer used by the frontend stepper.
**Auth required:** No

### GET /api/marketplace/purchase-requests/:id/payment-status

**Description:** Returns the latest payment + escrow state for the request.
**Auth required:** Bearer JWT

### POST /api/marketplace/purchase-requests/:id/sync-payment-status

**Description:** Force a re-check against the payment provider ([[Payment API]]).
**Auth required:** Bearer JWT

### POST /api/marketplace/purchase-requests/:id/confirm-payment

**Description:** Legacy buyer-confirmation endpoint kept for old clients.
**Auth required:** Bearer JWT

### POST /api/marketplace/purchase-requests/:id/release-payment

**Description:** Triggers admin escrow release (mirror of `POST /api/payment/shkeeper/:id/release`).
**Auth required:** Bearer JWT (admin)

### PUT /api/marketplace/purchase-requests/:id/delivery (controller route)

**Description:** Seller submits shipping details (carrier, tracking number, expected date).
**Auth required:** Bearer JWT (selected seller)

### POST /api/marketplace/purchase-requests/:id/update-delivery (legacy)

**Description:** Older equivalent retained for compatibility.

### POST /api/marketplace/requests/:id/start-delivery

**Description:** Marks the request as `delivery` and notifies the buyer.
**Auth required:** Bearer JWT (seller)
**Side effects:** Emits `purchase-request-update` event.

### PATCH /api/marketplace/purchase-requests/:id/confirm-delivery

**Description:** Buyer confirms goods received (transitions to `delivered`).
**Auth required:** Bearer JWT (buyer)

### POST /api/marketplace/purchase-requests/:id/final-approval

**Description:** Buyer's final approval that releases escrow to the seller.
**Auth required:** Bearer JWT (buyer)

### GET /api/marketplace/buyers/:buyerId/purchase-requests

**Description:** Admin/seller view of a buyer's request history.
**Auth required:** Bearer JWT

## Delivery codes

Six-digit codes the buyer hands to the seller at handover. Backed by `deliveryService`.

### POST /api/marketplace/purchase-requests/:id/delivery-code/generate

**Description:** Buyer generates a delivery code. Request must be in `delivery` status.
**Auth required:** Bearer JWT (buyer)
**Response 200:** `{ success: true, data: { deliveryCode: "123456" } }`
**Errors:** `400` wrong status, `403` not buyer, `404` request not found.
**Side effects:** Emits `delivery-code-generated` on `request-<id>`.

### POST /api/marketplace/purchase-requests/:id/delivery-code/verify

**Description:** Seller verifies the code. On success the status moves to `delivered`.
**Auth required:** Bearer JWT (selected seller)
**Request body:** `{ code: string }` (exactly 6 chars)
**Errors:** `400` bad code, `403` not the selected seller.
**Side effects:** Emits `delivery-confirmed` and `buyer-confirmed-delivery`.

### GET /api/marketplace/purchase-requests/:id/delivery-code

**Description:** Buyer or seller fetches the current code metadata.
**Auth required:** Bearer JWT

### GET /api/marketplace/purchase-requests/:id/delivery-code/status

**Description:** Returns `{ isValid, hasDeliveryCode, ... }` so the UI can decide whether to show "regenerate".
**Auth required:** Bearer JWT

## Seller Offers

Model: [[SellerOffer]].

Valid `status` values: `pending | accepted | rejected | withdrawn`

> **Note:** The status value `active` does not exist on SellerOffer. Earlier docs were incorrect.

### POST /api/marketplace/purchase-requests/:id/offers

**Description:** Submit an offer against the purchase request identified by `:id` in the path. The purchase request must be in `pending`, `received_offers`, or `active` status.
**Auth required:** Bearer JWT (seller)
**Path param:** `:id` — the `purchaseRequestId` (not a body field)
**Request body:**
```ts
{
  price: { amount: number; currency: "USDT" };  // USDT only for escrow MVP
  deliveryEstimate: { days: number; note?: string };
  notes?: string;
  attachments?: string[];
}
```
**Response 201:** `{ success, data: { offer } }`
**Side effects:** Emits `new-offer` to `buyer-<buyerId>` and `seller-offer-update` to `seller-<sellerId>`.

> **Note:** Currency is locked to `USDT` for the escrow MVP (commit 3aaa2fe). The frontend `CURRENCY_SYMBOLS` map in `src/sections/request/constants.ts` exposes only `USDT`.

### PUT /api/marketplace/purchase-requests/:id/offers (legacy)

**Description:** Older offer-update endpoint kept for compatibility.

### GET /api/marketplace/purchase-requests/:id/offers

**Description:** List all offers for a request.
**Auth required:** No (buyers and prospective sellers compare offers)

### GET /api/marketplace/purchase-requests/:id/has-offer

**Description:** Returns `{ hasOffer: boolean }` for the caller (seller-side helper).
**Auth required:** Bearer JWT (seller)

### GET /api/marketplace/purchase-requests/:id/offers/:sellerId

**Description:** Fetch a specific seller's offer on a request.
**Auth required:** No

### ⚠️ NOT IMPLEMENTED: GET /api/marketplace/offers/request/:requestId

This endpoint does not exist. Use `GET /api/marketplace/purchase-requests/:id/offers` instead.

### GET /api/marketplace/offers/seller/:sellerId

**Description:** Returns all offers submitted by the given seller, across all purchase requests. Used by the Offer Management dashboard page (`/dashboard/seller/marketplace/offers`).
**Auth required:** Bearer JWT (seller, own `:sellerId` only)
**Response 200:** `{ data: [SellerOffer, ...] }`
**Frontend action:** `getSellerOffers(sellerId)` in `src/actions/marketplace.ts` (added commit 240a668)

### PATCH /api/marketplace/offers/:id

**Description:** Seller edits their pending offer (price, delivery estimate, notes).
**Auth required:** Bearer JWT (offer owner)

> ✅ **Fixed (commit 240a668):** The frontend `updateOffer` and `acceptOffer` actions now correctly send `PATCH`.

### DELETE /api/marketplace/offers/:id

**Description:** Seller withdraws their offer.
**Auth required:** Bearer JWT (offer owner)

### PUT /api/marketplace/offers/:id/status

**Description:** Direct status mutation (admin override / counter-offer states). This is also the correct way to withdraw an offer programmatically — send `{ status: 'withdrawn' }`.
**Auth required:** Bearer JWT
**Request body:** `{ status: "pending" | "accepted" | "rejected" | "withdrawn" }`

### POST /api/marketplace/offers/:id/withdraw

**Description:** Seller withdraws their offer. Sets offer status to `withdrawn` using `sellerOfferService.withdrawOffer()`. Only the offer owner may call this.
**Auth required:** Bearer JWT (offer owner)
**Response 200:** `{ success: true, data: { /* updated offer */ } }`
**Errors:** `403` not the offer owner, `404` offer not found.

> **Note:** This endpoint was previously documented as NOT IMPLEMENTED. It was added to `backend/src/services/marketplace/routes.ts` (commit `3e47713`).

### POST /api/marketplace/purchase-requests/:id/select-offer

**Description:** Buyer selects/accepts an offer; this triggers payment intent creation in [[Payment API]] and rejects all other offers automatically once payment lands.
**Auth required:** Bearer JWT (buyer)
**Request body:** `{ offerId: string }`
**Side effects:**
- Persists `selectedOfferId` on [[PurchaseRequest]] (commit `023255f` — previously this field was not saved, causing it to be lost). Status moves toward `payment`.
- Rejects all **losing** offers (sets their status to `rejected`) when payment is confirmed (commit `023255f`).
- Emits `seller-offer-update` to all sellers for the request.

### POST /api/marketplace/offers/:id/accept (legacy)

**Description:** Older synonym retained for backward compatibility with old clients.

### DELETE /api/marketplace/offers/:id (controller route)

**Description:** Controller-pattern delete that also notifies the buyer.

## Request Templates

A [[RequestTemplate]] is a re-usable "shop product" a seller can publish. Buyers convert templates into actual purchase requests via the shareable link, individually or in bulk (cart checkout).

### POST /api/marketplace/request-templates

**Description:** Create a new template.
**Auth required:** Bearer JWT (seller)
**Request body:**
```ts
{
  title: string;                       // 1-200 chars
  description: string;                 // 1-2000
  categoryId: string;                  // MongoId
  productLink?: string;                // valid URL
  size?: string;                       // <=100
  color?: string;                      // <=100
  quantity?: number;                   // 1-10000
  budget?: { min?: number; max?: number; currency: "USD" | "EUR" | "IRR" | "USDT" | "USDC" };
  urgency?: "low" | "medium" | "high" | "urgent";
  deliveryInfo?: {
    deliveryType: "physical" | "online"; // seller-selected; buyer cannot override at checkout
    notes?: string;
    email?: string;                      // optional legacy field; empty string is accepted
  };
  paymentConfig?: {
    useShopDefault: boolean;             // false = template override, true = shop defaults
    allowedChains: number[];             // at least one positive chain id when paymentConfig is sent
    allowedTokens: string[];             // at least one non-empty token symbol when paymentConfig is sent
  };
  maxUsage?: number | null;            // 0/null = unlimited
  expiresAt?: string | null;           // ISO date
  images?: string[];                   // URLs from [[File API]]
}
```
**Response 201:** `{ data: { template } }` with a generated `shareableLink`.
**Validation:** If `paymentConfig` is present, both `allowedChains` and `allowedTokens` must be non-empty. The UI now defaults new templates to explicit template rails, so a seller must choose at least one chain and one token before publishing.

### GET /api/marketplace/request-templates

**Description:** Paginated list of the caller's templates.
**Auth required:** Bearer JWT (seller)
**Query params:** `page`, `limit` (1-100), `isActive`, `categoryId`, `search`

### GET /api/marketplace/request-templates/stats

**Description:** Aggregate counts of the caller's templates (active, expired, usage).
**Auth required:** Bearer JWT (seller)

### GET /api/marketplace/request-templates/:id

**Description:** Full template by id (owner view).
**Auth required:** Bearer JWT (owner)

### PUT /api/marketplace/request-templates/:id

**Description:** Update the template. Same body as create.
**Auth required:** Bearer JWT (owner)

### DELETE /api/marketplace/request-templates/:id

**Description:** Delete a template.
**Auth required:** Bearer JWT (owner)

### PATCH /api/marketplace/request-templates/:id/toggle-status

**Description:** Toggle `isActive`.
**Auth required:** Bearer JWT (owner)

### GET /api/marketplace/request-templates/public/:shareableLink

**Description:** Public read of a template via its shareable slug. Used by the shop preview page.
**Auth required:** No
**Errors:** `404` link not found or template inactive/expired.

### POST /api/marketplace/request-templates/:shareableLink/convert

**Description:** Buyer converts the template into a real [[PurchaseRequest]].
**Auth required:** Bearer JWT (buyer)
**Request body:** Overrides (quantity, address, etc.)
**Response 201:** `{ data: { purchaseRequest } }`

### POST /api/marketplace/request-templates/batch-convert

**Description:** Convert several templates at once (cart checkout). The seller's template delivery mode is preserved; buyer-supplied checkout details are only overlaid where that mode requires them.
**Auth required:** Bearer JWT (buyer)
**Request body:**
```ts
{
  items: Array<{
    shareableLink: string;
    quantity: number;        // 1-100
    sellerId: string;        // MongoId
  }>;
  status?: "pending" | "pending_payment" | "active";
  paymentConfirmed?: boolean;
  paymentData?: Record<string, unknown>;
  deliveryInfo?: {
    email?: string;          // copied to generated online requests
    billing?: {
      name?: string;
      phoneNumber?: string;
      address?: string;
      city?: string;
      state?: string;
      country?: string;
      zipCode?: string;
      addressType?: string;
      fullAddress?: string;  // copied to generated physical requests
    };
  };
}
```
**Delivery mapping:** `online` templates use `deliveryInfo.email`; `physical` templates use `deliveryInfo.billing` to fill `deliveryInfo.address` and `deliveryInfo.deliveryAddress` on the generated [[PurchaseRequest]].

### POST /api/marketplace/request-templates/complete-payment

**Description:** Marks the requests created by `batch-convert` as paid (called after a successful checkout).
**Auth required:** Bearer JWT
**Request body:**
```ts
{
  requestIds: string[];      // 1+ MongoIds
  newStatus?: "pending" | "active" | "processing";
  paymentData?: Record<string, unknown>;
}
```

## Shop Settings

Per-seller storefront preferences. Model: [[ShopSettings]].

### GET /api/marketplace/shop/settings/:sellerId

**Description:** Public shop settings for the given seller (used by the shop landing page).
**Auth required:** No
**Response 200:** `{ data: ShopSettings }`

### GET /api/marketplace/shop/settings

**Description:** The authenticated seller's own settings.
**Auth required:** Bearer JWT (seller)

### PUT /api/marketplace/shop/settings

**Description:** Update shop settings (banner, bio, policies, `allowSellerReviews`, `allowTemplateReviews`, ...).
**Auth required:** Bearer JWT (seller)

## Reviews

Model: [[Review]]. Reviews can target a seller or a template. Subject must be `seller` or `template`.

### GET /api/marketplace/reviews/:subjectType/:subjectId

**Description:** Published reviews + aggregate stats. Honours `allowSellerReviews` / `allowTemplateReviews` from [[ShopSettings]].
**Auth required:** No
**Query params:** `page` (default 1), `limit` (default 10)
**Response 200:**
```json
{
  "data": [Review, ...],
  "pagination": { "page": 1, "limit": 10, "total": 42, "pages": 5 },
  "stats": { "count": 42, "avg": 4.6, "one": 1, "two": 0, "three": 3, "four": 10, "five": 28 }
}
```
**Errors:** `400` bad subjectType / invalid id, `403` reviews disabled by seller.

### GET /api/marketplace/reviews/:subjectType/:subjectId/summary

**Description:** Stats only (no review list).
**Auth required:** No

### POST /api/marketplace/reviews

**Description:** Submit a review. The server computes `isVerifiedBuyer` when `purchaseRequestId` is given and the request is in a terminal state (`delivery`, `delivered`, `seller_paid`, `completed`).
**Auth required:** Bearer JWT
**Request body:**
```ts
{
  subjectType: "seller" | "template";
  subjectId: string;
  rating: 1 | 2 | 3 | 4 | 5;
  comment?: string;
  purchaseRequestId?: string;
}
```
**Errors:** `400` validation, `403` reviews disabled, `404` template not found, `409` duplicate review.
**Response 201:** `{ data: Review, stats: { ... } }`

## Payments (legacy under marketplace)

These routes are duplicates of the main [[Payment API]] kept under `/api/marketplace/payments/*` for backward compatibility with the early frontend. Prefer the canonical endpoints.

### POST /api/marketplace/payments
### GET /api/marketplace/payments
### GET /api/marketplace/payments/:paymentId
### PATCH /api/marketplace/payments/:paymentId

See [[Payment API]] for the canonical descriptions.

## Verify Web3 payment (legacy)

### POST /api/marketplace/payments/verify

**Description:** Legacy Web3 verification endpoint that records a transaction and moves the purchase request to `processing`. Modern flows use `POST /api/payment/shkeeper/confirm-transaction` instead.
**Auth required:** Bearer JWT
**Request body:**
```ts
{
  purchaseRequestId: string;
  sellerOfferId: string;
  buyerId: string;
  sellerId: string;
  amount: number;
  currency: "USDT" | string;
  paymentHash: string;
  paymentMethod?: string;
  token?: string;
  network?: string;
  escrowWalletAddress?: string;
  metadata?: Record<string, unknown>;
}
```
**Side effects:** Creates a [[Payment]] record, updates [[PurchaseRequest]] status, emits `payment-received` to `user-<sellerId>`.

## Real-time events

Most marketplace mutations fan out via `global.io` to the rooms below — see [[Socket Events]] for payloads:

- `purchase-request-update` → `request-<id>`
- `new-purchase-request` → `sellers`
- `new-offer` → `buyer-<buyerId>`
- `seller-offer-update` → `seller-<sellerId>` (and global on payment confirm)
- `delivery-code-generated` / `delivery-confirmed` / `delivery-update` → `request-<id>`
- `request-cancelled` → `user-<buyerId>`, `user-<sellerId>`
- `transaction-completed` → `user-<buyerId>`, `user-<sellerId>`

## Related

- [[Purchase Request Flow]]
- [[Seller Offer Flow]]
- [[Template Checkout Flow]]
- [[Delivery Code Flow]]
