Saltar al contenido
Envíos

Zonas y tarifas

Configura ubicaciones, perfil, zona, métodos y tarifas para envío a domicilio y retiro en tienda.

Una tienda en Lima ofrece envío a domicilio desde su almacén y retiro gratis en su tienda física. Así se configura con Admin API.

Cómo funciona

Cinco piezas deciden qué opciones de envío ve el comprador:

PiezaQué esEn esta guía
UbicaciónUn lugar desde donde despachas o donde se retira.Almacén Lima y Tienda Miraflores.
PerfilQué ubicaciones atienden a un grupo de productos.El perfil predeterminado de la tienda.
ZonaLos destinos que comparten precio.Lima Metropolitana.
MétodoLa opción que elige el comprador.Envío estándar y Retiro en tienda.
TarifaEl precio de un método en una zona.S/ 10, gratis desde S/ 150.

Un método sin tarifa no aparece en el carrito.

Requisitos

  • Un token de Admin con los permisos settings:read, settings:update y shipping_settings:update en ECOMIQ_TOKEN. Para asignar productos a un perfil, también catalog:update. Ver Inicio rápido.
  • Productos con peso, si vas a cobrar por kilo.
  • Node.js 18 o superior y Python 3 con requests, para los ejemplos. El cURL de Crea las tarifas usa jq.

Crea las ubicaciones

Una ubicación despacha envíos, acepta retiros o ambas cosas. Almacén Lima despacha a domicilio. Tienda Miraflores es el punto de retiro. Si ya existen, búscalas con Listar ubicaciones logísticas y usa su 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
  }'
Respuesta 201
{ "id": "7204558912004325378" }

Tienda Miraflores recibe 7204558912004325380. Los pasos siguientes reusan API y las cabeceras. Un code repetido responde 409 con Location.CodeExists.

CampoQué hace
typewarehouse, store o seller.
supportsShippingDespacha envíos a domicilio. Por defecto false.
supportsPickupAcepta retiros. Por defecto false.
supportsLocalDeliveryHace entregas locales con su propio reparto.
useForFulfillmentPrepara pedidos. Por defecto true.
isDefaultUbicación predeterminada. Marcar otra la reemplaza.
pickupInstructionsLo que el comprador debe presentar al retirar.
pickupLeadTimeInMinutesTiempo de preparación antes de que el pedido esté listo.

Carga el stock de cada ubicación con Inventario.

Asigna las ubicaciones al perfil

Un perfil agrupa productos y define desde qué ubicaciones salen. Tu tienda ya tiene un perfil predeterminado: lo usan los productos sin perfil asignado. Búscalo por isDefault: true y fija sus locationIds.

# 1. Busca el perfil con "isDefault": true
curl https://api.ecomiq.pe/api/v1/shipping/profiles \
  -H "Authorization: Bearer $ECOMIQ_TOKEN"

# 2. Fija sus ubicaciones
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
  }'
Respuesta (extracto)
{
  "id": "7204558912004325430",
  "name": "Perfil general",
  "locationIds": ["7204558912004325378", "7204558912004325380"],
  "isDefault": true,
  "isActive": true
}
  • PUT reemplaza el perfil completo: envía todos los campos.
  • Mantén isDefault: true e isActive: true. El perfil predeterminado no se desmarca ni se desactiva: la API responde 400. Para cambiarlo, marca otro perfil con isDefault: true.
  • Sin locationIds, el perfil usa todas las ubicaciones activas.
  • Si no tienes perfiles, crea uno con POST /v1/shipping/profiles: el primero queda como predeterminado.

Para que un producto salga con otro perfil, crea ese perfil y asígnale el producto. La respuesta es 204. DELETE en la misma ruta lo devuelve al perfil predeterminado.

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"] }'

Crea la zona

Una zona agrupa destinos con las mismas tarifas. Lima Metropolitana es la provincia de Lima, en el departamento de Lima. El Callao queda fuera: es otro departamento.

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"]
  }'
