Saltar al contenido principal

API de usuarios

Crea, actualiza y gestiona perfiles de usuario mediante programación.

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.

Autenticación​

Todas las solicitudes requieren autenticación mediante clave de API:

Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json

Track​

La única puerta de ingesta: quién es alguien, qué hizo y qué compró, en una sola llamada.

Cada campo acepta un objeto o un array, así que una petición puede cubrir a una persona o a mil.

Endpoint​

POST /users/track

Cuerpo de la petición​

CampoTipoObligatorioDescripción
attributesobjeto o arrayNoCampos de perfil. Persisten en la persona
eventsobjeto o arrayNoCosas que ocurrieron. Se añaden a la cronología
purchasesobjeto o arrayNoPedidos, con la misma forma que POST /orders

Se requiere al menos uno de los tres. Límites por petición: 1000 attributes, 500 events, 500 purchases. Una petición que supere un límite se rechaza en lugar de truncarse: procesar en silencio los primeros 500 de 5000 perdería el resto sin dejar rastro.

Los attributes se aplican primero

Dentro de una petición, attributes se escriben antes que events y purchases, de modo que un evento puede referirse a una persona creada en la misma llamada.

Identidad​

CampoDescripción
externalIdTu identificador, y la única clave de creación-o-actualización
userIdEl id interno de Joryio (24 hex). Solo actualiza: nunca crea
anonymousIdUna sesión sin identificar. Vincúlala después con POST /users/identify
No envíes Order Completed como evento

Ese evento se emite automáticamente al enviar una compra. Enviarlo a mano se rechaza, porque las dos formas en que falla son silenciosas: por sí solo registra ingresos sin un pedido detrás, así que la página de Pedidos, RFM y el valor de vida del cliente nunca ven la venta; junto a una compra real cuenta la venta dos veces.

Coincidencia por email​

Un elemento de attributes con email pero sin externalId ni userId se busca por el email (en minúsculas, sin espacios en los extremos), dentro del espacio de trabajo. Una dirección de email no es única - una pareja o una familia pueden compartir una, y una persona puede existir con dos identificadores - así que:

  • Ningún contacto tiene el email: se crea un contacto nuevo.
  • Exactamente uno lo tiene: se actualiza ese contacto.
  • Varios lo tienen: se actualiza exactamente uno - el activo más recientemente, el contacto cuyo perfil cambió más recientemente (en caso de empate, el creado más recientemente). La misma petición elige siempre el mismo contacto. Los demás quedan como están: Joryio nunca fusiona contactos ni los actualiza todos.
  • Una baja de una lista en el elemento sigue aplicándose a todos los contactos con ese email. Un alta en una lista se aplica solo al contacto actualizado.

Un elemento que coincidió con varios contactos aparece en attributes.matchedSeveral - un aviso, no un error: la escritura se realizó.

{
"attributes": {
"processed": 1,
"failed": [],
"matchedSeveral": [
{ "index": 0, "userId": "6650f1c2a9e4b3d2c1f0e9a8", "count": 2 }
]
}
}

userId es el contacto que se actualizó y count cuántos contactos comparten la dirección. matchedSeveral aparece solo cuando ocurrió. Para actualizar un contacto exacto, envía externalId (se busca solo por externalId, aunque el email sea compartido) o el userId del contacto. Las importaciones de archivos y las sincronizaciones desde data warehouse buscan de la misma forma.

Suscripciones a listas​

Un elemento de attributes también puede definir las suscripciones a listas de la persona, así que una solicitud crea o actualiza la persona, registra sus eventos y la suscribe a una lista.

CampoTipoObligatorioDescripción
subscriptions[].listIdstringSíEl id de la lista de suscripción (API de suscripciones)
subscriptions[].channelstringNoemail (predeterminado), sms, whatsapp, push o viber
subscriptions[].statusstringNosubscribed (predeterminado) o unsubscribed

