Ecom API Reference
Base URL: /api. All request/response bodies are JSON.
Users (Unified auth)
DELETE /api/users/:id JWT
Deletes the user and related Seller/Buyer records. Only the same user or an admin can perform this.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — User ID
- Body: none
Responses
- 200:
{ "message": "User and related records deleted" }
- 401:
{ "message": "Unauthorized" }
- 403:
{ "message": "Forbidden" }
- 404:
{ "message": "User not found" }
- 500:
{ "message": "Error deleting user", "error": "..." }
POST /api/users/resend-otp Public
Resends a new verification code (buyers or sellers).
Request body
{
"email": "user@example.com"
}
Responses
- 200:
{ "message": "OTP resent to buyer/seller email" }
- 400 | 404 | 200 (already verified): same as send-otp
POST /api/users/verify-email Public
Verifies the code, marks emailVerified, ensures a User exists, and returns JWT.
Request body
{
"email": "user@example.com",
"otp": "123456"
}
Responses
- 200:
{ message, token, user: { id, username, email, role } }
- 400:
email and otp required | No OTP pending | OTP expired | Invalid OTP
- 404:
Account not found
- 429:
Too many attempts. Request a new OTP
- 200 (already verified):
{ "message": "Email already verified" }
POST /api/users/login Public
Universal login for buyer, seller, and admin. Role is resolved from User.
Request body
{
"username": "username-or-email",
"password": "secret"
}
Responses
- 200:
{ message, token, user: { id, username, email, role } }
- 400/401/404:
User not found | Invalid password | All fields required
- 403 (buyer/seller):
Email not verified
- 403 (seller):
Seller not approved yet
Products
GET /api/products Public
Lists all products. Supports filtering by category. Admins can see soft-deleted products as well.
Request
- No authentication required for basic listing
- Query Parameters (optional):
category — Filter products by category (e.g., ?category=Mobile Phones & Tablets)
- Admins can include deleted products in results
Examples
GET /api/products — Get all products
GET /api/products?category=Mobile Phones & Tablets — Get only Mobile Phones & Tablets
GET /api/products?category=Computers & Laptops — Get only Computers & Laptops
Responses
- 200:
{ "success": true, "count": number, "data": Product[] }
- 500:
{ "success": false, "message": "Error retrieving products", "error": "..." }
GET /api/products/:id Public
Get a specific product by ID.
Request
- Params:
id — Product ID (required)
- Admins can retrieve soft-deleted products
Responses
- 200:
{ "success": true, "data": Product }
- 404:
{ "success": false, "message": "Product not found" }
- 500:
{ "success": false, "message": "Error retrieving product", "error": "..." }
POST /api/products JWT (Seller/Admin)
Creates a new product. Only authenticated sellers or admins can create products.
Request
- Headers:
Authorization: Bearer <token>
- Body (JSON):
{
"name": "string (required, min 3, max 100 chars)",
"description": "string (required, min 10 chars)",
"category": "string (required)",
"condition": "string (optional, enum: ['new', 'used', 'refurbished'], default: 'new')",
"price": "number (required, > 0)",
"images": "string[] (optional, array of image URLs)",
"ownerId": "number (optional, admin only - for assigning product to specific seller)",
"trialPolicy": { // optional trial policy configuration
"trialDays": "number (> 0)", // length of trial in days
"penaltyType": "string", // e.g. 'flat' or 'percentage'
"penaltyValue": "number (>= 0)", // penalty amount
"returnWindowHours": "number (> 0)" // allowed return window in hours
}
}
Responses
- 201:
{ "success": true, "data": Product }
- 400:
{ "success": false, "message": "Validation error", "errors": [...] }
- 401:
{ "success": false, "message": "Unauthorized" }
- 403:
{ "success": false, "message": "Only sellers and admins can create products" }
- 500:
{ "success": false, "message": "Error creating product", "error": "..." }
PATCH /api/products/:id JWT (Owner/Admin)
Partially updates an existing product. Only the product owner or admin can update it.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Product ID to update (required)
- Body (JSON, all fields optional except for the ones being updated):
{
"name": "string (min 3, max 100 chars)",
"description": "string (min 10 chars)",
"category": "string",
"condition": "string (enum: ['new', 'used', 'refurbished'])",
"price": "number (> 0)",
"images": "string[] (array of image URLs)",
"ownerId": "number (admin only - for transferring product ownership)",
"trialPolicy": { // optional create/update of trial policy
"trialDays": "number (> 0)",
"penaltyValue": "number (>= 0)",
"returnWindowHours": "number (> 0)"
}
}
Responses
- 200:
{ "success": true, "data": { product: UpdatedProduct, trialPolicy: TrialPolicy | null } }
- 400:
{ "success": false, "message": "Validation error", "errors": [...] }
- 401:
{ "success": false, "message": "Unauthorized" }
- 403:
{ "success": false, "message": "Not authorized to update this product" }
- 404:
{ "success": false, "message": "Product not found" }
- 500:
{ "success": false, "message": "Error updating product", "error": "..." }
DELETE /api/products/:id JWT (Owner/Admin)
Soft deletes a product. Only the product owner or admin can delete it. The product is not actually removed from the database but marked as deleted.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Product ID to delete (required)
Responses
- 200:
{ "success": true, "message": "Product deleted successfully" }
- 401:
{ "success": false, "message": "Unauthorized" }
- 403:
{ "success": false, "message": "Not authorized to delete this product" }
- 404:
{ "success": false, "message": "Product not found" }
- 500:
{ "success": false, "message": "Error deleting product", "error": "..." }
Trial Policies
POST /api/products/:id/trial-policy JWT (Seller/Admin)
Creates a trial policy for a product. Only authenticated sellers or admins can create trial policies.
Request
Responses
- 201:
{ "message": "Trial policy created successfully", "trialPolicy": TrialPolicy }
- 400:
{ "errors": [...] } | { "error": "Product not found" } | { "error": "Trial policy already exists for this product" }
- 401:
{ "message": "Unauthorized" }
- 500:
{ "error": "Internal server error" }
PATCH /api/products/:id/trial-policy JWT (Seller/Admin)
Updates an existing trial policy. Only the product owner or admin can update it. All fields are optional.
Request
Responses
- 200:
{ "message": "Trial policy updated successfully", "trialPolicy": TrialPolicy }
- 400:
{ "errors": [...] }
- 401:
{ "message": "Unauthorized" }
- 404:
{ "error": "Trial policy not found for this product" }
- 500:
{ "error": "Internal server error" }
DELETE /api/products/:id/trial-policy JWT (Seller/Admin)
Soft deletes/disables a trial policy. Only the product owner or admin can delete it.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Product ID (required)
Responses
- 200:
{ "message": "Trial policy disabled successfully" }
- 401:
{ "message": "Unauthorized" }
- 404:
{ "error": "Trial policy not found for this product" }
- 500:
{ "error": "Internal server error" }
GET /api/products/:id/trial-policy Public
Gets the trial policy for a specific product.
Request
- Params:
id — Product ID (required)
Responses
- 200:
{ "trialPolicy": TrialPolicy }
- 404:
{ "error": "Trial policy not found for this product" }
- 500:
{ "error": "Internal server error" }
Buyers
POST /api/buyers/register Public
Registers a buyer with a profile picture and sends OTP.
Request
- Content-Type:
multipart/form-data
- Form fields:
firstName: "John"
lastName: "Doe"
username: "johnd"
email: "john@example.com"
phoneNumber: "+1000000000"
password: "secret"
profilePic: <file> // required image file field
Responses
- 201:
{ message, buyer: { id, firstName, lastName, username, email, phoneNumber, emailVerified, profilePic } }
- 400:
All fields required | Profile picture required | Username/Email already exists
- 500:
Error registering buyer
GET /api/buyers JWT
List buyers.
GET /api/buyers/:id JWT
Get buyer by id.
PATCH /api/buyers/:id JWT (self/admin)
Update buyer fields.
DELETE /api/buyers/:id JWT (self/admin)
Delete buyer.
Sellers
POST /api/sellers/register Public
Registers a seller with license and profile picture uploads, then sends OTP. Login requires emailVerified=true and approved=true.
Request
- Content-Type:
multipart/form-data
- Form fields:
firstName: "Acme"
lastName: "Corp"
username: "acme"
email: "acme@example.com"
password: "secret"
address: "123 Main St"
phoneNumber: "+1000000000" // optional
storeName: "Acme Store" // optional
license: <file> // required image file field
profilePic: <file> // required image file field
Responses
- 201:
{ message, seller: { id, firstName, lastName, phoneNumber, storeName, username, email, address, approved, emailVerified, profilePic, license } }
- 400:
License image required | Profile image required | Username already taken
- 500:
Error registering seller
GET /api/sellers JWT (admin)
Get all sellers. Admin only.
Request
- Headers:
Authorization: Bearer <token>
Responses
- 200:
{ "message": "Sellers retrieved successfully", "sellers": [...] }
- 401:
{ "message": "Unauthorized" }
- 403:
{ "message": "Forbidden" }
- 500:
{ "message": "Error retrieving sellers", "error": "..." }
GET /api/sellers/:id JWT
Get seller by ID. Authenticated users only.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Seller ID
Responses
- 200:
{ "message": "Seller retrieved successfully", "seller": { id, firstName, lastName, phoneNumber, storeName, username, email, address, license, approved, emailVerified, profilePic } }
- 401:
{ "message": "Unauthorized" }
- 404:
{ "message": "Seller not found" }
- 500:
{ "message": "Error retrieving seller", "error": "..." }
PUT /api/sellers/:id JWT
Update seller information. Authenticated users only.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Seller ID
Request body (all fields optional)
{
"firstName": "Updated Name",
"lastName": "Updated Last",
"email": "updated@example.com",
"address": "Updated Address",
"license": "NEW-LIC-123",
"approved": true
}
Responses
200: { "message": "Seller updated successfully", "seller": { id, firstName, lastName, phoneNumber, storeName, username, email, address, license, approved, emailVerified, profilePic } }
401: { "message": "Unauthorized" }
404: { "message": "Seller not found" }
500: { "message": "Error updating seller", "error": "..." }
Orders
POST /api/orders JWT
Creates a new order. Only authenticated buyers can create orders.
Request
Responses
- 201:
{ "message": "Order created", "orderId": "UUID", "status": "pending|trial_active" }
- 400:
{ "message": "productId required | Product not available | Buyer not found" }
- 500:
{ "message": "Failed to create order" }
GET /api/orders Public
Lists all orders with optional filtering and pagination.
Request
- Query Parameters (optional):
status — Filter by order status ("pending", "trial_active", "paid", "returned", "cancelled")
buyerId — Filter by buyer ID
page — Page number (default: 1)
limit — Items per page (default: 20, max: 100)
Responses
- 200:
{ "total": number, "page": number, "limit": number, "orders": [...] }
- 500:
{ "message": "Error listing orders", "error": "..." }
GET /api/orders/:id JWT
Get a specific order by ID with product and buyer details.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Order ID (required)
Responses
- 200:
{ "order": {...} }
- 404:
{ "message": "Order not found" }
- 500:
{ "message": "Error fetching order", "error": "..." }
GET /api/orders/buyer/:buyerId JWT
Get all orders for a specific buyer with pagination. Uses authenticated buyer's username to find correct buyer ID.
Request
- Headers:
Authorization: Bearer <token>
- Params:
buyerId — Buyer ID (required, but function uses authenticated user's username)
- Query Parameters (optional):
page — Page number (default: 1)
limit — Items per page (default: 10, max: 100)
Responses
- 200:
{ "total": number, "page": number, "limit": number, "orders": [...] }
- 400:
{ "message": "Buyer not found" }
- 500:
{ "message": "Error fetching orders", "error": "..." }
GET /api/orders/seller/:sellerId JWT
Get all orders for products belonging to a specific seller. Uses INNER JOIN to filter orders by product ownership.
Request
- Headers:
Authorization: Bearer <token>
- Params:
sellerId — Seller/User ID (required)
- Query Parameters (optional):
status — Filter by order status ("pending", "trial_active", "paid", "returned", "cancelled", "return_requested", "disputed")
page — Page number (default: 1)
limit — Items per page (default: 10, max: 100)
Responses
- 200:
{ "total": number, "page": number, "limit": number, "orders": [...] }
- 500:
{ "message": "Error fetching seller orders", "error": "..." }
Returns
POST /api/returns JWT (Buyer)
Initiate a return for an order. Only allowed during active trial period. Creates return record with QR token and updates order status.
Request
- Headers:
Authorization: Bearer <token>
- Body (JSON):
{
"orderId": "UUID (required)",
"hasDefect": "boolean (optional)",
"defectDescription": "string (required if hasDefect is true)",
"defectPhotoUrl": "string (optional, required if hasDefect is true)"
}
Responses
- 201:
{ "message": "Return initiated successfully", "return": { id, orderId, returnToken, status, requestedAt, expiresAt, hasDefect } }
- 400:
{ "message": "Order ID is required | Order not found | Not authorized to return this order | Return can only be initiated during trial period | Trial period has expired | Return already initiated for this order" }
- 403:
{ "message": "Not authorized to return this order" }
- 404:
{ "message": "Order not found" }
- 500:
{ "message": "Error initiating return", "error": "..." }
POST /api/returns/scan JWT (Seller)
Seller scans QR code to accept return or claim defect. Supports file upload for defect photos.
Request
Responses
- 200:
{ "message": "Return confirmed - Money released to buyer" | "Defect claim submitted - Dispute created for admin review" }
- 400:
{ "message": "Return token is required | Action must be 'confirm' or 'claim_defect' | Photo and description required for defect claim | Return request has expired" }
- 403:
{ "message": "Unauthorized: You are not the seller" }
- 404:
{ "message": "Invalid QR Code or Return not found" }
- 500:
{ "message": "Error processing return scan", "error": "..." }
GET /api/returns/order/:orderId JWT
Get return details for a specific order. Only accessible by the order buyer or admins.
Request
- Headers:
Authorization: Bearer <token>
- Params:
orderId — Order ID (required)
Responses
- 200:
{ "return": { id, orderId, returnToken, status, requestedAt, expiresAt, scannedAt, defectPhotoUrl, defectDescription, createdAt, updatedAt, order } }
- 400:
{ "message": "Order ID is required" }
- 403:
{ "message": "Not authorized to view this return" }
- 404:
{ "message": "Return not found for this order" }
- 500:
{ "message": "Error fetching return", "error": "..." }
GET /api/returns/seller/:sellerId JWT
Get all returns for products belonging to a specific seller with pagination and filtering.
Request
- Headers:
Authorization: Bearer <token>
- Params:
sellerId — Seller ID (required)
- Query Parameters (optional):
status — Filter by return status ("pending", "confirmed", "defect_claimed", "expired")
page — Page number (default: 1)
limit — Items per page (default: 10, max: 100)
Responses
- 200:
{ "total": number, "page": number, "limit": number, "returns": [...] }
- 400:
{ "message": "Seller ID is required" }
- 500:
{ "message": "Error fetching seller returns", "error": "..." }
GET /api/returns/buyer/:buyerId JWT
Get all returns for a specific buyer with pagination and filtering.
Request
- Headers:
Authorization: Bearer <token>
- Params:
buyerId — Buyer ID (required)
- Query Parameters (optional):
status — Filter by return status ("pending", "confirmed", "defect_claimed", "expired")
page — Page number (default: 1)
limit — Items per page (default: 10, max: 100)
Responses
- 200:
{ "total": number, "page": number, "limit": number, "returns": [...] }
- 400:
{ "message": "Buyer ID is required" }
- 500:
{ "message": "Error fetching buyer returns", "error": "..." }
Admin
PATCH /api/admin/sellers/:id/approve JWT (Admin)
Approve a seller account. Only admins can approve sellers.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Seller ID
Responses
- 200:
{ "message": "Seller approved successfully" }
- 401:
{ "message": "Unauthorized" }
- 403:
{ "message": "Forbidden" }
- 404:
{ "message": "Seller not found" }
- 500:
{ "message": "Error approving seller", "error": "..." }
Rental Products
GET /api/rentals Public
Lists all available rental products with pagination.
Request
- Query Parameters (optional):
page — Page number (default: 1)
limit — Items per page (default: 10, max: 100)
Responses
- 200:
{ "success": true, "data": [...], "pagination": {...} }
- 500:
{ "success": false, "message": "Error retrieving rentables", "error": "..." }
GET /api/rentals/:id Public
Get a specific rental product by ID.
Request
- Params:
id — Product ID (required)
Responses
- 200:
{ "success": true, "data": {...} }
- 404:
{ "success": false, "message": "Rentable not found" }
- 500:
{ "success": false, "message": "Error retrieving rentable", "error": "..." }
POST /api/rentals JWT (Seller/Admin)
Creates a new product and rental listing in a single transaction. Supports image uploads.
Request
- Headers:
Authorization: Bearer <token>
- Content-Type:
multipart/form-data (for image uploads) or application/json
- Body fields:
name: "string (required)"
description: "string (required)"
category: "string (required)"
condition: "string (optional, enum: ['new', 'used', 'refurbished'], default: 'new')"
price: "number (required, > 0)"
dailyRate: "number (required, > 0)"
penaltyRate: "number (optional, default: 0.00)"
images: "file[] (optional, up to 10 images)"
ownerId: "UUID (optional, admin only - for assigning to specific seller)"
Responses
- 201:
{ "success": true, "message": "Product and Rental listing created successfully", "data": { product, rentable } }
- 400:
{ "success": false, "errors": [...] }
- 401:
{ "success": false, "message": "Unauthorized" }
- 403:
{ "success": false, "message": "Only sellers and admins can create products" }
- 500:
{ "success": false, "message": "Error creating rental product", "error": "..." }
PATCH /api/rentals/:id JWT (Owner/Admin)
Updates both product and rental details. Only the product owner or admin can update.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Product ID (required)
- Body (JSON, all fields optional):
{
"name": "string",
"description": "string",
"category": "string",
"condition": "string (enum: ['new', 'used', 'refurbished'])",
"price": "number (> 0)",
"dailyRate": "number (> 0)",
"penaltyRate": "number",
"available": "boolean"
}
Responses
- 200:
{ "success": true, "message": "Product and rental updated successfully", "product": {...} }
- 400:
{ "success": false, "errors": [...] }
- 401:
{ "success": false, "message": "Unauthorized" }
- 403:
{ "success": false, "message": "Not authorized to update this rental" }
- 404:
{ "success": false, "message": "Product not found or not rentable" }
- 500:
{ "success": false, "message": "Error updating rental", "error": "..." }
DELETE /api/rentals/:id JWT (Owner/Admin)
Deletes a rental product. Only the product owner or admin can delete.
Request
- Headers:
Authorization: Bearer <token>
- Params:
id — Product ID (required)
Responses
- 200:
{ "success": true, "message": "Rentable product deleted successfully" }
- 401:
{ "success": false, "message": "Unauthorized" }
- 403:
{ "success": false, "message": "Not authorized to delete this rental" }
- 404:
{ "success": false, "message": "Rentable product not found" }
- 500:
{ "success": false, "message": "Error deleting rental", "error": "..." }
GET /api/rentals/renter/:renterId JWT
Get all rental products listed by a specific seller (renter). Users can only view their own listings unless admin.
Request
- Headers:
Authorization: Bearer <token>
- Params:
renterId — Seller/User ID
- Query:
page, limit (optional)
Responses
- 200:
{ "success": true, "rentables": [...], "pagination": {...} }
- 401:
{ "success": false, "message": "Unauthorized" }
- 403:
{ "success": false, "message": "You can only view your own rentable listings" }
- 500:
{ "success": false, "message": "Error retrieving rentals", "error": "..." }
Payments
POST /api/payments/initiate JWT
Initiates payment with Chapa for a specific order. Creates payment record and returns checkout URL.
Request
Responses
- 200:
{ "checkoutUrl": "https://checkout.chapa.co/...", "hasTrial": boolean }
- 400:
{ "message": "orderId required | Order not payable" }
- 404:
{ "message": "Order not found" }
- 500:
{ "message": "Payment initiation failed | Failed to get checkout URL from Chapa" }
POST /api/payments/verify Public
Chapa webhook endpoint for payment verification. Processes payment callbacks and updates payment/order status. Redirects to success/failure pages.
Request
Responses
- 302: Redirect to
http://localhost:3000/payment-success or what ever the frontend is configured to redirect to (successful payment)
- 302: Redirect to
http://localhost:3000/payment-failed or what ever the frontend is configured to redirect to (failed payment)
- 400:
{ "message": "Transaction reference required" }
- 404:
{ "message": "Payment not found" }
- 500:
{ "message": "Payment verification failed" }
This is a webhook endpoint called by Chapa. Also handles browser redirects from payment completion.
GET /api/payments/verifyy Public
Browser redirect endpoint after payment completion. Simply redirects to success page.
Request
Responses
- 302: Redirect to
http://localhost:3000/payment-success
This endpoint handles the browser redirect from Chapa checkout. The 'y' in 'verifyy' distinguishes it from the webhook endpoint.
GET /api/payments/status Public
Checks the current status of a payment using transaction reference.
Request
- Query Parameters (required):
txRef - Chapa transaction reference
Responses
- 200:
{
"payment": {
"status": "pending|released_to_seller|held_in_escrow|failed",
"amount": "number",
"currency": "string",
"paidAt": "ISO date | null",
"chapaTxRef": "string"
},
"order": {
"id": "UUID",
"status": "pending|trial_active|paid|returned|cancelled",
"trialEndsAt": "ISO date | null",
"hasTrial": "boolean"
}
}
- 400:
{ "message": "Transaction reference (txRef) is required" }
- 404:
{ "message": "Payment not found" | "Order not found for this payment" }
- 500:
{ "message": "Failed to check payment status", "error": "..." }
Security
- JWT Secret:
JWT_SECRET env must be configured.
- Auth header:
Authorization: Bearer <token> for protected endpoints.