Developers

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
Get sandbox access
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.

Request
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.

Request
curl https://api.yesly.io/v1/balance \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Response · 401
{
  "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.

NumberWhat happens
+34600000001Confirms on WhatsApp.
+34600000002Asks for another date on WhatsApp and picks the first free slot.
+34600000003Cancels on WhatsApp, with a reason.
+34600000004Reads WhatsApp without answering, then confirms from the SMS link.
+34600000005Does not answer WhatsApp or SMS, then confirms on the call.
+34600000006Asks to talk to someone on the call: call.transferred.
+34600000007Not on WhatsApp: message.failed, then SMS, then confirms.
+34600000009Never answers: appointment.no_reply.
Request
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.

StatusMeaning
200, 201Done. 201 when a resource was created.
202Accepted and processed in the background (imports).
400Malformed JSON or a missing required parameter.
401No valid API key.
403The key cannot do this, or the IP is not allowed.
404The resource does not exist in this mode.
409Conflicts with an existing resource, such as a reused external_id with different data.
422Well formed but invalid, such as a slot in the past.
429Too many requests. See Rate limits.
500, 502, 503Our fault. Retry with backoff; your request may have been processed, so retry idempotently.
CodeWhen
phone_invalidcustomer.phone is not a valid E.164 number.
slot_offset_missingA time has no UTC offset.
slot_in_pastslot.start has already passed.
type_requiredThe account has several appointment types and none was sent.
type_unknown, location_unknownThe id does not exist in your account.
metadata_invalidToo many keys, a key or value too long, or a value that is not a string.
test_number_requiredA test key was used with a real phone number.
external_id_conflictThe external_id exists with different data.
appointment_closedThe appointment is cancelled or its slot has passed, so it cannot change.
Response · 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"
  }
}

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.

Request
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"
    ]
  }'
Response · 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."
  }
}

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.

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

Request
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.

Response · 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."
  }
}

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.

Request
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:

StatusMeaning
scheduledCreated. Waiting for first_contact_at.
in_progressYesly is contacting the customer.
confirmedThe customer will be there.
rescheduledThe customer moved it to one of your free slots. slot is the new one, previous_slot the old one.
cancelledCancelled by the customer or by you. See cancellation.by.
no_replyNo 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.
statusenum
scheduled, in_progress, confirmed, rescheduled, cancelled or no_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.
slotobject
start and end of the appointment. For exact-time types, end is start plus the type's duration.
previous_slotobject | null
The slot before the customer rescheduled, otherwise null.
customerobject
name and phone (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 | null
result, channel and at of the customer's answer, or null.
cancellationobject | null
by (customer or account), reason and at, or null.
attemptsarray
Each contact attempt: channel, status (queued, sent, delivered, read, answered, no_answer, failed, skipped), sent_at and cost.
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.
modeenum
live or test.
createdstring
When the appointment was created.
updatedstring
When anything in it last changed.
Response · 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"
}

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_id is 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.

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"
    }
  }'
Response · 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"
}

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.

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

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-DD in 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.

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

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.

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"
    }
  }'
Response · 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"
}

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.reason and shown in the dashboard.

Returns

The appointment with status: cancelled and cancellation.by: account. Fires 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"
  }'
Response · 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"
}

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.

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"
        }
      },
      "…"
    ]
  }'
Response · 202
{
  "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.

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

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.
Response · 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
}

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.

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
      }
    ]
  }'
Response · 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
    }
  ]
}

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.
availableboolean
true for 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.

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"
Response · 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
    }
  ]
}

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.

DELETE /v1/slots/{id}
curl -X DELETE https://api.yesly.io/v1/slots/slt_7Tn4Wb \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Response · 200
{
  "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_formatenum
window ("between 9:00 and 11:00") or exact ("at 17:30").
minutesinteger
Length of the window or duration of the appointment.
placeenum
customer (at the customer's address) or location (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.
Response · 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"
}

List appointment types

GET/v1/appointment_types

Lists the account's appointment types.

Returns

A list of appointment types.

GET /v1/appointment_types
curl https://api.yesly.io/v1/appointment_types \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Response · 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"
    }
  ]
}

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_formatenumrequired
window or exact.
minutesintegerrequired
Window length or duration.
placeenumrequired
customer or location.
instructionsstring
Optional instructions for the customer.
reschedulableboolean
Default true.

Returns

The appointment type.

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
  }'
Response · 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"
}

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.

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."
  }'
Response · 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"
}

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.
Response · 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"
}

List locations

GET/v1/locations

Lists the account's locations.

Returns

A list of locations.

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

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.

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"
  }'
Response · 201
{
  "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.

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"
  }'
Response · 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 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.

Response · 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"
    }
  }
}

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.

Request
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.

EventWhen
appointment.createdAn appointment was created by API, import, CSV or in the dashboard.
appointment.confirmedThe customer confirmed.
appointment.rescheduledThe customer moved it to another slot. slot is the new one.
appointment.cancelledCancelled by the customer (with the reason, if asked) or by you.
appointment.no_replyNo channel got an answer. Flag it for your team.
message.sentA WhatsApp message or SMS left Yesly.
message.deliveredThe carrier or WhatsApp confirmed delivery.
message.failedIt could not be delivered, such as a number not on WhatsApp. The next channel runs.
call.startedA call started ringing.
call.completedA call ended, with its duration and outcome.
call.transferredThe customer asked for a person and the call was passed to the location's phone.
balance.lowThe balance went below your alert threshold.
balance.topped_upA 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.
modeenum
live or test.
Request
{
  "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.
statusenum
enabled or disabled.
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.
Response · 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"
}

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.

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"
    ]
  }'
Response · 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"
}

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.

GET /v1/webhook_endpoints
curl https://api.yesly.io/v1/webhook_endpoints \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Response · 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"
    }
  ]
}

Update an endpoint

PATCH/v1/webhook_endpoints/{id}

Body parameters

urlstring
New URL.
enabled_eventsarray
Replaces the list of event types.
descriptionstring
New note.
disabledboolean
true stops sending events to it until you enable it again.

Returns

The updated endpoint.

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"
    ]
  }'
Response · 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"
}

Delete an endpoint

DELETE/v1/webhook_endpoints/{id}

Deletes the endpoint. Pending retries to it are dropped.

Returns

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"
Response · 200
{
  "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.

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"
  }'
Response · 200
{
  "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.

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"
Response · 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"
}

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.

GET /v1/events
curl https://api.yesly.io/v1/events?appointment=apt_8Kq2mZ4tR1 \
  -H "Authorization: Bearer ysk_test_4eC39HqLyjWDarjtT1zdp7dc"
Response · 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",
          "…": "…"
        }
      }
    }
  ]
}

Retrieve an event

GET/v1/events/{id}

Path and query parameters

idstringrequired
Id of the event, prefixed evt_.

Returns

The event.

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

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.

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

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

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

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

rangeenum
7d, 30d or 90d, ending today. Default 7d.
typestring
Only this appointment type.
locationstring
Only this location.

Returns

An analytics summary.

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

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.

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