Hasta 100 por elemento. Se aplican después de guardar la persona, por la misma vía de consentimiento que POST /subscriptions/contacts/:userId/lists/:listId, y se registran con el origen api en el historial de consentimiento.

  • Una baja existente nunca se reactiva. Si la persona ya se dio de baja de esa lista en ese canal, una solicitud subscribed se omite y se informa. Volver a suscribirla es una llamada deliberada al endpoint de suscripción. status: "unsubscribed" siempre se aplica.
  • La lista debe pertenecer a este workspace. Un id de lista desconocido o archivado, o una lista de otro workspace, se omite y se informa; nunca se escribe.
  • Sin email de confirmación. La API registra la suscripción directamente, incluso en una lista que exige doble opt-in: ese paso de confirmación corresponde a los formularios de alta. Envía por la API solo el consentimiento que ya hayas recogido.
  • Informe por índice de elemento. Cuando algún elemento envía subscriptions, el resultado de attributes añade subscriptions: { applied, skipped[] }, y cada omisión indica el index del elemento, el listId, el channel y el reason. Una suscripción omitida no hace fallar el elemento: la persona se crea o actualiza igualmente.
{
"attributes": {
"processed": 1,
"failed": [],
"subscriptions": {
"applied": 0,
"skipped": [
{
"index": 0,
"listId": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"channel": "email",
"reason": "Existing opt-out is not resurrected by user creation; use POST /subscriptions/contacts/:userId/lists/:listId for an explicit re-opt-in"
}
]
}
}
}

Ejemplo de petición​

curl -X POST https://api-eu1.joryio.com/api/users/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"externalId": "user_123",
"email": "[email protected]",
"attributes": { "firstName": "John", "plan": "premium" }
},
"events": {
"externalId": "user_123",
"name": "Product Viewed",
"properties": { "productId": "SKU-456", "price": 49.99 }
},
"purchases": {
"orderId": "ord_789",
"externalId": "user_123",
"total": 129.90,
"currency": "USD",
"items": [
{ "productId": "SKU-456", "name": "Blue T-Shirt", "quantity": 1, "price": 129.90, "total": 129.90 }
]
}
}'

Respuesta​

Cada array informa por separado. Un elemento incorrecto nunca hace fallar el lote: los fallos se detallan por índice y todo lo válido se procesa igualmente.

{
"attributes": { "processed": 1, "failed": [] },
"events": { "processed": 1, "failed": [] },
"purchases": { "processed": 1, "failed": [] }
}

Advertencias: aceptado, pero no alcanzable​

Una dirección de correo mal formada se almacena, no se rechaza: este endpoint lo usan integraciones de tipo fire-and-forget, donde un 400 que nadie lee significa perder el registro entero. La dirección se marca internamente y se excluye de todos los envíos de correo, así que no puede provocar un rebote duro.

Como eso no se ve en el valor almacenado, la respuesta lo indica:

{
"attributes": {
"processed": 2,
"failed": [],
"warnings": [
{
"index": 1,
"field": "email",
"reason": "Stored, but flagged undeliverable and excluded from email sends. Correct the address to make this contact reachable by email."
}
]
}
}

warnings solo aparece cuando hay algo que informar. Un contacto con advertencia se crea y actualiza con normalidad y es alcanzable por cualquier otro canal: es la dirección de correo la que nunca recibirá nada.

Números de teléfono​

Envía los números de teléfono con + y el código de país

Envía siempre los números de teléfono en formato internacional completo (E.164): +, el código de país y luego el número - por ejemplo +972501234567. Un número sin + se lee con el país predeterminado para números de teléfono del espacio de trabajo; si no hay ninguno, o los dígitos se pueden leer de dos formas, se guarda sin verificar y no se usa para mensajes. Cómo se tratan los números de teléfono

