Referencia de la API
Todo lo que tu sistema necesita para enviar citas a Yesly y recibir las respuestas. REST sobre HTTPS, JSON de entrada y de salida, webhooks firmados.
- URL base
- https://api.yesly.io/v1
- Versión
- 2026-10-01
Contenido
Primeros pasos
Introducción
La API de Yesly gira en torno a pocos recursos: citas, los huecos a los que el cliente puede moverse, tus tipos de cita y sedes, endpoints de webhook, eventos, tu saldo y la analítica. Peticiones y respuestas van en JSON sobre HTTPS; HTTP sin cifrar se rechaza.
La integración habitual son dos llamadas. Tu sistema crea la cita en cuanto se reserva (Crear una cita). Yesly contacta con el cliente el día anterior, por los canales y en el orden de tu cuenta, y envía la respuesta a tu endpoint de webhook (Cómo funcionan los webhooks). Lo demás es opcional.
Las horas van en ISO 8601 con desfase (2026-11-13T09:00:00+01:00). Los importes, en euros con dos decimales. Cada objeto tiene un id con prefijo (apt_, evt_, we_…) y un mode: live o test.
curl https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"Autenticación
Autentícate con una clave de API en la cabecera Authorization como token bearer. Las claves se crean en el panel, en Integraciones → Claves de API. Las de producción empiezan por ysk_live_ y las de prueba por ysk_test_. Una clave solo se muestra una vez, al crearla o rotarla.
Cada clave tiene un nivel de acceso: completo, solo lectura o restringido, en el que eliges ninguno, lectura o escritura por recurso (citas, catálogo, webhooks, analítica, saldo). También puedes limitarla a unos rangos de IP. Sin clave o con una revocada, la respuesta es 401; con una clave sin acceso al recurso, 403.
Las claves son secretos de servidor. Nunca las pongas en un navegador, una app móvil ni un repositorio.
curl https://api.yesly.io/v1/balance \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"error": {
"type": "authentication_failed",
"code": "api_key_invalid",
"message": "No valid API key was provided."
}
}Modo de prueba
Las peticiones con una clave ysk_test_ van en modo de prueba. No llega nada a ningún teléfono real ni se descuenta saldo, pero la cita pasa por los mismos estados y tu endpoint recibe los mismos eventos, marcados con "mode": "test".
En modo de prueba la cadena tarda segundos en vez de horas e ignora el horario de contacto. Usa estos números como customer.phone para elegir el resultado. Cualquier otro número se rechaza con test_number_required.
| Número | Qué pasa |
|---|---|
| +34600000001 | Confirma por WhatsApp. |
| +34600000002 | Pide otra fecha por WhatsApp y elige el primer hueco libre. |
| +34600000003 | Cancela por WhatsApp, con motivo. |
| +34600000004 | Lee el WhatsApp sin responder y confirma desde el enlace del SMS. |
| +34600000005 | No responde ni a WhatsApp ni a SMS y confirma en la llamada. |
| +34600000006 | Pide hablar con alguien en la llamada: call.transferred. |
| +34600000007 | No tiene WhatsApp: message.failed, luego SMS y confirma. |
| +34600000009 | Nunca responde: appointment.no_reply. |
curl -X POST https://api.yesly.io/v1/appointments \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"external_id": "TEST-0001",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"customer": {
"name": "Cliente de prueba",
"phone": "+34600000002"
},
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
}
}'Errores
Yesly usa los códigos de estado HTTP. 2xx es que ha ido bien, 4xx que hay algo que corregir en la petición y 5xx que algo ha fallado en nuestro lado y se puede reintentar. El cuerpo del error siempre lleva type y message; los de validación añaden code y el param que falla. Cada respuesta incluye la cabecera Request-Id, que también aparece en el registro del panel.
| Estado | Significado |
|---|---|
| 200, 201 | Hecho. 201 cuando se ha creado un recurso. |
| 202 | Aceptado y en proceso en segundo plano (importaciones). |
| 400 | JSON mal formado o falta un parámetro obligatorio. |
| 401 | No hay una clave de API válida. |
| 403 | La clave no puede hacer esto o la IP no está permitida. |
| 404 | El recurso no existe en este modo. |
| 409 | Choca con un recurso existente, como un external_id repetido con otros datos. |
| 422 | Bien formado pero no válido, como una franja en el pasado. |
| 429 | Demasiadas peticiones. Mira Límites de uso. |
| 500, 502, 503 | Error nuestro. Reintenta con espera creciente; puede que la petición se procesara, así que reintenta de forma idempotente. |
| Código | Cuándo |
|---|---|
| phone_invalid | customer.phone no es un número E.164 válido. |
| slot_offset_missing | Una hora no lleva desfase respecto a UTC. |
| slot_in_past | slot.start ya ha pasado. |
| type_required | La cuenta tiene varios tipos de cita y no se ha enviado ninguno. |
| type_unknown, location_unknown | El id no existe en tu cuenta. |
| metadata_invalid | Demasiadas claves, una clave o un valor demasiado largos o un valor que no es texto. |
| test_number_required | Se ha usado una clave de prueba con un teléfono real. |
| external_id_conflict | El external_id ya existe con otros datos. |
| appointment_closed | La cita está cancelada o su franja ya ha pasado, así que no se puede cambiar. |
{
"error": {
"type": "invalid_request",
"code": "phone_invalid",
"param": "customer.phone",
"message": "customer.phone must be a valid phone number in E.164 format, such as +34612345678.",
"request_id": "req_9aE8"
}
}Idempotencia
Las redes fallan, así que cualquier escritura se puede reintentar sin riesgo. Las citas son idempotentes por external_id: si envías la misma cita dos veces, recibes la existente con 200 en vez de crear otra. Si envías el mismo external_id con otros datos, recibes 409; para cambiar una cita usa PATCH.
En el resto de peticiones POST, envía la cabecera Idempotency-Key con un valor único, por ejemplo un UUID. Yesly guarda la primera respuesta 24 horas y la devuelve en cualquier reintento con la misma clave.
curl -X POST https://api.yesly.io/v1/webhook_endpoints \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Idempotency-Key: 0b8f2c4e-61d3-4f8a-9a57-3e1c7d2b9f10" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/yesly/webhook",
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply"
]
}'{
"error": {
"type": "conflict",
"code": "external_id_conflict",
"param": "external_id",
"message": "An appointment with external_id ORD-48213 already exists with different data. Use PATCH /v1/appointments/apt_8Kq2mZ4tR1 to change it."
}
}Paginación
Los listados devuelven { "object": "list", "data": [...], "has_more": … }, lo más reciente primero. Pasa limit (de 1 a 100, 20 por defecto) y, para la página siguiente, starting_after con el id del último elemento recibido. Sigue mientras has_more sea true.
curl https://api.yesly.io/v1/appointments?date=2026-11-13&limit=50&starting_after=apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"object": "list",
"url": "/v1/appointments",
"has_more": true,
"data": [
"…"
]
}Versiones
La API se versiona por fecha. La versión actual es 2026-10-01. Tu cuenta queda fijada a la versión vigente cuando hiciste la primera petición; envía Yesly-Version para usar otra en una petición concreta. Los endpoints de webhook tienen su propia api_version.
Los cambios incompatibles solo llegan con una versión nueva. Añadimos campos, tipos de evento y valores de enumeración sin cambiar de versión, así que ignora lo que no reconozcas.
curl https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Yesly-Version: 2026-10-01"Límites de uso
Cada cuenta puede hacer 100 peticiones por segundo en producción y 25 en modo de prueba. Por encima, la API responde 429 con la cabecera Retry-After, en segundos. Para enviar todo un día de citas de golpe, usa Importar citas en vez de miles de llamadas sueltas.
HTTP/1.1 429 Too Many Requests
Retry-After: 1
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 1
{
"error": {
"type": "rate_limited",
"message": "Too many requests. Retry after 1 second."
}
}Metadatos
Las citas admiten metadata: pares clave-valor tuyos, como el número de pedido, ruta o incidencia. Yesly los guarda tal cual, los muestra en el panel y los devuelve sin cambios en cada respuesta y webhook, para que asocies una respuesta con tus registros sin buscarla.
Hasta 20 claves, claves de hasta 40 caracteres, valores de hasta 500 y solo texto. Filtra listados con metadata[clave]=valor. No pongas datos personales en los metadatos: el nombre y el teléfono del cliente van en customer.
curl https://api.yesly.io/v1/appointments?metadata[route_id]=BIO-07&date=2026-11-13 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"Citas
El objeto cita
Una cita es una visita, entrega o reunión que Yesly confirma con tu cliente. Su status avanza según responde:
| Estado | Significado |
|---|---|
| scheduled | Creada. Espera a first_contact_at. |
| in_progress | Yesly está contactando con el cliente. |
| confirmed | El cliente estará. |
| rescheduled | El cliente la ha movido a uno de tus huecos libres. slot es el nuevo y previous_slot el anterior. |
| cancelled | Cancelada por el cliente o por ti. Mira cancellation.by. |
| no_reply | Sin respuesta en ningún canal. Tu equipo debería llamar. |
Atributos
idstring- Id único, con prefijo
apt_. external_idstring- Tu id de la cita, único por cuenta y modo.
statusenumscheduled,in_progress,confirmed,rescheduled,cancelledono_reply.typestring- Id del tipo de cita. Define cómo se describe la hora y las instrucciones del mensaje.
locationstring | null- Id de la sede, si la hay.
slotobjectstartyendde la cita. En los tipos con hora exacta,endesstartmás la duración del tipo.previous_slotobject | null- La franja anterior si el cliente la cambió; si no,
null. customerobjectnameyphone(E.164) de la persona a la que contacta Yesly.channelsarray- Canales que se usan en esta cita, en orden. Por defecto, los de tu cuenta.
first_contact_atstring | null- Cuándo sale el primer mensaje, teniendo en cuenta tu horario de contacto.
answerobject | nullresult,channelyatde la respuesta del cliente, onull.cancellationobject | nullby(customeroaccount),reasonyat, onull.attemptsarray- Cada intento de contacto:
channel,status(queued,sent,delivered,read,answered,no_answer,failed,skipped),sent_atycost. costnumber- Lo que ha costado la cita hasta ahora, en euros. WhatsApp 0,08 por mensaje, SMS 0,15 por mensaje, llamada 0,30 por minuto contestado; las llamadas sin contestar son gratis.
metadataobject- Tus pares clave-valor, tal como los enviaste.
modeenumliveotest.createdstring- Cuándo se creó la cita.
updatedstring- Cuándo cambió algo por última vez.
{
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "confirmed",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": {
"result": "confirmed",
"channel": "whatsapp",
"at": "2026-11-12T10:04:12+01:00"
},
"cancellation": null,
"attempts": [
{
"id": "att_3Fh7Qp",
"channel": "whatsapp",
"status": "answered",
"sent_at": "2026-11-12T10:00:03+01:00",
"cost": 0.08
}
],
"cost": 0.08,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-12T10:04:12+01:00"
}Crear una cita
POST/v1/appointments
Crea una cita y programa el primer contacto. Por defecto Yesly escribe el día anterior a las 10:00 y pasa por tus canales si no hay respuesta; si la cita se reserva después de ese momento, el contacto empieza en la siguiente hora permitida dentro de tu horario.
WhatsApp y SMS necesitan un móvil. Si customer.phone es un fijo, esos canales se saltan y solo se hace la llamada.
Parámetros del cuerpo
external_idstringobligatorio- Tu id de la cita, como el número de pedido o de incidencia. Reintentar con el mismo
external_ides seguro. typestring- Id del tipo de cita. Obligatorio si tu cuenta tiene más de un tipo.
locationstring- Id de la sede. Obligatorio si el tipo se hace en una de tus sedes; si no, opcional, y sirve para pasar llamadas al teléfono de esa sede.
slot.startstringobligatorio- Inicio de la cita o de la franja, con desfase.
slot.endstring- Fin de la franja. Omítelo en los tipos con hora exacta; se calcula con la duración del tipo.
customer.namestringobligatorio- Nombre que se usa en los mensajes. Basta con el nombre de pila.
customer.phonestringobligatorio- Teléfono en formato E.164, como
+34612345678. channelsarray- Cambia el orden de canales en esta cita, como
["whatsapp", "voice"]. Solo canales activos en tu cuenta. metadataobject- Hasta 20 pares clave-valor tuyos. Mira Metadatos.
Devuelve
La cita, con status: scheduled y first_contact_at. 201 si se ha creado, 200 si ya existía con los mismos datos.
curl -X POST https://api.yesly.io/v1/appointments \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"external_id": "ORD-48213",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
}
}'{
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "scheduled",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": null,
"cancellation": null,
"attempts": [],
"cost": 0,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-11T18:30:15+01:00"
}Consultar una cita
GET/v1/appointments/{id}
Devuelve una cita con sus intentos y su respuesta. Para buscarla por tu propio id, lista con external_id.
Parámetros de ruta y consulta
idstringobligatorio- Id de la cita, con prefijo
apt_.
Devuelve
La cita.
curl https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "confirmed",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": {
"result": "confirmed",
"channel": "whatsapp",
"at": "2026-11-12T10:04:12+01:00"
},
"cancellation": null,
"attempts": [
{
"id": "att_3Fh7Qp",
"channel": "whatsapp",
"status": "answered",
"sent_at": "2026-11-12T10:00:03+01:00",
"cost": 0.08
}
],
"cost": 0.08,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-12T10:04:12+01:00"
}Listar citas
GET/v1/appointments
Lista citas, lo más reciente primero. Un uso habitual: preguntar cada tarde qué citas de mañana han acabado en no_reply para que tu equipo las llame.
Parámetros de ruta y consulta
statusenum- Solo citas en este estado.
datestring- Solo citas cuya franja empieza este día,
AAAA-MM-DDen la zona horaria de tu cuenta. fromstring- La franja empieza en esta hora o después.
tostring- La franja empieza antes de esta hora.
typestring- Solo este tipo de cita.
locationstring- Solo esta sede.
external_idstring- La cita con este
external_id. metadata[key]string- Solo citas con este valor en los metadatos, como
metadata[route_id]=BIO-07. limitinteger- Entre 1 y 100. 20 por defecto.
starting_afterstring- Cursor: el id del último elemento de la página anterior.
Devuelve
Una lista de citas.
curl https://api.yesly.io/v1/appointments?status=no_reply&date=2026-11-13 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"object": "list",
"url": "/v1/appointments",
"has_more": false,
"data": [
{
"id": "apt_2Rx9Lc",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48251",
"status": "no_reply",
"type": "pickup",
"location": "bilbao",
"slot": {
"start": "2026-11-13T16:00:00+01:00",
"end": "2026-11-13T18:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Pablo Sánchez",
"phone": "+34600000009"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": null,
"cancellation": null,
"attempts": [
{
"id": "att_9Lm2",
"channel": "whatsapp",
"status": "delivered",
"sent_at": "2026-11-12T10:00:02+01:00",
"cost": 0.08
},
{
"id": "att_9Lm3",
"channel": "sms",
"status": "delivered",
"sent_at": "2026-11-12T12:00:01+01:00",
"cost": 0.15
},
{
"id": "att_9Lm4",
"channel": "voice",
"status": "no_answer",
"sent_at": "2026-11-12T14:00:00+01:00",
"cost": 0
}
],
"cost": 0.23,
"currency": "EUR",
"metadata": {
"order_id": "PV-18377",
"route_id": "BIO-04"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-11T18:30:15+01:00"
}
]
}Modificar una cita
PATCH/v1/appointments/{id}
Cambia una cita. Envía solo lo que cambia. metadata se combina: las claves que envías se fijan y una clave enviada como texto vacío se borra.
Si cambias la franja o el teléfono del cliente, Yesly vuelve a empezar la confirmación: el estado vuelve a scheduled, se borra la respuesta anterior y se calcula un nuevo first_contact_at. Cambiar los metadatos o el nombre nunca contacta con el cliente.
Parámetros del cuerpo
typestring- Nuevo id de tipo de cita.
locationstring- Nuevo id de sede.
slot.startstring- Nuevo inicio, con desfase.
slot.endstring- Nuevo fin de la franja.
customer.namestring- Nuevo nombre.
customer.phonestring- Nuevo teléfono, E.164.
channelsarray- Nuevo orden de canales para esta cita.
metadataobject- Claves a fijar. Un texto vacío borra la clave.
Devuelve
La cita modificada. 409 appointment_closed si está cancelada o su franja ya ha pasado.
curl -X PATCH https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"slot": {
"start": "2026-11-16T12:00:00+01:00",
"end": "2026-11-16T14:00:00+01:00"
},
"metadata": {
"route_id": "BIO-11"
}
}'{
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "scheduled",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-16T12:00:00+01:00",
"end": "2026-11-16T14:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-13T10:00:00+01:00",
"answer": null,
"cancellation": null,
"attempts": [],
"cost": 0,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-11"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-11T19:40:02+01:00"
}Cancelar una cita
POST/v1/appointments/{id}/cancel
Cancela una cita desde tu lado, por ejemplo cuando se anula el pedido. Yesly deja de contactar con el cliente al momento y no le envía nada más. Los intentos ya hechos se cobran.
Parámetros del cuerpo
reasonstring- Opcional. Se guarda en
cancellation.reasony se ve en el panel.
Devuelve
La cita con status: cancelled y cancellation.by: account. Lanza appointment.cancelled.
curl -X POST https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1/cancel \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"reason": "Pedido anulado por el cliente en tienda"
}'{
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "cancelled",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": null,
"cancellation": {
"by": "account",
"reason": "Pedido anulado por el cliente en tienda",
"at": "2026-11-11T19:02:40+01:00"
},
"attempts": [],
"cost": 0,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-11T19:02:40+01:00"
}Importar citas
POST/v1/appointments/import
Crea o modifica hasta 1.000 citas en una sola petición, para sistemas que planifican el día entero de una vez. Cada elemento lleva los mismos campos que Crear una cita; los que tienen un external_id existente se modifican. La importación se procesa en segundo plano.
¿Aún sin API? Sube los mismos datos como CSV en el panel, en Integraciones → Importación CSV, con una columna metadata.<clave> por cada clave de metadatos.
Parámetros del cuerpo
appointmentsarrayobligatorio- Entre 1 y 1.000 citas.
Devuelve
Un objeto importación con status: processing. Consulta Consultar una importación o sigue el progreso en el panel.
curl -X POST https://api.yesly.io/v1/appointments/import \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"appointments": [
{
"external_id": "ORD-48213",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
}
},
{
"external_id": "ORD-48214",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T11:00:00+01:00",
"end": "2026-11-13T13:00:00+01:00"
},
"customer": {
"name": "Andrés Gil",
"phone": "+34600000004"
},
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
}
},
"…"
]
}'{
"id": "imp_4Dw8Ty",
"object": "import",
"status": "processing",
"total": 128,
"created": 0,
"updated": 0,
"failed": 0,
"errors": []
}Consultar una importación
GET/v1/imports/{id}
Muestra cómo ha ido una importación. Los elementos que fallan no frenan a los demás; cada error señala el elemento por su posición y su external_id.
Parámetros de ruta y consulta
idstringobligatorio- Id de la importación, con prefijo
imp_.
Devuelve
La importación, con status processing, completed o failed, los recuentos y los errores.
curl https://api.yesly.io/v1/imports/imp_4Dw8Ty \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"id": "imp_4Dw8Ty",
"object": "import",
"status": "completed",
"total": 128,
"created": 121,
"updated": 5,
"failed": 2,
"errors": [
{
"index": 37,
"external_id": "ORD-48250",
"code": "phone_invalid",
"param": "customer.phone",
"message": "customer.phone must be a valid phone number in E.164 format."
},
{
"index": 90,
"external_id": "ORD-48303",
"code": "slot_in_past",
"param": "slot.start",
"message": "slot.start is in the past."
}
]
}Huecos
El objeto hueco
Los huecos son las horas libres que ofreces cuando un cliente pide otra fecha. Los publica tu sistema; Yesly solo ofrece huecos del mismo tipo y sede, dentro del plazo y la antelación mínima de Ajustes → Cambios y cancelaciones. Cuando un cliente coge uno, sube booked, la cita se mueve ahí y recibes appointment.rescheduled. Un hueco lleno deja de ofrecerse.
Si la cuenta permite cambios pero no hay huecos, al cliente se le dice que le llamaréis y la cita pasa a tu equipo.
Atributos
idstring- Id único, con prefijo
slt_. typestring- Tipo de cita al que corresponde.
locationstring | null- Sede, si el tipo la tiene.
startstring- Inicio, con desfase.
endstring- Fin, con desfase.
capacityinteger- Cuántas citas caben. 1 por defecto.
bookedinteger- Cuántos clientes se han movido a él a través de Yesly.
{
"id": "slt_7Tn4Wb",
"object": "slot",
"type": "delivery",
"location": "bilbao",
"start": "2026-11-16T12:00:00+01:00",
"end": "2026-11-16T14:00:00+01:00",
"capacity": 3,
"booked": 1
}Publicar huecos
POST/v1/slots
Publica huecos libres en bloque, hasta 500 por petición. Un hueco con el mismo tipo, sede e inicio que uno existente lo sustituye, así que puedes enviar tu disponibilidad completa cada pocas horas. No se puede bajar capacity por debajo de booked.
Parámetros del cuerpo
slotsarrayobligatorio- Entre 1 y 500 huecos.
slots[].typestringobligatorio- Id del tipo de cita.
slots[].locationstring- Id de la sede, si el tipo la tiene.
slots[].startstringobligatorio- Inicio, con desfase.
slots[].endstringobligatorio- Fin, con desfase.
slots[].capacityinteger- Citas que caben. 1 por defecto.
Devuelve
Los huecos tal como se han guardado.
curl -X POST https://api.yesly.io/v1/slots \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"slots": [
{
"type": "delivery",
"location": "bilbao",
"start": "2026-11-16T09:00:00+01:00",
"end": "2026-11-16T11:00:00+01:00",
"capacity": 4
},
{
"type": "delivery",
"location": "bilbao",
"start": "2026-11-16T12:00:00+01:00",
"end": "2026-11-16T14:00:00+01:00",
"capacity": 3
}
]
}'{
"object": "list",
"url": "/v1/slots",
"has_more": false,
"data": [
{
"id": "slt_7Tn4Wa",
"object": "slot",
"type": "delivery",
"location": "bilbao",
"start": "2026-11-16T09:00:00+01:00",
"end": "2026-11-16T11:00:00+01:00",
"capacity": 4,
"booked": 0
},
{
"id": "slt_7Tn4Wb",
"object": "slot",
"type": "delivery",
"location": "bilbao",
"start": "2026-11-16T12:00:00+01:00",
"end": "2026-11-16T14:00:00+01:00",
"capacity": 3,
"booked": 0
}
]
}Listar huecos
GET/v1/slots
Lista los huecos publicados, los más próximos primero.
Parámetros de ruta y consulta
typestring- Solo este tipo de cita.
locationstring- Solo esta sede.
fromstring- Huecos que empiezan este día u hora o después.
tostring- Huecos que empiezan antes de este día u hora.
availablebooleantruepara huecos con sitio libre.limitinteger- Entre 1 y 100. 20 por defecto.
starting_afterstring- Cursor: el id del último elemento de la página anterior.
Devuelve
Una lista de huecos.
curl https://api.yesly.io/v1/slots?type=delivery&location=bilbao&from=2026-11-16&available=true \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"object": "list",
"url": "/v1/slots",
"has_more": false,
"data": [
{
"id": "slt_7Tn4Wb",
"object": "slot",
"type": "delivery",
"location": "bilbao",
"start": "2026-11-16T12:00:00+01:00",
"end": "2026-11-16T14:00:00+01:00",
"capacity": 3,
"booked": 1
}
]
}Borrar un hueco
DELETE/v1/slots/{id}
Deja de ofrecer un hueco, por ejemplo cuando lo llena tu propio sistema. Las citas que ya se movieron ahí se quedan donde están.
Parámetros de ruta y consulta
idstringobligatorio- Id del hueco, con prefijo
slt_.
Devuelve
deleted: true.
curl -X DELETE https://api.yesly.io/v1/slots/slt_7Tn4Wb \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"id": "slt_7Tn4Wb",
"object": "slot",
"deleted": true
}Tipos de cita y sedes
El objeto tipo de cita
Los tipos de cita describen qué es cada cita: una entrega a domicilio, una visita técnica, una inspección. Definen cómo se muestra la hora al cliente y las instrucciones que se añaden al mensaje. Son los mismos que en Ajustes → Tipos de cita.
Atributos
idstring- Tu id para el tipo, como
delivery. Se envía con cada cita. namestring- Lo que lee el cliente en el mensaje.
time_formatenumwindow("entre las 9:00 y las 11:00") oexact("a las 17:30").minutesinteger- Longitud de la franja o duración de la cita.
placeenumcustomer(en la dirección del cliente) olocation(en una de tus sedes).instructionsstring | null- Lo que el cliente debe traer o preparar. Lo usa la variable
{instructions}de las plantillas. reschedulableboolean- Si el cliente puede pedir otra fecha.
{
"id": "delivery",
"object": "appointment_type",
"name": "Entrega a domicilio",
"time_format": "window",
"minutes": 120,
"place": "customer",
"instructions": "Asegúrate de que haya alguien en casa para recibirlo.",
"reschedulable": true,
"created": "2026-09-28T12:10:00+02:00"
}Listar tipos de cita
GET/v1/appointment_types
Lista los tipos de cita de la cuenta.
Devuelve
Una lista de tipos de cita.
curl https://api.yesly.io/v1/appointment_types \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"object": "list",
"url": "/v1/appointment_types",
"has_more": false,
"data": [
{
"id": "delivery",
"object": "appointment_type",
"name": "Entrega a domicilio",
"time_format": "window",
"minutes": 120,
"place": "customer",
"instructions": "Asegúrate de que haya alguien en casa para recibirlo.",
"reschedulable": true,
"created": "2026-09-28T12:10:00+02:00"
},
{
"id": "showroom",
"object": "appointment_type",
"name": "Cita en tienda",
"time_format": "exact",
"minutes": 45,
"place": "location",
"instructions": "Trae las medidas del espacio si las tienes.",
"reschedulable": true,
"created": "2026-09-28T12:14:00+02:00"
}
]
}Crear un tipo de cita
POST/v1/appointment_types
Añade un tipo de cita. Las plantillas lo usan al momento mediante {type}, {time} e {instructions}.
Parámetros del cuerpo
idstringobligatorio- Minúsculas, números,
-y_. No se puede cambiar después. namestringobligatorio- Nombre que ve el cliente.
time_formatenumobligatoriowindowoexact.minutesintegerobligatorio- Longitud de la franja o duración.
placeenumobligatoriocustomerolocation.instructionsstring- Instrucciones opcionales para el cliente.
reschedulablebooleantruepor defecto.
Devuelve
El tipo de cita.
curl -X POST https://api.yesly.io/v1/appointment_types \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"id": "repair",
"name": "Visita técnica",
"time_format": "window",
"minutes": 120,
"place": "customer",
"reschedulable": true
}'{
"id": "repair",
"object": "appointment_type",
"name": "Visita técnica",
"time_format": "window",
"minutes": 120,
"place": "customer",
"instructions": null,
"reschedulable": true,
"created": "2026-11-11T17:02:10+01:00"
}Modificar un tipo de cita
PATCH/v1/appointment_types/{id}
Cambia un tipo. Las citas ya contactadas conservan el texto que se les envió; los mensajes siguientes usan el nuevo.
Parámetros del cuerpo
namestring- Nuevo nombre.
minutesinteger- Nueva longitud de la franja o duración.
instructionsstring- Nuevas instrucciones. Un texto vacío las borra.
reschedulableboolean- Si el cliente puede pedir otra fecha.
Devuelve
El tipo de cita modificado.
curl -X PATCH https://api.yesly.io/v1/appointment_types/delivery \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"instructions": "Asegúrate de que haya alguien en casa y de que el ascensor funcione."
}'{
"id": "delivery",
"object": "appointment_type",
"name": "Entrega a domicilio",
"time_format": "window",
"minutes": 120,
"place": "customer",
"instructions": "Asegúrate de que haya alguien en casa y de que el ascensor funcione.",
"reschedulable": true,
"created": "2026-09-28T12:10:00+02:00"
}El objeto sede
Las sedes son tus centros, tiendas o almacenes: donde se hacen las citas en sede y el teléfono al que se pasa una llamada cuando el cliente quiere hablar con una persona.
Atributos
idstring- Tu id para la sede, como
bilbao. namestring- Nombre que ve el cliente.
addressstring- Dirección que se usa en los mensajes de las citas que se hacen allí.
transfer_phonestring | null- Adónde se pasan las llamadas cuando el cliente pide hablar con alguien. Sin él, Yesly ofrece que le llaméis y marca la cita para tu equipo.
{
"id": "bilbao",
"object": "location",
"name": "Bilbao",
"address": "Calle Mayor 12, 48001 Bilbao",
"transfer_phone": "+34944000112",
"created": "2026-09-28T12:20:00+02:00"
}curl https://api.yesly.io/v1/locations \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"object": "list",
"url": "/v1/locations",
"has_more": false,
"data": [
{
"id": "bilbao",
"object": "location",
"name": "Bilbao",
"address": "Calle Mayor 12, 48001 Bilbao",
"transfer_phone": "+34944000112",
"created": "2026-09-28T12:20:00+02:00"
},
{
"id": "vitoria",
"object": "location",
"name": "Vitoria-Gasteiz",
"address": "Calle Lermandabide 4, 01015 Vitoria-Gasteiz",
"transfer_phone": "+34945000240",
"created": "2026-09-28T12:22:00+02:00"
}
]
}Crear una sede
POST/v1/locations
Parámetros del cuerpo
idstringobligatorio- Minúsculas, números,
-y_. No se puede cambiar después. namestringobligatorio- Nombre que ve el cliente.
addressstringobligatorio- Dirección completa.
transfer_phonestring- Teléfono para pasar llamadas, E.164.
Devuelve
La sede.
curl -X POST https://api.yesly.io/v1/locations \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"id": "santander",
"name": "Santander",
"address": "Calle Castelar 8, 39004 Santander",
"transfer_phone": "+34942000318"
}'{
"id": "santander",
"object": "location",
"name": "Santander",
"address": "Calle Castelar 8, 39004 Santander",
"transfer_phone": "+34942000318",
"created": "2026-11-11T17:05:31+01:00"
}Modificar una sede
PATCH/v1/locations/{id}
Parámetros del cuerpo
namestring- Nuevo nombre.
addressstring- Nueva dirección.
transfer_phonestring- Nuevo teléfono para pasar llamadas. Un texto vacío lo borra.
Devuelve
La sede modificada.
curl -X PATCH https://api.yesly.io/v1/locations/bilbao \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"transfer_phone": "+34944000199"
}'{
"id": "bilbao",
"object": "location",
"name": "Bilbao",
"address": "Calle Mayor 12, 48001 Bilbao",
"transfer_phone": "+34944000199",
"created": "2026-09-28T12:20:00+02:00"
}Webhooks y eventos
Cómo funcionan los webhooks
Cuando le pasa algo a una cita, Yesly envía un evento como POST con cuerpo JSON a cada endpoint activo suscrito a ese tipo de evento. Añade endpoints en el panel, en Integraciones → Webhooks, o con la API. Los endpoints de producción y de prueba van por separado.
Responde con cualquier 2xx en menos de 10 segundos, antes de hacer trabajo lento. Cualquier otra cosa, o no responder, cuenta como fallo: Yesly reintenta con esperas crecientes durante 24 horas, y los envíos fallidos se pueden reenviar desde el panel.
La entrega es al menos una vez y el orden no está garantizado. Usa el id del evento para descartar duplicados y compara el updated de la cita con lo que tienes antes de sobrescribirlo.
Content-Type: application/json
User-Agent: Yesly-Webhooks/1.0
Yesly-Event: appointment.confirmed
Yesly-Signature: t=1794474252,v1=5f2c8e0b9d7a41c3e6f18a2d4b7c9e0f13a5d8b2c6e4f7a9d1b3c5e7f9a2b4c6
{
"id": "evt_5Hq1Kd",
"object": "event",
"type": "appointment.confirmed",
"api_version": "2026-10-01",
"mode": "test",
"created": "2026-11-12T10:04:12+01:00",
"data": {
"appointment": {
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "confirmed",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": {
"result": "confirmed",
"channel": "whatsapp",
"at": "2026-11-12T10:04:12+01:00"
},
"cancellation": null,
"attempts": [
{
"id": "att_3Fh7Qp",
"channel": "whatsapp",
"status": "answered",
"sent_at": "2026-11-12T10:00:03+01:00",
"cost": 0.08
}
],
"cost": 0.08,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-12T10:04:12+01:00"
}
}
}Verificar firmas
Cada envío lleva la cabecera Yesly-Signature: t=<hora unix>,v1=<firma>. La firma es un HMAC-SHA256, en hexadecimal, de la marca de tiempo, un punto y el cuerpo en bruto de la petición, con el secreto de firma del endpoint (whsec_…) como clave.
Calcúlala sobre los bytes en bruto, antes de parsear el JSON, y compara en tiempo constante. Rechaza los envíos cuya marca de tiempo se aleje más de cinco minutos de tu reloj, para evitar repeticiones. Los SDK hacen todo esto en una llamada.
Cuando rotas el secreto, el anterior sigue valiendo 24 horas y se envían las dos firmas, así que puedes desplegar el nuevo sin perder eventos.
import express from "express";
import Yesly from "@yesly/node";
const yesly = new Yesly(process.env.YESLY_API_KEY);
const app = express();
// Raw body: the signature is computed over the exact bytes we sent.
app.post("/yesly/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = yesly.webhooks.constructEvent(
req.body,
req.get("Yesly-Signature"),
process.env.YESLY_WEBHOOK_SECRET,
);
} catch {
return res.sendStatus(400);
}
if (event.type === "appointment.confirmed") {
const { external_id, metadata } = event.data.appointment;
// Mark the order as confirmed in your system.
}
res.json({ received: true });
});Tipos de evento
Suscribe cada endpoint solo a lo que necesita. La mayoría de integraciones usan los cuatro eventos de respuesta.
| Evento | Cuándo |
|---|---|
| appointment.created | Se ha creado una cita por API, importación, CSV o en el panel. |
| appointment.confirmed | El cliente ha confirmado. |
| appointment.rescheduled | El cliente la ha movido a otro hueco. slot es el nuevo. |
| appointment.cancelled | Cancelada por el cliente (con el motivo, si se pide) o por ti. |
| appointment.no_reply | Ningún canal ha conseguido respuesta. Pásala a tu equipo. |
| message.sent | Un WhatsApp o SMS ha salido de Yesly. |
| message.delivered | El operador o WhatsApp han confirmado la entrega. |
| message.failed | No se ha podido entregar, por ejemplo un número sin WhatsApp. Se pasa al siguiente canal. |
| call.started | Una llamada ha empezado a sonar. |
| call.completed | Una llamada ha terminado, con su duración y resultado. |
| call.transferred | El cliente ha pedido hablar con alguien y la llamada se ha pasado al teléfono de la sede. |
| balance.low | El saldo ha bajado de tu umbral de aviso. |
| balance.topped_up | Se ha pagado una recarga. Incluye el número de factura. |
El objeto evento
Los eventos tienen la misma forma lleguen por webhook o con Listar eventos. data lleva el objeto completo en el momento del evento: una appointment en appointment.*, un message o call en los eventos de canal, el balance en balance.*. Todos incluyen el external_id y los metadata de la cita.
Atributos
idstring- Id único, con prefijo
evt_. Úsalo para descartar duplicados. typestring- Tipo de evento, como
appointment.confirmed. api_versionstring- Versión con la que se genera
data, la del endpoint. createdstring- Cuándo ocurrió.
dataobject- El objeto al que se refiere el evento.
modeenumliveotest.
{
"id": "evt_5Hp7Sr",
"object": "event",
"type": "appointment.rescheduled",
"api_version": "2026-10-01",
"mode": "test",
"created": "2026-11-12T10:06:31+01:00",
"data": {
"appointment": {
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "rescheduled",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-16T12:00:00+01:00",
"end": "2026-11-16T14:00:00+01:00"
},
"previous_slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": {
"result": "rescheduled",
"channel": "whatsapp",
"at": "2026-11-12T10:06:31+01:00"
},
"cancellation": null,
"attempts": [
{
"id": "att_3Fh7Qp",
"channel": "whatsapp",
"status": "answered",
"sent_at": "2026-11-12T10:00:03+01:00",
"cost": 0.08
}
],
"cost": 0.08,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-12T10:06:31+01:00"
}
}
}El objeto endpoint de webhook
Atributos
idstring- Id único, con prefijo
we_. urlstring- URL HTTPS que recibe los eventos.
descriptionstring | null- Para qué sirve el endpoint.
enabled_eventsarray- Tipos de evento que se le envían.
["*"]para todos. statusenumenabledodisabled.api_versionstring- Versión con la que se generan los eventos que recibe.
secretstring- Secreto de firma. Solo se devuelve al crear el endpoint o rotar el secreto.
{
"id": "we_erp",
"object": "webhook_endpoint",
"mode": "test",
"url": "https://erp.example.com/yesly/webhook",
"description": "ERP: estado de cada cita",
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply"
],
"status": "enabled",
"api_version": "2026-10-01",
"created": "2026-11-02T09:12:44+01:00"
}Crear un endpoint
POST/v1/webhook_endpoints
Parámetros del cuerpo
urlstringobligatorio- URL HTTPS. No se aceptan certificados autofirmados.
enabled_eventsarrayobligatorio- Tipos de evento a enviar, o
["*"]. descriptionstring- Nota opcional.
api_versionstring- Por defecto, la versión de tu cuenta.
Devuelve
El endpoint, con su secret. Guárdalo: no se vuelve a mostrar.
curl -X POST https://api.yesly.io/v1/webhook_endpoints \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/yesly/webhook",
"description": "ERP: estado de cada cita",
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply"
]
}'{
"id": "we_erp",
"object": "webhook_endpoint",
"mode": "test",
"url": "https://erp.example.com/yesly/webhook",
"description": "ERP: estado de cada cita",
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply"
],
"status": "enabled",
"api_version": "2026-10-01",
"created": "2026-11-02T09:12:44+01:00",
"secret": "whsec_9QmT2vXk7LpR4sNw8YbE3cHd"
}Listar endpoints
GET/v1/webhook_endpoints
Parámetros de ruta y consulta
limitinteger- Entre 1 y 100. 20 por defecto.
starting_afterstring- Cursor: el id del último elemento de la página anterior.
Devuelve
Una lista de endpoints del modo de la clave.
curl https://api.yesly.io/v1/webhook_endpoints \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"object": "list",
"url": "/v1/webhook_endpoints",
"has_more": false,
"data": [
{
"id": "we_erp",
"object": "webhook_endpoint",
"mode": "test",
"url": "https://erp.example.com/yesly/webhook",
"description": "ERP: estado de cada cita",
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply"
],
"status": "enabled",
"api_version": "2026-10-01",
"created": "2026-11-02T09:12:44+01:00"
}
]
}Modificar un endpoint
PATCH/v1/webhook_endpoints/{id}
Parámetros del cuerpo
urlstring- Nueva URL.
enabled_eventsarray- Sustituye la lista de tipos de evento.
descriptionstring- Nueva nota.
disabledbooleantruedeja de enviarle eventos hasta que lo actives de nuevo.
Devuelve
El endpoint modificado.
curl -X PATCH https://api.yesly.io/v1/webhook_endpoints/we_erp \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply",
"call.transferred"
]
}'{
"id": "we_erp",
"object": "webhook_endpoint",
"mode": "test",
"url": "https://erp.example.com/yesly/webhook",
"description": "ERP: estado de cada cita",
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply",
"call.transferred"
],
"status": "enabled",
"api_version": "2026-10-01",
"created": "2026-11-02T09:12:44+01:00"
}Borrar un endpoint
DELETE/v1/webhook_endpoints/{id}
Borra el endpoint. Se descartan los reintentos pendientes hacia él.
Devuelve
deleted: true.
curl -X DELETE https://api.yesly.io/v1/webhook_endpoints/we_erp \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"id": "we_erp",
"object": "webhook_endpoint",
"deleted": true
}Enviar un evento de prueba
POST/v1/webhook_endpoints/{id}/test
Envía un evento de ejemplo, marcado con "mode": "test", al endpoint y devuelve lo que ha respondido. Sirve para comprobar la URL y tu verificación de firma antes de salir a producción.
Parámetros del cuerpo
typestring- Tipo de evento a simular.
appointment.confirmedpor defecto.
Devuelve
El código de estado, el tiempo y el cuerpo con que ha respondido tu endpoint.
curl -X POST https://api.yesly.io/v1/webhook_endpoints/we_erp/test \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Content-Type: application/json" \
-d '{
"type": "appointment.rescheduled"
}'{
"object": "webhook_test",
"event": "evt_t8Kd3",
"status_code": 200,
"duration_ms": 88,
"response_body": "{\"received\":true}"
}Rotar el secreto de firma
POST/v1/webhook_endpoints/{id}/roll_secret
Crea un secreto de firma nuevo. El anterior sigue valiendo 24 horas y, mientras tanto, se envían las dos firmas.
Devuelve
El endpoint con el nuevo secret y previous_secret_expires_at.
curl -X POST https://api.yesly.io/v1/webhook_endpoints/we_erp/roll_secret \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"id": "we_erp",
"object": "webhook_endpoint",
"mode": "test",
"url": "https://erp.example.com/yesly/webhook",
"description": "ERP: estado de cada cita",
"enabled_events": [
"appointment.confirmed",
"appointment.rescheduled",
"appointment.cancelled",
"appointment.no_reply"
],
"status": "enabled",
"api_version": "2026-10-01",
"created": "2026-11-02T09:12:44+01:00",
"secret": "whsec_Hd3cE8bYwN4sRpL7kXv2TmQ9",
"previous_secret_expires_at": "2026-11-12T19:20:00+01:00"
}Listar eventos
GET/v1/events
Lista los eventos de los últimos 30 días, lo más reciente primero. Úsalo para ponerte al día tras una caída en tu lado.
Parámetros de ruta y consulta
typestring- Solo este tipo de evento.
appointment.*incluye todo el grupo. appointmentstring- Solo eventos de esta cita.
fromstring- Eventos creados en esta hora o después.
limitinteger- Entre 1 y 100. 20 por defecto.
starting_afterstring- Cursor: el id del último elemento de la página anterior.
Devuelve
Una lista de eventos.
curl https://api.yesly.io/v1/events?appointment=apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"object": "list",
"url": "/v1/events",
"has_more": false,
"data": [
{
"id": "evt_5Hq1Kd",
"object": "event",
"type": "appointment.confirmed",
"api_version": "2026-10-01",
"mode": "test",
"created": "2026-11-12T10:04:12+01:00",
"data": {
"appointment": {
"id": "apt_8Kq2mZ4tR1",
"external_id": "ORD-48213",
"status": "confirmed",
"…": "…"
}
}
}
]
}Consultar un evento
GET/v1/events/{id}
Parámetros de ruta y consulta
idstringobligatorio- Id del evento, con prefijo
evt_.
Devuelve
El evento.
curl https://api.yesly.io/v1/events/evt_5Hq1Kd \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"{
"id": "evt_5Hq1Kd",
"object": "event",
"type": "appointment.confirmed",
"api_version": "2026-10-01",
"mode": "test",
"created": "2026-11-12T10:04:12+01:00",
"data": {
"appointment": {
"id": "apt_8Kq2mZ4tR1",
"object": "appointment",
"mode": "test",
"external_id": "ORD-48213",
"status": "confirmed",
"type": "delivery",
"location": "bilbao",
"slot": {
"start": "2026-11-13T09:00:00+01:00",
"end": "2026-11-13T11:00:00+01:00"
},
"previous_slot": null,
"customer": {
"name": "Marta López",
"phone": "+34600000001"
},
"channels": [
"whatsapp",
"sms",
"voice"
],
"first_contact_at": "2026-11-12T10:00:00+01:00",
"answer": {
"result": "confirmed",
"channel": "whatsapp",
"at": "2026-11-12T10:04:12+01:00"
},
"cancellation": null,
"attempts": [
{
"id": "att_3Fh7Qp",
"channel": "whatsapp",
"status": "answered",
"sent_at": "2026-11-12T10:00:03+01:00",
"cost": 0.08
}
],
"cost": 0.08,
"currency": "EUR",
"metadata": {
"order_id": "PV-18342",
"route_id": "BIO-07"
},
"created": "2026-11-11T18:30:15+01:00",
"updated": "2026-11-12T10:04:12+01:00"
}
}
}Saldo
Consultar el saldo
GET/v1/balance
Yesly es prepago. Cada WhatsApp, SMS y minuto de llamada contestada se descuenta del saldo al precio público; las llamadas que nadie coge son gratis. Recarga con tarjeta en el panel, en Saldo, con una factura por recarga. Las recargas no se pueden hacer por la API.
Si el saldo se acaba, Yesly deja de enviar mensajes y llamadas nuevos. Las citas se pueden seguir creando y esperan como scheduled; el contacto se reanuda con la siguiente recarga. Activa la recarga automática para evitarlo.
Devuelve
El objeto saldo: available, currency, low_balance_threshold y auto_top_up. En modo de prueba el saldo siempre es 0 y nunca bloquea.
curl https://api.yesly.io/v1/balance \
-H "Authorization: Bearer ysk_live_…"{
"object": "balance",
"mode": "live",
"available": 197.81,
"currency": "EUR",
"low_balance_threshold": 50,
"auto_top_up": {
"enabled": true,
"below": 50,
"amount": 200
}
}Listar movimientos del saldo
GET/v1/balance/transactions
Cada cargo y recarga, lo más reciente primero. Los cargos apuntan a la cita y a su external_id, para que puedas repercutir el coste a un pedido, ruta o cliente.
Parámetros de ruta y consulta
typeenumusageotop_up.appointmentstring- Solo cargos de esta cita.
fromstring- Este día u hora o después.
tostring- Antes de este día u hora.
limitinteger- Entre 1 y 100. 20 por defecto.
starting_afterstring- Cursor: el id del último elemento de la página anterior.
Devuelve
Una lista de movimientos del saldo.
curl https://api.yesly.io/v1/balance/transactions?from=2026-11-12 \
-H "Authorization: Bearer ysk_live_…"{
"object": "list",
"url": "/v1/balance/transactions",
"has_more": true,
"data": [
{
"id": "btx_8Wc1",
"object": "balance_transaction",
"type": "usage",
"channel": "sms",
"amount": -0.15,
"quantity": 1,
"unit": "message",
"appointment": "apt_mn4822",
"external_id": "MN-4822",
"created": "2026-11-12T12:00:01+01:00"
},
{
"id": "btx_8Wc0",
"object": "balance_transaction",
"type": "usage",
"channel": "voice",
"amount": -0.6,
"quantity": 2,
"unit": "minute",
"appointment": "apt_mn4831",
"external_id": "MN-4831",
"created": "2026-11-12T10:02:20+01:00"
},
{
"id": "btx_8Wb9",
"object": "balance_transaction",
"type": "top_up",
"amount": 200,
"invoice": "YS-2026-0118",
"created": "2026-11-09T09:30:12+01:00"
}
]
}Analítica
Resumen
GET/v1/analytics/summary
Las cifras de la página de Analítica del panel, para tus propios informes: resultados, qué canal consiguió la respuesta, cuánto tardaron los clientes, consumo y gasto. Los días siguen la zona horaria de tu cuenta.
Parámetros de ruta y consulta
rangeenum7d,30do90d, hasta hoy.7dpor defecto.typestring- Solo este tipo de cita.
locationstring- Solo esta sede.
Devuelve
Un resumen de analítica.
curl https://api.yesly.io/v1/analytics/summary?range=7d \
-H "Authorization: Bearer ysk_live_…"{
"object": "analytics_summary",
"range": "7d",
"from": "2026-11-06",
"to": "2026-11-12",
"appointments": 214,
"outcomes": {
"confirmed": 168,
"rescheduled": 14,
"cancelled": 9,
"no_reply": 11,
"in_progress": 12
},
"answered_by": {
"whatsapp": 139,
"sms": 31,
"voice": 21
},
"median_minutes_to_answer": 18,
"usage": {
"whatsapp_messages": 212,
"sms_messages": 64,
"voice_minutes": 38
},
"spend": {
"amount": 37.96,
"currency": "EUR"
}
}Librerías
SDK oficiales
Los SDK cubren todos los endpoints de esta referencia, reintentan de forma segura ante errores de red con claves de idempotencia, paginan por ti y verifican las firmas de los webhooks. Leen la clave de API del constructor; guárdala en una variable de entorno.
| Lenguaje | Instalación |
|---|---|
| Node.js | npm install @yesly/node |
| Python | pip install yesly |
| PHP | composer require yesly/yesly-php |
| Java | implementation("io.yesly:yesly-java:1.+") |
| .NET | dotnet add package Yesly |