Conecta lo que ya tienes.
—al mes
El mundo y el módulo son el mismo precio: aquí no pagas por un paquete.
- Sin comisiones sobre lo que vendes.
- Sin permanencia: cancelas cuando quieras.
Conecta lo que ya tienes.
118 conectores publicados, una API REST con contrato OpenAPI, webhooks firmados con HMAC y autoridad por campo entre tu ERP y cada canal.
$ curl -G https://suite.yubasync.com/api/v2/items \
-H 'Authorization: Bearer $TOKEN' \
-d max=2
{
"data": [
{ "code": "ARR-500", "description": "Arroz premium 500 g",
"totalQuantity": 1284, "existenciesOnRoutes": 96, "price": 24500 },
{ "code": "PAN-024", "description": "Panela cuadrada x24",
"totalQuantity": 340, "existenciesOnRoutes": 0, "price": 18900 }
],
"pagination": { "page": 1, "pageSize": 2, "totalPages": 1642, "totalRecords": 3283 }
}
Esta es la lista completa de conectores publicados, no una selección de logos. Búscalo por nombre o filtra por tipo. Si el tuyo no aparece, no está: preferimos decirlo antes de la reunión.
Los conectores marcados con un país tienen lógica fiscal o de negocio propia de ese país; los marcados «global» funcionan igual en cualquiera. Ninguno se cobra aparte.
REST sobre HTTPS, JSON en los dos sentidos, token Bearer y un OpenAPI público que puedes cargar en tu propio Swagger. El identificador de empresa viaja firmado dentro del token: no hay ningún parámetro de compañía que un cliente pueda manipular.
$ curl -X POST https://suite.yubasync.com/api/login/authenticate \
-H 'Content-Type: application/json' \
-d '{"username":"acme_prod","password":"••••••••••"}'
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJiYXQtYXBpIiwi…"
}
# El token dura 90 días; renovarlo es volver a llamar este endpoint.
# Máximo 10 intentos por minuto. Usuario inexistente y clave incorrecta
# devuelven exactamente la misma respuesta — a propósito.
# Revocar la credencial corta el acceso YA, sin esperar al vencimiento.
$ curl -G https://suite.yubasync.com/api/v2/salesOrder \
-H 'Authorization: Bearer $TOKEN' \
-d page=1 -d max=100 \
-d start=01/05/2026 -d end=31/05/2026 \
-d filterWithDate=lastUpdated
{
"data": [
{
"id": "SO-2026-004182",
"customer": "900123456",
"type": "Wholesale order",
"dateCreated": "2026-05-14T09:22:31.000Z",
"scheduledDateForDelivery": "2026-05-16T00:00:00.000Z",
"priceList": "MAY-01",
"totalBeforeTax": 1840000,
"totalTax": 349600,
"totalSales": 2189600,
"billable": true,
"billed": false,
"exported": true,
"externalId": "P0034182",
"networkSignalQuality": "POOR",
"errorMessageFromIntegration": null,
"items": [
{ "product": "ARR-500", "quantity": 40, "price": 24500, "total": 980000 },
{ "product": "PAN-024", "quantity": 50, "price": 17200, "total": 860000 }
]
}
],
"pagination": { "page": 1, "pageSize": 100, "totalPages": 7, "totalRecords": 683 }
}
$ curl -i https://suite.yubasync.com/api/v2/items
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="bat-api"
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
{
"errorCode": "INVALID_TOKEN",
"message": "Missing bearer token",
"details": "Header Authorization: Bearer {TOKEN} is required"
}
# Códigos estables: INVALID_TOKEN · TOKEN_EXPIRED · ACCESS_DENIED
# INVALID_CREDENTIALS · RATE_LIMIT_EXCEEDED
# El 429 siempre trae Retry-After: 60 — no hay que adivinar el backoff.
# `details` se omite cuando no aporta; nunca llega como null.
| Método | Ruta | Qué devuelve |
|---|---|---|
| POST | /api/login/authenticate | Emite el Bearer Token de 90 días a partir del usuario técnico. |
| GET | /api/v2/salesOrder | Pedidos de venta con sus líneas, totales, GPS y estado de exportación. |
| GET | /api/v2/customers | Clientes. Alias equivalente: /api/v2/customer. |
| GET | /api/v2/items | Existencias por producto, ya consolidadas entre bodegas y rutas. Alias: /api/v2/inventory. |
| GET | /api/v2/priceList | Listas de precios vigentes por producto. |
| GET | /api/v2/deliveryBalance | Cartera: saldos pendientes por cliente y documento. Alias: /api/v2/cartera. |
| GET | /api/v2/visits | Visitas y agenda del vendedor. Alias: /api/v2/calendarEvent. |
| GET | /api/v2/users | Usuarios de la compañía. Alias: /api/v2/user. |
| GET | /api/v2/openapi.json | El contrato OpenAPI completo. Público a propósito: se lee sin token. |
Toda lista responde con data y pagination. Nunca hay que deducir el final por «me llegaron menos de los que pedí» — que falla justo cuando el total es múltiplo exacto del tamaño de página.
Pedir max=5000 no devuelve un 400: se recorta a 500 y el valor realmente aplicado viene en pagination.pageSize.
start y end en dd/MM/yyyy, interpretadas en UTC-05:00. Un end sin hora se completa a las 23:59:59 para que el último día quede incluido.
El identificador de compañía sale del JWT firmado, jamás de un parámetro. Una credencial no sirve para leer otra compañía aunque se conozca su código.
Siempre { errorCode, message }, con details sólo cuando aporta. El mensaje crudo de una excepción no sale nunca: podría filtrar nombres de tabla.
Para los sistemas legacy que no publican nada a internet hay un agente on-premise: se instala del lado del cliente y abre un puente cifrado y auditable hacia el hub.
Cada entrega va firmada con HMAC SHA-256 sobre el cuerpo crudo más una marca de tiempo. Tu extremo puede comprobar que el mensaje salió de nosotros y que no es la repetición de uno viejo — sin confiar en la IP de origen.
X-Yuba-Event: shipment.delivered
X-Yuba-Delivery-Id: 6f1c1a9e-4b02-4f4e-9a31-2c0e7d5a11b8
X-Yuba-Webhook-Id: 0b2d94c7-1f88-4a55-bb10-8e6f0a7c3d21
X-Yuba-Signature: t=1779023011,v1=9f3c…c1
User-Agent: YubaWebhook/1.0
{ "id": "6f1c1a9e-…", "event": "shipment.delivered",
"ts": "2026-05-18T14:23:31.482Z", "data": { … } }
// Verificación del lado del receptor — la firma se calcula sobre `${t}.${body}`
import crypto from "node:crypto";
export function verificar(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
// Anti-replay: fuera de ±300 s se descarta aunque la firma cuadre.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const esperado = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// Comparación en tiempo constante: un `===` filtra información por timing.
return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(v1));
}
Tres intentos con espera creciente: 1 minuto, 5 minutos y 30 minutos. Después la entrega queda marcada como fallida y visible — no se reintenta para siempre contra un endpoint caído.
Cada envío trae su X-Yuba-Delivery-Id. Si un reintento llega dos veces, tu lado lo descarta con una comprobación de una línea.
Hay que verificar antes de parsear el JSON: reserializar el cuerpo cambia los espacios y rompe la firma. Es el fallo más común al integrar webhooks firmados.
Catálogo de eventos publicables
Un evento que no esté en esta lista no se puede suscribir: el emisor valida el nombre contra el catálogo antes de encolar, así una errata de tipeo falla en el registro y no seis meses después en silencio.
Cuando el mismo producto vive en el ERP, en la tienda y en dos marketplaces, «¿quién manda?» no tiene una respuesta, tiene una por campo. El stock lo manda el ERP porque es la única verdad de existencias; el precio de venta puede mandarlo la tienda; el catálogo, un PIM distinto. Declarar eso es lo que hace que dos integraciones corriendo en paralelo no se pisen.
| Campo canónico | Dueño | Dirección | Ante un cambio ajeno |
|---|---|---|---|
stock |
El ERP | inbound |
owner-wins — se rechaza y queda en la traza |
price |
El canal | bidirectional |
last-writer — gana el más reciente |
catalog |
El PIM | outbound |
manual — entra a revisión humana |
status |
El ERP | bidirectional |
owner-wins — se rechaza y queda en la traza |
Es un dato aceptado que no debía aceptarse. La tienda avisa «vendí 3» y baja el stock; el ERP, que ya lo sabía por su propio flujo, lo baja otra vez. Nada se cae y nadie se entera: simplemente se deja de vender un producto que sí había.
Un dueño sin declarar cae en manual (que decida una persona) y una dirección sin declarar cae en inbound (escuchar antes que empujar). Ante la duda no se pisa el dato bueno.
Cada cambio entrante queda como aceptado, rechazado o en conflicto, con su origen y su marca de tiempo. Cuando alguien pregunte por qué el precio cambió un martes, hay una respuesta.
El integrador no se cobra por conector, ni por evento, ni por volumen de llamadas. Se cobra por conexiones activas — y las dos primeras vienen incluidas.
No pudimos cargar el precio en este momento. Está siempre actualizado en la página de precios.
Conecta lo que ya tienes.
—al mes
El mundo y el módulo son el mismo precio: aquí no pagas por un paquete.
Link es un único módulo, así que aquí no hay paquete: contratas el integrador y punto. Y funciona al revés también — puedes conectar tu ERP con YUBASync sin comprar ningún otro mundo.
La documentación y el contrato OpenAPI son públicos: se leen, se cargan en tu Swagger y se prueban. Si después de eso quedan preguntas, hablas con quien escribió el conector — no con un comercial.
Los conectores no se cobran por unidad ni por volumen de eventos: vienen incluidos con el producto. Ver precios.