API reference
Everything your system needs to send appointments to Yesly and get the answers back. REST over HTTPS, JSON in and out, signed webhooks.
- Base URL
- https://api.yesly.io/v1
- Version
- 2026-10-01
Contents
Getting started
Introduction
The Yesly API is organised around a few resources: appointments, the slots customers can move to, your appointment types and locations, webhook endpoints, events, your balance and analytics. Requests and responses are JSON over HTTPS; plain HTTP is refused.
The usual integration is two calls. Your system creates an appointment as soon as it is booked (Create an appointment). Yesly contacts the customer the day before, on the channels and in the order set in your account, and posts the answer to your webhook endpoint (How webhooks work). Everything else is optional.
Times are ISO 8601 with an offset (2026-11-13T09:00:00+01:00). Amounts are in euros with two decimals. Every object has a prefixed id (apt_, evt_, we_…) and a mode: live or test.
curl https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"Authentication
Authenticate with an API key in the Authorization header as a bearer token. Create keys in the dashboard under Integrations → API keys. Live keys start with ysk_live_ and test keys with ysk_test_. A key is shown once, when it is created or rolled.
Each key has an access level: full, read only, or restricted, where you choose none, read or write for each resource (appointments, catalog, webhooks, analytics, balance). You can also limit a key to a list of IP ranges. A request with a missing or revoked key gets 401; a key without access to the resource gets 403.
Keys are server-side secrets. Never put them in a browser, a mobile app or a repository.
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."
}
}Test mode
Requests made with a ysk_test_ key run in test mode. Nothing reaches a real phone and nothing is charged to your balance, but the appointment goes through the same states and your endpoint gets the same events, marked "mode": "test".
In test mode the chain runs in seconds instead of hours and ignores contact hours. Use these numbers as customer.phone to choose the outcome. Any other number is refused with test_number_required.
| Number | What happens |
|---|---|
| +34600000001 | Confirms on WhatsApp. |
| +34600000002 | Asks for another date on WhatsApp and picks the first free slot. |
| +34600000003 | Cancels on WhatsApp, with a reason. |
| +34600000004 | Reads WhatsApp without answering, then confirms from the SMS link. |
| +34600000005 | Does not answer WhatsApp or SMS, then confirms on the call. |
| +34600000006 | Asks to talk to someone on the call: call.transferred. |
| +34600000007 | Not on WhatsApp: message.failed, then SMS, then confirms. |
| +34600000009 | Never answers: 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"
}
}'Errors
Yesly uses HTTP status codes. 2xx means it worked, 4xx means something in the request needs fixing, and 5xx means something went wrong on our side and the request can be retried. The error body always has a type and a message; validation errors add a code and the param at fault. Every response carries a Request-Id header, also shown in the dashboard log.
| Status | Meaning |
|---|---|
| 200, 201 | Done. 201 when a resource was created. |
| 202 | Accepted and processed in the background (imports). |
| 400 | Malformed JSON or a missing required parameter. |
| 401 | No valid API key. |
| 403 | The key cannot do this, or the IP is not allowed. |
| 404 | The resource does not exist in this mode. |
| 409 | Conflicts with an existing resource, such as a reused external_id with different data. |
| 422 | Well formed but invalid, such as a slot in the past. |
| 429 | Too many requests. See Rate limits. |
| 500, 502, 503 | Our fault. Retry with backoff; your request may have been processed, so retry idempotently. |
| Code | When |
|---|---|
| phone_invalid | customer.phone is not a valid E.164 number. |
| slot_offset_missing | A time has no UTC offset. |
| slot_in_past | slot.start has already passed. |
| type_required | The account has several appointment types and none was sent. |
| type_unknown, location_unknown | The id does not exist in your account. |
| metadata_invalid | Too many keys, a key or value too long, or a value that is not a string. |
| test_number_required | A test key was used with a real phone number. |
| external_id_conflict | The external_id exists with different data. |
| appointment_closed | The appointment is cancelled or its slot has passed, so it cannot change. |
{
"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"
}
}Idempotency
Networks fail, so every write can be retried safely. Appointments are idempotent by external_id: posting the same appointment twice returns the existing one with 200 instead of creating a second. Posting the same external_id with different data returns 409; change an appointment with PATCH.
For other POST requests, send an Idempotency-Key header with a unique value, such as a UUID. Yesly stores the first response for 24 hours and returns it again for any retry with the same key.
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."
}
}Pagination
List endpoints return { "object": "list", "data": [...], "has_more": … }, newest first. Pass limit (1 to 100, default 20) and, to get the next page, starting_after with the id of the last item you received. Keep going while has_more is 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": [
"…"
]
}Versioning
The API is versioned by date. The current version is 2026-10-01. Your account is pinned to the version that was current when you made your first request; send Yesly-Version to use another one for a single request. Webhook endpoints have their own api_version.
Breaking changes only ship in a new version. We add fields, event types and enum values without a new version, so ignore what you do not recognise.
curl https://api.yesly.io/v1/appointments/apt_8Kq2mZ4tR1 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc" \
-H "Yesly-Version: 2026-10-01"Rate limits
Each account can make 100 requests per second in live mode and 25 in test mode. Above that the API answers 429 with a Retry-After header, in seconds. To send a whole day of appointments at once, use Import appointments instead of thousands of single calls.
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."
}
}Metadata
Appointments accept metadata: your own key-value pairs, such as the order, route or ticket number. Yesly stores them as given, shows them in the dashboard and returns them unchanged in every response and webhook, so you can match an answer to your records without a lookup.
Up to 20 keys, keys up to 40 characters, values up to 500 characters, strings only. Filter lists with metadata[key]=value. Do not put personal data in metadata: the customer's name and phone go in customer.
curl https://api.yesly.io/v1/appointments?metadata[route_id]=BIO-07&date=2026-11-13 \
-H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"Appointments
The appointment object
An appointment is a visit, delivery or meeting Yesly confirms with your customer. Its status moves forward as the customer answers:
| Status | Meaning |
|---|---|
| scheduled | Created. Waiting for first_contact_at. |
| in_progress | Yesly is contacting the customer. |
| confirmed | The customer will be there. |
| rescheduled | The customer moved it to one of your free slots. slot is the new one, previous_slot the old one. |
| cancelled | Cancelled by the customer or by you. See cancellation.by. |
| no_reply | No answer on any channel. Your team should call. |
Attributes
idstring- Unique id, prefixed
apt_. external_idstring- Your id for the appointment, unique per account and mode.
statusenumscheduled,in_progress,confirmed,rescheduled,cancelledorno_reply.typestring- Id of the appointment type. Sets how the time is described and the instructions in the message.
locationstring | null- Id of the location, if any.
slotobjectstartandendof the appointment. For exact-time types,endisstartplus the type's duration.previous_slotobject | null- The slot before the customer rescheduled, otherwise
null. customerobjectnameandphone(E.164) of the person Yesly contacts.channelsarray- Channels used for this appointment, in order. By default, the ones set in your account.
first_contact_atstring | null- When the first message goes out, taking your contact hours into account.
answerobject | nullresult,channelandatof the customer's answer, ornull.cancellationobject | nullby(customeroraccount),reasonandat, ornull.attemptsarray- Each contact attempt:
channel,status(queued,sent,delivered,read,answered,no_answer,failed,skipped),sent_atandcost. costnumber- What this appointment has cost so far, in euros. WhatsApp 0.08 per message, SMS 0.15 per message, voice 0.30 per answered minute; unanswered calls are free.
metadataobject- Your key-value pairs, as you sent them.
modeenumliveortest.createdstring- When the appointment was created.
updatedstring- When anything in it last changed.
{
"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"
}Create an appointment
POST/v1/appointments
Creates an appointment and schedules the first contact. By default Yesly writes the day before at 10:00 and moves through your channels if there is no answer; if the appointment is booked after that moment, contact starts at the next allowed time within your contact hours.
WhatsApp and SMS need a mobile number. If customer.phone is a landline, those channels are skipped and only the call runs.
Body parameters
external_idstringrequired- Your id for this appointment, such as the order or ticket number. Retrying with the same
external_idis safe. typestring- Appointment type id. Required if your account has more than one type.
locationstring- Location id. Required when the type happens at one of your locations; otherwise optional, and used to pass calls to that location's phone.
slot.startstringrequired- Start of the appointment or of the time window, with offset.
slot.endstring- End of the window. Leave it out for exact-time types; it is computed from the type's duration.
customer.namestringrequired- Name used in the messages. A first name is enough.
customer.phonestringrequired- Phone number in E.164 format, such as
+34612345678. channelsarray- Override the channel order for this appointment, such as
["whatsapp", "voice"]. Only channels enabled in your account. metadataobject- Up to 20 key-value pairs of your own. See Metadata.
Returns
The appointment, with status: scheduled and first_contact_at. 201 when created, 200 when it already existed with the same data.
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"
}Retrieve an appointment
GET/v1/appointments/{id}
Fetches an appointment with its attempts and answer. To find one by your own id, list with external_id.
Path and query parameters
idstringrequired- Id of the appointment, prefixed
apt_.
Returns
The appointment.
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"
}List appointments
GET/v1/appointments
Lists appointments, newest first. A common use is to ask, each afternoon, which of tomorrow's appointments ended with no_reply so your team can call them.
Path and query parameters
statusenum- Only appointments in this status.
datestring- Only appointments whose slot starts on this day,
YYYY-MM-DDin your account's time zone. fromstring- Slot starts on or after this time.
tostring- Slot starts before this time.
typestring- Only this appointment type.
locationstring- Only this location.
external_idstring- The appointment with this
external_id. metadata[key]string- Only appointments whose metadata has this value, such as
metadata[route_id]=BIO-07. limitinteger- Between 1 and 100. Default 20.
starting_afterstring- Cursor: the id of the last item of the previous page.
Returns
A list of appointments.
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"
}
]
}Update an appointment
PATCH/v1/appointments/{id}
Changes an appointment. Send only what changes. metadata is merged: keys you send are set, a key sent as an empty string is removed.
If you change the slot or the customer's phone, Yesly starts the confirmation again: the status goes back to scheduled, any previous answer is cleared and a new first_contact_at is computed. Changes to metadata or the name never contact the customer.
Body parameters
typestring- New appointment type id.
locationstring- New location id.
slot.startstring- New start, with offset.
slot.endstring- New end of the window.
customer.namestring- New name.
customer.phonestring- New phone, E.164.
channelsarray- New channel order for this appointment.
metadataobject- Keys to set. An empty string removes the key.
Returns
The updated appointment. 409 appointment_closed if it is cancelled or its slot has passed.
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"
}Cancel an appointment
POST/v1/appointments/{id}/cancel
Cancels an appointment from your side, for example when the order is cancelled. Yesly stops contacting the customer at once and does not send them anything else. Attempts already made are still charged.
Body parameters
reasonstring- Optional. Stored in
cancellation.reasonand shown in the dashboard.
Returns
The appointment with status: cancelled and cancellation.by: account. Fires 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"
}Import appointments
POST/v1/appointments/import
Creates or updates up to 1,000 appointments in one request, for systems that plan the whole day at once. Each item takes the same fields as Create an appointment; items whose external_id already exists are updated. The import runs in the background.
No API yet? Upload the same data as a CSV in the dashboard under Integrations → CSV import, with one metadata.<key> column per metadata key.
Body parameters
appointmentsarrayrequired- Between 1 and 1,000 appointments.
Returns
An import object with status: processing. Poll Retrieve an import or wait for it in the dashboard.
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": []
}Retrieve an import
GET/v1/imports/{id}
Shows how an import went. Items that fail do not stop the others; each error points to the item by its position and external_id.
Path and query parameters
idstringrequired- Id of the import, prefixed
imp_.
Returns
The import, with status processing, completed or failed, the counts and the errors.
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."
}
]
}Slots
The slot object
Slots are the free times you offer when a customer asks for another date. Your system publishes them; Yesly only offers slots of the same type and location, within the window and minimum notice set in Settings → Changes and cancellations. When a customer takes one, booked goes up, the appointment moves there and you get appointment.rescheduled. A full slot is no longer offered.
If the account allows rescheduling but there are no slots, the customer is told someone will call them and the appointment goes to your team.
Attributes
idstring- Unique id, prefixed
slt_. typestring- Appointment type this slot is for.
locationstring | null- Location, if the type has one.
startstring- Start, with offset.
endstring- End, with offset.
capacityinteger- How many appointments fit. Default 1.
bookedinteger- How many customers have moved into it through 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
}Publish slots
POST/v1/slots
Publishes free slots in bulk, up to 500 per request. A slot with the same type, location and start as an existing one replaces it, so you can send your full availability every few hours. Lowering capacity below booked is refused.
Body parameters
slotsarrayrequired- Between 1 and 500 slots.
slots[].typestringrequired- Appointment type id.
slots[].locationstring- Location id, if the type has one.
slots[].startstringrequired- Start, with offset.
slots[].endstringrequired- End, with offset.
slots[].capacityinteger- Appointments that fit. Default 1.
Returns
The slots as stored.
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
}
]
}List slots
GET/v1/slots
Lists published slots, soonest first.
Path and query parameters
typestring- Only this appointment type.
locationstring- Only this location.
fromstring- Slots starting on or after this day or time.
tostring- Slots starting before this day or time.
availablebooleantruefor slots with room left.limitinteger- Between 1 and 100. Default 20.
starting_afterstring- Cursor: the id of the last item of the previous page.
Returns
A list of slots.
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
}
]
}Delete a slot
DELETE/v1/slots/{id}
Stops offering a slot, for example when your own system fills it. Appointments already moved into it stay where they are.
Path and query parameters
idstringrequired- Id of the slot, prefixed
slt_.
Returns
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
}Appointment types and locations
The appointment type object
Appointment types describe what each appointment is: a home delivery, a technical visit, an inspection. They set how the time is shown to the customer and the instructions added to the message. Types are the same as in Settings → Appointment types.
Attributes
idstring- Your id for the type, such as
delivery. Sent with each appointment. namestring- What the customer reads in the message.
time_formatenumwindow("between 9:00 and 11:00") orexact("at 17:30").minutesinteger- Length of the window or duration of the appointment.
placeenumcustomer(at the customer's address) orlocation(at one of your locations).instructionsstring | null- What the customer should bring or prepare. Used by the
{instructions}variable in templates. reschedulableboolean- Whether the customer may ask for another date.
{
"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"
}List appointment types
GET/v1/appointment_types
Lists the account's appointment types.
Returns
A list of appointment types.
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"
}
]
}Create an appointment type
POST/v1/appointment_types
Adds an appointment type. Message templates pick it up at once through {type}, {time} and {instructions}.
Body parameters
idstringrequired- Lowercase letters, numbers,
-and_. Cannot change later. namestringrequired- Name shown to the customer.
time_formatenumrequiredwindoworexact.minutesintegerrequired- Window length or duration.
placeenumrequiredcustomerorlocation.instructionsstring- Optional instructions for the customer.
reschedulableboolean- Default
true.
Returns
The appointment type.
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"
}Update an appointment type
PATCH/v1/appointment_types/{id}
Changes a type. Appointments already contacted keep the text they were sent; the next messages use the new one.
Body parameters
namestring- New name.
minutesinteger- New window length or duration.
instructionsstring- New instructions. An empty string removes them.
reschedulableboolean- Whether the customer may ask for another date.
Returns
The updated appointment type.
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"
}The location object
Locations are your centres, shops or depots: where location appointments happen, and the phone a call is passed to when the customer wants to talk to a person.
Attributes
idstring- Your id for the location, such as
bilbao. namestring- Name shown to the customer.
addressstring- Address used in the messages for appointments held there.
transfer_phonestring | null- Where calls are passed when the customer asks for a person. Without it, Yesly offers a call back and flags the appointment for your team.
{
"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"
}
]
}Create a location
POST/v1/locations
Body parameters
idstringrequired- Lowercase letters, numbers,
-and_. Cannot change later. namestringrequired- Name shown to the customer.
addressstringrequired- Full address.
transfer_phonestring- Phone for call transfers, E.164.
Returns
The location.
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"
}Update a location
PATCH/v1/locations/{id}
Body parameters
namestring- New name.
addressstring- New address.
transfer_phonestring- New transfer phone. An empty string removes it.
Returns
The updated location.
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 and events
How webhooks work
When something happens to an appointment, Yesly sends an event as a POST with a JSON body to every enabled endpoint subscribed to that event type. Add endpoints in the dashboard under Integrations → Webhooks or with the API. Live and test endpoints are separate.
Answer with any 2xx within 10 seconds, before doing slow work. Anything else, or no answer, counts as a failure: Yesly retries with growing waits for 24 hours, and failed deliveries can be resent from the dashboard.
Delivery is at least once and order is not guaranteed. Use the event id to ignore duplicates, and compare the appointment's updated with what you have before overwriting it.
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"
}
}
}Verify signatures
Every delivery carries a Yesly-Signature header: t=<unix time>,v1=<signature>. The signature is an HMAC-SHA256, in hex, of the timestamp, a dot and the raw request body, keyed with the endpoint's signing secret (whsec_…).
Compute it over the raw bytes, before parsing the JSON, and compare in constant time. Reject deliveries whose timestamp is more than five minutes away from your clock, to stop replays. The SDKs do all of this in one call.
When you roll the secret, the previous one keeps working for 24 hours and both signatures are sent, so you can deploy the new one without losing events.
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 });
});Event types
Subscribe each endpoint only to what it needs. Most integrations use the four answer events.
| Event | When |
|---|---|
| appointment.created | An appointment was created by API, import, CSV or in the dashboard. |
| appointment.confirmed | The customer confirmed. |
| appointment.rescheduled | The customer moved it to another slot. slot is the new one. |
| appointment.cancelled | Cancelled by the customer (with the reason, if asked) or by you. |
| appointment.no_reply | No channel got an answer. Flag it for your team. |
| message.sent | A WhatsApp message or SMS left Yesly. |
| message.delivered | The carrier or WhatsApp confirmed delivery. |
| message.failed | It could not be delivered, such as a number not on WhatsApp. The next channel runs. |
| call.started | A call started ringing. |
| call.completed | A call ended, with its duration and outcome. |
| call.transferred | The customer asked for a person and the call was passed to the location's phone. |
| balance.low | The balance went below your alert threshold. |
| balance.topped_up | A top-up was paid. Includes the invoice number. |
The event object
Events have the same shape whether they arrive by webhook or through List events. data holds the full object at the time of the event: an appointment for appointment.*, a message or call for channel events, the balance for balance.*. All of them carry the appointment's external_id and metadata.
Attributes
idstring- Unique id, prefixed
evt_. Use it to ignore duplicates. typestring- Event type, such as
appointment.confirmed. api_versionstring- Version used to render
data, that of the endpoint. createdstring- When it happened.
dataobject- The object the event is about.
modeenumliveortest.
{
"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"
}
}
}The webhook endpoint object
Attributes
idstring- Unique id, prefixed
we_. urlstring- HTTPS URL that receives the events.
descriptionstring | null- What the endpoint is for.
enabled_eventsarray- Event types sent to it.
["*"]for all. statusenumenabledordisabled.api_versionstring- Version used to render the events it receives.
secretstring- Signing secret. Only returned when the endpoint is created or the secret is rolled.
{
"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"
}Create an endpoint
POST/v1/webhook_endpoints
Body parameters
urlstringrequired- HTTPS URL. Self-signed certificates are refused.
enabled_eventsarrayrequired- Event types to send, or
["*"]. descriptionstring- Optional note.
api_versionstring- Defaults to your account's version.
Returns
The endpoint, with its secret. Store it: it is not shown again.
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"
}List endpoints
GET/v1/webhook_endpoints
Path and query parameters
limitinteger- Between 1 and 100. Default 20.
starting_afterstring- Cursor: the id of the last item of the previous page.
Returns
A list of endpoints in the key's mode.
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"
}
]
}Update an endpoint
PATCH/v1/webhook_endpoints/{id}
Body parameters
urlstring- New URL.
enabled_eventsarray- Replaces the list of event types.
descriptionstring- New note.
disabledbooleantruestops sending events to it until you enable it again.
Returns
The updated endpoint.
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"
}Delete an endpoint
DELETE/v1/webhook_endpoints/{id}
Deletes the endpoint. Pending retries to it are dropped.
Returns
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
}Send a test event
POST/v1/webhook_endpoints/{id}/test
Sends a sample event, marked "mode": "test", to the endpoint and returns what it answered. Useful to check the URL and your signature check before going live.
Body parameters
typestring- Event type to simulate. Default
appointment.confirmed.
Returns
The status code, time and body your endpoint answered with.
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}"
}Roll the signing secret
POST/v1/webhook_endpoints/{id}/roll_secret
Creates a new signing secret. The previous one stays valid for 24 hours, and both signatures are sent meanwhile.
Returns
The endpoint with the new secret and 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"
}List events
GET/v1/events
Lists events from the last 30 days, newest first. Use it to catch up after downtime on your side.
Path and query parameters
typestring- Only this event type.
appointment.*matches a whole group. appointmentstring- Only events about this appointment.
fromstring- Events created on or after this time.
limitinteger- Between 1 and 100. Default 20.
starting_afterstring- Cursor: the id of the last item of the previous page.
Returns
A list of events.
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",
"…": "…"
}
}
}
]
}Retrieve an event
GET/v1/events/{id}
Path and query parameters
idstringrequired- Id of the event, prefixed
evt_.
Returns
The event.
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"
}
}
}Balance
Retrieve the balance
GET/v1/balance
Yesly is prepaid. Each WhatsApp message, SMS and minute of answered call is taken from the balance at the public price; calls nobody picks up are free. Top up by card in the dashboard under Balance, with one invoice per top-up. Top-ups cannot be made through the API.
If the balance runs out, Yesly stops sending new messages and calls. Appointments can still be created and wait as scheduled; contact resumes after the next top-up. Turn on auto top-up to avoid it.
Returns
The balance object: available, currency, low_balance_threshold and auto_top_up. Test mode always returns a balance of 0 and never blocks.
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
}
}List balance transactions
GET/v1/balance/transactions
Every charge and top-up, newest first. Charges point to the appointment and its external_id, so you can pass the cost on to an order, route or customer.
Path and query parameters
typeenumusageortop_up.appointmentstring- Only charges for this appointment.
fromstring- On or after this day or time.
tostring- Before this day or time.
limitinteger- Between 1 and 100. Default 20.
starting_afterstring- Cursor: the id of the last item of the previous page.
Returns
A list of balance transactions.
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"
}
]
}Analytics
Summary
GET/v1/analytics/summary
The figures behind the dashboard's Analytics page, for your own reports: outcomes, which channel got the answer, how long customers took, usage and spend. Days follow your account's time zone.
Path and query parameters
rangeenum7d,30dor90d, ending today. Default7d.typestring- Only this appointment type.
locationstring- Only this location.
Returns
An analytics summary.
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"
}
}Libraries
Official SDKs
The SDKs wrap every endpoint in this reference, retry safely on network errors with idempotency keys, paginate for you and verify webhook signatures. They read the API key from the constructor; keep it in an environment variable.
| Language | Install |
|---|---|
| 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 |