Envía phone en formato internacional (+972501234567). Un número escrito sin código de país (0501234567) se interpreta con el país predeterminado para números de teléfono del workspace: con Israel se guarda como +972501234567. Joryio nunca adivina el país: sin país predeterminado, ese número no se convierte.

Un teléfono que no es un número válido se conserva, no se descarta: se guarda en un campo aparte del contacto, phoneUnverified, y el phone actual del contacto no cambia. Un número sin verificar nunca se usa para enviar mensajes, bloqueos, identificar contactos ni filtros de teléfono en segmentos. Enviar después un teléfono válido lo sustituye. Los dígitos que incluyen el código de país sin + (972501234567) se aceptan si son válidos, salvo una cadena de 10 dígitos, que podría ser un número local o de EE. UU. y se guarda sin verificar.

Con un país predeterminado, los dígitos que son un número válido de las dos formas - como número nacional de ese país y como número de otro país con su código (13123456789 con China es un móvil chino y también +1 312...) - son ambiguos: Joryio nunca elige uno de los dos y los guarda sin verificar. Envía esos números con +.

La respuesta lo indica en el mismo array warnings:

{
"attributes": {
"processed": 1,
"failed": [],
"warnings": [
{
"index": 0,
"field": "phone",
"reason": "Phone saved as unverified and not used for messages - the number is not valid as written. Add the country code (for example +972501234567), or set a default country for phone numbers in workspace settings."
}
]
}
}

Los contactos que devuelve la API incluyen phoneUnverified cuando lo tienen. Las importaciones de archivos y las sincronizaciones desde el almacén de datos siguen las mismas reglas.

En la página del contacto, un número sin verificar se puede corregir (se guarda como teléfono - si el contacto ya tiene teléfono, se reemplaza, y la página lo avisa antes) o descartar. Mediante la API, PUT /users/{userId} con {"clearPhoneUnverified": true} elimina el número sin verificar y deja phone como está.

Identify​

Vincula el comportamiento registrado en una sesión anónima a una persona conocida.

CampoTipoObligatorioDescripción
anonymousIdstringSíLa sesión en la que se registró el comportamiento
externalIdstringSíTu identificador al que vincularla
attributesobjetoNoCampos de perfil a establecer al identificar
curl -X POST https://api-eu1.joryio.com/api/users/identify \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "anonymousId": "anon_abc123", "externalId": "user_123" }'

En el primer inicio de sesión - cuando ningún perfil tiene aún ese externalId - el propio perfil anónimo pasa a ser el conocido y conserva su id y su historial. Si la persona ya tiene un perfil, el anónimo se fusiona con él y su historial se reasigna.

Dispositivos push​

Asocia tokens push de forma masiva: es la vía para migrar desde otro proveedor.

POST /api/users/track
Content-Type: application/json

{
"devices": [
{ "externalId": "user-123", "platform": "ios", "pushToken": "a1b2c3…", "appId": "<ios-app-id>",
"deviceInfo": { "model": "iPhone15,2" } },
{ "externalId": "user-456", "platform": "android", "pushToken": "d4e5f6…", "appId": "<android-app-id>" }
]
}
CampoTipoObligatorioDescripción
externalIdstringSíTu identificador de la persona. Envía userId en su lugar si tienes el id interno de Joryio
platformios | android | webSí
pushTokenstringSíEl token tal como lo registró tu proveedor anterior
appIdstringNo*A cuál de tus apps pertenece el token. Los envíos de campañas filtran por este campo, así que un token importado sin él se guarda pero nunca se usa. Opcional cuando el workspace tiene exactamente una app en esa plataforma: se resuelve automáticamente. Obligatorio cuando tienes varias. Obtén los ids con GET /apps
deviceInfoobjectNoModelo, versión del sistema, versión de la app: formato libre

Hasta 1000 dispositivos por solicitud. Los dispositivos se aplican después de attributes, así que una llamada puede crear las personas y asociar sus tokens.

