Una ruta de Webflow Cloud puede mostrar cotizaciones de transportistas de Shippo en tiempo real en las páginas de productos, como estimaciones junto a los gastos de envío que Webflow cobra al finalizar la compra.
Los compradores quieren saber cuánto costará el envío antes de añadir productos al carrito. Webflow Ecommerce calcula los gastos de envío según las reglas que establezcas por zona, y la integración de Shippo en Webflow gestiona las etiquetas después de realizar el pedido, por lo que ninguna de las dos opciones muestra los precios de los transportistas en tiempo real en la página del producto.
Yo cierro esa brecha con una pequeña aplicación de Next.js en Webflow Cloud. Mantiene tu token de Shippo en el servidor y devuelve las tarifas en tiempo real para la dirección del comprador, empezando por la más económica. Las cotizaciones son estimaciones porque el proceso de pago de Webflow sigue cobrando lo que indican tus ajustes de envío, y ninguna API permite que una aplicación personalizada cambie ese cargo.
¿Qué necesitas para configurar las tarifas de envío de Shippo en Webflow?
Necesitas cinco cosas:
- Sitio de Webflow Ecommerce: Un sitio de Webflow configurado para admitir Ecommerce y al menos una página de producto donde se ubicará el formulario de tarifas.
- Acceso a Webflow Cloud: Incluido desde el plan de sitio gratuito Starter en adelante.
- Un plan para el dominio personalizado y el código incrustado: Montar la aplicación en un dominio personalizado requiere un plan Premium o superior, y el código incrustado (Code Embed) de la página de producto necesita un plan de pago, el cual está cubierto por todos los planes de Ecommerce.
- Cuenta de Shippo con un transportista activo: Al menos una cuenta de transportista que admita cotizaciones de tarifas, además de un token de API de prueba para el desarrollo local.
- Node.js 22 y npm: Instalados localmente, utilizando npm como gestor de paquetes, ya que Webflow Cloud solo realiza compilaciones con npm.
Con estos requisitos previos listos, ya puedes configurar la ruta de la API de Shippo y conectar tu página de producto de Webflow.
6 pasos para configurar las tarifas de envío de Shippo en Webflow Cloud
Una ruta POST en Webflow Cloud llama al endpoint de envíos de Shippo, y un formulario en la página de producto llama a esa ruta. Como la aplicación comparte el dominio de la tienda, la llamada es del mismo origen y no necesita encabezados CORS.
Configura primero el token y la aplicación, luego conecta el formulario y despliega.
1. Genera un token de prueba de Shippo
En Shippo, abre la configuración de la API (API Configuration) y crea una clave de prueba en las claves de desarrollador (Developer keys). Las claves de prueba comienzan con shippo_test_ y las claves reales con shippo_live_, y Shippo solo muestra la clave completa una vez. Desarrolla usando la clave de prueba hasta que todo el flujo funcione, para que nada de lo que pruebes durante la creación afecte a tu cuenta real.
Mientras estés en el panel de control, confirma que al menos una cuenta de transportista esté activa. Shippo ofrece cotizaciones a través de las cuentas de transportistas conectadas a la tuya, por lo que una cuenta sin transportistas activos no tiene nada que cotizar.
Guarda el token en un gestor de contraseñas y no lo dejes en archivos de notas.
2. Crea la estructura de una aplicación Next.js con npm
Crea la aplicación con create-next-app fijado en la versión 15 y fuerza a npm como gestor de paquetes. La guía de Webflow bring-your-own-app indica: "Actualmente, Webflow Cloud solo admite el gestor de paquetes npm". Fijar la versión 15 mantiene middleware.ts funcionando en Webflow Cloud, lo cual es importante si añades límites de velocidad más adelante.
Ejecuta esto desde la carpeta donde guardas tus proyectos:
npx create-next-app@15 shippo-rates --typescript --app --use-npm
cd shippo-rates
npm run dev
El comando instala Next.js 15 con el App Router, donde residen los controladores de rutas. La ruta llama a la API REST de Shippo con fetch estándar, lo que evita instalar un SDK de Shippo que podría esperar APIs de Node no disponibles en Workers.
Abre http://localhost:3000 en un navegador. Deberías ver la página de inicio predeterminada de Next.js, lo que confirma que el proyecto compila antes de añadir cualquier código de Shippo.
3. Almacena el token de Shippo y la dirección de origen como variables de entorno
Mantén el token de Shippo y los detalles de origen en variables de entorno, fuera del código de la ruta. Localmente, esto significa un archivo .env.local en la raíz del proyecto; en Webflow Cloud, se refiere a los ajustes de entorno para cada despliegue, marcando el token como secreto.
La ruta requiere las dos primeras variables, mientras que las otras dos son opcionales:
- SHIPPO_API_TOKEN: El token de prueba del panel de Shippo, que se cambiará por uno real en producción. Márcalo como secreto para que Webflow Cloud lo oculte en los registros de compilación.
- SHIP_FROM_ADDRESS: La dirección de tu almacén o estudio como cadena JSON, para que puedas cambiar el origen sin tener que volver a desplegar el código o editar la ruta.
- ALLOWED_ORIGIN: El origen exacto de tu tienda, como tu dominio personalizado con https. La ruta lo compara con la cabecera Origin del navegador y omite la comprobación si no está configurado.
- NEXT_PUBLIC_BASE_PATH: Opcional en esta compilación: la ruta de montaje para cualquier componente de cliente dentro de la aplicación Next.js, ya que las llamadas fetch del lado del cliente no se reescriben automáticamente para incluirla.
Añade estos valores a .env.local, con el JSON entre comillas simples:
SHIPPO_API_TOKEN=shippo_test_your_token_here
SHIP_FROM_ADDRESS='{"name":"Your Store","street1":"Your street address","city":"Austin","state":"TX","zip":"78701","country":"US"}'
ALLOWED_ORIGIN=http://localhost:3000
NEXT_PUBLIC_BASE_PATH=
Cuando pegues SHIP_FROM_ADDRESS en Webflow Cloud, pega solo el JSON, sin las comillas simples. La página de variables de entorno de Webflow indica que tanto las variables secretas como las normales están disponibles durante la compilación y en tiempo de ejecución, y que los valores secretos se ocultan en los registros de compilación.
4. Escribe el controlador de la ruta de tarifas
Crea app/api/rates/route.ts. El controlador verifica el origen, valida la dirección de destino, selecciona un paquete de una lista preestablecida en el servidor y envía una única solicitud al endpoint de envíos de Shippo. La solicitud establece async: false, lo que hace que Shippo devuelva las tarifas en la respuesta del envío.
Los nombres de los campos siguen la referencia de envíos de Shippo. Shippo permite 50 solicitudes de envío por minuto con una clave de prueba y 500 con una real, devolviendo un error 429 si se supera, el cual la ruta transmite para que el formulario pueda pedir al cliente que espere.
La comprobación de origen filtra el uso casual entre sitios, pero los clientes con scripts pueden falsificar cabeceras, y las solicitudes sin cabecera Origin, como curl, también pasan. Añade límites de velocidad en middleware.ts antes del despliegue público. El token nunca recibe un NEXT_PUBLIC_ nombre, por lo que permanece en el servidor.
Código del controlador de ruta
Crea la ruta de tarifas con este controlador:
// app/api/rates/route.ts
import { NextResponse } from "next/server";
// Do not add `export const runtime = "edge"` to this file. Webflow Cloud already
// runs the route on Cloudflare Workers, and the OpenNext adapter does not support
// the Next.js edge runtime target.
type Parcel = {
length: string;
width: string;
height: string;
distance_unit: "in";
weight: string;
mass_unit: "lb";
};
// Parcel presets live on the server so shoppers cannot send fake weights.
const PARCELS: Record<string, Parcel> = {
small: { length: "8", width: "6", height: "4", distance_unit: "in", weight: "1", mass_unit: "lb" },
medium: { length: "12", width: "10", height: "6", distance_unit: "in", weight: "3", mass_unit: "lb" },
};
const REQUIRED_FIELDS = ["name", "street1", "city", "state", "zip", "country"] as const;
type ShippoRate = {
object_id: string;
amount: string;
currency: string;
provider: string;
servicelevel?: { name?: string };
estimated_days?: number | null;
};
export async function POST(request: Request) {
const allowedOrigin = process.env.ALLOWED_ORIGIN;
const origin = request.headers.get("origin");
if (allowedOrigin && origin && origin !== allowedOrigin) {
return NextResponse.json({ error: "Origin not allowed" }, { status: 403 });
}
const token = process.env.SHIPPO_API_TOKEN;
const fromRaw = process.env.SHIP_FROM_ADDRESS;
if (!token || !fromRaw) {
return NextResponse.json({ error: "Shipping is not configured" }, { status: 500 });
}
let addressFrom: Record<string, string>;
try {
addressFrom = JSON.parse(fromRaw);
} catch {
return NextResponse.json({ error: "SHIP_FROM_ADDRESS is not valid JSON" }, { status: 500 });
}
let body: { parcel?: string; to?: Record<string, unknown> };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Request body must be JSON" }, { status: 400 });
}
const parcel = body.parcel ? PARCELS[body.parcel] : undefined;
if (!parcel) {
return NextResponse.json({ error: "Unknown parcel" }, { status: 400 });
}
const to = body.to ?? {};
const addressTo: Record<string, string> = {};
for (const field of REQUIRED_FIELDS) {
const value = to[field];
if (typeof value !== "string" || value.trim() === "" || value.length > 100) {
return NextResponse.json({ error: `Missing or invalid field: ${field}` }, { status: 400 });
}
addressTo[field] = value.trim();
}
let shippoRes: Response;
try {
shippoRes = await fetch("https://api.goshippo.com/shipments/", {
method: "POST",
headers: {
Authorization: `ShippoToken ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
address_from: addressFrom,
address_to: addressTo,
parcels: [parcel],
async: false,
}),
signal: AbortSignal.timeout(15000),
});
} catch (err) {
console.error("Shippo request failed", err);
return NextResponse.json({ error: "Rate lookup timed out" }, { status: 504 });
}
if (shippoRes.status === 429) {
return NextResponse.json({ error: "Too many rate requests, try again shortly" }, { status: 429 });
}
if (!shippoRes.ok) {
console.error("Shippo error", shippoRes.status, await shippoRes.text());
return NextResponse.json({ error: "Rate lookup failed" }, { status: 502 });
}
const shipment = (await shippoRes.json()) as { rates?: ShippoRate[]; messages?: unknown[] };
const rawRates = shipment.rates ?? [];
if (rawRates.length === 0 && shipment.messages?.length) {
console.warn("Shippo returned no rates", JSON.stringify(shipment.messages));
}
const rates = rawRates
.map((rate) => ({
id: rate.object_id,
amount: rate.amount,
currency: rate.currency,
provider: rate.provider,
service: rate.servicelevel?.name ?? "",
days: rate.estimated_days ?? null,
}))
.sort((a, b) => parseFloat(a.amount) - parseFloat(b.amount));
return NextResponse.json({ rates });
}
Si el navegador enviara el peso y las dimensiones, cualquiera podría solicitar tarifas para paquetes arbitrarios en tu cuenta de Shippo. Con async: false, Shippo espera a cada transportista antes de responder, por lo que el tiempo de espera de 15 segundos mantiene a un transportista lento dentro del límite de 20 segundos de Webflow Cloud.
Probar la ruta localmente
Con en ejecución, prueba la ruta desde una segunda terminal:npm run dev
curl -X POST http://localhost:3000/api/rates \
-H "Content-Type: application/json" \
-d '{"parcel":"small","to":{"name":"Test Buyer","street1":"1 Market St","city":"San Francisco","state":"CA","zip":"94105","country":"US"}}'
Deberías recibir un JSON con una matriz , ordenada de menor a mayor precio, donde cada entrada incluya el proveedor, el nombre del servicio, el importe y la divisa. Si la matriz llega vacía, revisa la configuración del transportista en Shippo antes de modificar la ruta.rates
5. Añade el formulario de tarifas a tu página de producto en Webflow
Añade un código incrustado (Code Embed) en la plantilla de producto donde los clientes consultan los gastos de envío. El formulario llama a una ruta relativa bajo la ruta de montaje de la aplicación. Como las tarifas dependen del destino, el formulario solicita una dirección completa; si tus transportistas cotizan solo por código postal, puedes simplificarlo a los campos de código postal, estado y país, y rellenar los marcadores de posición en el servidor.
Pega esto en el elemento incrustado y configura con la ruta donde montarás la aplicación:MOUNT_PATH
<form id="rate-form">
<input name="name" placeholder="Full name" required>
<input name="street1" placeholder="Street address" required>
<input name="city" placeholder="City" required>
<input name="state" placeholder="State or region" required>
<input name="zip" placeholder="ZIP or postal code" required>
<input name="country" placeholder="Country code, e.g. US" maxlength="2" required>
<button type="submit">Get shipping rates</button>
</form>
<p class="rate-note">Estimated carrier rates. Final shipping is calculated at checkout.</p>
<ul id="rate-list"></ul>
<script>
const MOUNT_PATH = "/shipping"; // must match the Webflow Cloud mount path
const PARCEL_KEY = "small"; // must match a key in PARCELS in route.ts
const form = document.getElementById("rate-form");
const list = document.getElementById("rate-list");
form.addEventListener("submit", async (event) => {
event.preventDefault();
list.textContent = "Fetching rates...";
const to = Object.fromEntries(new FormData(form));
try {
const res = await fetch(MOUNT_PATH + "/api/rates", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parcel: PARCEL_KEY, to: to }),
});
const isJson = (res.headers.get("content-type") || "").includes("application/json");
const data = isJson ? await res.json() : {};
if (!res.ok) throw new Error(data.error || "Request failed (" + res.status + ")");
list.innerHTML = "";
const rates = data.rates || [];
if (rates.length === 0) {
list.textContent = "No rates returned for this address.";
return;
}
for (const rate of rates) {
const li = document.createElement("li");
const days = rate.days != null ? " (" + rate.days + " days)" : "";
li.textContent = rate.provider + " " + rate.service + ": " + rate.amount + " " + rate.currency + days;
list.appendChild(li);
}
} catch (err) {
list.textContent = err.message;
}
});
</script>
Cada producto comparte la misma plantilla, por lo que un elemento incrustado significa un para todo el catálogo; para variarlo según el producto, utiliza el valor de un campo a nivel de producto en lugar de dejarlo fijo, y añade cualquier tamaño nuevo a primero.PARCEL_KEYPARCELS
Coloca el elemento incrustado debajo del botón de añadir al carrito. La nota sobre la lista es importante: de lo contrario, los clientes podrían confundir la primera cotización con el cargo final en el proceso de pago. El formulario no devolverá tarifas hasta que la aplicación esté desplegada y montada. Publica la página y comprueba que los campos de dirección y el botón sean visibles.
6. Despliega la aplicación y compara las cotizaciones con los envíos del proceso de pago
Despliega la aplicación a través de tu proyecto de Webflow Cloud y móntala en tu dominio personalizado en la misma ruta configurada en .MOUNT_PATH
Confirma que las tres variables de entorno obligatorias existen en el entorno donde estás desplegando, con configurada como el origen HTTPS de la tienda y, si añadiste componentes de cliente, configurada como la ruta de montaje.ALLOWED_ORIGINNEXT_PUBLIC_BASE_PATH
Publica el sitio de Webflow, abre una página de producto real y envía una dirección válida. Una vez que las cotizaciones con la clave de prueba se vean correctas, cambia por tu clave real solo en el entorno de producción.SHIPPO_API_TOKEN
Si conectaste tus propias cuentas de transportista durante las pruebas, vuelve a conectarlas con la clave real, ya que las cuentas de transportista en modo de prueba no se transfieren y las tarifas de prueba pueden diferir de las reales.
Solicita cotizaciones para algunos destinos representativos para cada configuración de paquete y compáralas con lo que cobra tu proceso de pago. Si hay discrepancias, ajusta los métodos de envío de tu tienda; los métodos basados en el peso son los que mejor reflejan los precios de los transportistas, ya que Webflow no dispone de una opción de cálculo por transportista.
Yo guardo una lista corta de direcciones de prueba para esto y la ejecuto cada vez que cambian los precios de los transportistas.
¿Qué causa que las tarifas de envío de Shippo fallen en Webflow Cloud?
La mayoría de los fallos provienen de la compilación o de la ruta de montaje; el resto se debe a las credenciales o a un envío que no devuelve ninguna cotización. El error que muestra el formulario te indicará por dónde empezar.
El despliegue falla aunque la ruta funciona con next dev
Causa: El archivo de ruta contiene . Webflow Cloud despliega Next.js a través del adaptador OpenNext de Cloudflare, el cual no es compatible con el destino del entorno de ejecución (runtime) edge de Next.js.export const runtime = 'edge'
La confusión proviene de la propia página de Webflow sobre cómo integrar tu propia aplicación, que todavía indica a los lectores que añadan esa directiva a las rutas de la API. Esa misma página también dice que Webflow Cloud "despliega tu aplicación usando el entorno de ejecución Edge", y esa frase describe la plataforma: Cloudflare Workers, una red edge. El ajuste del entorno de ejecución de Next.js es un interruptor independiente, y activarlo rompe la compilación.
Solución: Elimina la directiva de cada gestor de rutas y vuelve a desplegar. La ruta ya funciona en Workers sin ella. Mantén el comentario en la parte superior de route.ts para que el próximo desarrollador que lea la documentación no la vuelva a añadir.
El formulario de tarifas muestra "Request failed (404)" en el sitio en vivo
Causa: La solicitud se dirige a /api/rates en la raíz de la tienda en lugar de bajo la ruta de montaje de la aplicación. Webflow Cloud inyecta la ruta base en la compilación de Next.js, pero las llamadas fetch del navegador no se reescriben, y la documentación de Webflow indica que las llamadas fetch del lado del cliente deben incluir manualmente la ruta base. El embed de la página de Webflow se encuentra totalmente fuera de la aplicación, por lo que nada añade el prefijo correspondiente.
Solución: Configura MOUNT_PATH en el embed con la ruta de montaje exacta, incluyendo la barra diagonal inicial, y vuelve a publicar el sitio de Webflow.
Para los componentes de cliente dentro de la aplicación Next.js, construye las URLs a partir de process.env.NEXT_PUBLIC_BASE_PATH en lugar de leer basePath desde next.config. Abre la pestaña de red del navegador para confirmar que la URL de la solicitud ahora incluye el prefijo.
El formulario de tarifas en vivo muestra "Shipping is not configured" o "Rate lookup failed"
Causa: Al entorno desplegado le falta SHIPPO_API_TOKEN, tiene un nombre diferente o contiene un token que Shippo ya no acepta. Localmente, Next.js lee .env.local, un archivo que nunca llega a Webflow Cloud.
Webflow Cloud establece variables por entorno, así que asegúrate de que el token exista tanto en producción como en staging.
Las dos ramas devuelven errores distintos. Una variable faltante o mal nombrada hace que la ruta devuelva un 500 con "Shipping is not configured". Un token incorrecto hace que Shippo rechace la llamada, lo cual la ruta registra y devuelve como un 502 con "Rate lookup failed".
Solución: Abre el entorno de producción en tu proyecto de Webflow Cloud, confirma que el nombre de la variable coincida exactamente con el código y pega el token de nuevo sin espacios alrededor. Comprueba que el despliegue en vivo utilice una clave shippo_live_ y vuelve a desplegar para que se aplique el nuevo valor.
El formulario devuelve "No rates returned for this address"
Causa: Shippo aceptó la solicitud pero no envió ninguna cotización. La ruta registra el array messages de Shippo para que puedas leer directamente la explicación de Shippo sobre el resultado vacío.
Solución: Busca la línea "Shippo returned no rates" en la salida de ejecución de tu aplicación; bajo next dev local, se imprime en la terminal donde corre el servidor.
Si menciona un error de cuenta de transportista, soluciona esa cuenta en el panel de control de Shippo. Si menciona la dirección, vuelve a ejecutar la solicitud curl desde tu terminal con ese destino y una dirección postal completa. Si solo falla un ajuste preestablecido, compara sus dimensiones con lo que indica el mensaje de texto.
Qué puedes construir a continuación con Shippo y Webflow
Una vez que los compradores puedan ver los precios de los transportistas, los ajustes preestablecidos de paquetes por producto son el siguiente paso lógico, para que cada cotización coincida con la caja en la que se envía el artículo; asigna cada slug de producto a una clave preestablecida en el servidor y mantén las unidades alineadas con el sistema de unidades de tu tienda.
Para el cumplimiento de pedidos más allá de las etiquetas, el puente de Printful muestra cómo una aplicación de Webflow Cloud puede gestionar pedidos y actualizaciones de envío.
En cuanto a la plataforma, la documentación para desarrolladores de Webflow cubre cómo Webflow Cloud ejecuta Next.js, incluyendo sus reglas de middleware.
Preguntas frecuentes
¿Funcionará el formulario para direcciones fuera de EE. UU.?
Solo donde la dirección tenga estado o región. REQUIRED_FIELDS en route.ts incluye el estado, por lo que la ruta rechaza cualquier dirección con un campo de estado vacío. Para países sin estados, elimina "state" de REQUIRED_FIELDS y quita required de esa entrada en el embed. La documentación de la API de Shippo enumera los campos adicionales que requieren los envíos internacionales.
¿Puedo cargar ajustes preestablecidos de paquetes desde un archivo JSON con node:fs?
No. El entorno de ejecución de Workers de Webflow Cloud no tiene acceso al sistema de archivos, tal como indica su página de compatibilidad con Node.js. Importa el JSON como un módulo para que se empaquete durante la compilación o guarda los ajustes en una variable de entorno.
¿Dónde debería colocar la limitación de tasa si actualizo a Next.js 16?
Para que los límites de tasa funcionen en Webflow Cloud, te recomiendo mantener middleware.ts, aunque Next.js 16 lo renombre a proxy.ts. Proxy se ejecuta en Node y la documentación de Webflow indica: "Solo el middleware del entorno de ejecución Edge funciona en el entorno de ejecución de Workers". Renombrar el archivo genera un middleware que no puede ejecutarse en Webflow Cloud, dejando la ruta de tasas sin limitación.
¿Funcionará una comprobación con timingSafeEqual si añado una ruta de webhook firmada más adelante?
Funciona una vez desplegado porque timingSafeEqual existe en el entorno de ejecución de Workers que usa Webflow Cloud. No existe en el entorno local next dev, por lo que el mismo código puede fallar en tu equipo. Protege la llamada con una verificación de funcionalidad y prueba la comparación en un despliegue de pruebas en lugar de confiar solo en los resultados locales.