Respuesta 201
{ "id": "7204558912004325435" }
ListaCompara conEjemplo
countryCodesPaís de la dirección, en ISO 3166-1.PE
provinceCodesDepartamento, por nombre o ubigeo.Lima o 15
cityNamesProvincia o ciudad, por nombre o ubigeo.Lima o 1501
districtNamesDistrito, por nombre o ubigeo.Miraflores o 150122
postalCodesCódigo postal exacto.15074
  • Llena al menos una lista.
  • Dentro de una lista, basta que coincida un valor.
  • Si llenas varias listas, el destino debe cumplir todas.
  • No distingue mayúsculas.

Busca nombres y ubigeos con Listar áreas geográficas: GET /v1/geo/areas?countryCode=PE&type=level2&search=lima. level1 es el departamento, level2 la provincia y level3 el distrito.

code identifica la zona en las tarifas y es único en la tienda. Sin code, la API genera uno como ZON-001. Uno repetido responde 409 con ShippingZone.CodeExists.

Con isActive: false, las tarifas de la zona dejan de aplicar.

Crea los métodos

Un método es lo que elige el comprador. Crea Envío estándar y Retiro en tienda en el perfil de Asigna las ubicaciones al perfil.

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"
  }'
Respuesta 201
{ "id": "7204558912004325440" }

Retiro en tienda recibe 7204558912004325442. Un método nuevo queda activo.

typeQué ofreceAparece si
regular, express, sameDayEnvío a domicilioUna ubicación del perfil tiene supportsShipping.
pickupRetiropickupEnabled está activo y hay una ubicación con supportsPickup o un punto de retiro.
localDeliveryEntrega locallocalDeliveryEnabled está activo y una ubicación con supportsLocalDelivery cubre el destino.
CampoQué hace
shippingProfileIdPerfil del método. Un perfil con métodos propios solo ofrece esos. Uno sin métodos ofrece los que no tienen perfil.
displayNameNombre visible. Si falta, se usa name.
estimatedDeliveryDaysDías hábiles de tránsito.
minOrderAmount, maxOrderAmountSubtotal en el que se ofrece el método.
minWeight, maxWeightPeso en kilos en el que se ofrece.
visibleInCheckoutCon false, el método no sale en el carrito. Por defecto true.
isActiveCon false, el método se pausa. Por defecto true.
sortOrderOrden en el carrito, de menor a mayor.
pickupPointIdsSolo en pickup: limita los puntos de retiro. Vacío ofrece todos.

En envíos y retiros, el comprador paga el precio de la tarifa, no basePrice. Para ocultar o renombrar un método según monto, peso, día u hora, usa Crear personalización de envío.

Puntos de retiro

Un método pickup ofrece cada ubicación del perfil con supportsPickup. Crea un punto de retiro solo para un lugar que no es tuyo, como la agencia de un courier, con Crear punto de retiro. Un punto con locationId reemplaza a esa ubicación en el carrito: el comprador la ve una sola vez.

Traduce los textos visibles

Métodos, tarifas y personalizaciones de envío guardan baseLanguage y translations. En un método, name es interno y displayName es lo que ve el comprador.

{
  "baseLanguage": "es",
  "displayName": "Envío estándar",
  "translations": {
    "en": { "displayName": "Standard delivery" },
    "pt": { "displayName": "Entrega padrão" }
  }
}
RecursoCada traducción lleva
MétododisplayName, obligatorio.
Tarifadescription, o null.
PersonalizacióndisplayName, obligatorio.
  • translations lleva solo los otros idiomas, no el base.
  • Al actualizar, omitir baseLanguage o translations (o enviarlos null) conserva el valor. translations: {} borra todas.
  • Se elige el idioma exacto, luego el principal y luego el texto base: pt-BR → pt → base. Una tarifa con description: null en una traducción queda sin descripción en ese idioma.
  • El carrito usa el idioma de la tienda; los borradores de Admin, el del miembro.