Un contacto que no existe hace fallar ese elemento en lugar de crearse: un dispositivo sin perfil puede recibir un push y no pertenece a nadie. Envía esas personas en attributes en la misma solicitud.

Qué sobrevive realmente a una migración​

No todo, y esto decide el plan más que la API:

  • iOS / APNs: los tokens se emiten por (bundle id, entorno), no por proveedor. Con el mismo bundle id y tu propio .p8, se conservan.
  • Android / FCM: los tokens están ligados a un sender de Firebase. Con el mismo proyecto funcionan; con un proyecto distinto, todos estaban ya muertos.
  • Web push: una suscripción está ligada a la clave VAPID que la creó. No puede transferirse. Esos usuarios deben volver a recibir la solicitud de permiso, prometa lo que prometa cualquier API.

Un token que ya pertenece a otra persona​

Se mueve. Un token push identifica un dispositivo, no una persona, y las reinstalaciones, los teléfonos revendidos y las tabletas compartidas son normales, así que quien tiene el dispositivo es quien recibe el push.

El movimiento queda registrado en ambos lados. El dispositivo del contacto anterior se marca con deactivatedReason: "reassigned", reassignedToUserId y deactivatedAt, de modo que "¿por qué esta persona dejó de recibir push?" tiene respuesta en lugar de una fila inactiva que también podría significar una desinstalación.

Consentimiento y verificación​

Registrar un token nunca concede consentimiento push. Tener un token de un proveedor anterior no prueba que haya permiso, así que las reglas de suscripción no cambian: la supresión sigue aplicándose y conceder el permiso sigue siendo una acción de cumplimiento. Es la misma regla que sigue la importación de archivos.

Los tokens se aceptan tal cual y no se comprueban. Se presume que un token es válido hasta que un envío rebota, momento en el que la entrega lo desactiva.

Obtener un usuario por ID​

Recupera el perfil y los atributos de un usuario. Existen dos rutas de consulta:

  • GET /users/by-user-id/:userId: consulta mediante tu identificador (el externalId con el que creaste al usuario). Es la ruta recomendada para integraciones.
  • GET /users/:userId: consulta mediante el ID interno de Joryio (el id de 24 caracteres hexadecimales devuelto en las respuestas).

Endpoint​

GET /users/by-user-id/:userId

Parámetros de ruta​

ParámetroTipoDescripción
userIdcadenaTu identificador externo (externalId).

Solicitud de ejemplo​

curl -X GET https://api-eu1.joryio.com/api/users/by-user-id/user_123 \
-H "Authorization: Bearer jry_live_your_api_key"

Respuesta​

{
"id": "665f1e2a9b3c4d5e6f7a8b9c",
"userId": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"email": "[email protected]",
"phone": "+1234567890",
"attributes": {
"firstName": "John",
"lastName": "Doe",
"plan": "premium",
"lifetimeValue": 1250.50
},
"segments": [],
"rndBucket": 4821,
"createdVia": "woocommerce",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-20T14:20:00.000Z"
}

Listar usuarios​

Lista todos los usuarios con paginación, y opcionalmente se limita a una sola persona por coincidencia exacta. Los resultados se ordenan por última actualización, del más reciente al más antiguo. No hay más sintaxis de filtrado ni de ordenación que los parámetros siguientes: usa Segmentos para segmentar por atributos, y GET /users/search?query=... para coincidencias parciales o por prefijo en nombre, email, teléfono o ID.

Endpoint​

GET /users

Parámetros de consulta​

ParámetroTipoPredeterminadoDescripción
limitnúmero50Resultados por página (máx. 1000).
offsetnúmero0Número de usuarios que se omitirán.
emailstring-Coincidencia exacta, sin distinguir mayúsculas.
externalIdstring-Coincidencia exacta con tu identificador. GET /users/by-user-id/:userId devuelve ese registro directamente.
userIdstring-Coincidencia exacta con el id interno de Joryio (24 caracteres hex).
qstring-Coincidencia parcial: prefijo en email, externalId y nombre; dígitos en cualquier parte del teléfono; valor completo para un id de 24 hex o anonymousId. Se ignora si hay un parámetro exacto. Consulta Encontrar a una persona

