api
Products API
- Base Path:
/api/v1/products - Auth: Endpoints marked "Auth: required" expect a valid
Authorizationheader. Public endpoints do not require auth.
Product Model
- _id: string (Mongo ObjectId)
- name: string
- description: string
- price: number
- category: string
- user: string (User ID)
- images: string[]
- quantity: number
- condition: 'New' | 'LIKE_NEW' | 'GOOD' | 'FAIR'
- game: string | null (ObjectId)
- leaderboard: string | null (ObjectId)
- fulfillment: string | null (ObjectId)
- tournament: string | null (ObjectId)
- country: string
- approvalStatus: 'PENDING' | 'APPROVED' | 'REJECTED'
- processingStatus: alias for
approvalStatus; provided for UI convenience - approvalUpdatedAt: string | null (ISO datetime of the last approval status change)
- rejectionReason: string (present when
approvalStatusisREJECTED) - createdAt: string (ISO datetime)
- updatedAt: string (ISO datetime)
- __v: number
Example Product document (as returned by GET /api/v1/products/:id with populated game and tournament):
{
"_id": "66d8c2c4e8d8b8a1f3a9d123",
"name": "Wireless Controller",
"description": "Pro-grade wireless controller with haptics",
"price": 5999,
"category": "ACCESSORY",
"user": "user_1234abcd",
"images": [
"https://cdn.example.com/p/ctrl-1.jpg",
"https://cdn.example.com/p/ctrl-2.jpg"
],
"quantity": 5,
"condition": "New",
"game": {
"_id": "66d8c2c4e8d8b8a1f3a9d999",
"name": "Space Dash",
"description": "Dodge and sprint in space!",
"tabcode": "spacedash",
"thumbnail": "https://cdn.example.com/games/spacedash.png",
},
"leaderboard": null,
"fulfillment": null,
"tournament": {
"_id": "66d8c2c4e8d8b8a1f3a9d777",
"status": "OPEN",
"startAt": "2025-09-04T10:00:00.000Z",
"endedAt": null,
"totalSeats": 100,
"numberOfPlayers": 12,
"expectedPlayers": 100
},
"createdAt": "2025-09-03T12:30:11.222Z",
"updatedAt": "2025-09-03T12:30:11.222Z",
"__v": 0
}
Note: In list and detail responses, game and tournament are populated objects (selected fields). Additional convenience fields are included in list responses; see below.
List Products
- Method: GET
- Path:
/api/v1/products - Auth: not required
- Query:
user: string (optional) — filter products by owner user ID.page: number (optional, default 1, min 1)limit: number (optional, default 20, max 100)count: boolean (optional, default false) — whentrue, responsemetaincludestotalCountandtotalPages.category: string (optional) — filter by a single category id.categories: string (optional) — comma‑separated category ids (e.g.,ACCESSORY,CONSOLE).entryFeeMin: number (optional) — minimum tournament entry fee (points).entryFeeMax: number (optional) — maximum tournament entry fee (points).progressMin: number (optional, 0–100) — minimum fill progress percent, computed as(numberOfPlayers / expectedPlayers) * 100.progressMax: number (optional, 0–100) — maximum fill progress percent.timeLeftMin: number (optional) — minimum time left before tournament end, in hours from now.timeLeftMax: number (optional) — maximum time left before tournament end, in hours from now.sort: 'newest' | 'oldest' | 'popular' | 'ending' (optional; default 'newest')newest: sort by productcreatedAtdescendingoldest: sort by productcreatedAtascendingpopular: sort bytournament.numberOfPlayersdescending; ties fall back to newestending: sort bytournament.endedAtascending; products withoutendedAtappear last
- Automatically excludes products whose
approvalStatusis notAPPROVED. - The caller must have a
country; results are limited to products with the samecountry. - Response 200: Array of product objects, with:
- Body shape:
\{ result: Product[], meta: \{ page, limit, totalCount?, totalPages? \} \} - Each Product includes:
- All fields from Product Model
game: populated Game object with fields:_id,name,description,tabcode,thumbnail,status(ornull)tournament: populated Tournament object includes_id,status,startAt,endedAt,totalSeats,numberOfPlayers,expectedPlayers,entryFee,rules(ornull)ownerVerified: booleanownerUsername: stringuser: string (always the user ID)country: stringapprovalStatus: 'PENDING' | 'APPROVED' | 'REJECTED'processingStatus: 'PENDING' | 'APPROVED' | 'REJECTED' (same value asapprovalStatus)rejectionReason: string (when rejected)approvalUpdatedAt: string | null
- Body shape:
Example 200 response (truncated):
{
"result": [
{
"_id": "66d8c2c4e8d8b8a1f3a9d123",
"name": "Wireless Controller",
"description": "Pro-grade wireless controller with haptics",
"price": 5999,
"category": "ACCESSORY",
"user": "user_1234abcd",
"images": ["https://cdn.example.com/p/ctrl-1.jpg"],
"quantity": 5,
"condition": "New",
"game": {
"_id": "66d8c2c4e8d8b8a1f3a9d999",
"name": "Space Dash",
"description": "Dodge and sprint in space!",
"tabcode": "spacedash",
"thumbnail": "https://cdn.example.com/games/spacedash.png",
"status": "PUBLISHED"
},
"leaderboard": null,
"fulfillment": null,
"tournament": {
"_id": "66d8c2c4e8d8b8a1f3a9d777",
"status": "OPEN",
"startAt": "2025-09-04T10:00:00.000Z",
"endedAt": null,
"totalSeats": 100,
"numberOfPlayers": 12,
"expectedPlayers": 100,
"entryFee": 10,
"rules": "No cheating; be respectful."
},
"createdAt": "2025-09-03T12:30:11.222Z",
"updatedAt": "2025-09-03T12:30:11.222Z",
"__v": 0,
"ownerVerified": true,
"ownerUsername": "john_doe"
}
], "meta": { "page": 1, "limit": 20, "totalCount": 123, "totalPages": 7 } }
- All fields from Product Model
ownerVerified: booleanownerUsername: stringuser: string (always the user ID)
Example 200 response item:
{
"_id": "66d8c2c4e8d8b8a1f3a9d123",
"name": "Wireless Controller",
"description": "Pro-grade wireless controller with haptics",
"price": 5999,
"category": "ACCESSORY",
"user": "user_1234abcd",
"images": ["https://cdn.example.com/p/ctrl-1.jpg"],
"game": {
"_id": "66d8c2c4e8d8b8a1f3a9d999",
"name": "Space Dash",
"description": "Dodge and sprint in space!",
"tabcode": "spacedash",
"thumbnail": "https://cdn.example.com/games/spacedash.png",
},
"leaderboard": null,
"fulfillment": null,
"tournament": {
"_id": "66d8c2c4e8d8b8a1f3a9d777",
"status": "OPEN",
"startAt": "2025-09-04T10:00:00.000Z",
"endedAt": null,
"totalSeats": 100,
"numberOfPlayers": 12,
"expectedPlayers": 100
},
"createdAt": "2025-09-03T12:30:11.222Z",
"updatedAt": "2025-09-03T12:30:11.222Z",
"__v": 0,
"ownerVerified": true,
"ownerUsername": "john_doe"
}
- Errors:
- 500: { "error": "Failed to fetch products" }
Filter examples
- Categories and entry fee range:
- GET
/api/v1/products?categories=ACCESSORY,CONSOLE&entryFeeMin=5&entryFeeMax=20
- GET
- Progress between 40% and 80%:
- GET
/api/v1/products?progressMin=40&progressMax=80
- GET
- Ending within the next 24 hours:
- GET
/api/v1/products?timeLeftMax=24&sort=ending
- GET
Get Product By ID
- Method: GET
- Path:
/api/v1/products/:id - Auth: required
- Path Params:
id: string (ObjectId)
- Request Body: none
- Response 200: A single Product document with
gameandtournamentpopulated as described above. NoownerVerified/ownerUsernameenrichment. - Errors:
- 401: { "error": "UNAUTHORIZED", "message": "Unauthorized access not allowed" }
- 404: { "error": "Product not found" }
- 500: { "error": "Failed to fetch product" }
List My Favourite Products
- Method: GET
- Path:
/api/v1/products/favorites - Auth: required (guests are not allowed)
- Request Body: none
- Query:
page: number (optional, default 1, min 1)limit: number (optional, default 20, max 100)count: boolean (optional, default false) — whentrue, responsemetaincludestotalCountandtotalPages.
- Response 200:
\{ result: Product[], meta: \{ page, limit, totalCount?, totalPages? \} \}where each Product matches List Products (populatedgame,tournament,ownerVerified,ownerUsername, andusernormalized to ID). May be an emptyresultarray if no favourites. - Errors:
- 401: { "error": "Unauthorized", "message": "Login required" }
- 500: { "error": "Failed to fetch favourite products" }
Mark Product as Favourite
- Method: POST
- Path:
/api/v1/products/:id/favorite - Auth: required (guests are not allowed)
- Path Params:
id: string (ObjectId)
- Request Body: none
- Response 200: { "ok": true }
- Errors:
- 400: { "error": "Invalid product id" }
- 401: { "error": "Unauthorized", "message": "Login required" }
- 404: { "error": "Product not found" }
- 500: { "error": "Failed to mark favourite" }
Unmark Product as Favourite
- Method: DELETE
- Path:
/api/v1/products/:id/favorite - Auth: required (guests are not allowed)
- Path Params:
id: string (ObjectId)
- Request Body: none
- Response 200: { "ok": true }
- Errors:
- 400: { "error": "Invalid product id" }
- 401: { "error": "Unauthorized", "message": "Login required" }
- 500: { "error": "Failed to unmark favourite" }
Notes
- Normalization in lists: For list and favourites endpoints, the
userfield is always returned as the owner’s ID (string). The extra fieldsownerVerified(boolean) andownerUsername(string) are also included. - IDs: All ObjectIds are returned as strings.
- Empty arrays: When no favourites exist,
/favoritesreturns[].
Categories
- Base Path:
/api/admin/categories - Auth: Admin
- List Categories
- Method: GET
- Path:
/api/admin/categories - Response 200:
[\{ _id: string, name: string, active: boolean, createdAt, updatedAt \}]
- See also:
docs/categories-api.mdfor full admin categories API details (create, model notes).