Marketplace Multi-Seller Shipping
This guide shows how to integrate Envia into a marketplace platform where multiple sellers ship orders to buyers. Your platform handles carrier selection and label generation on behalf of each seller.
What you'll build
flowchart LR Order["Buyer Places Order"] --> Route["Route to Seller"] --> Rate["Get Rates"] --> Label["Generate Label"] --> Track["Track and Notify"]
Your marketplace will:
- Let sellers fulfill orders using carriers available at their location
- Show buyers accurate shipping rates from the seller's origin
- Generate labels on behalf of sellers after purchase
- Provide unified tracking across all sellers and carriers
Architecture overview
Buyer places order (items from multiple sellers)
→ Your platform identifies each seller
→ Your backend calls Envia GET /carrier?country_code={seller.country}
→ Returns available carriers for the seller's location
→ Your backend calls Envia POST /ship/rate/ (using seller's address as origin)
→ Returns rates + delivery estimates per seller
→ Buyer selects rates and pays
→ Your backend calls Envia POST /ship/generate/ (one label per seller)
→ Returns tracking number + label PDF per seller
→ You email a unified tracking page to the buyer
→ Envia webhooks notify you of delivery status changes per shipment
Setup
All code examples below use these base URLs and token. Set ENVIA_ENV=production when you are ready to go live.
// Configuration — defaults to sandbox
const ENVIA_BASE = "https://api-test.envia.com";
const QUERIES_BASE = "https://queries-test.envia.com";
const GEOCODES_BASE = "https://geocodes.envia.com";
// Set ENVIA_TOKEN as an environment variable (see Authentication guide)# Configuration — defaults to sandbox
ENVIA_BASE = "https://api-test.envia.com"
QUERIES_BASE = "https://queries-test.envia.com"
GEOCODES_BASE = "https://geocodes.envia.com"
ENVIA_TOKEN = os.environ["ENVIA_TOKEN"]Step 1 — Discover carriers per seller location
Different sellers ship from different locations. Use the Queries API to find which carriers serve each seller's region.
async function getCarriersForSeller(seller) {
const response = await fetch(
QUERIES_BASE + "/carrier?country_code=" + seller.country,
{ headers: { Authorization: "Bearer " + process.env.ENVIA_TOKEN } }
);
return response.json();
}def get_carriers_for_seller(seller: dict) -> dict:
"""Check available carriers for a seller's country."""
response = requests.get(
f"{QUERIES_BASE}/carrier",
params={"country_code": seller["country"]},
headers={"Authorization": f"Bearer {ENVIA_TOKEN}"},
)
return response.json()Step 2 — Get rates from the seller's origin
When a buyer adds a product to their cart, fetch rates using the seller's address as origin.
async function getMarketplaceRates(seller, buyerAddress, packages) {
var origin = {
name: seller.businessName,
phone: seller.phone,
street: seller.street,
city: seller.city,
state: seller.state,
country: seller.country,
postalCode: seller.postalCode,
};
var carriers = seller.preferredCarriers || ["dhl", "fedex", "estafeta"];
var ratePromises = carriers.map(function (carrier) {
return fetch(ENVIA_BASE + "/ship/rate/", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ENVIA_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({
origin: origin,
destination: buyerAddress,
packages: packages,
shipment: { type: 1, carrier: carrier },
}),
}).then(function (r) { return r.json(); });
});
var results = await Promise.allSettled(ratePromises);
return results
.filter(function (r) { return r.status === "fulfilled" && r.value.data; })
.flatMap(function (r) { return r.value.data; })
.sort(function (a, b) { return parseFloat(a.totalPrice) - parseFloat(b.totalPrice); });
}def get_marketplace_rates(seller, buyer_address, packages):
"""Get rates using seller's address as origin."""
origin = {
"name": seller["business_name"],
"phone": seller["phone"],
"street": seller["street"],
"city": seller["city"],
"state": seller["state"],
"country": seller["country"],
"postalCode": seller["postal_code"],
}
carriers = seller.get("preferred_carriers", ["dhl", "fedex", "estafeta"])
rates = []
for carrier in carriers:
response = requests.post(
f"{ENVIA_BASE}/ship/rate/",
headers={"Authorization": f"Bearer {ENVIA_TOKEN}"},
json={
"origin": origin,
"destination": buyer_address,
"packages": packages,
"shipment": {"type": 1, "carrier": carrier},
},
)
data = response.json()
if "data" in data and data["data"]:
rates.extend(data["data"])
return sorted(rates, key=lambda r: float(r.get("totalPrice", 0)))Step 3 — Handle multi-seller orders
When a buyer purchases from multiple sellers in one order, each seller ships their items separately. Your platform generates one label per seller.
async function fulfillMultiSellerOrder(order) {
var sellerGroups = groupBySeller(order.items);
var shipments = [];
var sellerIds = Object.keys(sellerGroups);
for (var i = 0; i < sellerIds.length; i++) {
var sellerId = sellerIds[i];
var items = sellerGroups[sellerId];
var seller = await getSeller(sellerId);
var packages = buildPackages(items);
var label = await fetch(ENVIA_BASE + "/ship/generate/", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ENVIA_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({
origin: seller.address,
destination: order.shippingAddress,
packages: packages,
shipment: {
type: 1,
carrier: order.selectedCarriers[sellerId],
service: order.selectedServices[sellerId],
},
}),
}).then(function (r) { return r.json(); });
shipments.push({
sellerId: sellerId,
trackingNumber: label.data[0].trackingNumber,
labelUrl: label.data[0].label,
});
}
return shipments;
}# Generate a label for each seller in the order
curl --request POST \
--url https://api-test.envia.com/ship/generate/ \
--header "Authorization: Bearer $ENVIA_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"origin": {
"name": "Seller A Store",
"phone": "+52 8180000000",
"street": "Av. Benito Juarez",
"city": "Monterrey",
"state": "NL",
"country": "MX",
"postalCode": "64060"
},
"destination": {
"name": "Buyer Name",
"phone": "+52 5511111111",
"street": "Insurgentes Sur",
"city": "Ciudad de Mexico",
"state": "CX",
"country": "MX",
"postalCode": "03100"
},
"packages": [{
"type": "box",
"content": "Products from Seller A",
"amount": 1,
"declaredValue": 1200,
"lengthUnit": "CM",
"weightUnit": "KG",
"weight": 2.5,
"dimensions": { "length": 30, "width": 20, "height": 10 }
}],
"shipment": { "type": 1, "carrier": "dhl", "service": "ground" }
}'Step 4 — Unified tracking for buyers
Register a webhook to receive tracking updates across all carriers and sellers. Present a unified tracking page to the buyer.
curl --request POST \
--url https://queries-test.envia.com/webhooks \
--header "Authorization: Bearer $ENVIA_TOKEN" \
--header "Content-Type: application/json" \
--data '{"type_id": 3, "url": "https://your-marketplace.com/webhooks/tracking", "active": 1}'app.post("/webhooks/tracking", function (req, res) {
res.status(200).json({ received: true });
var trackingNumber = req.body.data.trackingNumber;
var status = req.body.data.status;
updateOrderTracking(trackingNumber, status);
notifyBuyer(trackingNumber, status);
});Marketplace-specific considerations
| Concern | Recommendation |
|---|---|
| Who pays for shipping? | Use a single Envia account for your marketplace. Charge sellers via your platform settlement process. |
| Seller onboarding | Collect and validate seller addresses during onboarding using the Geocodes API. |
| Carrier preferences | Let sellers set preferred carriers. Fall back to all available carriers. |
| International sellers | Use the International Shipping Workflow for cross-border orders. |
| Returns | Use the Cancel Shipment endpoint to void labels before carrier scan. Refunds are applied automatically to your account balance. |
Related pages
- Core Workflow — Full request/response examples for every shipping step
- International Shipping — Cross-border customs and HS codes
- Webhooks Guide — Real-time tracking notifications
- Production Readiness Checklist — Go-live checklist
Updated 4 months ago