Solicitud de ejemplo​

curl -X GET "https://api-eu1.joryio.com/api/users?limit=50&offset=0" \
-H "Authorization: Bearer jry_live_your_api_key"

Respuesta​

{
"data": [
{
"id": "665f1e2a9b3c4d5e6f7a8b9c",
"userId": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"email": "[email protected]",
"attributes": {
"plan": "premium"
}
},
{
"id": "665f1e2a9b3c4d5e6f7a8b9d",
"userId": "665f1e2a9b3c4d5e6f7a8b9d",
"externalId": "user_456",
"email": "[email protected]",
"attributes": {
"plan": "premium"
}
}
],
"pagination": {
"total": 156,
"limit": 50,
"offset": 0,
"hasMore": true
}
}

Actualizar un usuario​

Actualiza el email, teléfono o atributos de un usuario sin reemplazar el perfil completo. Los atributos se combinan por clave. Usa PUT (no hay ruta PATCH):

  • PUT /users/by-user-id/:userId: actualiza mediante tu identificador.
  • PUT /users/:userId: actualiza mediante el ID interno de Joryio.

Endpoint​

PUT /users/by-user-id/:userId

Cuerpo de la solicitud​

CampoTipoObligatorioDescripción
emailcadenaNoDirección de email nueva.
phonecadenaNoNúmero de teléfono nuevo.
attributesobjetoNoAtributos que se combinarán (actualización por clave).

Solicitud de ejemplo​

curl -X PUT https://api-eu1.joryio.com/api/users/by-user-id/user_123 \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"plan": "enterprise",
"mrr": 499
}
}'

Respuesta​

El objeto de usuario actualizado se devuelve directamente:

{
"id": "665f1e2a9b3c4d5e6f7a8b9c",
"userId": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"attributes": {
"firstName": "John",
"plan": "enterprise",
"mrr": 499,
"signupDate": "2024-01-15T10:30:00Z"
},
"updatedAt": "2024-01-20T15:30:00.000Z"
}

Eliminar un usuario​

Elimina permanentemente un usuario y todos sus datos asociados.

Endpoint​

DELETE /users/:userId

El parámetro de ruta es el ID de usuario interno de Joryio. Si solo tienes tu propio identificador, consúltalo primero mediante GET /users/by-user-id/:userId. Requiere el scope users:delete.

Solicitud de ejemplo​

curl -X DELETE https://api-eu1.joryio.com/api/users/665f1e2a9b3c4d5e6f7a8b9c \
-H "Authorization: Bearer jry_live_your_api_key"

Respuesta​

204 No Content: el cuerpo de respuesta está vacío.

Notas​

  • Esta acción es permanente y no se puede deshacer.
  • Elimina el perfil, los eventos y el historial de campañas del usuario.
  • El usuario se elimina de todos los segmentos.
  • Las campañas activas dejarán de dirigirse a este usuario.

Operaciones masivas (cuerpo de matriz)​

No hay un endpoint masivo independiente, ni ninguna ruta que acepte una matriz suelta de usuarios. Los lotes pasan por POST /users/track: cada uno de sus campos - attributes, events, purchases, devices - acepta un objeto único o una matriz, así que una solicitud puede llevar muchas personas, su actividad y sus suscripciones a listas.

Endpoint​

POST /users/track
Content-Type: application/json

{ "attributes": [ { ... }, { ... } ], "events": [ { ... } ] }

Cuerpo de la solicitud​

El cuerpo debe ser un objeto JSON con al menos uno de attributes, events, purchases, devices. Una matriz suelta ([ {...}, {...} ]) se rechaza con 400 "Body must be an object with any of: attributes, events, purchases, devices.", igual que un objeto vacío.

