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
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
attributes | objeto o array | No | Campos de perfil. Persisten en la persona |
events | objeto o array | No | Cosas que ocurrieron. Se añaden a la cronología |
purchases | objeto o array | No | Pedidos, 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.
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
| Campo | Descripción |
|---|---|
externalId | Tu identificador, y la única clave de creación-o-actualización |
userId | El id interno de Joryio (24 hex). Solo actualiza: nunca crea |
anonymousId | Una sesión sin identificar. Vincúlala después con POST /users/identify |
Order Completed como eventoEse 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.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
subscriptions[].listId | string | Sí | El id de la lista de suscripción (API de suscripciones) |
subscriptions[].channel | string | No | email (predeterminado), sms, whatsapp, push o viber |
subscriptions[].status | string | No | subscribed (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
subscribedse 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 deattributesañadesubscriptions: { applied, skipped[] }, y cada omisión indica elindexdel elemento, ellistId, elchannely elreason. 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 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.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
anonymousId | string | Sí | La sesión en la que se registró el comportamiento |
externalId | string | Sí | Tu identificador al que vincularla |
attributes | objeto | No | Campos 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>" }
]
}
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
externalId | string | Sí | Tu identificador de la persona. Envía userId en su lugar si tienes el id interno de Joryio |
platform | ios | android | web | Sí | |
pushToken | string | Sí | El token tal como lo registró tu proveedor anterior |
appId | string | No* | 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 |
deviceInfo | object | No | Modelo, 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 (elexternalIdcon el que creaste al usuario). Es la ruta recomendada para integraciones.GET /users/:userId: consulta mediante el ID interno de Joryio (elidde 24 caracteres hexadecimales devuelto en las respuestas).
Endpoint
GET /users/by-user-id/:userId
Parámetros de ruta
| Parámetro | Tipo | Descripción |
|---|---|---|
userId | cadena | Tu 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",
"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ámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
limit | número | 50 | Resultados por página (máx. 1000). |
offset | número | 0 | Número de usuarios que se omitirán. |
email | string | - | Coincidencia exacta, sin distinguir mayúsculas. |
externalId | string | - | Coincidencia exacta con tu identificador. GET /users/by-user-id/:userId devuelve ese registro directamente. |
userId | string | - | Coincidencia exacta con el id interno de Joryio (24 caracteres hex). |
q | string | - | 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",
"attributes": {
"plan": "premium"
}
},
{
"id": "665f1e2a9b3c4d5e6f7a8b9d",
"userId": "665f1e2a9b3c4d5e6f7a8b9d",
"externalId": "user_456",
"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
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
email | cadena | No | Dirección de email nueva. |
phone | cadena | No | Número de teléfono nuevo. |
attributes | objeto | No | Atributos 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, 500events, 500purchases, 1000devices. - Una solicitud que supera un límite se rechaza con 400 (por ejemplo "
eventsaccepts 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/trackes un upsert porexternalId, las compras se identifican pororderIdy los eventos pueden llevar unclientEventIdpara deduplicarse.
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ámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
limit | número | 50 | Número de eventos que se devolverán (máx. 1000). |
offset | número | 0 | Desplazamiento de paginación. |
startDate | cadena | Filtra eventos posteriores a esta fecha (ISO 8601). | |
endDate | cadena | Filtra eventos anteriores a esta fecha (ISO 8601). | |
eventName | cadena | Filtra por nombre de evento. | |
groupBySession | booleano | false | Devuelve también los eventos agrupados por sesión. |
includeJourneySteps | booleano | true | false 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:
| Atributo | Tipo | Descripción |
|---|---|---|
firstName | cadena | Nombre del usuario (se usa en búsqueda y visualización). |
lastName | cadena | Apellido del usuario (se usa en búsqueda y visualización). |
language | cadena | Idioma preferido (los SDK lo establecen automáticamente cuando está disponible). |
timezone | cadena | Zona horaria del usuario en formato IANA (controla las horas tranquilas y las entregas de journeys en hora local). |
country | cadena | Có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
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
eventName | string | No | Limita 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" }
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.