Coupons Module
The promotional engine β create, validate, and apply discounts with strict business rules.
π Table of Contentsβ
Base Route : /api/v1/couponsβ
All endpoints require authentication. Admin endpoints (POST /, PATCH /:id, GET /) are restricted to ADMIN role. The public endpoint /available is accessible to any authenticated user (USER or ADMIN).
Endpoints Overviewβ
| Method | Endpoint | Access | Description |
|---|---|---|---|
POST | / | Admin | Create a new coupon |
PATCH | /:id | Admin | Update an existing coupon (toggle isActive, change limits, etc.) |
GET | / | Admin | Fetch all coupons with pagination and filtering |
GET | /available | Authenticated User | Fetch coupons that are active, not expired, and valid for the current cart value |
Rate limits:
standardLimiterapplies (100 requests per 15 minutes per IP). No specific coupon limiter.
Endpoint Detailsβ
Admin Endpointsβ
POST /β
Create a new promotional coupon.
Headers: Authorization: Bearer <admin_access_token>
Request body:
{
"code": "DIWALI500",
"discountType": "FLAT",
"discountValue": 500,
"minCartValue": 2500,
"startDate": "2026-05-01T00:00:00.000Z",
"expiryDate": "2026-12-31T23:59:59.000Z",
"usageLimit": 100
}
Discount types:
FLATβ fixed amount off (e.g., βΉ500 off)PERCENTAGEβ percentage off (max 100%)
Response (201 Created):
{
"success": true,
"statusCode": 201,
"message": "Coupon created successfully",
"data": {
"_id": "...",
"code": "DIWALI500",
"discountType": "FLAT",
"discountValue": 500,
"minCartValue": 2500,
"startDate": "2026-05-01T00:00:00.000Z",
"expiryDate": "2026-12-31T23:59:59.000Z",
"usageLimit": 100,
"usedCount": 0,
"isActive": true,
"createdAt": "...",
"updatedAt": "..."
},
"timestamp": "2026-05-22T10:30:00.000Z"
}
PATCH /:idβ
Update an existing coupon. All fields are optional. Useful for:
- Changing the discount value or minimum cart value.
- Extending the expiry date.
- Disabling a coupon (
isActive: false) without deleting it. - Increasing the
usageLimit.
Headers: Authorization: Bearer <admin_access_token>
URL Parameter: id β coupon ObjectId
Request body (partial):
{
"discountValue": 600,
"expiryDate": "2027-01-31T23:59:59.000Z",
"isActive": false
}
Response (200 OK): Returns the updated coupon.
To reactivate a coupon, set
isActive: true. Expired coupons (expiryDatein the past) cannot be reactivated unless the date is also updated.
GET /β
Fetch all coupons with pagination and optional filtering (by isActive, discountType, etc.). Admin dashboard use only.
Headers: Authorization: Bearer <admin_access_token>
Query parameters (optional):
| Param | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Items per page (max 100) |
isActive | boolean | β | Filter by active/inactive |
discountType | string | β | FLAT or PERCENTAGE |
Response (200 OK):
{
"success": true,
"statusCode": 200,
"message": "Coupons fetched successfully",
"data": {
"coupons": [ ... ],
"meta": {
"total": 50,
"limit": 20,
"page": 1
}
},
"timestamp": "2026-05-22T10:35:00.000Z"
}
Public Endpointβ
GET /availableβ
Fetch coupons that are active, not expired, and valid for the current cart value. The frontend uses this to show available discounts before the user applies a code.
Headers: Authorization: Bearer <user_access_token>
Query parameter:
| Param | Type | Required | Description |
|---|---|---|---|
cartValue | number | Yes | The current cart subtotal (used to filter by minCartValue) |
Example request:
GET /api/v1/coupons/available?cartValue=3000
Response (200 OK):
{
"success": true,
"statusCode": 200,
"message": "Available coupons retrieved",
"data": {
"coupons": [
{
"_id": "...",
"code": "DIWALI500",
"discountType": "FLAT",
"discountValue": 500,
"minCartValue": 2500,
"expiryDate": "2026-12-31T23:59:59.000Z"
},
{
"_id": "...",
"code": "SUMMER10",
"discountType": "PERCENTAGE",
"discountValue": 10,
"minCartValue": 1000,
"expiryDate": "2026-08-31T23:59:59.000Z"
}
]
},
"timestamp": "2026-05-22T10:40:00.000Z"
}
Only coupons with
isActive: true,expiryDatein the future, andminCartValue β€ cartValueare returned.
Validation Rules (Zod)β
| Endpoint | Field | Rules |
|---|---|---|
POST / | code | string, 1β50 chars, uppercase alphanumeric + underscore, required |
discountType | enum ["FLAT", "PERCENTAGE"], required | |
discountValue | number > 0, required. For PERCENTAGE: β€ 100 | |
minCartValue | number β₯ 0, default 0 | |
startDate | ISO 8601 datetime, required | |
expiryDate | ISO 8601 datetime, must be after startDate, required | |
usageLimit | integer β₯ 1, optional (default null = unlimited) | |
PATCH /:id | All fields optional, same rules as above | |
GET /available | cartValue | number β₯ 0, required (coerced from query string) |
All schemas use
.strict()β extra fields are rejected.
Error Responseβ
| Status | Code | Example message | When |
|---|---|---|---|
| 400 | BAD_REQUEST | "discountValue for PERCENTAGE cannot exceed 100" | Validation fails |
| 400 | BAD_REQUEST | "expiryDate must be after startDate" | Date logic error |
| 401 | UNAUTHORIZED | "Authentication required" | Missing or invalid token |
| 403 | FORBIDDEN | "You do not have permission to perform this action" | Nonβadmin tries admin endpoint |
| 404 | NOT_FOUND | "Coupon not found" | Invalid coupon ID in PATCH /:id |
| 409 | CONFLICT | "Coupon code already exists" | Duplicate code on creation |
| 409 | CONFLICT | "This coupon has expired" | Applying expired coupon to cart |
Runbookβ
For manual testing with Thunder Client / Postman, see ../../testing/coupon-runbook.md. The runbook covers:
- Admin creating flat and percentage coupons
- Public
GET /availablefiltering by cart value - Applying coupons to cart (via cart module)
- Edge cases: expired coupons, usage limits, min cart value
Related Documentationβ
- Cart Module β coupon application endpoints (
/cart/coupon/applyand/cart/coupon/remove). - Error Handling Guide β status codes.
- Authentication Guide β admin vs user tokens.
Flexible discounts, strict limits β the ReshmaβCore coupon engine.