Fulfillments API
Base Path: /api/v1/fulfillments
Authentication: All endpoints require Authorization: Bearer <token> header.
Overview
The Fulfillments API manages the delivery and confirmation process between sellers and winners for tournament products. Key features:
- Auto-creation: Fulfillment records are automatically created when needed (for ended tournaments with a winner)
- Country Locking: The
countryfield inpickupAddressesis always set to the product's listed country and cannot be overridden - Tournament Status: Fulfillments can only be created for tournaments with status
OVER - Winner Selection: Winners are determined by the lowest non-zero score on the product's leaderboard
Endpoints
1. Get My Fulfillments
Retrieve all fulfillments for the authenticated user, either as winner or seller.
Method: GET
Path: /api/fulfillments/me
Query Parameters:
role(optional):"winner"or"seller"(default:"winner")
Response: Array of fulfillment objects sorted by creation date (newest first)
Example Request:
curl -X GET "http://localhost:3000/api/fulfillments/me?role=seller" \
-H "Authorization: Bearer YOUR_TOKEN"
Example Response:
[
{
"_id": "64f123abc...",
"tournament": "64f456def...",
"product": "64f789ghi...",
"winner": "user_winner_id",
"seller": "user_seller_id",
"status": "RECEIVED",
"received": true,
"deliveryMethod": "COURIER",
"courierService": "Bluedart",
"trackingNumber": "BD-123456",
"dateOfDelivery": "2025-09-06T12:34:00.000Z",
"dateOfReceive": "2025-09-07T10:00:00.000Z",
"sellerMedia": {
"photos": ["https://cdn.example.com/p1.jpg"],
"videos": ["https://cdn.example.com/v1.mp4"]
},
"receiverConfirmation": {
"confirmedAt": "2025-09-07T10:00:00.000Z",
"videoUrl": "https://cdn.example.com/recv.mp4",
"notes": "Received in good condition"
},
"createdAt": "2025-09-06T10:00:00.000Z",
"updatedAt": "2025-09-07T10:00:00.000Z"
}
]
2. Get Fulfillment by Product (Legacy Path)
Method: GET
Path: /api/v1/fulfillments/by-product/:productId
URL Parameters:
productId(required): MongoDB ObjectId of the product
Response: Single fulfillment object populated with product and winner details
Auto-creation: If no fulfillment exists for the product, one will be created automatically (only if tournament is OVER and has a valid winner)
Example Request:
curl -X GET "http://localhost:3000/api/v1/fulfillments/by-product/64f789ghi..." \
-H "Authorization: Bearer YOUR_TOKEN"
Example Response:
{
"_id": "64f123abc...",
"tournament": "64f456def...",
"product": {
"_id": "64f789ghi...",
"title": "Amazing Product",
"price": 1000,
"delivery": "COURIER_DOMESTIC",
"country": "IN"
},
"winner": {
"_id": "user_winner_id",
"username": "johndoe",
"avatar": "https://cdn.example.com/avatar.jpg",
"verified": true
},
"seller": "user_seller_id",
"status": "PENDING",
"received": false,
"verificationStatus": "PENDING",
"deliveryMethod": "IN_PERSON",
"sellerMedia": {
"photos": [],
"videos": []
},
"receiverMedia": {
"photos": [],
"videos": []
},
"receiverConfirmation": {
"confirmedAt": null,
"videoUrl": null,
"notes": null
},
"createdAt": "2025-09-06T10:00:00.000Z",
"updatedAt": "2025-09-06T10:00:00.000Z"
}
3. Get Fulfillment by Product (Preferred Path)
Method: GET
Path: /api/v1/fulfillments/product/:productId
URL Parameters:
productId(required): MongoDB ObjectId of the product
Response: Same as /by-product/:productId above
Auto-creation: Same behavior - creates fulfillment if none exists
Example Request:
curl -X GET "http://localhost:3000/api/v1/fulfillments/product/64f789ghi..." \
-H "Authorization: Bearer YOUR_TOKEN"
Note: This is the preferred endpoint path. Both /by-product/:productId and /product/:productId work identically.
4. Create Fulfillment Manually
Create a fulfillment record manually (typically not needed as auto-creation handles most cases).
Method: POST
Path: /api/v1/fulfillments
Request Body:
{
tournament: string; // Required: MongoDB ObjectId
product: string; // Required: MongoDB ObjectId
winner: string; // Required: User ID
seller: string; // Required: User ID
deliveryMethod?: "IN_PERSON" | "COURIER"; // Optional, default: "IN_PERSON"
courierService?: string; // Optional (required if deliveryMethod is COURIER)
trackingNumber?: string; // Optional (required if deliveryMethod is COURIER)
deliveredAt?: string; // Optional: ISO date string
sellerNotes?: string; // Optional
pickupAddresses?: { // Optional
seller?: {
line1?: string;
line2?: string;
landmark?: string;
pincode?: string;
city?: string;
state?: string;
country?: string; // Will be overridden to product's country
phoneNumber?: string;
};
receiver?: {
line1?: string;
line2?: string;
landmark?: string;
pincode?: string;
city?: string;
state?: string;
country?: string; // Will be overridden to product's country
phoneNumber?: string;
};
};
}
Validation:
- If
deliveryMethodis"COURIER", bothcourierServiceandtrackingNumberare required - The
courierServicemust match an enabled service in the system - The
countryin addresses is always set to the product's country regardless of input
Response: Created fulfillment object (201 status)
Example Request:
curl -X POST "http://localhost:3000/api/v1/fulfillments" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"tournament": "64f456def...",
"product": "64f789ghi...",
"winner": "user_winner_id",
"seller": "user_seller_id",
"deliveryMethod": "COURIER",
"courierService": "Bluedart",
"trackingNumber": "BD-123456",
"pickupAddresses": {
"seller": {
"line1": "123 Seller Street",
"city": "Mumbai",
"state": "Maharashtra",
"pincode": "400001",
"phoneNumber": "+919876543210"
},
"receiver": {
"line1": "456 Receiver Avenue",
"city": "Delhi",
"state": "Delhi",
"pincode": "110001",
"phoneNumber": "+919123456780"
}
}
}'
5. Add Seller Proof (by Fulfillment ID)
Update fulfillment with seller's delivery proof using the fulfillment ID.
Method: PATCH
Path: /api/v1/fulfillments/:id/seller-proof
URL Parameters:
id(required): MongoDB ObjectId of the fulfillment
Request Body: (all fields optional, but at least one required)
{
photos?: string[]; // Array of photo URLs
videos?: string[]; // Array of video URLs
sellerNotes?: string; // Seller's notes
deliveredAt?: string; // ISO date string (legacy, use dateOfDelivery)
dateOfDelivery?: string; // ISO date string (preferred)
courierService?: string; // Courier service name
trackingNumber?: string; // Tracking number
pickupAddresses?: { // Optional address update
seller?: { /* same structure as create */ };
receiver?: { /* same structure as create */ };
};
}
Behavior:
- Photos and videos are appended to existing
sellerMedia - Sets
statusto"PICKED_UP" - Automatically derives
deliveryMethodfrom product'sdeliveryfield:"COURIER"if product delivery isCOURIER_DOMESTICorCOURIER_INTERNATIONAL"IN_PERSON"otherwise
- If
deliveryMethodis"IN_PERSON", clearscourierServiceandtrackingNumber - If
deliveryMethodis"COURIER"andcourierServiceis provided, validates it against enabled services - Both
dateOfDeliveryand legacydeliveredAtare synced to the same value - Country in addresses is locked to product's country
- Sends notification to winner about seller's update
Response: Updated fulfillment object
Example Request:
curl -X PATCH "http://localhost:3000/api/v1/fulfillments/64f123abc.../seller-proof" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"photos": ["https://cdn.example.com/packed.jpg"],
"videos": ["https://cdn.example.com/packing-video.mp4"],
"sellerNotes": "Packed securely and handed to courier",
"dateOfDelivery": "2025-09-06T12:34:00.000Z",
"courierService": "Bluedart",
"trackingNumber": "BD-123456"
}'
6. Add Seller Proof (by Product ID)
Update fulfillment with seller's delivery proof using the product ID. Auto-creates fulfillment if needed.
Method: PATCH
Path: /api/v1/fulfillments/product/:productId/seller-proof
URL Parameters:
productId(required): MongoDB ObjectId of the product
Request Body: Same as endpoint #5 (by fulfillment ID)
Behavior:
- Identical to endpoint #5
- If no fulfillment exists, creates one automatically (tournament must be
OVER)
Response: Updated fulfillment object
Example Request:
curl -X PATCH "http://localhost:3000/api/v1/fulfillments/product/64f789ghi.../seller-proof" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"photos": ["https://cdn.example.com/shipped.jpg"],
"sellerNotes": "Package shipped via courier",
"dateOfDelivery": "2025-09-06T15:00:00.000Z",
"courierService": "FedEx",
"trackingNumber": "FDX987654321"
}'
Note: This is the preferred endpoint when working with products directly.
7. Add Receiver Proof (by Fulfillment ID)
Update fulfillment with receiver's confirmation using the fulfillment ID.
Method: PATCH
Path: /api/v1/fulfillments/:id/receiver-proof
URL Parameters:
id(required): MongoDB ObjectId of the fulfillment
Request Body: (all fields optional)
{
photos?: string[]; // Array of photo URLs
videos?: string[]; // Array of video URLs
videoUrl?: string; // Single confirmation video URL
notes?: string; // Receiver's notes
deliveredAt?: string; // ISO date string (legacy, use dateOfReceive)
dateOfReceive?: string; // ISO date string (preferred)
pickupAddresses?: { // Optional address finalization
seller?: { /* same structure as create */ };
receiver?: { /* same structure as create */ };
};
}
Behavior:
- Photos and videos are appended to existing
receiverMedia - Sets
receivedtotrue - Sets
statusto"RECEIVED" - Updates
receiverConfirmationobject with:confirmedAt: Current timestampvideoUrl: Provided or retains existingnotes: Provided or retains existing
- If
dateOfReceivenot provided, defaults to current time - Both
dateOfReceiveand legacydeliveredAtare synced - Country in addresses is locked to product's country
- Sends notification to seller about receiver's confirmation
Response: Updated fulfillment object
Example Request:
curl -X PATCH "http://localhost:3000/api/v1/fulfillments/64f123abc.../receiver-proof" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"videoUrl": "https://cdn.example.com/unboxing.mp4",
"photos": ["https://cdn.example.com/received.jpg"],
"notes": "Product received in excellent condition!",
"dateOfReceive": "2025-09-07T10:00:00.000Z"
}'
8. Add Receiver Proof (by Product ID)
Update fulfillment with receiver's confirmation using the product ID. Auto-creates fulfillment if needed.
Method: PATCH
Path: /api/v1/fulfillments/product/:productId/receiver-proof
URL Parameters:
productId(required): MongoDB ObjectId of the product
Request Body: Same as endpoint #7 (by fulfillment ID)
Behavior:
- Identical to endpoint #7
- If no fulfillment exists, creates one automatically (tournament must be
OVER)
Response: Updated fulfillment object
Example Request:
curl -X PATCH "http://localhost:3000/api/v1/fulfillments/product/64f789ghi.../receiver-proof" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"videoUrl": "https://cdn.example.com/confirmation.mp4",
"notes": "Everything looks great, thanks!",
"dateOfReceive": "2025-09-08T14:30:00.000Z"
}'
Note: This is the preferred endpoint when working with products directly.
Data Models
Fulfillment Object
{
_id: string; // MongoDB ObjectId
tournament: string | object; // Tournament ID or populated object
product: string | object; // Product ID or populated object
winner: string | object; // Winner user ID or populated user
seller: string; // Seller user ID
// Status fields
verificationStatus: "PENDING" | "VERIFIED" | "FAILED";
status: "PENDING" | "AWAITING_PICKUP" | "PICKED_UP" | "RECEIVED" | "PAID";
received: boolean;
// Media
sellerMedia: {
photos: string[]; // Array of photo URLs
videos: string[]; // Array of video URLs
};
receiverMedia: {
photos: string[]; // Array of photo URLs
videos: string[]; // Array of video URLs
};
// Receiver confirmation
receiverConfirmation: {
confirmedAt: Date | null;
videoUrl: string | null;
notes: string | null;
};
// Delivery details
deliveryMethod: "IN_PERSON" | "COURIER";
courierService: string | null;
trackingNumber: string | null;
// Dates
deliveredAt: Date | null; // Legacy field (synced with dateOfDelivery)
dateOfDelivery: Date | null; // When seller shipped/delivered (preferred)
dateOfReceive: Date | null; // When receiver confirmed receipt
pickupScheduledAt: Date | null;
pickupCompletedAt: Date | null;
// Notes
sellerNotes: string | null;
// Addresses (for self-pickup flow)
pickupAddresses: {
seller: {
line1: string;
line2: string;
landmark: string;
pincode: string;
city: string;
state: string;
country: string; // Always set to product's country
phoneNumber: string;
} | null;
receiver: {
line1: string;
line2: string;
landmark: string;
pincode: string;
city: string;
state: string;
country: string; // Always set to product's country
phoneNumber: string;
} | null;
} | null;
// Payout reference
payout: string | null; // Payout ID reference
// Wallet credit (internal use)
walletCredit: {
amount: number | null;
creditedAt: Date | null;
wallet: string | null; // Wallet ID reference
} | null;
// Timestamps
createdAt: Date;
updatedAt: Date;
}
Address Object
{
line1?: string;
line2?: string;
landmark?: string;
pincode?: string;
city?: string;
state?: string;
country?: string; // Always overridden to product's country
phoneNumber?: string;
}
Status Flow
Verification Status
PENDING: Initial state, awaiting verificationVERIFIED: Admin has verified the fulfillmentFAILED: Verification failed
Fulfillment Status
PENDING: Fulfillment created, no action taken yetAWAITING_PICKUP: Scheduled for pickup (rarely used)PICKED_UP: Seller has submitted proof of delivery/shipmentRECEIVED: Receiver has confirmed receiptPAID: Payout has been processed to seller
Typical Flow:
PENDING → PICKED_UP → RECEIVED → PAID
Important Notes
Auto-Creation Behavior
When you call GET /product/:productId, PATCH /product/:productId/seller-proof, or PATCH /product/:productId/receiver-proof:
- If fulfillment exists, it's returned/updated
- If not, system attempts auto-creation:
- Validates tournament status is
OVER - Finds winner from product's leaderboard (lowest non-zero score)
- For
SELF_PICKUPproducts, pre-fillspickupAddressesfrom user profiles - Links the fulfillment back to the product
- Validates tournament status is
Country Locking
The country field in pickupAddresses is always set to the product's listed country. Any country value sent by the client is ignored and overridden. This ensures delivery compliance with product listings.
Delivery Method
The deliveryMethod is automatically derived from the product's delivery field:
- Product delivery types
COURIER_DOMESTICorCOURIER_INTERNATIONAL→deliveryMethod: "COURIER" - Other types (including
SELF_PICKUP,DIGITAL) →deliveryMethod: "IN_PERSON"
Clients cannot manually override this - it's always set based on product configuration.
Courier Service Validation
If deliveryMethod is "COURIER" and a courierService is provided:
- The service name must exactly match an enabled courier service in the admin system
- Invalid or disabled services will result in a
400 BAD_REQUESTerror
Date Fields
- Legacy:
deliveredAt(kept for backward compatibility) - Preferred: Use
dateOfDelivery(seller) anddateOfReceive(receiver) - Both legacy and new fields are kept in sync automatically
Media Handling
sellerMedia: Photos/videos from seller showing proof of shipment/deliveryreceiverMedia: Photos/videos from receiver showing proof of receipt- Media arrays are appended to, not replaced, when updating
Notifications
- When seller adds proof → Winner receives notification
- When receiver confirms → Seller receives notification
Phone Numbers
The phoneNumber field in addresses is available for contact purposes during pickup/delivery coordination.
Error Responses
All endpoints return standard error responses:
401 Unauthorized
{
"error": "UNAUTHORIZED",
"message": "UNAUTHORIZED"
}
400 Bad Request
{
"error": "BAD_REQUEST",
"message": "Courier service not allowed"
}
404 Not Found
{
"error": "NOT_FOUND",
"message": "Product not found"
}
Examples of validation errors:
- Missing required fields when
deliveryMethodisCOURIER - Invalid MongoDB ObjectId format
- Tournament not in
OVERstatus - No winner found for product
- Courier service not enabled in system
Best Practices
For Frontend Developers
-
Use Product-based Endpoints: Prefer
/product/:productId/*endpoints over fulfillment ID-based ones for easier integration -
Handle Auto-Creation: The GET and PATCH endpoints auto-create fulfillments, so you don't need to call POST first
-
Upload Media First: Upload photos/videos to your CDN/storage service before calling these endpoints
-
Progressive Updates: You can call seller-proof or receiver-proof endpoints multiple times to add more media or update information
-
Status Indicators: Use the
statusfield to show progress to users:PENDING→ "Awaiting seller action"PICKED_UP→ "Shipped/handed over"RECEIVED→ "Confirmed by receiver"PAID→ "Completed"
-
Country Handling: Don't worry about setting the country - it's handled automatically from the product
-
Date Handling: Use
dateOfDeliveryanddateOfReceive(not the legacydeliveredAt) -
Role-based Views: Use
/api/fulfillments/me?role=sellerorrole=winnerto show user-specific fulfillments -
Courier Products: For courier deliveries, ensure you collect and submit both
courierServiceandtrackingNumber -
Error Handling: Always handle tournament status errors - fulfillments only work for ended tournaments
Migration Notes (Old → New)
Changes from previous implementation:
Field Renames
pickupMedia→receiverMedia(receiver-side media)
New Fields
sellerMedia: Seller-side photos/videos (separate from receiver media)dateOfDelivery: When seller shipped/delivered (replaces overloaded use ofdeliveredAt)dateOfReceive: When receiver confirmed (replaces overloaded use ofdeliveredAt)pickupAddresses.*.phoneNumber: Contact phone numbers
Behavior Changes
deliveryMethodnow strictly derived from product configurationcountryin addresses now strictly enforced to product country- Media arrays are now appended to, not replaced
Backward Compatibility
deliveredAtstill exists and is synced with the new date fields- Both
/by-product/:productIdand/product/:productIdpaths work deliveredAtcan still be sent in requests (maps todateOfDelivery)