La petición suele ser la misma: que la web muestre los precios y el stock reales, que los pedidos entren solos en el ERP y que el cliente pueda ver sus facturas sin llamar a nadie. Todo eso se puede hacer con Business Central, pero hay una forma de montarlo que funciona y varias que acaban en pedidos duplicados, stock desfasado o integraciones bloqueadas por exceso de llamadas.
Este artículo es la versión práctica: arquitectura, qué sincronizar en cada sentido y el código de las partes delicadas.
Antes de programar: ¿existe ya un conector?
Para algunas plataformas no hace falta empezar de cero:
- Shopify: Business Central incluye un conector de Shopify publicado por Microsoft.
- WooCommerce, PrestaShop, Adobe Commerce (Magento) y otras: hay conectores de terceros en AppSource.
Si tu tienda es estándar y el conector cubre tu proceso, suele ser la opción más barata. Una integración a medida tiene sentido cuando la web es propia, cuando hay reglas B2B específicas (tarifas por cliente, condiciones, aprobaciones) o cuando lo que quieres es un portal de clientes y no una tienda.
La arquitectura que funciona
Navegador → tu backend (Node, PHP, .NET...) → API de Business Central
↑
webhooks de Business Central
Tres reglas que no conviene saltarse:
- El navegador nunca habla con Business Central. El secreto de la aplicación de Entra ID y los permisos viven en tu servidor. Si llamas desde el navegador, cualquiera puede ver tus credenciales.
- Tu backend es el que tiene el token y lo reutiliza hasta que caduca. Si no sabes cómo obtenerlo, lo explico en OData y API en Business Central.
- Lo que se lee mucho, se cachea. El catálogo y los precios no cambian en cada visita: guárdalos en tu base de datos o en caché y actualízalos cuando Business Central avise de un cambio.
Qué sincronizar y en qué sentido
| Dato | Sentido | Cómo |
|---|---|---|
| Artículos y descripciones | BC → web | Lectura incremental por lastModifiedDateTime o webhook |
| Precios | BC → web | El precio base está en items. Los precios por cliente o por cantidad viven en las listas de precios y suelen necesitar una API propia |
| Stock | BC → web | inventory en items es el total. Si lo necesitas por almacén, API propia |
| Clientes | En los dos sentidos | Alta desde la web; datos maestros y condiciones desde BC |
| Pedidos | Web → BC | POST a salesOrders con sus líneas |
| Estado, facturas y PDF | BC → web | salesOrders, salesInvoices y su pdfDocument |
La regla general: cada dato tiene un dueño. Los precios, el stock y las condiciones mandan en Business Central; la web los muestra. Si permites editarlos en los dos sitios, tarde o temprano no sabrás cuál es el bueno.
Crear un pedido desde la web sin duplicados
Primero, un pequeño cliente HTTP con reintentos, porque Business Central limita las peticiones por usuario y antes o después recibirás un 429:
async function bc(path, init = {}, retries = 3) {
const res = await fetch(BASE_URL + path, {
...init,
headers: {
Authorization: `Bearer ${await getToken()}`,
'Content-Type': 'application/json',
...init.headers
}
});
if ((res.status === 429 || res.status === 503) && retries > 0) {
await new Promise(r => setTimeout(r, (4 - retries) * 2000)); // 2 s, 4 s, 6 s
return bc(path, init, retries - 1);
}
if (!res.ok) throw new Error(`Business Central ${res.status}: ${await res.text()}`);
return res.status === 204 ? null : res.json();
}
Ahora el pedido. La API v2.0 permite crear la cabecera y las líneas en una sola llamada:
async function createOrder(webOrder) {
const ref = `WEB-${webOrder.id}`;
// ¿Ya existe? Un reintento o un doble clic no debe crear otro pedido.
const { value: existing } = await bc(
`/companies(${COMPANY_ID})/salesOrders?$filter=externalDocumentNumber eq '${ref}'&$select=id,number`
);
if (existing.length) return existing[0];
return bc(`/companies(${COMPANY_ID})/salesOrders`, {
method: 'POST',
body: JSON.stringify({
customerNumber: webOrder.customerNumber,
externalDocumentNumber: ref,
salesOrderLines: webOrder.lines.map(l => ({
lineType: 'Item',
lineObjectNumber: l.sku,
quantity: l.quantity
}))
})
});
}
La clave es externalDocumentNumber: guardas ahí el número de pedido de la web y compruebas si ya existe antes de crearlo. Sin esa comprobación, un timeout seguido de un reintento te deja dos pedidos iguales en el ERP.
Fíjate también en lo que no se envía: el precio. Si la web manda el precio, Business Central lo acepta tal cual; si no lo manda, lo calcula con sus propias reglas. Decide quién manda (normalmente, el ERP) y sé coherente.
Enterarte de los cambios: webhooks
En lugar de preguntar cada cinco minutos si ha cambiado algo, puedes pedirle a Business Central que te avise. Te suscribes a una entidad:
POST https://api.businesscentral.dynamics.com/v2.0/{tenant}/{entorno}/api/v2.0/subscriptions
{
"notificationUrl": "https://tu-backend.com/bc/webhook",
"resource": "/api/v2.0/companies({companyId})/items",
"clientState": "un-secreto-compartido"
}
Antes de dar de alta la suscripción, Business Central llama a tu notificationUrl con un parámetro validationToken, y tienes que devolverlo tal cual con un 200. Después, cada aviso llega a la misma URL:
app.post('/bc/webhook', express.json(), (req, res) => {
// Alta y renovación: devolver el token tal cual.
if (req.query.validationToken) return res.type('text/plain').send(req.query.validationToken);
// Avisos: responder rápido y quedarse solo con los de nuestra suscripción.
// Un error 4xx haría que Business Central borrase la suscripción, así que se ignoran sin más.
const notifications = (req.body.value || []).filter(n => n.clientState === process.env.BC_CLIENT_STATE);
res.sendStatus(200);
queue.push(...notifications); // procesar después: leer cada `resource` con la API
});
Lo que conviene saber antes de fiarte de ellos:
- El aviso no trae los datos: trae qué recurso ha cambiado (
created,updatedodeleted). Luego tienes que leerlo. - No son instantáneos: Business Central espera unos 30 segundos para agrupar cambios. Si en ese tiempo cambian más de 1.000 registros, recibes un único aviso de tipo
collectioncon un filtro para leerlos todos. - Caducan a los tres días. Hay que renovarlos con un
PATCH, que vuelve a pedir elvalidationToken. Programa la renovación a diario. - Responde rápido y procesa después. Si tu servidor contesta
408,429o un5xx, Business Central reintenta durante 36 horas. Con cualquier otro error, borra la suscripción. - Hay un máximo de 200 suscripciones por entorno, y no todas las entidades admiten webhooks: artículos, clientes, pedidos y facturas sí, y también tus APIs propias si cumplen ciertas condiciones (clave de un solo campo, como
SystemId; tabla no temporal; página y no consulta).
Un buen esquema combina las dos cosas: webhooks para enterarte rápido y una sincronización incremental nocturna por lastModifiedDateTime para recuperar cualquier aviso que se haya perdido.
Errores típicos
- Llamar a Business Central desde el navegador. Credenciales expuestas y permisos de más.
- Leerlo todo cada pocos minutos. Con 6.000 peticiones cada 5 minutos por usuario, una sincronización completa y frecuente agota el límite y bloquea al resto de integraciones.
- Pedidos duplicados por reintentos sin comprobar
externalDocumentNumber. - Integraciones sobre páginas de Microsoft publicadas como OData, que dejarán de funcionar en la versión 30. Usa la API v2.0 o una API propia.
- No registrar los errores. Una integración que falla en silencio es peor que una que no existe: guarda cada error con el pedido o el artículo afectado y avisa a alguien.
Resumen
Una integración web con Business Central bien hecha tiene un backend en medio, sincroniza de forma incremental, se entera de los cambios con webhooks y crea pedidos que no se duplican. Si estás planteando una tienda, un portal B2B o quieres revisar una integración que da problemas, cuéntame tu caso.