Cada elemento de una matriz tiene la misma forma que el objeto único descrito en Track. Cada elemento se valida por separado, y las claves desconocidas se rechazan en lugar de ignorarse.

Solicitud de ejemplo​

Crea o actualiza dos personas, suscribe la primera a una lista y registra un evento de alta para cada una, en una sola llamada. attributes se aplica antes que events, así que los eventos pueden referirse a personas creadas en la misma solicitud.

curl -X POST https://api-eu1.joryio.com/api/users/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"attributes": [
{
"externalId": "user_001",
"email": "[email protected]",
"attributes": { "firstName": "John", "plan": "free" },
"subscriptions": [
{ "listId": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d", "channel": "email" }
]
},
{
"externalId": "user_002",
"email": "[email protected]",
"attributes": { "firstName": "Jane", "plan": "premium" }
}
],
"events": [
{ "externalId": "user_001", "name": "Signed Up", "properties": { "source": "landing_page" } },
{ "externalId": "user_002", "name": "Signed Up", "properties": { "source": "referral" } }
]
}'

Respuesta​

Un resumen por matriz. processed cuenta los elementos aplicados; failed enumera el resto por su índice en esa matriz. warnings aparece solo cuando un elemento se guardó pero requiere atención (consulta Track).

{
"attributes": {
"processed": 2,
"failed": [],
"subscriptions": { "applied": 1, "skipped": [] }
},
"events": { "processed": 2, "failed": [] },
"purchases": { "processed": 0, "failed": [] },
"devices": { "processed": 0, "failed": [] }
}

Respuesta con errores​

Un elemento mal formado nunca hace fallar el lote - todo lo válido se procesa igualmente:

{
"attributes": {
"processed": 1,
"failed": [
{ "index": 1, "reason": "property emial should not exist" }
],
"subscriptions": {
"applied": 0,
"skipped": [
{
"index": 0,
"listId": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"channel": "email",
"reason": "Existing opt-out is not resurrected by user creation; use POST /subscriptions/contacts/:userId/lists/:listId for an explicit re-opt-in"
}
]
}
},
"events": {
"processed": 1,
"failed": [
{ "index": 0, "reason": "name should not be empty" }
]
},
"purchases": { "processed": 0, "failed": [] },
"devices": { "processed": 0, "failed": [] }
}

Límites​

  • Por solicitud: 1000 attributes, 500 events, 500 purchases, 1000 devices.
  • Una solicitud que supera un límite se rechaza con 400 (por ejemplo "events accepts at most 500 items per request (received 600). Split the batch.") en lugar de truncarse - no se procesa nada de ella.
  • Para lotes mayores, divídelos en solicitudes consecutivas de como máximo el límite de cada campo. Reintentar es seguro: /users/track es un upsert por externalId, las compras se identifican por orderId y los eventos pueden llevar un clientEventId para deduplicarse.
Las suscripciones a listas van en la misma llamada

Cada elemento de attributes puede llevar subscriptions (consulta Suscripciones a listas). En la respuesta anterior, el elemento 0 de attributes quedó suscrito a la lista; en la respuesta con errores, su baja existente se mantuvo y se informó, mientras la persona se actualizó igualmente. Para cambiar una suscripción más adelante, usa POST /subscriptions/contacts/:userId/lists/:listId.

El formato de matriz es para lotes programáticos desde tu backend. Para archivos y migraciones completas, no implementes manualmente bucles de lotes: usa Importación masiva en el dashboard (CSV/JSON con asignación de columnas, deduplicación, reglas de consentimiento e informe de errores) o la sincronización con warehouse para cargas recurrentes.


Eventos de usuario​

Obtener eventos de un usuario​

Recupera todos los eventos de un usuario concreto.

Endpoint​

GET /users/:userId/events

El parámetro de ruta es el ID de usuario interno de Joryio.

Parámetros de consulta​

ParámetroTipoPredeterminadoDescripción
limitnúmero50Número de eventos que se devolverán (máx. 1000).
offsetnúmero0Desplazamiento de paginación.
startDatecadenaFiltra eventos posteriores a esta fecha (ISO 8601).
endDatecadenaFiltra eventos anteriores a esta fecha (ISO 8601).
eventNamecadenaFiltra por nombre de evento.
groupBySessionbooleanofalseDevuelve también los eventos agrupados por sesión.
includeJourneyStepsbooleanotruefalse deja fuera los eventos de pasos de recorrido (canvas.step.entered / canvas.step.left) de los eventos y del total.

Solicitud de ejemplo​

curl -X GET "https://api-eu1.joryio.com/api/users/665f1e2a9b3c4d5e6f7a8b9c/events?limit=20" \
-H "Authorization: Bearer jry_live_your_api_key"

Respuesta​

Los campos de evento usan snake_case (proceden del almacén de analítica):

{
"data": [
{
"event_id": "9b2f6c1e-4a8d-4f0b-9c3d-2e1f5a6b7c8d",
"event_name": "Order Completed",
"properties": {
"orderId": "order_456",
"total": 99.99
},
"timestamp": "2024-01-20 14:30:00"
},
{
"event_id": "1c3e5a7b-9d2f-4b6c-8e0a-3f5d7b9c1e2a",
"event_name": "Page Viewed",
"properties": {
"page": "/pricing"
},
"timestamp": "2024-01-20 14:25:00"
}
],
"total": 156,
"limit": 20,
"offset": 0
}

Atributos comunes​

Campos y atributos especiales​

email y phone son campos de perfil de nivel superior, no atributos; envíalos en el nivel superior del cuerpo de la solicitud.

Estos atributos tienen un significado especial en Joryio:

AtributoTipoDescripción
firstNamecadenaNombre del usuario (se usa en búsqueda y visualización).
lastNamecadenaApellido del usuario (se usa en búsqueda y visualización).
languagecadenaIdioma preferido (los SDK lo establecen automáticamente cuando está disponible).
timezonecadenaZona horaria del usuario en formato IANA (controla las horas tranquilas y las entregas de journeys en hora local).
countrycadenaCódigo de país (indexado para segmentación).

Atributos personalizados​

Puedes añadir atributos personalizados ilimitados:

{
"attributes": {
"plan": "premium",
"mrr": 99,
"signupSource": "google_ads",
"lifetimeValue": 1250.50,
"tags": ["vip", "early-adopter"],
"preferences": {
"emailNotifications": true,
"smsNotifications": false
}
}
}

Tipos de datos compatibles​

  • Cadena: "premium".
  • Número: 99.99.
  • Booleano: true / false.
  • Fecha: "2024-01-15T10:30:00Z" (ISO 8601).
  • Matriz: ["tag1", "tag2"].
  • Objeto: { "nested": "value" }.

Respuestas de error​

Todos los errores usan el cuerpo de error estándar. Consulta resumen de la API: respuesta de error.

400 Solicitud incorrecta​

{
"statusCode": 400,
"message": "Cannot create or update a user without a valid identifier (externalId or email to create; userId only updates an existing user)",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/users"
}

401 No autorizado​

{
"statusCode": 401,
"message": "Invalid or expired API key",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/users"
}

404 No encontrado​

{
"statusCode": 404,
"message": "User with userId 'user_123' not found",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/users/by-user-id/user_123"
}

Prácticas recomendadas​

1. Los reintentos son seguros: POST /users/track es un upsert​

POST /users/track usa tu externalId como clave: al reintentar la misma solicitud, se actualiza el mismo perfil en vez de crear un duplicado. No hay encabezado Idempotency-Key; vuelve a intentar la solicitud tal cual ante fallos de red:

curl -X POST https://api-eu1.joryio.com/api/users/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-d '{...}'

