Skip to main content

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 country field in pickupAddresses is 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 deliveryMethod is "COURIER", both courierService and trackingNumber are required
  • The courierService must match an enabled service in the system
  • The country in 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 status to "PICKED_UP"
  • Automatically derives deliveryMethod from product's delivery field:
    • "COURIER" if product delivery is COURIER_DOMESTIC or COURIER_INTERNATIONAL
    • "IN_PERSON" otherwise
  • If deliveryMethod is "IN_PERSON", clears courierService and trackingNumber
  • If deliveryMethod is "COURIER" and courierService is provided, validates it against enabled services
  • Both dateOfDelivery and legacy deliveredAt are 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 received to true
  • Sets status to "RECEIVED"
  • Updates receiverConfirmation object with:
    • confirmedAt: Current timestamp
    • videoUrl: Provided or retains existing
    • notes: Provided or retains existing
  • If dateOfReceive not provided, defaults to current time
  • Both dateOfReceive and legacy deliveredAt are 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 verification
  • VERIFIED: Admin has verified the fulfillment
  • FAILED: Verification failed

Fulfillment Status

  • PENDING: Fulfillment created, no action taken yet
  • AWAITING_PICKUP: Scheduled for pickup (rarely used)
  • PICKED_UP: Seller has submitted proof of delivery/shipment
  • RECEIVED: Receiver has confirmed receipt
  • PAID: 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:

  1. If fulfillment exists, it's returned/updated
  2. If not, system attempts auto-creation:
    • Validates tournament status is OVER
    • Finds winner from product's leaderboard (lowest non-zero score)
    • For SELF_PICKUP products, pre-fills pickupAddresses from user profiles
    • Links the fulfillment back to the product

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_DOMESTIC or COURIER_INTERNATIONALdeliveryMethod: "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_REQUEST error

Date Fields

  • Legacy: deliveredAt (kept for backward compatibility)
  • Preferred: Use dateOfDelivery (seller) and dateOfReceive (receiver)
  • Both legacy and new fields are kept in sync automatically

Media Handling

  • sellerMedia: Photos/videos from seller showing proof of shipment/delivery
  • receiverMedia: 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 deliveryMethod is COURIER
  • Invalid MongoDB ObjectId format
  • Tournament not in OVER status
  • No winner found for product
  • Courier service not enabled in system

Best Practices

For Frontend Developers

  1. Use Product-based Endpoints: Prefer /product/:productId/* endpoints over fulfillment ID-based ones for easier integration

  2. Handle Auto-Creation: The GET and PATCH endpoints auto-create fulfillments, so you don't need to call POST first

  3. Upload Media First: Upload photos/videos to your CDN/storage service before calling these endpoints

  4. Progressive Updates: You can call seller-proof or receiver-proof endpoints multiple times to add more media or update information

  5. Status Indicators: Use the status field to show progress to users:

    • PENDING → "Awaiting seller action"
    • PICKED_UP → "Shipped/handed over"
    • RECEIVED → "Confirmed by receiver"
    • PAID → "Completed"
  6. Country Handling: Don't worry about setting the country - it's handled automatically from the product

  7. Date Handling: Use dateOfDelivery and dateOfReceive (not the legacy deliveredAt)

  8. Role-based Views: Use /api/fulfillments/me?role=seller or role=winner to show user-specific fulfillments

  9. Courier Products: For courier deliveries, ensure you collect and submit both courierService and trackingNumber

  10. Error Handling: Always handle tournament status errors - fulfillments only work for ended tournaments


Migration Notes (Old → New)

Changes from previous implementation:

Field Renames

  • pickupMediareceiverMedia (receiver-side media)

New Fields

  • sellerMedia: Seller-side photos/videos (separate from receiver media)
  • dateOfDelivery: When seller shipped/delivered (replaces overloaded use of deliveredAt)
  • dateOfReceive: When receiver confirmed (replaces overloaded use of deliveredAt)
  • pickupAddresses.*.phoneNumber: Contact phone numbers

Behavior Changes

  • deliveryMethod now strictly derived from product configuration
  • country in addresses now strictly enforced to product country
  • Media arrays are now appended to, not replaced

Backward Compatibility

  • deliveredAt still exists and is synced with the new date fields
  • Both /by-product/:productId and /product/:productId paths work
  • deliveredAt can still be sent in requests (maps to dateOfDelivery)