Overview
Base URL
http://localhost:3000Pricing
1 OMR = 1 Knot = 100 points
Auth is enabled globally. Endpoints marked as Public work without a token.
Default platform seller fee:
3.00% (applied on escrow release; recorded in profit logs).CORS: the API allows requests from any origin (useful for web apps during development).
Auth
POST
/auth/register Publiccurl -X POST http://localhost:3000/auth/register \
-H 'Content-Type: application/json' \
-d '{
"email":"user@example.com",
"password":"P@ssw0rd123",
"firstName":"Zain",
"lastName":"Ali"
}'Example response
{
"userId": "<USER_UUID>",
"expiresAt": "2026-01-12T00:10:00.000Z"
}POST
/auth/login Publiccurl -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","password":"P@ssw0rd123"}'Example response
{
"requiresOtp": true,
"expiresAt": "2026-01-12T00:10:00.000Z"
}POST
/auth/otp/verify Publiccurl -X POST http://localhost:3000/auth/otp/verify \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","purpose":"login","code":"123456"}'Use the returned
accessToken in Authorization: Bearer ...Example response
{
"accessToken": "<JWT>",
"user": {
"id": "<USER_UUID>",
"email": "user@example.com",
"firstName": "Zain",
"lastName": "Ali",
"phone": null,
"emailVerified": true,
"isVendor": false,
"isAdmin": false,
"isCarrier": false,
"vendorTaxPercent": "10.00",
"vendorProfile": null,
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}
}Email verify variant
# purpose=email_verify
curl -X POST http://localhost:3000/auth/otp/verify \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","purpose":"email_verify","code":"123456"}'{
"verified": true,
"accessToken": "<JWT>",
"user": {
"id": "<USER_UUID>",
"email": "user@example.com",
"emailVerified": true,
"vendorProfile": null
}
}POST
/auth/password/forgot PublicSend an OTP to the user's email for password reset.
curl -X POST http://localhost:3000/auth/password/forgot \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com"}'Example response
{
"expiresAt": "2026-01-12T00:10:00.000Z"
}POST
/auth/password/reset PublicVerify OTP and set a new password.
curl -X POST http://localhost:3000/auth/password/reset \
-H 'Content-Type: application/json' \
-d '{
"email":"user@example.com",
"code":"123456",
"newPassword":"NewP@ssw0rd123"
}'Example response
{
"success": true
}POST
/auth/password/changeChange password for the authenticated user by providing the old password.
curl -X POST http://localhost:3000/auth/password/change \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"oldPassword":"P@ssw0rd123",
"newPassword":"NewP@ssw0rd123"
}'Example response
{
"success": true
}Users
Auth required unless marked Public. Admin endpoints require an admin token.
GET
/users/meReturns the authenticated user (sanitized; no
passwordHash).curl http://localhost:3000/users/me \
-H 'Authorization: Bearer <TOKEN>'PATCH
/users/meUpdates your profile fields:
firstName, lastName, phone.curl -X PATCH http://localhost:3000/users/me \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"firstName":"Zain","lastName":"Ali","phone":"+966500000000"}'Admin
GET
/users/adminPaginated users list. Optional search:
?q=curl 'http://localhost:3000/users/admin?page=1&limit=20&q=user' \
-H 'Authorization: Bearer <ADMIN_TOKEN>'GET
/users/admin/<USER_UUID>curl http://localhost:3000/users/admin/<USER_UUID> \
-H 'Authorization: Bearer <ADMIN_TOKEN>'PATCH
/users/admin/<USER_UUID>Updates user flags/fields (examples:
isAdmin, isVendor, emailVerified, vendorTaxPercent).curl -X PATCH http://localhost:3000/users/admin/<USER_UUID> \
-H 'Authorization: Bearer <ADMIN_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"isAdmin":true,"emailVerified":true}'DELETE
/users/admin/<USER_UUID>curl -X DELETE http://localhost:3000/users/admin/<USER_UUID> \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Admin
Admin-only endpoints (requires
@AdminOnly + JWT).GET
/admin/statsAggregated system stats for admin dashboards: users, deals, orders, wallet stats, and platform profits (OMR + points).
curl http://localhost:3000/admin/stats \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Example response
{
"users": { "total": 123, "vendors": 12, "admins": 2 },
"deals": { "total": 45, "open": 10, "closed": 30, "expired": 5 },
"orders": { "total": 90, "pending": 20, "paid": 30, "shipped": 10, "delivered": 10, "completed": 15, "cancelled": 5 },
"wallet": {
"totalCirculatingPoints": 500000,
"totalFrozenPoints": 20000,
"totalUsersWithWallet": 120,
"totalTopUpAmountInCurrency": 350,
"currency": "OMR"
},
"profit": {
"totalFeePoints": 300,
"totalGrossPoints": 10000,
"totalNetPoints": 9700,
"totalFeeOmr": 3,
"totalGrossOmr": 100,
"totalNetOmr": 97,
"currency": "OMR"
},
"timestamp": "2026-01-12T00:00:00.000Z"
}POST
/wallet/withdrawalsCreate a withdrawal request. Bank details are now referenced via a saved
bankAccountId (see Bank Accounts). Freezes the requested points immediately.Optional fields:
reason (enum), reasonNotes, attachmentUrl, userNote.curl -X POST http://localhost:3000/wallet/withdrawals \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"amount": 1000,
"bankAccountId": "<BANK_ACCOUNT_UUID>",
"reason": "selling_various_materials",
"userNote": "Please process ASAP"
}'Example response
{
"id": "<WITHDRAWAL_UUID>",
"userId": "<USER_UUID>",
"amount": "1000",
"bankAccountId": "<BANK_ACCOUNT_UUID>",
"bankAccount": {
"id": "<BANK_ACCOUNT_UUID>",
"bankName": "Bank Muscat",
"accountNumber": "1234567890",
"accountHolderName": "Zain Ali",
"iban": "OM810080000000123456789"
},
"status": "pending",
"currencyAmount": "10000",
"currency": "OMR",
"reason": "selling_various_materials",
"userNote": "Please process ASAP",
"createdAt": "2026-01-12T00:00:00.000Z"
}Localization
The platform supports DB-backed localization using a
localizations table with:
key, language, value.
Client language is determined from the
Accept-Language header (example: en-US,en;q=0.9,ar;q=0.8).
The server falls back to DEFAULT_LANGUAGE (defaults to en).
Localized response messages
Success and error responses include a
messageKey and a localized human message message.
The message is resolved from localizations using the request language.
curl http://localhost:3000/products \
-H 'Accept-Language: ar'Example (error) response
{
"statusCode": 404,
"error": "Not Found",
"messageKey": "errors.product_not_found",
"message": "Product not found"
}Admin: manage localization entries
Admin-only endpoints (requires
@AdminOnly + JWT).POST
/localizationsCreates a localization row (unique by
key + language).curl -X POST http://localhost:3000/localizations \
-H 'Authorization: Bearer <ADMIN_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"key":"errors.product_not_found",
"language":"ar",
"value":"المنتج غير موجود"
}'PATCH
/localizations/<LOCALIZATION_UUID>Updates a localization row by id.
DELETE
/localizations/<LOCALIZATION_UUID>Deletes a localization row by id.
GET
/localizations/translate?key=<KEY>&language=<LANG>Direct lookup for a key/language pair.
curl 'http://localhost:3000/localizations/translate?key=errors.product_not_found&language=ar' \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Categories
Public endpoints support localization for
name and description using Accept-Language.GET
/categories Publiccurl http://localhost:3000/categories \
-H 'Accept-Language: ar'Example response
[
{
"id": "<CATEGORY_UUID>",
"slug": "electronics",
"name": "الكترونيات",
"description": "جوالات ولابتوبات واكسسوارات",
"isActive": true,
"localizations": [
{ "id": "<LOCALIZATION_UUID>", "key": "categories.<CATEGORY_UUID>.name", "language": "ar", "value": "الكترونيات" },
{ "id": "<LOCALIZATION_UUID>", "key": "categories.<CATEGORY_UUID>.description", "language": "ar", "value": "جوالات ولابتوبات واكسسوارات" }
]
}
]GET
/categories/slug/<SLUG> Publiccurl http://localhost:3000/categories/slug/electronics \
-H 'Accept-Language: en'GET
/categories/<CATEGORY_UUID> Publiccurl http://localhost:3000/categories/<CATEGORY_UUID> \
-H 'Accept-Language: en'Admin
Admin-only endpoints (requires
@AdminOnly + JWT).POST
/categoriesCreates a category. Optional localized fields can be passed as JSON maps.
curl -X POST http://localhost:3000/categories \
-H 'Authorization: Bearer <ADMIN_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"slug":"electronics",
"name":"Electronics",
"description":"Phones, laptops, and accessories",
"nameLocalized": {"ar":"الكترونيات"},
"descriptionLocalized": {"ar":"جوالات ولابتوبات واكسسوارات"},
"isActive": true
}'PATCH
/categories/<CATEGORY_UUID>Updates slug/name/description/isActive and/or localized maps.
DELETE
/categories/<CATEGORY_UUID>Deletes a category. Deals using it will have
categoryId set to null.Addresses
Auth required. Addresses are scoped to the authenticated user. Only one address can be
isDefault=true.When selecting a point on the map (
lat and lng provided), only label, phone, and additionalDetails are required. Otherwise, addressLine1, city, and country are required.GET
/addressesLists your addresses (default first).
curl http://localhost:3000/addresses \
-H 'Authorization: Bearer <TOKEN>'POST
/addressesCreates a new address. If
isDefault=true, it becomes the only default address.For map point selection: provide
lat and lng, and addressLine1, city, country become optional.# Full address with all fields
curl -X POST http://localhost:3000/addresses \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"label":"Home",
"fullName":"John Doe",
"phone":"+966500000000",
"additionalDetails":"Near the park",
"addressLine1":"Riyadh, King Fahd Rd",
"addressLine2":"Apt 12",
"city":"Riyadh",
"country":"SA",
"zip":"12345",
"lat":24.713551,
"lng":46.675296,
"isDefault":true
}'
# Map point selection (minimal fields)
curl -X POST http://localhost:3000/addresses \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"label":"Home",
"phone":"+966500000000",
"additionalDetails":"Near the park",
"lat":24.713551,
"lng":46.675296,
"isDefault":true
}'GET
/addresses/<ADDRESS_UUID>curl http://localhost:3000/addresses/<ADDRESS_UUID> \
-H 'Authorization: Bearer <TOKEN>'PATCH
/addresses/<ADDRESS_UUID>Updates an address. Setting
isDefault=true clears default from others.curl -X PATCH http://localhost:3000/addresses/<ADDRESS_UUID> \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"city":"Jeddah"}'PATCH
/addresses/<ADDRESS_UUID>/defaultSets the address as default (clears other defaults).
curl -X PATCH http://localhost:3000/addresses/<ADDRESS_UUID>/default \
-H 'Authorization: Bearer <TOKEN>'DELETE
/addresses/<ADDRESS_UUID>Deletes an address. If it was default, the newest remaining address becomes default.
curl -X DELETE http://localhost:3000/addresses/<ADDRESS_UUID> \
-H 'Authorization: Bearer <TOKEN>'Vendors
Vendor status lifecycle:
pending_document_upload → pending_admin_approval → active. Admin can set rejected or blocked.GET
/vendors Publiccurl http://localhost:3000/vendorsExample response
[
{
"id": "<VENDOR_UUID>",
"displayName": "iPhone Store",
"commercialRegistrationNumber": "1234567890",
"profileImageUrl": "/uploads/<UUID>.png",
"isActive": true,
"status": "active",
"ratingAvg": "4.50",
"ratingCount": 10,
"user": { "id": "<USER_UUID>", "email": "vendor@example.com" }
}
]POST
/vendors/signup PublicCreates a new user and a vendor profile request in one step. The vendor starts as
isActive=false and requires admin acceptance.curl -X POST http://localhost:3000/vendors/signup \
-H 'Content-Type: application/json' \
-d '{
"email":"vendor@example.com",
"password":"P@ssw0rd123",
"firstName":"Zain",
"lastName":"Ali",
"displayName":"iPhone Store",
"commercialRegistrationNumber":"1234567890"
}'Example response
{
"userId": "<USER_UUID>",
"vendorId": "<VENDOR_UUID>",
"expiresAt": "2026-01-12T00:10:00.000Z"
}GET
/vendors/me/productsAuth required (Vendor). Lists your products.
curl http://localhost:3000/vendors/me/products \
-H 'Authorization: Bearer <TOKEN>'GET
/vendors/me/dealsAuth required (Vendor). Lists your deals. Optional filters:
?type=b2b|b2c|both (default: both).Pagination:
?page=1 (default) and ?limit=20 (default).# all your deals
curl http://localhost:3000/vendors/me/deals \
-H 'Authorization: Bearer <TOKEN>'
# second page
curl 'http://localhost:3000/vendors/me/deals?page=2&limit=20' \
-H 'Authorization: Bearer <TOKEN>'
# only your B2B deals
curl 'http://localhost:3000/vendors/me/deals?type=b2b' \
-H 'Authorization: Bearer <TOKEN>'GET
/vendors/me/documentsAuth required. Lists your uploaded vendor documents (useful before admin acceptance).
curl http://localhost:3000/vendors/me/documents \
-H 'Authorization: Bearer <TOKEN>'POST
/vendors/me/documentsAuth required. Upload a vendor document as
multipart/form-data with field name file and provide document type. Allowed types: cr, occl, sme, other. Max file size: 10MB.curl -X POST http://localhost:3000/vendors/me/documents \
-H 'Authorization: Bearer <TOKEN>' \
-F 'type=cr' \
-F 'file=@/path/to/document.pdf'Example response
{
"id": "<DOCUMENT_UUID>",
"type": "cr",
"url": "/uploads/<FILENAME>",
"filename": "<FILENAME>",
"mimeType": "application/pdf",
"sizeBytes": 12345,
"status": "pending",
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}DELETE
/vendors/me/documents/<DOCUMENT_UUID>Auth required. Deletes one of your uploaded vendor documents.
curl -X DELETE http://localhost:3000/vendors/me/documents/<DOCUMENT_UUID> \
-H 'Authorization: Bearer <TOKEN>'POST
/vendors/me/setupAdmin only. Allows an admin to create their own vendor profile with
isActive=true and status=active without requiring document upload or admin approval. Enables the admin to use vendor endpoints immediately.curl -X POST http://localhost:3000/vendors/me/setup \
-H 'Authorization: Bearer <ADMIN_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"displayName":"Admin Store",
"commercialRegistrationNumber":"1234567890"
}'Example response
{
"id": "<VENDOR_UUID>",
"displayName": "Admin Store",
"commercialRegistrationNumber": "1234567890",
"profileImageUrl": null,
"isActive": true,
"status": "active",
"ratingAvg": "0.00",
"ratingCount": 0
}POST
/vendors PublicCreates a vendor profile request. New vendors start as
isActive=false and require admin acceptance before vendor-only endpoints work.curl -X POST http://localhost:3000/vendors \
-H 'Content-Type: application/json' \
-d '{"userId":"<USER_UUID>","displayName":"iPhone Store","commercialRegistrationNumber":"1234567890"}'Example response
{
"id": "<VENDOR_UUID>",
"displayName": "iPhone Store",
"commercialRegistrationNumber": "1234567890",
"profileImageUrl": null,
"isActive": false,
"status": "pending_document_upload",
"ratingAvg": "0.00",
"ratingCount": 0
}Admin
Admin-only endpoints (requires admin JWT).
GET
/vendors/admin/<VENDOR_UUID>Admin-only. Returns the vendor profile including uploaded
documents.curl http://localhost:3000/vendors/admin/<VENDOR_UUID> \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Example response
{
"id": "<VENDOR_UUID>",
"displayName": "iPhone Store",
"commercialRegistrationNumber": "1234567890",
"profileImageUrl": "/uploads/<UUID>.png",
"isActive": true,
"status": "active",
"ratingAvg": "4.50",
"ratingCount": 10,
"user": { "id": "<USER_UUID>", "email": "vendor@example.com" },
"documents": [
{
"id": "<DOCUMENT_UUID>",
"type": "cr",
"url": "/uploads/<FILENAME>",
"filename": "<FILENAME>",
"mimeType": "application/pdf",
"sizeBytes": 12345,
"status": "pending",
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}
]
}POST
/vendors/<VENDOR_UUID>/acceptAccept vendor request. Sets
vendor.isActive=true and marks the user as vendor for RBAC. Optionally set vendorTaxPercent (0..100) on the vendor user.curl -X POST http://localhost:3000/vendors/<VENDOR_UUID>/accept \
-H 'Authorization: Bearer <ADMIN_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"vendorTaxPercent":15}'POST
/vendors/<VENDOR_UUID>/rejectReject vendor request. Sets vendor
status=rejected and ensures the user is not marked as vendor.curl -X POST http://localhost:3000/vendors/<VENDOR_UUID>/reject \
-H 'Authorization: Bearer <ADMIN_TOKEN>'POST
/vendors/<VENDOR_UUID>/blockBlock a vendor. Sets
status=blocked, isActive=false, and removes vendor role from the user.curl -X POST http://localhost:3000/vendors/<VENDOR_UUID>/block \
-H 'Authorization: Bearer <ADMIN_TOKEN>'POST
/vendors/<VENDOR_UUID>/unblockUnblock a vendor. Sets
status=pending_admin_approval and keeps isActive=false until accepted again.curl -X POST http://localhost:3000/vendors/<VENDOR_UUID>/unblock \
-H 'Authorization: Bearer <ADMIN_TOKEN>'POST
/vendors/<VENDOR_UUID>/profile-image PublicUpload vendor profile image as
multipart/form-data using field name file. Max file size: 10MB.curl -X POST http://localhost:3000/vendors/<VENDOR_UUID>/profile-image \
-F 'file=@/path/to/image.png'Example response
{
"id": "<VENDOR_UUID>",
"displayName": "iPhone Store",
"commercialRegistrationNumber": "1234567890",
"profileImageUrl": "/uploads/<UUID>.png",
"isActive": true,
"status": "active",
"ratingAvg": "0.00",
"ratingCount": 0
}Ratings
GET
/vendors/<VENDOR_UUID>/ratings Publiccurl http://localhost:3000/vendors/<VENDOR_UUID>/ratingsExample response
[
{
"id": "<RATING_UUID>",
"rating": 5,
"comment": "Fast delivery",
"user": { "id": "<USER_UUID>", "email": "user@example.com" },
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}
]POST
/vendors/ratingsAuth required. Creates or updates your rating for a vendor (1..5).
curl -X POST http://localhost:3000/vendors/ratings \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"vendorId":"<VENDOR_UUID>",
"rating":5,
"comment":"Fast delivery"
}'Example response
{
"id": "<RATING_UUID>",
"rating": 5,
"comment": "Fast delivery",
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}DELETE
/vendors/<VENDOR_UUID>/ratings/meAuth required. Deletes your rating for the vendor.
curl -X DELETE http://localhost:3000/vendors/<VENDOR_UUID>/ratings/me \
-H 'Authorization: Bearer <TOKEN>'Example response
{
"deleted": true
}Deals
POST
/products/with-dealAuth required (Vendor/Admin). Creates a product + deal and uploads images in the same request (
multipart/form-data).File fields:
productImages (0..10), dealImages (0..10), and dealVideo (0..1). Max file size: 50MB.Alternative to uploading
dealVideo: provide videoUrl field with an external video URL (e.g., YouTube, Vimeo, or CDN link). If both are provided, the uploaded file takes precedence.On successful creation, the API triggers an async WhatsApp broadcast to all users with a phone number if Twilio env vars are configured:
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_WHATSAPP_FROM. Optional: APP_PUBLIC_URL to include a clickable deal link.# With uploaded video file
curl -X POST http://localhost:3000/products/with-deal \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: multipart/form-data' \
-F 'vendorId=<VENDOR_UUID>' \
-F 'categoryId=<CATEGORY_UUID>' \
-F 'name=iPhone 16' \
-F 'description=128GB' \
-F 'nameLocalized={"ar":"ايفون 16"}' \
-F 'descriptionLocalized={"ar":"128 جيجا"}' \
-F 'unitPriceSar=1.00' \
-F 'minOrderQuantity=1' \
-F 'stockQuantity=100' \
-F 'isActive=true' \
-F 'targetQuantity=50' \
-F 'minQuantityToStartShipping=0' \
-F 'expiresAt=2026-02-10T00:00:00.000Z' \
-F 'dealType=b2c' \
-F 'productImages=@/path/to/product-1.png' \
-F 'productImages=@/path/to/product-2.png' \
-F 'dealImages=@/path/to/deal-1.png' \
-F 'dealVideo=@/path/to/deal.mp4'
# With external video URL instead of uploading
curl -X POST http://localhost:3000/products/with-deal \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: multipart/form-data' \
-F 'vendorId=<VENDOR_UUID>' \
-F 'categoryId=<CATEGORY_UUID>' \
-F 'name=iPhone 16' \
-F 'description=128GB' \
-F 'unitPriceSar=1.00' \
-F 'stockQuantity=100' \
-F 'targetQuantity=50' \
-F 'expiresAt=2026-02-10T00:00:00.000Z' \
-F 'videoUrl=https://youtube.com/watch?v=abc123'Example response
{
"product": { "id": "<PRODUCT_UUID>", "localizations": [ { "key": "products.<PRODUCT_UUID>.name", "language": "ar", "value": "ايفون 16" } ] },
"deal": { "id": "<DEAL_UUID>", "status": "open" },
"productImages": [ { "id": "<IMAGE_UUID>", "url": "/uploads/...", "filename": "..." } ],
"dealImages": [ { "id": "<IMAGE_UUID>", "url": "/uploads/...", "filename": "..." } ],
"dealVideo": { "id": "<VIDEO_UUID>", "url": "/uploads/...", "filename": "..." }
}GET
/deals Public
Optional filters:
?type=b2b|b2c|both (default: both) and ?categoryId=<CATEGORY_UUID>.
Pagination:
?page=1 (default) and ?limit=20 (default).# all deal types (default)
curl http://localhost:3000/deals
# second page
curl 'http://localhost:3000/deals?page=2&limit=20'
# only B2B deals
curl 'http://localhost:3000/deals?type=b2b'
# only B2C deals
curl 'http://localhost:3000/deals?type=b2c'
# filter by category
curl 'http://localhost:3000/deals?categoryId=<CATEGORY_UUID>'
# filter by type + category
curl 'http://localhost:3000/deals?type=b2c&categoryId=<CATEGORY_UUID>'Example response
[
{
"id": "<DEAL_UUID>",
"dealType": "b2c",
"targetQuantity": 100,
"reservedQuantity": 10,
"minQuantityToStartShipping": 0,
"status": "open",
"expiresAt": "2026-01-15T10:00:00.000Z",
"product": {
"id": "<PRODUCT_UUID>",
"localizations": [
{ "id": "<LOCALIZATION_UUID>", "key": "products.<PRODUCT_UUID>.name", "language": "ar", "value": "ايفون 16" }
]
},
"vendor": { "id": "<VENDOR_UUID>" },
"category": {
"id": "<CATEGORY_UUID>",
"slug": "electronics",
"name": "Electronics",
"localizations": [
{ "id": "<LOCALIZATION_UUID>", "key": "categories.<CATEGORY_UUID>.name", "language": "ar", "value": "الكترونيات" }
]
},
"images": []
}
]PATCH
/deals/<DEAL_UUID>Auth required (Admin or Vendor). Vendors can only update their own deals.
Supports updating deal fields and the linked product fields via nested
product object.To set or update the deal video URL, include
videoUrl field (external video link). Setting videoUrl will create or update the video record for the deal.curl -X PATCH http://localhost:3000/deals/<DEAL_UUID> \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"targetQuantity": 50,
"minQuantityToStartShipping": 0,
"expiresAt": "2026-02-10T00:00:00.000Z",
"status": "open",
"dealType": "b2c",
"categoryId": "<CATEGORY_UUID>",
"videoUrl": "https://youtube.com/watch?v=abc123",
"product": {
"name": "iPhone 16",
"description": "128GB",
"unitPriceSar": "1.00",
"minOrderQuantity": 1,
"stockQuantity": 100,
"isActive": true,
"nameLocalized": {"ar":"ايفون 16"},
"descriptionLocalized": {"ar":"128 جيجا"}
}
}'Example response
{
"id": "<DEAL_UUID>",
"dealType": "b2c",
"targetQuantity": 50,
"reservedQuantity": 0,
"minQuantityToStartShipping": 0,
"status": "open",
"expiresAt": "2026-02-10T00:00:00.000Z",
"product": { "id": "<PRODUCT_UUID>" },
"vendor": { "id": "<VENDOR_UUID>" },
"category": { "id": "<CATEGORY_UUID>", "slug": "electronics", "name": "Electronics" },
"images": [
{ "id": "<IMAGE_UUID>", "url": "/uploads/deal-image.png", "filename": "deal-image.png" },
{ "id": "<VIDEO_UUID>", "url": "https://youtube.com/watch?v=abc123", "filename": "external-video", "mimeType": "video/mp4" }
]
}POST
/deals/<DEAL_UUID>/joinAuth required. Use
Authorization: Bearer <TOKEN>.For
dealType=b2b deals, only users with a vendor account can join.Vendors cannot join their own deals.
If authenticated, the deal list and single-deal responses include an
isParticipated: boolean field indicating whether the current user has already joined that deal.# WALLET (escrow freezes points immediately)
curl -X POST http://localhost:3000/deals/<DEAL_UUID>/join \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"paymentMethod":"wallet","quantity":1,"addressId":"<ADDRESS_UUID>"}'
# WALLET with explicit gateway + termUrl3ds (for auto top-up redirect)
curl -X POST http://localhost:3000/deals/<DEAL_UUID>/join \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"paymentMethod": "wallet",
"quantity": 1,
"addressId": "<ADDRESS_UUID>",
"termUrl3ds": "https://yourapp.com/payment/callback",
"gateway": "thawani"
}'
# COD
curl -X POST http://localhost:3000/deals/<DEAL_UUID>/join \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"paymentMethod": "cod",
"quantity": 1,
"addressId": "<ADDRESS_UUID>"
}'
Successful join reserves quantity and freezes points in escrow immediately.
Success response (sufficient balance)
{
"dealId": "<DEAL_UUID>",
"reservedQuantity": 11,
"targetQuantity": 100,
"orderId": "<ORDER_UUID>",
"escrowId": "<ESCROW_UUID_OR_NULL>"
}Auto top-up response (insufficient wallet balance)
When
paymentMethod=wallet and the user does not have enough points, the API automatically creates a top-up for the exact shortfall and returns a payment URL instead of an error. After the user completes payment the webhook credits their wallet; they then retry the join request.{
"paymentRequired": true,
"paymentUrl": "https://checkout.thawani.om/pay/...",
"topUpId": "<TOP_UP_UUID>",
"requiredPoints": 500,
"availablePoints": 200,
"shortfallPoints": 300
}Fields:
termUrl3ds (optional, redirect URL after 3DS), gateway (optional, thawani or edfapay, defaults to thawani).Bank Accounts
Auth required. Each user can manage their saved bank accounts. Only one account can be the default at a time. Bank accounts are linked to withdrawal requests via
bankAccountId.POST
/bank-accountsCreates a new bank account. Automatically becomes default if it is the first account, or if
isDefault=true is passed (clears default from others).curl -X POST http://localhost:3000/bank-accounts \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"bankName": "Bank Muscat",
"countryName": "Oman",
"countryCode": "OM",
"accountNumber": "1234567890",
"accountHolderName": "Zain Ali",
"iban": "OM810080000000123456789",
"isDefault": true
}'Example response
{
"id": "<BANK_ACCOUNT_UUID>",
"userId": "<USER_UUID>",
"bankName": "Bank Muscat",
"countryName": "Oman",
"countryCode": "OM",
"accountNumber": "1234567890",
"accountHolderName": "Zain Ali",
"iban": "OM810080000000123456789",
"isDefault": true,
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}GET
/bank-accountsLists all bank accounts for the authenticated user. Default account is returned first.
curl http://localhost:3000/bank-accounts \
-H 'Authorization: Bearer <TOKEN>'GET
/bank-accounts/<BANK_ACCOUNT_UUID>curl http://localhost:3000/bank-accounts/<BANK_ACCOUNT_UUID> \
-H 'Authorization: Bearer <TOKEN>'PATCH
/bank-accounts/<BANK_ACCOUNT_UUID>Updates any field. Setting
isDefault=true clears default from others.curl -X PATCH http://localhost:3000/bank-accounts/<BANK_ACCOUNT_UUID> \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"bankName": "HSBC Oman"}'POST
/bank-accounts/<BANK_ACCOUNT_UUID>/set-defaultSets the specified account as the default (clears default from all others).
curl -X POST http://localhost:3000/bank-accounts/<BANK_ACCOUNT_UUID>/set-default \
-H 'Authorization: Bearer <TOKEN>'DELETE
/bank-accounts/<BANK_ACCOUNT_UUID>curl -X DELETE http://localhost:3000/bank-accounts/<BANK_ACCOUNT_UUID> \
-H 'Authorization: Bearer <TOKEN>'Example response
{ "deleted": true }Orders
GET
/ordersAuth required. Lists your orders (as buyer or seller). Admin can use
?scope=all.# My orders
curl http://localhost:3000/orders \
-H 'Authorization: Bearer <TOKEN>'
# Admin: all orders
curl 'http://localhost:3000/orders?scope=all' \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Example response
[
{
"id": "<ORDER_UUID>",
"quantity": 1,
"totalPricePoints": "100",
"taxPoints": "10",
"paymentMethod": "wallet",
"escrowId": "<ESCROW_UUID>",
"address": { "id": "<ADDRESS_UUID>" },
"orderStatus": "pending",
"buyerUser": { "id": "<USER_UUID>", "email": "user@example.com", "vendorProfile": null },
"sellerUser": { "id": "<VENDOR_USER_UUID>", "email": "vendor@example.com", "vendorProfile": null },
"deal": { "id": "<DEAL_UUID>", "product": { "id": "<PRODUCT_UUID>" } },
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}
]GET
/orders/<ORDER_UUID>curl http://localhost:3000/orders/<ORDER_UUID> \
-H 'Authorization: Bearer <TOKEN>'Example response
{
"id": "<ORDER_UUID>",
"quantity": 1,
"totalPricePoints": "100",
"taxPoints": "10",
"paymentMethod": "wallet",
"escrowId": "<ESCROW_UUID>",
"address": { "id": "<ADDRESS_UUID>" },
"orderStatus": "pending",
"buyerUser": { "id": "<USER_UUID>", "email": "user@example.com", "vendorProfile": null },
"sellerUser": { "id": "<VENDOR_USER_UUID>", "email": "vendor@example.com", "vendorProfile": null },
"deal": { "id": "<DEAL_UUID>", "product": { "id": "<PRODUCT_UUID>" } },
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}GET
/orders/<ORDER_UUID>/activitiesAuth required. Allowed for Admin, Buyer, or Seller of the order. Returns newest first. Each row has
createdAt timestamp.curl http://localhost:3000/orders/<ORDER_UUID>/activities \
-H 'Authorization: Bearer <TOKEN>'Example response
[
{
"id": "<ACTIVITY_UUID>",
"orderId": "<ORDER_UUID>",
"actorUserId": "<USER_UUID_OR_NULL>",
"action": "order_created",
"metadata": { "orderStatus": "pending" },
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}
]PATCH
/orders/<ORDER_UUID>Admin can update
addressId and/or override orderStatus. Seller can only manage shipping lifecycle via action endpoints.curl -X PATCH http://localhost:3000/orders/<ORDER_UUID> \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"addressId":"<ADDRESS_UUID>"}'Example response
{
"id": "<ORDER_UUID>",
"address": { "id": "<ADDRESS_UUID>" },
"orderStatus": "pending"
}POST
/orders/<ORDER_UUID>/shipSeller/Admin marks order shipped. If a shipment exists, shipping fee must be approved first.
curl -X POST http://localhost:3000/orders/<ORDER_UUID>/ship \
-H 'Authorization: Bearer <TOKEN>'Example response
{
"id": "<ORDER_UUID>",
"orderStatus": "shipped"
}Shipment flow
Buyer creates shipment request to the order address, seller sets shipping fee, then buyer approves/rejects.
POST
/orders/<ORDER_UUID>/shipmentBuyer/Admin creates shipment request. Shipment status becomes
orders_shipping.curl -X POST http://localhost:3000/orders/<ORDER_UUID>/shipment \
-H 'Authorization: Bearer <TOKEN>'PATCH
/orders/<ORDER_UUID>/shipment/feeSeller/Admin sets shipping fee (points). Shipment status becomes
seller_set_the_price.curl -X PATCH http://localhost:3000/orders/<ORDER_UUID>/shipment/fee \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"shippingFeePoints":"500"}'POST
/orders/<ORDER_UUID>/shipment/approveBuyer/Admin approves shipping fee. Shipment status becomes
buyer_approved_price.curl -X POST http://localhost:3000/orders/<ORDER_UUID>/shipment/approve \
-H 'Authorization: Bearer <TOKEN>'POST
/orders/<ORDER_UUID>/shipment/rejectBuyer/Admin rejects shipping fee. Shipment status becomes
buyer_rejected_price.curl -X POST http://localhost:3000/orders/<ORDER_UUID>/shipment/reject \
-H 'Authorization: Bearer <TOKEN>'POST
/orders/<ORDER_UUID>/deliverSeller/Admin marks order delivered.
curl -X POST http://localhost:3000/orders/<ORDER_UUID>/deliver \
-H 'Authorization: Bearer <TOKEN>'Example response
{
"id": "<ORDER_UUID>",
"orderStatus": "delivered"
}POST
/orders/<ORDER_UUID>/cod-collectedOnly for COD orders. Marks COD collected (sets status to PAID) after delivered.
curl -X POST http://localhost:3000/orders/<ORDER_UUID>/cod-collected \
-H 'Authorization: Bearer <TOKEN>'Example response
{
"id": "<ORDER_UUID>",
"paymentMethod": "cod",
"orderStatus": "paid"
}POST
/orders/<ORDER_UUID>/cancelBuyer/Seller/Admin can cancel only while PENDING. Wallet orders will refund escrow.
curl -X POST http://localhost:3000/orders/<ORDER_UUID>/cancel \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"reason":"Customer requested cancellation"}'Example response
{
"id": "<ORDER_UUID>",
"orderStatus": "cancelled"
}Uploads
Public endpoints used to upload images and associate them to a Product or Deal.
Uploaded files are stored in
public/uploads and are accessible via /uploads/<filename>.GET
/uploads/images PublicLists uploaded images (newest first). Includes related
product and deal when present.curl http://localhost:3000/uploads/imagesExample response
[
{
"id": "<IMAGE_UUID>",
"filename": "<UUID>.png",
"url": "/uploads/<UUID>.png",
"mimeType": "image/png",
"sizeBytes": 12345,
"product": { "id": "<PRODUCT_UUID>" },
"deal": null,
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}
]POST
/uploads/image PublicUpload a single file as
multipart/form-data using field name file.Optional body fields:
productId (UUID) or dealId (UUID). Provide at most one.Max file size:
10MB.curl -X POST http://localhost:3000/uploads/image \
-F 'file=@/path/to/image.png' \
-F 'productId=<PRODUCT_UUID>'Example response
{
"id": "<IMAGE_UUID>",
"url": "/uploads/<UUID>.png",
"filename": "<UUID>.png",
"mimeType": "image/png",
"sizeBytes": 12345
}Possible errors
400: Only one of productId or dealId can be provided404: Product not found / Deal not foundDELETE
/uploads/images/<IMAGE_UUID> PublicDeletes the image DB record.
curl -X DELETE http://localhost:3000/uploads/images/<IMAGE_UUID>Example response
{
"deleted": true
}Platform Fees & Profit Tracking
Admin-only endpoints (requires
@AdminOnly + JWT).GET
/platform/configcurl http://localhost:3000/platform/config \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Example response
{
"sellerFeePercent": 3,
"taxPercent": 10
}PATCH
/platform/configUpdates seller fee percent and tax percent (0..100). Example sets fee to 3% and tax to 10%.
curl -X PATCH http://localhost:3000/platform/config \
-H 'Authorization: Bearer <ADMIN_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"sellerFeePercent":3,"taxPercent":10}'Example response
{
"sellerFeePercent": 3,
"taxPercent": 10
}GET
/platform/profitsPaginated profit records created per escrow release.
curl 'http://localhost:3000/platform/profits?page=1&limit=20' \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Example response
{
"total": 1,
"page": 1,
"limit": 20,
"items": [
{
"id": "<UUID>",
"escrowId": "<ESCROW_UUID>",
"orderId": "<ORDER_UUID>",
"grossPoints": "10000",
"feePoints": "300",
"netPoints": "9700",
"feePercentApplied": "3.00",
"createdAt": "2026-01-12T00:00:00.000Z",
"updatedAt": "2026-01-12T00:00:00.000Z"
}
]
}GET
/platform/profits/summaryTotals across all profit records (gross/fee/net in points).
curl http://localhost:3000/platform/profits/summary \
-H 'Authorization: Bearer <ADMIN_TOKEN>'Example response
{
"totalFeePoints": 300,
"totalGrossPoints": 10000,
"totalNetPoints": 9700
}Notes
- Deals auto-expire in a scheduler and refund escrow.
- Some endpoints are currently Public; RBAC will tighten this.
- You can view wallet-specific docs at /wallet-docs.
- Some endpoints are currently Public; RBAC will tighten this.
- You can view wallet-specific docs at /wallet-docs.