Zones and rates
Set up locations, a profile, a zone, methods, and rates for home delivery and in-store pickup.
A store in Lima offers home delivery from its warehouse and free pickup at its physical store. This is how you set it up with the Admin API.
How it works
Five pieces decide which shipping options the buyer sees:
| Piece | What it is | In this guide |
|---|---|---|
| Location | A place you ship from or where buyers pick up. | Almacén Lima and Tienda Miraflores. |
| Profile | Which locations serve a group of products. | The store's default profile. |
| Zone | Destinations that share a price. | Metropolitan Lima. |
| Method | The option the buyer picks. | Envío estándar and Retiro en tienda. |
| Rate | The price of a method in a zone. | S/ 10, free from S/ 150. |
A method without a rate does not show up in the cart.
Requirements
- An Admin token with the
settings:read,settings:update, andshipping_settings:updatepermissions inECOMIQ_TOKEN. To assign products to a profile, alsocatalog:update. See Quickstart. - Products with a weight, if you charge by the kilo.
- Node.js 18 or later and Python 3 with
requests, for the examples. The cURL example in Create the rates usesjq.
Create the locations
A location ships orders, accepts pickups, or both. Almacén Lima ships to
customers' homes. Tienda Miraflores is the pickup point. If they already exist,
find them with
List locations
and use their id.
curl -X POST https://api.ecomiq.pe/api/v1/fulfillment/locations \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Almacén Lima",
"code": "ALM-LIMA",
"type": "warehouse",
"address": "Av. Argentina 2450",
"city": "Lima",
"state": "Lima",
"country": "PE",
"postalCode": "15081",
"isDefault": true,
"supportsShipping": true
}'
curl -X POST https://api.ecomiq.pe/api/v1/fulfillment/locations \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Tienda Miraflores",
"code": "TDA-MIRAFLORES",
"type": "store",
"address": "Calle Berlín 245",
"city": "Lima",
"state": "Lima",
"country": "PE",
"postalCode": "15074",
"isPhysicalStore": true,
"supportsPickup": true,
"pickupInstructions": "Presenta tu DNI y el número de pedido en caja.",
"pickupLeadTimeInMinutes": 120
}'{ "id": "7204558912004325378" }Tienda Miraflores gets 7204558912004325380. The next steps reuse API and
the headers. A repeated code returns 409 with Location.CodeExists.
| Field | What it does |
|---|---|
type | warehouse, store, or seller. |
supportsShipping | Ships to customers' homes. Defaults to false. |
supportsPickup | Accepts pickups. Defaults to false. |
supportsLocalDelivery | Delivers locally with its own couriers. |
useForFulfillment | Fulfills orders. Defaults to true. |
isDefault | Default location. Marking another one replaces it. |
pickupInstructions | What the customer must show at pickup. |
pickupLeadTimeInMinutes | Preparation time before the order is ready. |
Load the stock of each location with Inventory.
Assign the locations to the profile
A profile groups products and sets the locations they ship from. Your store
already has a default profile: products without an assigned profile use it.
Find it by isDefault: true and set its locationIds.
# 1. Find the profile with "isDefault": true
curl https://api.ecomiq.pe/api/v1/shipping/profiles \
-H "Authorization: Bearer $ECOMIQ_TOKEN"
# 2. Set its locations
curl -X PUT https://api.ecomiq.pe/api/v1/shipping/profiles/7204558912004325430 \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Perfil general",
"description": "Sale de Almacén Lima o se retira en Tienda Miraflores.",
"locationIds": ["7204558912004325378", "7204558912004325380"],
"isDefault": true,
"isActive": true
}'{
"id": "7204558912004325430",
"name": "Perfil general",
"locationIds": ["7204558912004325378", "7204558912004325380"],
"isDefault": true,
"isActive": true
}PUTreplaces the whole profile: send every field.- Keep
isDefault: trueandisActive: true. You can't unset or deactivate the default profile: the API returns400. To change it, mark another profile withisDefault: true. - Without
locationIds, the profile uses every active location. - If you have no profiles, create one with
POST /: the first one becomes the default.v1/ shipping/ profiles
To ship a product with another profile, create that profile and assign the
product to it. The response is 204. DELETE on the same path returns the
product to the default profile.
curl -X POST https://api.ecomiq.pe/api/v1/shipping/profiles/7204558912004325431/products \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "productIds": ["7204558912004325302"] }'Create the zone
A zone groups destinations that share rates. Metropolitan Lima is the province of Lima, in the department of Lima. Callao is left out: it is a separate department.
curl -X POST https://api.ecomiq.pe/api/v1/shipping/zones \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"code": "LIMA",
"name": "Lima Metropolitana",
"description": "Provincia de Lima.",
"countryCodes": ["PE"],
"provinceCodes": ["Lima"],
"cityNames": ["Lima"]
}'{ "id": "7204558912004325435" }| List | Matches | Example |
|---|---|---|
countryCodes | Address country, in ISO 3166-1. | PE |
provinceCodes | Department, by name or ubigeo code. | Lima or 15 |
cityNames | Province or city, by name or ubigeo code. | Lima or 1501 |
districtNames | District, by name or ubigeo code. | Miraflores or 150122 |
postalCodes | Exact postal code. | 15074 |
- Fill at least one list.
- Within a list, one matching value is enough.
- If you fill several lists, the destination must match all of them.
- Matching is case-insensitive.
Look up names and ubigeo codes with
List areas:
GET /. level1 is the
department, level2 the province, and level3 the district.
code identifies the zone in rates and is unique in the store. Without a
code, the API generates one such as ZON-001. A repeated one returns 409
with ShippingZone.CodeExists.
With isActive: false, the zone's rates stop applying.
Create the methods
A method is what the customer chooses. Create Envío estándar and Retiro en tienda in the profile from Assign the locations to the profile.
curl -X POST https://api.ecomiq.pe/api/v1/shipping/methods \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Envío estándar",
"baseLanguage": "es",
"type": "regular",
"basePrice": 10,
"currency": "PEN",
"estimatedDeliveryDays": 2,
"sortOrder": 1,
"shippingProfileId": "7204558912004325430"
}'
curl -X POST https://api.ecomiq.pe/api/v1/shipping/methods \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Retiro en tienda",
"baseLanguage": "es",
"type": "pickup",
"basePrice": 0,
"currency": "PEN",
"sortOrder": 2,
"shippingProfileId": "7204558912004325430"
}'{ "id": "7204558912004325440" }Retiro en tienda gets 7204558912004325442. A new method starts active.
type | What it offers | Shows up when |
|---|---|---|
regular, express, sameDay | Home delivery | A location in the profile has supportsShipping. |
pickup | Pickup | pickupEnabled is on and a location has supportsPickup or a pickup point exists. |
localDelivery | Local delivery | localDeliveryEnabled is on and a location with supportsLocalDelivery covers the destination. |
| Field | What it does |
|---|---|
shippingProfileId | The method's profile. A profile with its own methods offers only those. One without methods offers the ones with no profile. |
displayName | Visible name. Without it, name is used. |
estimatedDeliveryDays | Business days in transit. |
minOrderAmount, maxOrderAmount | Subtotal range where the method is offered. |
minWeight, maxWeight | Weight range in kilos where it is offered. |
visibleInCheckout | With false, the method does not show in the cart. Defaults to true. |
isActive | With false, the method is paused. Defaults to true. |
sortOrder | Order in the cart, lowest first. |
pickupPointIds | pickup only: limits the pickup points. Empty offers all of them. |
For shipping and pickup, the customer pays the rate price, not basePrice. To
hide or rename a method by amount, weight, day, or time, use
Create shipping customization.
Pickup points
A pickup method offers each location in the profile with supportsPickup.
Create a pickup point only for a place you do not run, such as a courier
agency, with Create pickup
point.
A point with a locationId replaces that location in the cart: the customer
sees it once.
Translate visible text
Shipping methods, rates, and customizations store baseLanguage and
translations. On a method, name is internal and displayName is what the
buyer sees.
{
"baseLanguage": "es",
"displayName": "Envío estándar",
"translations": {
"en": { "displayName": "Standard delivery" },
"pt": { "displayName": "Entrega padrão" }
}
}| Resource | Each translation has |
|---|---|
| Method | displayName, required. |
| Rate | description, or null. |
| Customization | displayName, required. |
translationsholds only the other languages, not the base one.- On update, omitting
baseLanguageortranslations(or sendingnull) keeps the value.translations: {}removes them all. - The exact language wins, then the primary language, then the base text:
pt-BR → pt → base. A rate translation withdescription: nullhas no description in that language. - Carts use the store's language; Admin drafts use the member's.
Create the rates
A rate sets the price of a method. Without a matching rate, the method does not show in the cart. Envío estándar costs S/ 10 in Metropolitan Lima and is free from S/ 150. Retiro en tienda is free.
[
{
"baseLanguage": "es",
"shippingMethodId": "7204558912004325440",
"type": "flat",
"zoneCode": "LIMA",
"price": 10,
"maxOrderPrice": 149.99,
"currency": "PEN",
"description": "Envío a Lima Metropolitana"
},
{
"baseLanguage": "es",
"shippingMethodId": "7204558912004325440",
"type": "freeShipping",
"zoneCode": "LIMA",
"minOrderPrice": 150,
"currency": "PEN",
"description": "Envío gratis desde S/ 150"
},
{
"baseLanguage": "es",
"shippingMethodId": "7204558912004325442",
"type": "freeShipping",
"currency": "PEN",
"description": "Retiro gratis"
}
]jq -c '.[]' tarifas.json | while read -r rate; do
curl -X POST https://api.ecomiq.pe/api/v1/shipping/rates \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d "$rate"
done{ "id": "7204558912004325441" }The other two get 7204558912004325443 and 7204558912004325444.
Every matching rate is one more option in the cart. That is why the S/ 10 rate
ends at maxOrderPrice: 149.99: from S/ 150, only the free one remains.
type | Price |
|---|---|
flat | price. |
priceRange | price. Requires minOrderPrice or maxOrderPrice. |
postalCode | price. Requires postalCodes. |
weightBased | basePrice plus pricePerKg per kilo. |
tableCost | basePrice, plus pricePerKg per kilo and pricePerUnit per unit. |
freeShipping | Always 0. |
| Field | What it does |
|---|---|
zoneCode | The zone's code. Empty or general applies to any destination. |
postalCodes | Limits the rate to those postal codes. |
minOrderPrice, maxOrderPrice | Subtotal range. Both ends are included. |
minWeight, maxWeight | Weight range in kilos. |
description | Option name in the cart. Without it, the method's displayName is used. |
surcharge | Amount added to the price. |
discountPercentage | Discount on the price, from 0 to 100. |
Copy the zone code exactly. One that does not exist returns 404 with
ShippingZone.NotFound. A rate with the same method, type, zone, postal codes,
and ranges as another returns 409 with ShippingRate.Duplicate. More fields
in
Create shipping rate.
Adjust the store's shipping settings
PATCH / changes only the fields you send. Turn on pickup
and the estimated delivery date.
curl -X PATCH https://api.ecomiq.pe/api/v1/shipping/settings \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"defaultLocationId": "7204558912004325378",
"pickupEnabled": true,
"estimatedDeliveryEnabled": true,
"estimatedDeliveryFulfillmentDays": 1
}'{
"storeId": "7204558912004325300",
"defaultLocationId": "7204558912004325378",
"estimatedDeliveryEnabled": true,
"estimatedDeliveryFulfillmentDays": 1,
"pickupEnabled": true,
"localDeliveryEnabled": false,
"useLiveRates": false
}| Field | What it does |
|---|---|
pickupEnabled | Offers pickup methods. |
localDeliveryEnabled | Offers localDelivery methods. |
estimatedDeliveryEnabled | Calculates the estimated delivery date. |
estimatedDeliveryFulfillmentDays | Business days of preparation. |
useLiveRates | Quotes live with the method's carrier (connectionId). If the quote fails, the rates apply. |
defaultLocationId | Default shipping location. It must be active. |
The estimated date adds the preparation days and the method's
estimatedDeliveryDays, in business days. It skips Saturdays, Sundays, and
holidays. An order placed on Monday, October 5, 2026 adds 1 + 2 business days.
Thursday the 8th is a holiday, so the estimated date is Friday the 9th.
A new store comes with Peru's national holidays for that year and the next. An order scheduled for a holiday offers no pickup or local delivery. Add your own closures with Create non-working date range:
curl -X POST https://api.ecomiq.pe/api/v1/shipping/holidays \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Inventario anual",
"fromDate": "2026-12-30",
"toDate": "2026-12-31"
}'Check the cart
The customer sees the options in GET /
of the Storefront API. That route takes no token: the store goes in
x-Store-Domain. The full flow is in
Headless store.
curl https://storefront.ecomiq.pe/v1/carts/<cart-token>/shipping-options \
-H "x-Store-Domain: mi-tienda"This cart holds one Polo de algodón at S/ 79 and an address in Miraflores:
{
"groups": [
{
"shippingProfileId": "7204558912004325430",
"orderAmount": 79,
"rates": [
{
"shippingMethodId": "7204558912004325440",
"name": "Envío a Lima Metropolitana",
"displayName": "Envío estándar",
"type": "regular",
"target": "automatic",
"targetId": null,
"price": 10,
"currency": "PEN",
"rateId": "7204558912004325441",
"estimatedDeliveryFromDate": "2026-10-09"
},
{
"shippingMethodId": "7204558912004325442",
"name": "Retiro gratis",
"displayName": "Retiro en tienda",
"type": "pickup",
"target": "location",
"targetId": "7204558912004325380",
"targetName": "Tienda Miraflores",
"price": 0,
"currency": "PEN",
"rateId": "7204558912004325444"
}
]
}
]
}- The cart groups items by profile. Each group has its own options.
- Each item in
ratesis an option. The customer picks one withPUT /: sendv1/ carts/ {cartToken}/ shipping-method shippingMethodId,rateId,target,targetId, and the group'sshippingProfileId. - Zones by country, department, province, or district need the cart's shipping
address. In Peru, send
department,province, anddistrict. For zones by ubigeo code, also senddepartmentCode,provinceCode, anddistrictCode. The cart keeps them for later requests. - With only
postalCode, rates without a zone and zones by postal code apply.
If an option is missing, check:
| Missing | Check |
|---|---|
| Envío estándar | An active rate for that subtotal and weight, the address zone, and supportsShipping on Almacén Lima. |
| Retiro en tienda | pickupEnabled, an active rate, and Tienda Miraflores in the profile with supportsPickup. |
| Both | The method's visibleInCheckout and the profile the product uses. |
Change rates in a batch
POST / creates or changes up to 100 rates in one call.
Each row has an action:
createneedsshippingMethodId.updateneedsshippingRateIdand changes only the fields you send.
Values go as text, the same as in a file: "12.00", not 12. This batch
creates a rate for Callao and raises the Lima rate to S/ 12.
{
"rows": [
{
"action": "create",
"shippingMethodId": "7204558912004325440",
"type": "flat",
"zoneCode": "CALLAO",
"price": "12.00",
"currency": "PEN",
"description": "Envío al Callao"
},
{
"action": "update",
"shippingRateId": "7204558912004325441",
"price": "12.00"
}
]
}curl -X POST https://api.ecomiq.pe/api/v1/shipping/rates/batch \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tarifas-2026-10-05" \
--data @tarifas-lote.json{
"results": [
{
"index": 0,
"id": null,
"status": "failed",
"errors": [
{
"field": null,
"code": "ShippingRates.ImportInvalidRows",
"message": "Row 1: zone_code 'CALLAO' was not found."
}
]
},
{
"index": 1,
"id": "7204558912004325441",
"status": "updated",
"errors": []
}
]
}200 returns one result per row, in the order received. Valid rows are
applied even if others fail. Here the CALLAO zone is missing: create it as in Create the zone and resend only that row.
- The batch rejects a
zoneCodethat does not exist.generalleaves the rate without a zone. - A
createrow with the same method, type, zone, postal codes, and ranges as another rate fails. Repeating the batch does not duplicate rates. Idempotency-Keyis optional. Keep the key when you retry the same batch.
Import rates from a file
For hundreds of rates, upload a file to POST /. It
works like Product import: it returns
202 with an operation, you follow it until it finishes, and you read the
report of rejected rows. Formats and limits are the same.
This file raises the free shipping threshold to S/ 200:
shipping_rate_id,min_order_price,max_order_price
7204558912004325441,,199.99
7204558912004325443,200,curl -X POST https://api.ecomiq.pe/api/v1/shipping/rates/imports \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: text/csv" \
-H "Idempotency-Key: umbral-gratis-2026-10-05" \
--data-binary @tarifas.csv{
"id": "7204558912004325460",
"status": "pending",
"resource": "shipping-rates",
"format": "csv",
"createdOn": "2026-10-05T15:30:00Z"
}Follow the operation with GET /. Add ?validateOnly=true
to the URL to check the file without changing rates.
Headers can be the template's (in Spanish or English) or these keys:
| Column | What it holds |
|---|---|
shipping_method (Shipping method) | Method name, as in the Lists tab, or its ID. Required to create. |
shipping_rate_id (Internal ID) | Without an ID the rate is created. With an ID that rate is updated. |
type (Rate type) | Fixed amount, Free, By weight, Weight + items, By order price or By postal codes. Also accepts Flat, FreeShipping, WeightBased, TableCost, PriceRange and PostalCode. Empty: Fixed amount. |
zone (Zone) | Zone name or code. Empty, general or All zones: every zone. |
price, base_price, price_per_kg, price_per_unit, surcharge | Amounts. The Instructions tab shows which columns each type uses. |
min_order_price, max_order_price, min_weight, max_weight | Ranges. In update, an empty cell keeps the value. |
postal_codes | Comma-separated codes. In CSV, wrap them in quotes. |
checkout_label (Checkout text) | Option name in the cart. |
is_active, tax_included | Yes or No. Also accepts true, false, sí and no. |
action | Optional: create or update. Without it, the Internal ID decides. |
Get the template
POST / with { "format": "xlsx", "template": true }
builds the template: the Rates tab with the columns, Instructions with what to
enter in each one, and Lists with your methods, your zones and the types. It is
an operation: when it finishes, download its resultUrl.
Next steps
Last updated: