Desarrolladores

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
Pedir acceso al sandbox
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.

Petición
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.

Petición
curl https://api.yesly.io/v1/balance \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 401
{
  "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úmeroQué pasa
+34600000001Confirma por WhatsApp.
+34600000002Pide otra fecha por WhatsApp y elige el primer hueco libre.
+34600000003Cancela por WhatsApp, con motivo.
+34600000004Lee el WhatsApp sin responder y confirma desde el enlace del SMS.
+34600000005No responde ni a WhatsApp ni a SMS y confirma en la llamada.
+34600000006Pide hablar con alguien en la llamada: call.transferred.
+34600000007No tiene WhatsApp: message.failed, luego SMS y confirma.
+34600000009Nunca responde: appointment.no_reply.
Petición
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.

EstadoSignificado
200, 201Hecho. 201 cuando se ha creado un recurso.
202Aceptado y en proceso en segundo plano (importaciones).
400JSON mal formado o falta un parámetro obligatorio.
401No hay una clave de API válida.
403La clave no puede hacer esto o la IP no está permitida.
404El recurso no existe en este modo.
409Choca con un recurso existente, como un external_id repetido con otros datos.
422Bien formado pero no válido, como una franja en el pasado.
429Demasiadas peticiones. Mira Límites de uso.
500, 502, 503Error nuestro. Reintenta con espera creciente; puede que la petición se procesara, así que reintenta de forma idempotente.
CódigoCuándo
phone_invalidcustomer.phone no es un número E.164 válido.
slot_offset_missingUna hora no lleva desfase respecto a UTC.
slot_in_pastslot.start ya ha pasado.
type_requiredLa cuenta tiene varios tipos de cita y no se ha enviado ninguno.
type_unknown, location_unknownEl id no existe en tu cuenta.
metadata_invalidDemasiadas claves, una clave o un valor demasiado largos o un valor que no es texto.
test_number_requiredSe ha usado una clave de prueba con un teléfono real.
external_id_conflictEl external_id ya existe con otros datos.
appointment_closedLa cita está cancelada o su franja ya ha pasado, así que no se puede cambiar.
Respuesta · 422
{
  "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.

Petición
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"
    ]
  }'
Respuesta · 409
{
  "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.

Petición
curl https://api.yesly.io/v1/appointments?date=2026-11-13&limit=50&starting_after=apt_8Kq2mZ4tR1 \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta
{
  "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.

Petición
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.

Respuesta · 429
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.

Petición
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:

EstadoSignificado
scheduledCreada. Espera a first_contact_at.
in_progressYesly está contactando con el cliente.
confirmedEl cliente estará.
rescheduledEl cliente la ha movido a uno de tus huecos libres. slot es el nuevo y previous_slot el anterior.
cancelledCancelada por el cliente o por ti. Mira cancellation.by.
no_replySin 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.
statusenum
scheduled, in_progress, confirmed, rescheduled, cancelled o no_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.
slotobject
start y end de la cita. En los tipos con hora exacta, end es start más la duración del tipo.
previous_slotobject | null
La franja anterior si el cliente la cambió; si no, null.
customerobject
name y phone (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 | null
result, channel y at de la respuesta del cliente, o null.
cancellationobject | null
by (customer o account), reason y at, o null.
attemptsarray
Cada intento de contacto: channel, status (queued, sent, delivered, read, answered, no_answer, failed, skipped), sent_at y cost.
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.
modeenum
live o test.
createdstring
Cuándo se creó la cita.
updatedstring
Cuándo cambió algo por última vez.
Respuesta · The appointment object
{
  "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_id es 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.

POST /v1/appointments
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"
    }
  }'
Respuesta · 201
{
  "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.

GET /v1/appointments/{id}
curl https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1 \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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-DD en 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.

GET /v1/appointments
curl https://api.yesly.io/v1/appointments?status=no_reply&date=2026-11-13 \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.

PATCH /v1/appointments/{id}
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"
    }
  }'
Respuesta · 200
{
  "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.reason y se ve en el panel.

Devuelve

La cita con status: cancelled y cancellation.by: account. Lanza appointment.cancelled.

POST /v1/appointments/{id}/cancel
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"
  }'
Respuesta · 200
{
  "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.

POST /v1/appointments/import
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"
        }
      },
      "…"
    ]
  }'
Respuesta · 202
{
  "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.

GET /v1/imports/{id}
curl https://api.yesly.io/v1/imports/imp_4Dw8Ty \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.
Respuesta · The slot object
{
  "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.

POST /v1/slots
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
      }
    ]
  }'
