API de e-commerce
Gestiona tu catálogo de productos, rastrea pedidos, supervisa la actividad de los carritos y analiza segmentos RFM de clientes.
Todos los endpoints de esta página son relativos a la URL base: https://api-eu1.joryio.com/api. Consulta el resumen de la API.
Permisos
Leer datos de catálogo, carrito y pedidos requiere analytics:read. Crear o actualizar productos, carritos y pedidos requiere ecommerce:write. Ambos se pueden conceder a una clave de API en Configuración → Claves de API.
Las rutas de escritura requerían antes integrations:write, que no se ofrece en las claves de API porque también rige el OAuth de los conectores y la configuración de sincronización del almacén. Siguen aceptándolo, así que las integraciones existentes no se ven afectadas.
Convención de nombres de ID
Joryio usa una convención clara para los identificadores:
Tus ID (úsalos al crear o rastrear datos)
| Campo | Descripción | Ejemplo |
|---|---|---|
productId | Tu identificador de producto (SKU o ID de producto de tu plataforma de e-commerce). | "SKU-12345" |
orderId | Tu identificador de pedido (número de pedido de tu plataforma). | "ORD-2024-001" |
userId | Tu identificador de usuario. | "user-123" |
ID de Joryio (devueltos en las respuestas API)
| Campo | Descripción | Cuándo usarlo |
|---|---|---|
id | ID interno de Joryio para productos/pedidos. | Se devuelve en respuestas API; úsalo para actualizar/eliminar. |
joryioUserId | ID de usuario interno de Joryio (el id de 24 caracteres hexadecimales devuelto por la API de usuarios). | Alternativa a userId cuando quieras referenciar mediante el ID de Joryio. |
Identificación flexible de usuario
Al crear pedidos o rastrear eventos, puedes identificar usuarios mediante uno de estos campos:
userId: tu identificador de usuario (el más habitual).joryioUserId: ID de usuario interno de Joryio.
// Usando tu ID de usuario (recomendado)
{ "orderId": "ORD-001", "userId": "user-123", ... }
// Usando el ID de usuario interno de Joryio
{ "orderId": "ORD-001", "joryioUserId": "66a1f2c3d4e5f6a7b8c9d0e1", ... }
Autenticación
Todas las solicitudes requieren autenticación JWT:
Authorization: Bearer your_jwt_token
Content-Type: application/json
X-Workspace-Id: your_workspace_id
Catálogo de productos
Crear un producto
Crea un producto nuevo en tu catálogo.
POST /catalog/products
Cuerpo de la solicitud:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
productId | cadena | Sí | Tu identificador de producto único (SKU o ID de producto). |
name | cadena | Sí | Nombre del producto. |
price | número | Sí | Precio del producto. |
description | cadena | No | Descripción del producto. |
compareAtPrice | número | No | Precio original (para descuentos). |
currency | cadena | No | Código de moneda (predeterminado: USD). |
categories | cadena[] | No | Categorías de producto. |
tags | cadena[] | No | Etiquetas de producto. |
brand | cadena | No | Nombre de la marca. |
imageUrl | cadena | No | URL de imagen principal de producto. |
url | cadena | No | URL de página de producto. |
inStock | booleano | No | Disponibilidad de stock (predeterminado: true). |
sku | cadena | No | Unidad de mantenimiento de stock. |
variants | objeto[] | No | Variantes de producto. |
customFields | objeto | No | Atributos personalizados. |
Solicitud de ejemplo:
curl -X POST https://api-eu1.joryio.com/api/catalog/products \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"productId": "SKU-12345",
"name": "Classic Blue T-Shirt",
"price": 29.99,
"compareAtPrice": 39.99,
"currency": "USD",
"categories": ["Clothing", "T-Shirts"],
"tags": ["sale", "bestseller"],
"brand": "Acme Apparel",
"imageUrl": "https://example.com/images/blue-tshirt.jpg",
"url": "https://example.com/products/blue-tshirt",
"inStock": true,
"sku": "BTS-001-BL",
"variants": [
{
"id": "var-s",
"name": "Small",
"sku": "BTS-001-BL-S",
"price": 29.99,
"inStock": true,
"options": { "size": "S", "color": "Blue" }
},
{
"id": "var-m",
"name": "Medium",
"sku": "BTS-001-BL-M",
"price": 29.99,
"inStock": true,
"options": { "size": "M", "color": "Blue" }
}
]
}'
Respuesta:
{
"id": "prod_abc123",
"productId": "SKU-12345",
"name": "Classic Blue T-Shirt",
"price": 29.99,
"createdAt": "2024-01-20T10:00:00.000Z"
}
Upsert masivo de productos (cuerpo de matriz)
No hay un endpoint masivo independiente: POST /catalog/products acepta un objeto de producto único o una matriz JSON de objetos de producto, sin objeto envoltorio. El formato de matriz crea o actualiza hasta 500 productos en una solicitud, mediante upsert por productId.
POST /catalog/products
Cuerpo de la solicitud:
Una matriz JSON de hasta 500 elementos; una matriz vacía o con más de 500 devuelve 400. Cada elemento sigue el mismo formato que el objeto único, incluido el campo opcional source por producto.
Cada elemento recibe una validación completa. Un elemento no válido se informa en failed mediante su índice de matriz, nunca se acepta silenciosamente, y los elementos válidos restantes reciben upsert de todos modos.
[
{ "productId": "SKU-001", "name": "Product 1", "price": 19.99 },
{ "productId": "SKU-002", "name": "Product 2", "price": 29.99, "source": "shopify" }
]
Respuesta:
A diferencia del formato de objeto, que devuelve el producto creado, el formato de matriz devuelve un resumen agregado:
{
"processed": 2,
"upserted": 2,
"failed": []
}
| Campo | Descripción |
|---|---|
processed | Número de elementos recibidos en la matriz de solicitud. |
upserted | Productos creados o actualizados realmente. |
failed | Errores por elemento: index (posición en la matriz de solicitud), productId / sku (cuando existen en el elemento) y reason. |
Listar productos
Consulta productos con filtrado y paginación.
GET /catalog/products
Parámetros de consulta:
| Parámetro | Tipo | Descripción |
|---|---|---|
search | cadena | Busca por nombre, descripción o SKU. |
categories | cadena[] | Filtra por categorías. |
tags | cadena[] | Filtra por etiquetas. |
brand | cadena | Filtra por marca. |
inStock | booleano | Filtra por estado de stock. |
minPrice | número | Precio mínimo. |
maxPrice | número | Precio máximo. |
limit | número | Resultados por página (predeterminado: 50). |
offset | número | Desplazamiento de paginación. |
sortBy | cadena | Campo de orden: name, price, createdAt, updatedAt. |
sortOrder | cadena | asc o desc. |
Ejemplo:
curl "https://api-eu1.joryio.com/api/catalog/products?categories=T-Shirts&inStock=true&limit=20" \
-H "Authorization: Bearer $TOKEN"
Campos que rellenan las integraciones de tienda. Las respuestas de producto también incluyen campos que rellena una tienda conectada (WooCommerce) y que las rutas de creación y actualización no aceptan: gtin (GTIN, UPC, EAN o ISBN, o null), attributes (nombre del atributo a sus valores, por ejemplo {"Material": ["Linen"]}, o null), variants[].gtin y sourceModifiedAt (cuándo cambió la tienda el producto por última vez). brand y description (texto sin formato) se rellenan igual en los productos sincronizados.
Obtener categorías
Lista todas las categorías de producto.
GET /catalog/categories
Obtener marcas
Lista todas las marcas de producto.
GET /catalog/brands
Pedidos
Crear un pedido
Rastrea un pedido nuevo.
POST /orders
Cuerpo de la solicitud:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
orderId | cadena | Sí | Tu ID de pedido (número de pedido de tu plataforma). |
userId | cadena | Uno de los dos | ID de usuario de tu cliente. |
joryioUserId | cadena | Uno de los dos | ID de usuario interno de Joryio (alternativa a userId). |
total | número | Sí | Total del pedido. |
items | objeto[] | Sí | Líneas de pedido. |
status | cadena | No | Estado del pedido (predeterminado: pending). |
paymentStatus | cadena | No | paid o unpaid: si el pedido se ha pagado. Un pedido contra reembolso o por transferencia bancaria está processing y unpaid hasta que llega el dinero, y en la página Pedidos se muestra como Pendiente de pago en lugar de Pagado. Sin él, un pedido en proceso / enviado / entregado cuenta como pagado (un pedido existente conserva su valor). Los ingresos no dependen de él. Consulta Pagado o pendiente de pago. |
currency | cadena | No | Código de moneda. |
subtotal | número | No | Subtotal antes de descuentos. |
discount | número | No | Importe del descuento. |
shipping | número | No | Coste de envío. |
tax | número | No | Importe de impuestos. |
totalRefunded | número | No | Importe reembolsado hasta ahora, en la moneda del pedido (predeterminado 0). Para un reembolso parcial, envía el importe parcial con status: partiallyRefunded; para un reembolso total, define total con status: refunded. Los reembolsos se deducen de los ingresos atribuidos. |
refunds | objeto[] | No | Todos los reembolsos hasta ahora (hasta 100): { "id": "RF-1", "amount": 29.99, "createdAt": "2026-06-03T09:00:00Z", "reason": "Wrong size", "items": [{ "productId": "SKU-1", "quantity": 1 }] }; solo amount es obligatorio. Cada reembolso nuevo emite un evento Order Refunded (por id; sin él, cuando sube el importe reembolsado) y un reembolso ya informado no vuelve a emitir nada. Si se envían sin totalRefunded, su suma es el importe reembolsado. Pasar a status: cancelled (o POST /orders/:id/cancel) emite un Order Cancelled. Consulta Eventos de reembolso y cancelación. |
couponCode | cadena | No | Cupón aplicado. |
coupons | object[] | No | Cada código de cupón que usó el pedido (hasta 50): { "code": "SUMMER10", "amount": 10.0, "type": "fixed_amount" }. amount (el descuento que dio el código) y type son opcionales. En el upsert, coupons reemplaza los códigos guardados ([] los borra); omítelo para conservarlos. Si se envían ambos, coupons prevalece sobre couponCode. |
shippingAddress | objeto | No | Dirección de envío. |
campaignId | cadena | No | Campaña de atribución. |
canvasId | cadena | No | Canvas de atribución. |
source | cadena | No | Fuente del pedido (email, sms, direct). |
utmSource | cadena | No | Fuente UTM. |
utmMedium | cadena | No | Medio UTM. |
utmCampaign | cadena | No | Campaña UTM. |
salesChannel | cadena | No | Dónde se hizo el pedido: online, pos, app, phone, marketplace u other (mayúsculas o minúsculas). Cualquier otro valor devuelve 400. Si se omite, un pedido nuevo es online y uno existente conserva su canal. Consulta Tienda y canal de venta. |
storeId | cadena | No | Tu propio ID para la tienda a la que pertenece el pedido (una tienda física, una sucursal, un segundo sitio web), hasta 100 caracteres. Lo comparan el filtro de Tienda de la página de Pedidos y la condición de segmento "Compró en la tienda". |
storeName | cadena | No | El nombre visible de la tienda, hasta 255 caracteres, que se muestra junto a storeId. |
Debes proporcionar userId o joryioUserId. Usa userId con tus propios identificadores de usuario (lo más habitual). Usa joryioUserId si dispones del ID de usuario interno de Joryio (el id de 24 caracteres hexadecimales devuelto por la API de usuarios).
Estructura de línea de pedido:
{
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"sku": "BTS-001",
"quantity": 2,
"price": 29.99,
"total": 59.98,
"imageUrl": "https://example.com/image.jpg"
}
Solicitud de ejemplo:
curl -X POST https://api-eu1.joryio.com/api/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orderId": "ORD-2024-001",
"userId": "user-123",
"total": 89.97,
"subtotal": 99.97,
"discount": 10.00,
"shipping": 5.99,
"tax": 4.99,
"currency": "USD",
"coupons": [
{ "code": "SAVE10", "amount": 10.00, "type": "fixed_amount" }
],
"items": [
{
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"quantity": 2,
"price": 29.99,
"total": 59.98
},
{
"productId": "prod_def456",
"name": "Black Jeans",
"quantity": 1,
"price": 49.99,
"total": 49.99
}
],
"source": "email",
"salesChannel": "pos",
"storeId": "tlv-dizengoff",
"storeName": "Tel Aviv - Dizengoff",
"campaignId": "camp_xyz789"
}'
Tienda y canal de venta
Cada pedido indica dónde se vendió (salesChannel) y, cuando la nombra, a qué tienda pertenece (storeId). Así, un comercio con una tienda física y un sitio web, o con dos tiendas, puede filtrar y dividir los pedidos por cada una y crear segmentos como "compró en la tienda física en los últimos 90 días".
salesChannel | Significado |
|---|---|
online | Tu propio sitio web o checkout web (el valor por defecto) |
pos | Un punto de venta en una tienda física |
app | Tu propia app móvil |
phone | Introducido por el personal: pedidos por teléfono, email o manuales, borradores |
marketplace | Un marketplace de terceros |
other | Cualquier otra cosa |
Los valores son fijos e iguales en todas partes: en esta API, en las propiedades de eventos (sales_channel), en las condiciones de segmento y en la exportación CSV de Pedidos.
Las tiendas conectadas rellenan ambos por ti. La tienda es la propia conexión de tienda; el canal sale de lo que registra la plataforma:
| Tienda | Canal de venta |
|---|---|
| Shopify | Del source_name del pedido: web, online_store, checkout y los pedidos de suscripción son online; pos es pos; shopify_draft_order, iphone y android (pedidos que crea el personal en el admin o en la app de Shopify) son phone; cualquier otro es other |
| WooCommerce | De created_via: checkout y store-api son online; admin es phone; un plugin de punto de venta es pos; rest-api y cualquier otro es other |
| Magento | Un pedido creado en el admin (sin IP del comprador) es phone; cualquier otro es online |
| PrestaShop | Siempre online - PrestaShop no registra dónde se hizo el pedido |
Un evento Order Completed del SDK puede enviar sales_channel (y store_id, store_name). Sin él, un pedido desde una app iOS o Android es app y uno desde un sitio web es online. Los eventos Order Completed, Order Refunded y Order Cancelled llevan sales_channel y, cuando el pedido tiene tienda, store_id, para que un recorrido pueda dispararse con ellos.
Completar un pedido
Marca un pedido como enviado.
POST /orders/:id/fulfill
Cuerpo de la solicitud:
{
"trackingNumber": "1Z999AA10123456784",
"carrier": "UPS"
}
Cancelar un pedido
Cancela un pedido.
POST /orders/:id/cancel
Cuerpo de la solicitud:
{
"reason": "Customer requested cancellation"
}
Reembolsar un pedido
Procesa un reembolso.
POST /orders/:id/refund
Cuerpo de la solicitud:
{
"refundAmount": 29.99,
"reason": "Product defective",
"partial": true
}
Estadísticas de pedidos
Obtiene estadísticas de pedidos para un intervalo de fechas.
GET /orders/stats?startDate=2024-01-01&endDate=2024-01-31
Respuesta:
{
"totalOrders": 156,
"totalRevenue": 12450.50,
"averageOrderValue": 79.81,
"ordersByStatus": {
"pending": 5,
"processing": 12,
"shipped": 45,
"delivered": 90,
"cancelled": 4
}
}
Rastreo de carritos
Actualizar un carrito
Rastrea o actualiza el carrito de un usuario.
PUT /carts
Cuerpo de la solicitud:
{
"userId": "user_123",
"items": [
{
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"price": 29.99,
"quantity": 2,
"total": 59.98,
"imageUrl": "https://example.com/image.jpg"
}
],
"value": 59.98,
"subtotal": 59.98,
"currency": "USD",
"checkoutUrl": "https://store.example.com/checkout?cart=abc123"
}
value es el total del carrito tal como lo cuentas (puede incluir envío e impuestos). subtotal es opcional: el subtotal de los artículos, antes de envío e impuestos - si no se envía, se calcula a partir de los artículos (el total de cada línea, o si no price x quantity). El subtotal es el valor del carrito en E-Commerce → Carritos, en el filtro de segmento Valor del carrito, en minValue y en totalAbandonedValue.
Añadir al carrito
Añade un artículo al carrito de un usuario.
POST /carts/add
Cuerpo de la solicitud:
{
"userId": "user_123",
"item": {
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"price": 29.99,
"quantity": 1,
"total": 29.99
}
}
Obtener carritos abandonados
Lista carritos abandonados para campañas de recuperación.
GET /carts/abandoned
Parámetros de consulta:
| Parámetro | Tipo | Descripción |
|---|---|---|
minValue | número | Valor mínimo del carrito. |
abandonedMinutesAgo | número | Máximo de minutos desde el abandono. |
limit | número | Resultados por página. |
offset | número | Desplazamiento de paginación. |
Respuesta:
{
"data": [
{
"id": "cart_123",
"userId": "user_456",
"items": [...],
"value": 149.99,
"abandonedAt": "2024-01-20T15:30:00.000Z",
"checkoutUrl": "https://store.example.com/checkout?cart=abc"
}
],
"total": 42,
"hasMore": true
}
Estadísticas de carritos
Obtiene estadísticas de carritos y abandonos.
GET /carts/stats
Respuesta:
{
"totalCarts": 1250,
"abandonedCarts": 312,
"recoveredCarts": 87,
"totalAbandonedValue": 45670.50,
"recoveryRate": 27.88
}
Análisis RFM
El análisis RFM (recencia, frecuencia y valor monetario) segmenta a los clientes según su comportamiento de compra.
Obtener datos RFM de un usuario
Obtiene las puntuaciones y el segmento RFM de un usuario concreto.
GET /rfm/user/:userId
:userId es el id interno del usuario o el id externo con el que tu app identifica a la persona; un id desconocido devuelve 404.
Respuesta:
{
"totalOrders": 12,
"totalSpent": 849.50,
"averageOrderValue": 70.79,
"firstOrderDate": "2023-06-15T10:00:00.000Z",
"lastOrderDate": "2024-01-18T14:30:00.000Z",
"daysSinceLastOrder": 2,
"hasActiveCart": false,
"rfmRecency": 5,
"rfmFrequency": 4,
"rfmMonetary": 4,
"rfmScore": "544",
"rfmSegment": "champions"
}
Los campos RFM son null para un contacto sin pedidos contados (los pedidos cancelados y reembolsados no cuentan).
Obtener distribución RFM
Obtiene la distribución de clientes entre los segmentos RFM.
GET /rfm/distribution
Respuesta:
{
"champions": { "count": 150, "totalValue": 125000 },
"loyal": { "count": 320, "totalValue": 89000 },
"potential_loyalists": { "count": 180, "totalValue": 32000 },
"new_customers": { "count": 450, "totalValue": 28000 },
"at_risk": { "count": 95, "totalValue": 42000 },
"lost": { "count": 280, "totalValue": 15000 },
"scoring": {
"computedAt": "2026-09-02T02:00:00.000Z",
"method": "quantile",
"buyers": 1475
}
}
Los niveles son quintiles dentro de la tienda (method: "quantile"): la recencia, la frecuencia y el gasto neto se ordenan entre los compradores de la tienda y se dividen en cinco grupos iguales, de modo que el nivel superior es el quinto superior de tus clientes, sea cual sea la moneda. Una tienda con menos de 25 compradores usa umbrales fijos (method: "fixed"). Las puntuaciones se recalculan cada noche y con cada pedido; scoring.computedAt indica cuándo.
Segmentos RFM
| Segmento | Descripción | Puntuaciones RFM típicas |
|---|---|---|
| Campeones | Mejores clientes; compran a menudo y gastan más. | 555, 554, 545 |
| Leales | Clientes constantes. | 444, 443, 434 |
| Leales potenciales | Recientes, con frecuencia media. | 433, 343, 333 |
| Clientes nuevos | Acaban de hacer su primera compra. | 511, 512, 411 |
| Prometedores | Recientes, pero con frecuencia baja. | 422, 322, 312 |
| Necesitan atención | Interacción media en descenso. | 332, 322, 233 |
| A punto de dormirse | Por debajo de la media, en riesgo. | 211, 212, 221 |
| En riesgo | Fueron leales, pero no han comprado recientemente. | 144, 143, 244 |
| No se pueden perder | Fueron los mejores clientes y ahora están inactivos. | 155, 154, 255 |
| Hibernando | Interacción baja, inactivos desde hace tiempo. | 122, 121, 112 |
| Perdidos | Puntuaciones más bajas, probablemente abandonaron. | 111 |
Seguimiento de la tienda Shopify
Activa (o actualiza) el píxel de Joryio en la tienda Shopify conectada - eventos de la tienda y del checkout - y escribe los ajustes que lee el app embed de Joryio. Cuando el píxel está activo, el antiguo script de seguimiento de la tienda (tiendas conectadas antes de octubre de 2026) deja de enviar eventos - Joryio no lo elimina; Shopify lo quita al desinstalar la app y deja de cargarlo el 1 de marzo de 2027. Se puede volver a llamar sin riesgo. Requiere integrations:write.
POST /api/integrations/shopify/storefront-tracking
Authorization: Bearer YOUR_API_KEY
{
"pixel": { "status": "active", "id": "gid://shopify/WebPixel/123", "error": null, "checkedAt": "2026-10-03T10:00:00.000Z" },
"missingScopes": [],
"legacyScriptTag": false,
"appEmbed": {
"configured": true,
"themeEditorUrl": "https://my-store.myshopify.com/admin/themes/current/editor?context=apps&activateAppId=<api-key>/joryio-embed"
}
}
pixel.status | Significado |
|---|---|
active | El píxel está en la tienda y envía eventos |
needs_scopes | La tienda no ha aprobado write_pixels y read_customer_events. Vuelve a iniciar la conexión con Shopify (pide al propietario que los apruebe); missingScopes los enumera |
error | Shopify rechazó el píxel; pixel.error indica el motivo |
not_registered | Aún no está configurado |
GET /api/integrations/shopify devuelve el mismo objeto como storefrontTracking. Consulta Integración con Shopify.
También devuelve themeEmbed, la fila Ventanas emergentes en tu tienda: activateUrl abre el editor de temas con el embed de Joryio activado (el propietario pulsa Guardar; null cuando el servidor no tiene una app de Shopify configurada), y status es on cuando el SDK de la tienda ha informado en los últimos 7 días, si no not_reported. Consulta Activar las ventanas emergentes.
"themeEmbed": {
"activateUrl": "https://my-store.myshopify.com/admin/themes/current/editor?context=apps&activateAppId=<api-key>/joryio-embed",
"status": "not_reported"
}
installUrl (también cuando no hay ninguna tienda conectada) es la dirección a la que Instalar Joryio desde Shopify envía a un comerciante que aún no ha instalado Joryio: la ficha de la app Joryio en la Shopify App Store o un enlace de instalación, o null cuando el servidor no tiene ninguna configurada (el panel enlaza entonces con los pasos de instalación). Joryio nunca pide la dirección de una tienda - la tienda se instala desde Shopify y se vincula. Consulta Conectar tu tienda.
Apertura de la aplicación desde Shopify
Cuando un comerciante instala o abre la aplicación de Joryio desde Shopify, Shopify añade a la URL de la aplicación parámetros firmados (shop, hmac, timestamp, host). Envíalos sin cambios; Joryio comprueba la firma de Shopify y que el enlace no tenga más de una hora, y devuelve el estado de la tienda en el espacio de trabajo actual. Nunca conecta nada. Requiere integrations:read.
POST /api/integrations/shopify/app-launch
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "query": { "shop": "my-store.myshopify.com", "host": "YWRtaW4uc2hvcGlmeS5jb20vc3RvcmUvbXktc3RvcmU", "timestamp": "1760000000", "hmac": "<from Shopify>" } }
{ "status": "not_connected", "shopDomain": "my-store.myshopify.com" }
status | Significado |
|---|---|
connected_here | La tienda está conectada a este espacio de trabajo |
connected_elsewhere | Conectada a otro espacio de trabajo de la organización; workspaceId lo indica |
not_connected | No está conectada en la organización - inicia la conexión con POST /api/integrations/shopify/oauth/initiate |
Los parámetros sin firma, alterados o con más de una hora reciben 400. Consulta Integración de Shopify.
Ajuste de nueva suscripción en el pago de la tienda
Activa o desactiva el ajuste Permitir que las suscripciones en el pago vuelvan a suscribir a quien se dio de baja de la conexión de la tienda. :platform es shopify, woocommerce, magento o prestashop. Desactivado por defecto. Si está activado, una suscripción en el pago o en el registro del cliente de esa tienda - incluidos los pagos como invitado - con fecha posterior a la última baja del contacto lo vuelve a suscribir. Nunca pasa por encima de una queja de spam, un rebote permanente, un STOP por SMS (solo una confirmación de doble opt-in), un bloqueo de toda la organización ni la baja de otro contacto. Cada cambio queda en el registro de auditoría. Requiere settings:write.
PUT /api/integrations/shopify/consent-settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "checkoutResubscribe": true }
{ "checkoutResubscribe": true }
GET /api/integrations/<platform> devuelve el valor actual en checkoutResubscribe. Consulta Consentimiento de marketing de tiendas.
Salvaguardas de las casillas de consentimiento en el pago (WooCommerce, Magento, PrestaShop)
La misma ruta acepta las salvaguardas de Record consent immediately de una conexión WooCommerce o Magento, y las mismas salvaguardas para la casilla de newsletter del checkout de PrestaShop, que siempre llega a Joryio antes del pedido (módulo 1.2.8 o posterior - consulta Cuándo cuenta una marca). :platform es woocommerce, magento o prestashop; Shopify recibe 400. Una marca en el checkout que llega antes de un pedido cuenta al momento solo para un comprador o suscriptor conocido; cualquier otro queda pendiente hasta su pedido o el enlace de confirmación - consulta Cuándo cuenta una marca. checkoutConsentConfirmEmail (por defecto false) envía a ese invitado un enlace de confirmación por correo. checkoutConsentTickLimit (1-100, por defecto 3) es cuántas marcas se aceptan por dirección IP del comprador, por checkout y por dirección de correo cada hora; una marca por encima se ignora. Envía uno o ambos, solos o con checkoutResubscribe. Cada cambio queda en el registro de auditoría. Requiere settings:write.
PUT /api/integrations/magento/consent-settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "checkoutConsentConfirmEmail": true, "checkoutConsentTickLimit": 5 }
{ "checkoutConsentConfirmEmail": true, "checkoutConsentTickLimit": 5 }
GET /api/integrations/woocommerce, GET /api/integrations/magento y GET /api/integrations/prestashop devuelven ambos valores.
newShopperOptIns - Consentimientos de compradores nuevos (que aún no han comprado) - es hold (predeterminado) o immediate. Rige todo consentimiento que llega sin pedido de alguien que Joryio aún no conoce: una casilla marcada en el checkout enviada antes del pedido, y un consentimiento del registro, de la página de la cuenta, del boletín o de la sincronización de clientes. hold lo deja pendiente hasta el pedido o el enlace de confirmación; immediate lo cuenta al momento (menos estricto - cualquier visitante puede introducir cualquier correo). Los compradores conocidos y los suscriptores existentes cuentan al momento en ambos casos, una baja siempre se aplica al momento y checkoutConsentTickLimit sigue vigente. La misma ruta y plataformas, solo o con los demás campos; cada cambio queda en el registro de auditoría; requiere settings:write. GET /api/integrations/<platform> lo devuelve como newShopperOptIns.
PUT /api/integrations/woocommerce/consent-settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "newShopperOptIns": "immediate" }
{ "newShopperOptIns": "immediate" }
GET /api/integrations/woocommerce y GET /api/integrations/magento también devuelven storeCheckoutOptIn: los ajustes de consentimiento en el pago del propio plugin tal como los informó por última vez (plugin de WooCommerce 1.6.16 o posterior, extensión de Magento 1.4.17 o posterior). Son de solo lectura - se configuran en la tienda, nunca con esta API - y valen null hasta que el plugin los informa. recordMode es order (solo cuando se realiza el pedido) o immediate (registrar el consentimiento de inmediato).
{ "storeCheckoutOptIn": { "pluginVersion": "1.6.16", "checkoutBox": true, "preTicked": false, "recordMode": "order", "reportedAt": "2026-10-27T10:00:00.000Z" } }
Identidad del visitante: reconocer a los visitantes por el email que escriben
Recognise visitors by the email they type - consulta WooCommerce y Shopify. Activado por defecto: un email escrito en el checkout o en un formulario identifica el navegador del visitante como el contacto con ese email. Desactivado (identidad estricta): un email escrito nunca identifica el navegador; los carritos, pedidos y envíos de formularios siguen yendo al contacto del email.
GET /api/integrations/<platform> devuelve visitorIdentity: identifyByTypedEmail, source (default - aún no se ha informado ni configurado nada, es decir, activado; plugin - informado por el plugin de WooCommerce 1.6.22 o posterior, la extensión de Magento 1.4.21 o posterior o el módulo de PrestaShop 1.2.10 o posterior; joryio - configurado en Joryio), pluginVersion y reportedAt. En WooCommerce, Magento y PrestaShop el ajuste vive en el plugin de la tienda y aquí es solo de lectura. En Shopify, configúralo con la ruta siguiente; las demás plataformas reciben 400. Cada cambio queda en el registro de auditoría. Requiere settings:write.
PUT /api/integrations/shopify/visitor-identity
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "identifyByTypedEmail": false }
{ "visitorIdentity": { "identifyByTypedEmail": false, "source": "joryio", "pluginVersion": null, "reportedAt": "2026-11-01T10:00:00.000Z" } }
Conexión con PrestaShop (acceso anticipado)
Lee y gestiona la conexión con PrestaShop del espacio de trabajo (una tienda PrestaShop por conexión; consulta Integración con PrestaShop). GET devuelve { "connected": false, "platform": "prestashop" } cuando no hay nada conectado. La ruta api-key devuelve la clave jry_ps_ que pegas en el módulo (requiere integrations:write, igual que settings, sync y DELETE). GET también devuelve workspaceId, prestashopVersion y moduleVersion. sync acepta type: full (por defecto), delta (productos, clientes y pedidos que cambiaron desde la última sincronización correcta), products, customers, orders o carts (carritos abiertos de las últimas 24 horas), y responde al momento mientras la sincronización se ejecuta. DELETE desconecta la tienda y revoca la clave.
GET /api/integrations/prestashop
Authorization: Bearer YOUR_API_KEY
{
"connected": true,
"status": "connected",
"platform": "prestashop",
"storeName": "My Shop",
"storeUrl": "https://shop.example.com",
"shopId": 1,
"workspaceId": "6f1c2d3e-...",
"prestashopVersion": "8.2.1",
"moduleVersion": "1.0.0",
"syncSettings": { "syncProducts": true, "syncOrders": true, "syncCustomers": true, "syncCarts": true, "historicalDays": 90 },
"checkoutResubscribe": false,
"syncStats": { "productCount": 120, "orderCount": 860, "customerCount": 540 },
"lastSyncAt": "2026-10-21T10:00:00.000Z",
"connectedAt": "2026-10-21T09:58:00.000Z"
}
connectedAt es la última vez que la tienda se conectó (cada reconexión lo actualiza); GET /api/integrations/shopify, /woocommerce y /magento también lo devuelven. Para una conexión anterior a que se registrara, se devuelve la fecha de creación del registro.
GET /api/integrations/prestashop/api-key
Authorization: Bearer YOUR_API_KEY
{ "apiKey": "jry_ps_...", "isNew": false }
PUT /api/integrations/prestashop/settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "syncCarts": true, "historicalDays": 90 }
POST /api/integrations/prestashop/sync
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "type": "full" }
DELETE /api/integrations/prestashop
Authorization: Bearer YOUR_API_KEY
Webhooks
Los eventos de e-commerce emiten webhooks que pueden activar flujos de Canvas:
| Evento | Descripción |
|---|---|
ecommerce.order.created | Se realizó un pedido nuevo. |
ecommerce.order.fulfilled | Pedido enviado. |
ecommerce.order.cancelled | Pedido cancelado. |
ecommerce.order.refunded | Pedido reembolsado. |
ecommerce.order.status_changed | Cambió el estado del pedido. |
ecommerce.cart.abandoned | Carrito marcado como abandonado. |
ecommerce.cart.recovered | Carrito abandonado recuperado. |
ecommerce.cart.updated | Cambió el contenido del carrito. |
Consultar por tus ID
Obtener un producto por tu ID de producto
Recupera un producto mediante tu identificador de producto.
GET /catalog/products/by-product-id/:productId
Un identificador sin registro asociado responde 404, no 200 con un cuerpo vacío.
Ejemplo:
curl "https://api-eu1.joryio.com/api/catalog/products/by-product-id/SKU-12345" \
-H "Authorization: Bearer $TOKEN"
Obtener un pedido por tu ID de pedido
Recupera un pedido mediante tu identificador de pedido.
GET /orders/by-order-id/:orderId
Un identificador sin registro asociado responde 404, no 200 con un cuerpo vacío.
Ejemplo:
curl "https://api-eu1.joryio.com/api/orders/by-order-id/ORD-2024-001" \
-H "Authorization: Bearer $TOKEN"
Pedidos, carritos y RFM por API
Alta/actualización masiva de pedidos
POST /orders/upsert
La forma principal en que una tienda que no está en Shopify, WooCommerce o Magento envía sus pedidos. Idempotente por orderId: reenviar el mismo pedido lo actualiza en lugar de duplicarlo, así que reintentar tras un fallo de red es seguro.
Envía cada pedido en la moneda en que se pagó, sin convertir nada: Joryio convierte total a la moneda de los informes del espacio de trabajo al tipo del Banco Central Europeo de la fecha placedAt del pedido, y los informes suman ese importe convertido.
Cuerpo de la petición:
{
"orders": [
{
"orderId": "1001",
"userId": "user_123",
"total": 89.90,
"currency": "EUR",
"placedAt": "2026-08-27T10:15:00Z",
"status": "completed",
"items": [
{ "productId": "prod_abc123", "quantity": 2, "price": 29.95 }
]
}
]
}
Respuesta:
{ "upserted": 1, "created": 1, "updated": 0 }
Tendencia de ingresos
GET /orders/revenue-trend?days=30&granularity=day
Ingresos netos en el tiempo (sin reembolsos ni cancelaciones). granularity es day, week o month.
Respuesta:
{
"currency": "EUR",
"points": [
{ "date": "2026-08-25", "revenue": 1240.50, "orders": 14 },
{ "date": "2026-08-26", "revenue": 980.00, "orders": 11 }
]
}
Ciclo de vida del carrito
POST /carts/remove
POST /carts/clear
POST /carts/mark-abandoned
POST /carts/recovered
Cuatro acciones que impulsan los recorridos de recuperación de carrito. clear y recovered terminan ambos el carrito y NO son intercambiables: recovered dice que la recuperación funcionó y le atribuye el pedido; clear dice que el carrito simplemente ya no existe. Usar clear tras una compra borra en silencio la prueba de que tu recorrido de recuperación aportó algo.
{ "userId": "user_123", "orderId": "1001" }
Listar segmentos RFM
GET /rfm/segments
Los segmentos RFM del espacio de trabajo con sus tamaños actuales, para que una integración pueda segmentar sin recalcular la puntuación.
Respuesta:
{
"segments": [
{ "segment": "champions", "users": 412, "avgRecencyDays": 8, "avgOrders": 6.2 },
{ "segment": "at_risk", "users": 1180, "avgRecencyDays": 96, "avgOrders": 2.1 }
],
"computedAt": "2026-08-27T02:10:00Z"
}
Estadísticas del catálogo
GET /catalog/stats
Totales y salud del catálogo, contados sobre todos los productos, nunca sobre una página.
Respuesta:
{
"productCount": 8214,
"categoryCount": 37,
"brandCount": 12,
"outOfStockCount": 341,
"lowStockCount": 96,
"unclassifiedCount": 18,
"lowStockThreshold": 10
}
outOfStockCount cuenta los productos con quantity 0, o inStock: false cuando no se controla cantidad; lowStockCount, los que están entre 1 y lowStockThreshold; unclassifiedCount, los que no tienen categoría o marca.
Respuestas de error
{
"statusCode": 404,
"message": "Product SKU-12345 not found",
"error": "Not Found"
}
| Código de estado | Descripción |
|---|---|
| 400 | Cuerpo de solicitud no válido. |
| 401 | Se requiere autenticación. |
| 403 | Permisos insuficientes. |
| 404 | Recurso no encontrado. |
| 409 | Conflicto (ID externo duplicado). |
| 500 | Error interno del servidor. |