Crea las tarifas

La tarifa pone el precio de un método. Sin una tarifa que aplique, el método no sale en el carrito. Envío estándar cuesta S/ 10 en Lima Metropolitana y es gratis desde S/ 150. Retiro en tienda es gratis.

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
Respuesta 201
{ "id": "7204558912004325441" }

Las otras dos reciben 7204558912004325443 y 7204558912004325444.

Cada tarifa que aplica es una opción más en el carrito. Por eso la de S/ 10 termina en maxOrderPrice: 149.99: desde S/ 150 solo queda la gratuita.

typePrecio
flatprice.
priceRangeprice. Exige minOrderPrice o maxOrderPrice.
postalCodeprice. Exige postalCodes.
weightBasedbasePrice más pricePerKg por kilo.
tableCostbasePrice, más pricePerKg por kilo y pricePerUnit por unidad.
freeShippingSiempre 0.
CampoQué hace
zoneCodecode de la zona. Vacío o general vale para cualquier destino.
postalCodesLimita la tarifa a esos códigos postales.
minOrderPrice, maxOrderPriceRango del subtotal. Incluye ambos extremos.
minWeight, maxWeightRango de peso en kilos.
descriptionNombre de la opción en el carrito. Si falta, se usa el displayName del método.
surchargeMonto que se suma al precio.
discountPercentageDescuento sobre el precio, de 0 a 100.

Copia el code de la zona tal cual. Uno que no existe responde 404 con ShippingZone.NotFound. Una tarifa con el mismo método, tipo, zona, códigos postales y rangos que otra responde 409 con ShippingRate.Duplicate. Más campos en Crear tarifa de envío.

Ajusta los envíos de la tienda

PATCH /v1/shipping/settings cambia solo los campos que envías. Activa el retiro y la fecha estimada de entrega.

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
  }'
Respuesta
{
  "storeId": "7204558912004325300",
  "defaultLocationId": "7204558912004325378",
  "estimatedDeliveryEnabled": true,
  "estimatedDeliveryFulfillmentDays": 1,
  "pickupEnabled": true,
  "localDeliveryEnabled": false,
  "useLiveRates": false
}
CampoQué hace
pickupEnabledOfrece los métodos pickup.
localDeliveryEnabledOfrece los métodos localDelivery.
estimatedDeliveryEnabledCalcula la fecha estimada de entrega.
estimatedDeliveryFulfillmentDaysDías hábiles de preparación.
useLiveRatesCotiza en vivo con el transportista del método (connectionId). Si falla, usa las tarifas.
defaultLocationIdUbicación predeterminada de envíos. Debe estar activa.

La fecha estimada suma los días de preparación y los estimatedDeliveryDays del método, en días hábiles. Salta sábados, domingos y feriados. Un pedido del lunes 5 de octubre de 2026 suma 1 + 2 días hábiles. El jueves 8 es feriado, así que la fecha estimada es el viernes 9.

Una tienda nueva trae los feriados nacionales del Perú de ese año y del siguiente. Un pedido programado para un feriado no ofrece retiro ni entrega local. Agrega tus cierres con Crear rango no laborable:

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"
  }'

Revisa el carrito

El comprador ve las opciones en GET /v1/carts/{cartToken}/shipping-options de Storefront API. Esa ruta no usa token: la tienda va en x-Store-Domain. El flujo completo está en Tienda headless.

curl https://storefront.ecomiq.pe/v1/carts/<cart-token>/shipping-options \
  -H "x-Store-Domain: mi-tienda"

Este carrito tiene un Polo de algodón de S/ 79 y una dirección en Miraflores:

