Skip to content
Shipping

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:

PieceWhat it isIn this guide
LocationA place you ship from or where buyers pick up.Almacén Lima and Tienda Miraflores.
ProfileWhich locations serve a group of products.The store's default profile.
ZoneDestinations that share a price.Metropolitan Lima.
MethodThe option the buyer picks.Envío estándar and Retiro en tienda.
RateThe 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, and shipping_settings:update permissions in ECOMIQ_TOKEN. To assign products to a profile, also catalog: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 uses jq.

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
  }'
Response 201
{ "id": "7204558912004325378" }

Tienda Miraflores gets 7204558912004325380. The next steps reuse API and the headers. A repeated code returns 409 with Location.CodeExists.

FieldWhat it does
typewarehouse, store, or seller.
supportsShippingShips to customers' homes. Defaults to false.
supportsPickupAccepts pickups. Defaults to false.
supportsLocalDeliveryDelivers locally with its own couriers.
useForFulfillmentFulfills orders. Defaults to true.
isDefaultDefault location. Marking another one replaces it.
pickupInstructionsWhat the customer must show at pickup.
pickupLeadTimeInMinutesPreparation 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
  }'
Response (excerpt)
{
  "id": "7204558912004325430",
  "name": "Perfil general",
  "locationIds": ["7204558912004325378", "7204558912004325380"],
  "isDefault": true,
  "isActive": true
}
  • PUT replaces the whole profile: send every field.
  • Keep isDefault: true and isActive: true. You can't unset or deactivate the default profile: the API returns 400. To change it, mark another profile with isDefault: true.
  • Without locationIds, the profile uses every active location.
  • If you have no profiles, create one with POST /v1/shipping/profiles: the first one becomes the default.

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"]
  }'
Response 201
{ "id": "7204558912004325435" }
ListMatchesExample
countryCodesAddress country, in ISO 3166-1.PE
provinceCodesDepartment, by name or ubigeo code.Lima or 15
cityNamesProvince or city, by name or ubigeo code.Lima or 1501
districtNamesDistrict, by name or ubigeo code.Miraflores or 150122
postalCodesExact 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 /v1/geo/areas?countryCode=PE&type=level2&search=lima. 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"
  }'
Response 201
{ "id": "7204558912004325440" }

Retiro en tienda gets 7204558912004325442. A new method starts active.

typeWhat it offersShows up when
regular, express, sameDayHome deliveryA location in the profile has supportsShipping.
pickupPickuppickupEnabled is on and a location has supportsPickup or a pickup point exists.
localDeliveryLocal deliverylocalDeliveryEnabled is on and a location with supportsLocalDelivery covers the destination.
FieldWhat it does
shippingProfileIdThe method's profile. A profile with its own methods offers only those. One without methods offers the ones with no profile.
displayNameVisible name. Without it, name is used.
estimatedDeliveryDaysBusiness days in transit.
minOrderAmount, maxOrderAmountSubtotal range where the method is offered.
minWeight, maxWeightWeight range in kilos where it is offered.
visibleInCheckoutWith false, the method does not show in the cart. Defaults to true.
isActiveWith false, the method is paused. Defaults to true.
sortOrderOrder in the cart, lowest first.
pickupPointIdspickup 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" }
  }
}
ResourceEach translation has
MethoddisplayName, required.
Ratedescription, or null.
CustomizationdisplayName, required.
  • translations holds only the other languages, not the base one.
  • On update, omitting baseLanguage or translations (or sending null) 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 with description: null has 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.

tarifas.json
[
  {
    "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
Response 201
{ "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.

typePrice
flatprice.
priceRangeprice. Requires minOrderPrice or maxOrderPrice.
postalCodeprice. Requires postalCodes.
weightBasedbasePrice plus pricePerKg per kilo.
tableCostbasePrice, plus pricePerKg per kilo and pricePerUnit per unit.
freeShippingAlways 0.
FieldWhat it does
zoneCodeThe zone's code. Empty or general applies to any destination.
postalCodesLimits the rate to those postal codes.
minOrderPrice, maxOrderPriceSubtotal range. Both ends are included.
minWeight, maxWeightWeight range in kilos.
descriptionOption name in the cart. Without it, the method's displayName is used.
surchargeAmount added to the price.
discountPercentageDiscount 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 /v1/shipping/settings 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
  }'
Response
{
  "storeId": "7204558912004325300",
  "defaultLocationId": "7204558912004325378",
  "estimatedDeliveryEnabled": true,
  "estimatedDeliveryFulfillmentDays": 1,
  "pickupEnabled": true,
  "localDeliveryEnabled": false,
  "useLiveRates": false
}
FieldWhat it does
pickupEnabledOffers pickup methods.
localDeliveryEnabledOffers localDelivery methods.
estimatedDeliveryEnabledCalculates the estimated delivery date.
estimatedDeliveryFulfillmentDaysBusiness days of preparation.
useLiveRatesQuotes live with the method's carrier (connectionId). If the quote fails, the rates apply.
defaultLocationIdDefault 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 /v1/carts/{cartToken}/shipping-options 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:

Response (excerpt)
{
  "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 rates is an option. The customer picks one with PUT /v1/carts/{cartToken}/shipping-method: send shippingMethodId, rateId, target, targetId, and the group's shippingProfileId.
  • Zones by country, department, province, or district need the cart's shipping address. In Peru, send department, province, and district. For zones by ubigeo code, also send departmentCode, provinceCode, and districtCode. 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:

MissingCheck
Envío estándarAn active rate for that subtotal and weight, the address zone, and supportsShipping on Almacén Lima.
Retiro en tiendapickupEnabled, an active rate, and Tienda Miraflores in the profile with supportsPickup.
BothThe method's visibleInCheckout and the profile the product uses.

Change rates in a batch

POST /v1/shipping/rates/batch creates or changes up to 100 rates in one call. Each row has an action:

  • create needs shippingMethodId.
  • update needs shippingRateId and 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.

tarifas-lote.json
{
  "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
Response
{
  "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 zoneCode that does not exist. general leaves the rate without a zone.
  • A create row with the same method, type, zone, postal codes, and ranges as another rate fails. Repeating the batch does not duplicate rates.
  • Idempotency-Key is optional. Keep the key when you retry the same batch.

Import rates from a file

For hundreds of rates, upload a file to POST /v1/shipping/rates/imports. 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:

tarifas.csv
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
Response 202
{
  "id": "7204558912004325460",
  "status": "pending",
  "resource": "shipping-rates",
  "format": "csv",
  "createdOn": "2026-10-05T15:30:00Z"
}

Follow the operation with GET /v1/operations/{id}. 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:

ColumnWhat 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, surchargeAmounts. The Instructions tab shows which columns each type uses.
min_order_price, max_order_price, min_weight, max_weightRanges. In update, an empty cell keeps the value.
postal_codesComma-separated codes. In CSV, wrap them in quotes.
checkout_label (Checkout text)Option name in the cart.
is_active, tax_includedYes or No. Also accepts true, false, sí and no.
actionOptional: create or update. Without it, the Internal ID decides.

Get the template

POST /v1/shipping/rates/exports 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:

On this page