2. Nombres de atributos​

Usa nombres de atributos coherentes y descriptivos:

Bien:

{
"signupDate": "2024-01-15",
"lifetimeValue": 1250.50,
"plan": "premium"
}

Mal:

{
"sd": "2024-01-15",
"ltv": 1250.50,
"p": "premium"
}

3. Formato de número de teléfono​

Envía los números de teléfono en formato E.164 - +, el código de país y luego el número. Consulta Números de teléfono para ver cómo se trata un número sin +.

Bien: "+972501234567", "+12125551234". Evita: "050-123-4567", "(212) 555-1234" - sin + dependen del país predeterminado del espacio de trabajo.


Límites de frecuencia​

La API de usuarios no tiene hoy límites de frecuencia fijos por endpoint. Consulta resumen de la API: límites de frecuencia para conocer el comportamiento de toda la plataforma y cómo gestionar respuestas 429.


Estadísticas de contactos​

GET /users/stats
GET /users/stats/growth?days=30

Cifras para conciliar con tu propia fuente de verdad: si tu tienda tiene 40.000 clientes y aquí pone 31.000, algo no está llegando. GET /users/stats/growth?days=30 devuelve un array JSON de { "date", "activeUsers", "newUsers" }: una entrada por día (día local del espacio de trabajo, del más antiguo al más reciente). activeUsers son las personas que hicieron algo ese día y newUsers las que tuvieron su primera actividad ese día. days es 30 por defecto y se acota a 1-365; no existe el parámetro granularity: la serie siempre es diaria.

{
"total": 31204,
"thisWeek": 1840,
"lastWeek": 2110,
"growthPercent": -12.8,
"growthType": "down",
"lifetimeSessions": 96420,
"activeUsers30d": 7310,
"activeUsersPrev30d": 6980
}

total es el número de contactos, excluidos los perfiles de rastreadores. Todas las demás cifras cuentan personas que HICIERON algo - recibir un mensaje no es actividad - y son null en lugar de 0 cuando el almacén analítico no está disponible, de modo que una laguna nunca se informa como cero. Las semanas usan la zona horaria del espacio de trabajo y empiezan el domingo; thisWeek es una semana parcial por construcción, así que growthPercent se lee bajo al principio de la semana. lifetimeSessions cuenta sesiones distintas, no eventos de inicio de sesión.

Siguientes pasos​

Contactos eliminados​

GET /users/deleted

Requiere el scope users:read. Parámetros: query (coincidencia por email/externalId por prefijo, teléfono o id original), limit (por defecto 50, máximo 200) y offset.

curl "https://api-eu1.joryio.com/api/users/deleted?limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"

Volver a detectar tipos de atributos​

Endpoint​

POST /attributes/rescan-types

Los tipos de atributo detectados se deciden una vez, a partir de una muestra de tus contactos, y luego se conservan, de modo que un tipo no varía entre lecturas a medida que llegan datos nuevos. El precio es que una decisión puede quedar obsoleta cuando los datos cambian de verdad. Este endpoint descarta las decisiones conservadas para que se tomen de nuevo con los datos actuales.

Cuerpo de la petición​

CampoTipoObligatorioDescripción
eventNamestringNoLimita el reanálisis a las propiedades de un evento. Si se omite, se reanalizan los atributos de contacto del espacio de trabajo

Los tipos que tu equipo ha definido nunca se tocan: la decisión de una persona no queda obsoleta porque los datos se hayan movido. Los campos estándar (price, total, lastEventTime, ...) los tipa la plataforma y tampoco se ven afectados.

Respuesta​

{ "success": true, "message": "Types will be detected again from current data" }
Cuándo llamarlo

Rara vez. La decisión conservada es el objetivo: es lo que hace que un segmento signifique hoy lo mismo que ayer. Llámalo cuando el significado de una propiedad haya cambiado de verdad en el origen, no según un calendario.