Respuesta · 200
{
  "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.
availableboolean
true para 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.

GET /v1/slots
curl https://api.yesly.io/v1/slots?type=delivery&location=bilbao&from=2026-11-16&available=true \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.

DELETE /v1/slots/{id}
curl -X DELETE https://api.yesly.io/v1/slots/slt_7Tn4Wb \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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_formatenum
window ("entre las 9:00 y las 11:00") o exact ("a las 17:30").
minutesinteger
Longitud de la franja o duración de la cita.
placeenum
customer (en la dirección del cliente) o location (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.
Respuesta · The appointment type object
{
  "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.

GET /v1/appointment_types
curl https://api.yesly.io/v1/appointment_types \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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_formatenumobligatorio
window o exact.
minutesintegerobligatorio
Longitud de la franja o duración.
placeenumobligatorio
customer o location.
instructionsstring
Instrucciones opcionales para el cliente.
reschedulableboolean
true por defecto.

Devuelve

El tipo de cita.

POST /v1/appointment_types
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
  }'
Respuesta · 201
{
  "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.

PATCH /v1/appointment_types/{id}
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."
  }'
Respuesta · 200
{
  "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.
Respuesta · The location object
{
  "id": "bilbao",
  "object": "location",
  "name": "Bilbao",
  "address": "Calle Mayor 12, 48001 Bilbao",
  "transfer_phone": "+34944000112",
  "created": "2026-09-28T12:20:00+02:00"
}

Listar sedes

GET/v1/locations

Lista las sedes de la cuenta.

Devuelve

Una lista de sedes.

GET /v1/locations
curl https://api.yesly.io/v1/locations \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.

POST /v1/locations
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"
  }'
Respuesta · 201
{
  "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.

PATCH /v1/locations/{id}
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"
  }'
Respuesta · 200
{
  "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.

Respuesta · POST https://erp.example.com/yesly/webhook
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.

Petición
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.

EventoCuándo
appointment.createdSe ha creado una cita por API, importación, CSV o en el panel.
appointment.confirmedEl cliente ha confirmado.
appointment.rescheduledEl cliente la ha movido a otro hueco. slot es el nuevo.
appointment.cancelledCancelada por el cliente (con el motivo, si se pide) o por ti.
appointment.no_replyNingún canal ha conseguido respuesta. Pásala a tu equipo.
message.sentUn WhatsApp o SMS ha salido de Yesly.
message.deliveredEl operador o WhatsApp han confirmado la entrega.
message.failedNo se ha podido entregar, por ejemplo un número sin WhatsApp. Se pasa al siguiente canal.
call.startedUna llamada ha empezado a sonar.
call.completedUna llamada ha terminado, con su duración y resultado.
call.transferredEl cliente ha pedido hablar con alguien y la llamada se ha pasado al teléfono de la sede.
balance.lowEl saldo ha bajado de tu umbral de aviso.
balance.topped_upSe 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.
modeenum
live o test.
Petición
{
  "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.
statusenum
enabled o disabled.
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.
Respuesta · The webhook endpoint object
{
  "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.

POST /v1/webhook_endpoints
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"
    ]
  }'
Respuesta · 201
{
  "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.

GET /v1/webhook_endpoints
curl https://api.yesly.io/v1/webhook_endpoints \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.
disabledboolean
true deja de enviarle eventos hasta que lo actives de nuevo.

Devuelve

El endpoint modificado.

PATCH /v1/webhook_endpoints/{id}
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"
    ]
  }'
Respuesta · 200
{
  "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.

DELETE /v1/webhook_endpoints/{id}
curl -X DELETE https://api.yesly.io/v1/webhook_endpoints/we_erp \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.confirmed por defecto.

Devuelve

El código de estado, el tiempo y el cuerpo con que ha respondido tu endpoint.

POST /v1/webhook_endpoints/{id}/test
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"
  }'
Respuesta · 200
{
  "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.

POST /v1/webhook_endpoints/{id}/roll_secret
curl -X POST https://api.yesly.io/v1/webhook_endpoints/we_erp/roll_secret \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.

GET /v1/events
curl https://api.yesly.io/v1/events?appointment=apt_8Kq2mZ4tR1 \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.

GET /v1/events/{id}
curl https://api.yesly.io/v1/events/evt_5Hq1Kd \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Respuesta · 200
{
  "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.

GET /v1/balance
curl https://api.yesly.io/v1/balance \
  -H "Authorization: Bearer ysk_live_…"
Respuesta · 200
{
  "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

typeenum
usage o top_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.

GET /v1/balance/transactions
curl https://api.yesly.io/v1/balance/transactions?from=2026-11-12 \
  -H "Authorization: Bearer ysk_live_…"
Respuesta · 200
{
  "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

rangeenum
7d, 30d o 90d, hasta hoy. 7d por defecto.
typestring
Solo este tipo de cita.
locationstring
Solo esta sede.

Devuelve

Un resumen de analítica.

GET /v1/analytics/summary
curl https://api.yesly.io/v1/analytics/summary?range=7d \
  -H "Authorization: Bearer ysk_live_…"
Respuesta · 200
{
  "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.

LenguajeInstalación
Node.jsnpm install @yesly/node
Pythonpip install yesly
PHPcomposer require yesly/yesly-php
Javaimplementation("io.yesly:yesly-java:1.+")
.NETdotnet add package Yesly