Respuesta (extracto)
{
  "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"
        }
      ]
    }
  ]
}
  • El carrito agrupa los artículos por perfil. Cada grupo trae sus opciones.
  • Cada elemento de rates es una opción. El comprador elige una con PUT /v1/carts/{cartToken}/shipping-method: envía shippingMethodId, rateId, target, targetId y el shippingProfileId del grupo.
  • Las zonas por país, departamento, provincia o distrito necesitan la dirección de envío del carrito. En Perú, envía department, province y district. Para zonas por ubigeo, envía también departmentCode, provinceCode y districtCode. El carrito los guarda para las siguientes consultas.
  • Con solo postalCode, aplican las tarifas sin zona y las zonas por código postal.

Si falta una opción, revisa:

FaltaRevisa
Envío estándarUna tarifa activa para ese subtotal y peso, la zona de la dirección y supportsShipping en Almacén Lima.
Retiro en tiendapickupEnabled, una tarifa activa y Tienda Miraflores en el perfil con supportsPickup.
AmbasvisibleInCheckout del método y el perfil que usa el producto.

Cambia tarifas en lote

POST /v1/shipping/rates/batch crea o cambia hasta 100 tarifas en una llamada. Cada fila lleva action:

  • create necesita shippingMethodId.
  • update necesita shippingRateId y solo cambia los campos que envías.

Los valores van como texto, igual que en un archivo: "12.00", no 12. Este lote crea una tarifa para el Callao y sube a S/ 12 la de Lima.

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
Respuesta
{
  "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 trae un resultado por fila, en el orden recibido. Las filas válidas se aplican aunque otras fallen. Aquí falta la zona CALLAO: créala como en Crea la zona y reenvía solo esa fila.

  • El lote rechaza un zoneCode que no existe. general deja la tarifa sin zona.
  • Una fila create con el mismo método, tipo, zona, códigos postales y rangos que otra tarifa falla. Repetir el lote no duplica tarifas.
  • Idempotency-Key es opcional. Conserva la clave al reintentar el mismo lote.

Importa tarifas desde un archivo

Para cientos de tarifas, sube un archivo a POST /v1/shipping/rates/imports. Funciona como Importación de productos: responde 202 con una operación, la sigues hasta que termina y lees el reporte de filas rechazadas. Los formatos y límites son los mismos.

Este archivo sube a S/ 200 el monto del envío gratis:

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

Sigue la operación con GET /v1/operations/{id}. Agrega ?validateOnly=true a la URL para revisar el archivo sin cambiar tarifas.

Los encabezados pueden ir como en la plantilla (en español o en inglés) o con estas claves:

ColumnaQué lleva
shipping_method (Método de envío)Nombre del método, como en la pestaña Listas, o su ID. Obligatoria al crear.
shipping_rate_id (ID interno)Sin ID se crea la tarifa. Con ID se actualiza esa tarifa.
type (Tipo de tarifa)Monto fijo, Gratis, Por peso, Peso + artículos, Por precio del pedido o Por códigos postales. También acepta Flat, FreeShipping, WeightBased, TableCost, PriceRange y PostalCode. Vacío: Monto fijo.
zone (Zona)Nombre o código de la zona. Vacío, general o Todas las zonas: todas.
price, base_price, price_per_kg, price_per_unit, surchargeMontos. Qué columnas usa cada tipo está en la pestaña Instrucciones.
min_order_price, max_order_price, min_weight, max_weightRangos. En update, una celda vacía conserva el valor.
postal_codesCódigos separados por comas. En CSV, entre comillas.
checkout_label (Texto en el checkout)Nombre de la opción en el carrito.
is_active, tax_includedSí o No. También acepta true, false, yes y no.
actionOpcional: create o update. Sin ella decide el ID interno.

Pide la plantilla

POST /v1/shipping/rates/exports con { "format": "xlsx", "template": true } genera la plantilla: la pestaña Tarifas con las columnas, Instrucciones con qué poner en cada una, y Listas con tus métodos, tus zonas y los tipos. Es una operación: al terminar, descarga su resultUrl.

Siguientes pasos

Última actualización:

En esta página