OpenAPI spec (JSON) · Markdown
Wylto API reference
- Base URL:
https://server.wylto.com - Authentication:
Authorization: Bearer <token> - OpenAPI spec: https://wylto.com/openapi.json
- Interactive reference: https://wylto.com/docs/api
Send WhatsApp messages and templates, manage contacts and run campaigns from your own systems, and get webhooks when messages arrive or bookings change.
Getting started
- In Wylto, open CRM → Settings → API Settings and copy your API token. Only the app owner can see it.
- Send it as a Bearer token:
Authorization: Bearer <token>. - Call the API at
https://server.wylto.com. Requests and responses are JSON.
Webhooks
- Messages: Wylto posts each new WhatsApp message to your URL.
- Bookings: set up under Appointments → Settings → Webhooks to get created, rescheduled, cancelled and completed bookings, signed with your secret.
Errors
Failed requests return a 4xx or 5xx status with a JSON body that describes the problem.
Send messages and templates, manage templates, download media and read your phone number details.
Send a message to a phone number
POST https://server.wylto.com/api/v1/wa/send
Request body
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | The phone number to which the message is being sent. | |
clientMessageId |
string | Unique identifier of the message in your system. | |
message |
object | ||
message.type |
string: text, template, image, document, video, audio, buttons, list |
The type of the message. | |
message.text |
object | Text message content. Required when type is 'text'. | |
message.text.message |
string | yes | The text content of the message. |
message.image |
object | Image message content. Required when type is 'image'. | |
message.image.url |
string | yes | The URL of the image. |
message.image.mimeType |
string | yes | The MIME type of the image (e.g., image/jpeg, image/png). |
message.image.message |
string | Optional caption for the image. | |
message.document |
object | Document message content. Required when type is 'document'. | |
message.document.url |
string | yes | The URL of the document. |
message.document.filename |
string | The filename of the document. | |
message.document.mimeType |
string | yes | The MIME type of the document. |
message.document.message |
string | Optional caption for the document. | |
message.video |
object | Video message content. Required when type is 'video'. | |
message.video.url |
string | yes | The URL of the video. |
message.video.mimeType |
string | yes | The MIME type of the video. |
message.video.message |
string | Optional caption for the video. | |
message.audio |
object | Audio message content. Required when type is 'audio'. | |
message.audio.url |
string | yes | The URL of the audio file. |
message.audio.mimeType |
string | yes | The MIME type of the audio. |
message.buttons |
object | Quick reply buttons message content. Required when type is 'buttons'. WhatsApp allows up to 3 quick reply buttons. | |
message.buttons.imageURL |
string | Optional image URL shown as a header above the message. | |
message.buttons.heading |
string | Optional text header shown above the message (ignored when imageURL is set). | |
message.buttons.message |
string | yes | The body text shown above the buttons. |
message.buttons.footer |
string | Optional footer text shown below the buttons. | |
message.buttons.buttons |
object[] | yes | Up to 3 quick reply buttons. |
message.buttons.buttons.id |
string | yes | Stable identifier returned in the user's reply payload. |
message.buttons.buttons.name |
string | yes | Label shown on the button (max 20 characters). |
message.list |
object | List message content. Required when type is 'list'. WhatsApp allows up to 10 list rows. | |
message.list.imageURL |
string | Optional image URL shown as a header above the message. | |
message.list.heading |
string | Optional text header shown above the message (ignored when imageURL is set). | |
message.list.message |
string | yes | The body text shown above the list. |
message.list.footer |
string | Optional footer text shown below the list. | |
message.list.buttonTitle |
string | yes | Label of the button that opens the list (max 20 characters). |
message.list.listItems |
object[] | yes | Up to 10 selectable rows. |
message.list.listItems.id |
string | yes | Stable identifier returned in the user's selection payload. |
message.list.listItems.name |
string | yes | Row title shown to the user (max 24 characters). |
message.list.listItems.description |
string | Optional secondary line shown under the title (max 72 characters). | |
message.template |
object | Template message content. Required when type is 'template'. | |
message.template.templateName |
string | The name of the template. | |
message.template.language |
string | The language of the template. Refer https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates/supported-languages/ for the codes | |
message.template.header |
object | ||
message.template.header.type |
string: text |
The type of the header. | |
message.template.header.text |
string | The text content of the header. | |
message.template.body |
object[] | ||
message.template.body.type |
string: text |
The type of the body item. | |
message.template.body.text |
string | The text content of the body item. | |
message.template.buttons |
object[] | ||
message.template.buttons.type |
string | The type of the button. | |
message.template.buttons.payload |
string | The payload of the button. | |
message.template.buttons.index |
integer | The index of the button. |
Example:
{
"to": "+919876012345",
"message": {
"type": "text",
"text": {
"message": "Hi, this is a test message"
}
}
}
Responses
200: Message sent successfully.400: Bad Request. The request was unacceptable, often due to missing a required parameter.401: Unauthorized. The request did not include a correct authorization token.500: Internal Server Error. Something went wrong on the server side.
Resolve WhatsApp media id to download URL (or stream file)
GET https://server.wylto.com/api/v1/wa/media/:mediaId
Uses the app's default WhatsApp connection to call the Meta Graph API for the given media id (e.g. message.image.id). By default returns JSON with a short-lived url and mime_type. With query action=download, resolves and streams the file in one request (WhatsApp token used server-side). Alternatively use POST /api/v1/wa/media/download with the returned url.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | yes | WhatsApp media object id from the Cloud API (e.g. message.image.id). |
action |
query | string: download |
If set to download, response body is the raw media bytes instead of JSON. Content-Type comes from Meta mime_type when present. | |
filename |
query | string | When action=download, optional filename for Content-Disposition attachment (sanitized to a base name). |
Responses
200: Without action=download: JSON with url and mime_type. With action=download: raw file bytes (Content-Type from media metadata, or application/octet-stream).
{
"url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...",
"mime_type": "image/jpeg"
}
401: Unauthorized. Missing or invalid API bearer token.500: Internal Server Error. Failed to resolve media (for example invalid media id or WhatsApp connection error).502: Bad gateway. Returned when action=download and the download from Meta failed, or the resolved URL host was not allowed.
Download WhatsApp media from temporary URL
POST https://server.wylto.com/api/v1/wa/media/download
Fetches binary content from a short-lived url returned by GET /api/v1/wa/media/:mediaId, using the app's default WhatsApp connection Bearer token when calling Meta. Response body is the raw file. Only https URLs on allowed Meta hosts (lookaside.fbsbx.com and *.fbsbx.com) are permitted.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
url |
string (uri) | yes | Temporary download URL from the WhatsApp Cloud API media response (field url). |
mime_type |
string | Optional MIME type for the response Content-Type (e.g. from the media metadata response). Defaults to application/octet-stream. | |
filename |
string | Optional filename for Content-Disposition attachment. Path segments are stripped to a base name. |
Example:
{
"url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...",
"mime_type": "image/jpeg",
"filename": "photo.jpg"
}
Responses
200: Raw file bytes. Content-Type is taken from mime_type in the request when provided, otherwise application/octet-stream.400: Bad request (missing url, invalid URL, or host not allowed).401: Unauthorized. Missing or invalid API bearer token.502: Bad gateway. Meta returned a non-success status or the download failed.
Send a message to a contact
POST https://server.wylto.com/api/v1/wa/send/:contactId
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
contactId |
path | string | yes | The ID of the contact to whom the message is being sent. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
message |
object | ||
message.type |
string: text, template, image, document, video, audio, buttons, list |
The type of the message. | |
message.text |
object | Text message content. Required when type is 'text'. | |
message.text.message |
string | yes | The text content of the message. |
message.image |
object | Image message content. Required when type is 'image'. | |
message.image.url |
string | yes | The URL of the image. |
message.image.mimeType |
string | yes | The MIME type of the image (e.g., image/jpeg). |
message.image.message |
string | Optional caption for the image. | |
message.document |
object | Document message content. Required when type is 'document'. | |
message.document.url |
string | yes | The URL of the document. |
message.document.filename |
string | The filename of the document. | |
message.document.mimeType |
string | yes | The MIME type of the document. |
message.document.message |
string | Optional caption for the document. | |
message.video |
object | Video message content. Required when type is 'video'. | |
message.video.url |
string | yes | The URL of the video. |
message.video.mimeType |
string | yes | The MIME type of the video. |
message.video.message |
string | Optional caption for the video. | |
message.audio |
object | Audio message content. Required when type is 'audio'. | |
message.audio.url |
string | yes | The URL of the audio file. |
message.audio.mimeType |
string | yes | The MIME type of the audio. |
message.buttons |
object | Quick reply buttons message content. Required when type is 'buttons'. WhatsApp allows up to 3 quick reply buttons. | |
message.buttons.imageURL |
string | Optional image URL shown as a header above the message. | |
message.buttons.heading |
string | Optional text header shown above the message (ignored when imageURL is set). | |
message.buttons.message |
string | yes | The body text shown above the buttons. |
message.buttons.footer |
string | Optional footer text shown below the buttons. | |
message.buttons.buttons |
object[] | yes | Up to 3 quick reply buttons. |
message.buttons.buttons.id |
string | yes | Stable identifier returned in the user's reply payload. |
message.buttons.buttons.name |
string | yes | Label shown on the button (max 20 characters). |
message.list |
object | List message content. Required when type is 'list'. WhatsApp allows up to 10 list rows. | |
message.list.imageURL |
string | Optional image URL shown as a header above the message. | |
message.list.heading |
string | Optional text header shown above the message (ignored when imageURL is set). | |
message.list.message |
string | yes | The body text shown above the list. |
message.list.footer |
string | Optional footer text shown below the list. | |
message.list.buttonTitle |
string | yes | Label of the button that opens the list (max 20 characters). |
message.list.listItems |
object[] | yes | Up to 10 selectable rows. |
message.list.listItems.id |
string | yes | Stable identifier returned in the user's selection payload. |
message.list.listItems.name |
string | yes | Row title shown to the user (max 24 characters). |
message.list.listItems.description |
string | Optional secondary line shown under the title (max 72 characters). | |
message.template |
object | ||
message.template.templateName |
string | The name of the template. | |
message.template.header |
object | ||
message.template.header.type |
string: text |
The type of the header. | |
message.template.header.text |
string | The text content of the header. | |
message.template.body |
object[] | ||
message.template.body.type |
string: text |
The type of the body item. | |
message.template.body.text |
string | The text content of the body item. | |
message.template.buttons |
object[] | ||
message.template.buttons.type |
string | The type of the button. | |
message.template.buttons.payload |
string | The payload of the button. | |
message.template.buttons.index |
integer | The index of the button. |
Example:
{
"message": {
"type": "template",
"template": {
"templateName": "example_template_name",
"header": {
"type": "text",
"text": "Your header text here"
},
"body": [
{
"type": "text",
"text": "Your body text here"
}
],
"buttons": [
{
"type": "your_button_type",
"payload": "your_button_payload",
"index": 0
}
]
}
}
}
Responses
200: Message sent successfully.400: Bad Request. The request was unacceptable, often due to missing a required parameter.401: Unauthorized. The request did not include a correct authorization token.500: Internal Server Error. Something went wrong on the server side.
List WhatsApp templates
GET https://server.wylto.com/api/v1/wa/templates
Returns all WhatsApp message templates for the app's default connection.
Responses
200: Templates retrieved successfully.401: Unauthorized.500: Internal Server Error.
Create a WhatsApp template
POST https://server.wylto.com/api/v1/wa/templates
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Template name (lowercase, underscores only). |
language |
string | yes | Language code (e.g., en, en_US). |
category |
string: MARKETING, UTILITY, AUTHENTICATION |
yes | Template category. |
components |
object[] | yes | Template components. |
components.type |
string: HEADER, BODY, FOOTER, BUTTONS |
||
components.format |
string: TEXT, IMAGE, VIDEO, DOCUMENT |
||
components.text |
string | ||
components.mediaUrl |
string | For media HEADERs (IMAGE/VIDEO/DOCUMENT): a public URL to the media. The server uploads it to WhatsApp and generates the media handle automatically. Alternatively, supply a pre-uploaded handle via example.header_handle. | |
components.buttons |
object[] | ||
components.buttons.type |
string: QUICK_REPLY, URL, PHONE_NUMBER |
||
components.buttons.text |
string | ||
components.buttons.url |
string | ||
components.buttons.phone_number |
string |
Example:
{
"name": "welcome_message",
"language": "en",
"category": "MARKETING",
"components": [
{
"type": "HEADER",
"format": "TEXT",
"text": "Welcome to {{1}}!"
},
{
"type": "BODY",
"text": "Hi {{1}}, thank you for joining us. We're excited to have you!"
},
{
"type": "FOOTER",
"text": "Reply STOP to unsubscribe"
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "Learn More"
}
]
}
]
}
Responses
200: Template created successfully.400: Bad Request. Invalid template format or missing required fields.401: Unauthorized.500: Internal Server Error.
Update a WhatsApp template
PUT https://server.wylto.com/api/v1/wa/templates/:templateId
Replaces the template's components. Name, language and category can't be changed. Meta reviews the template again after an edit, and limits how often an approved template can be edited.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
templateId |
path | string | yes | The WhatsApp template id (the id returned by Create or List templates). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
components |
object[] | yes | The full new list of components, same format as Create template. For media headers, pass the URL in mediaUrl. |
Example:
{
"components": [
{
"type": "BODY",
"text": "Your order #{{1}} is confirmed. It will reach you by {{2}}.",
"example": {
"body_text": [
[
"1001",
"Friday"
]
]
}
}
]
}
Responses
200: Template updated.
{
"success": true
}
400: Bad Request. Invalid components, or Meta rejected the edit.401: Unauthorized.500: Internal Server Error.
Delete a WhatsApp template
DELETE https://server.wylto.com/api/v1/wa/templates/:templateName
Deletes the template by name. This deletes every language of that template. A deleted name can't be reused for 30 days.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
templateName |
path | string | yes | The template name, e.g. order_confirmation. |
Responses
200: Template deleted.
{
"success": true
}
400: Bad Request. Template not found or Meta rejected the delete.401: Unauthorized.500: Internal Server Error.
Get template quality score
GET https://server.wylto.com/api/v1/wa/templates/:templateId/quality
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
templateId |
path | string | yes | The ID of the template. |
Responses
200: Template quality retrieved successfully.
{
"id": "123456789",
"name": "welcome_message",
"status": "APPROVED",
"qualityScore": "GREEN"
}
401: Unauthorized.404: Template not found.500: Internal Server Error.
Get phone number details
GET https://server.wylto.com/api/v1/wa/phone-number
Returns details about the WhatsApp phone number including quality rating and messaging limits.
Responses
200: Phone number details retrieved successfully.
{
"verified_name": "My Business",
"display_phone_number": "+1234567890",
"quality_rating": "GREEN",
"messaging_limit_tier": "TIER_10K",
"platform_type": "CLOUD_API",
"throughput": {
"level": "STANDARD"
},
"code_verification_status": "VERIFIED",
"last_onboarded_time": "2024-01-01T00:00:00Z",
"id": "180876681785984"
}
400: WhatsApp connection not found.401: Unauthorized.500: Internal Server Error.
Contact
Create, update and read contacts, including UTM and activities.
Get a contact by phone number
GET https://server.wylto.com/api/v1/contact
Fetches a single contact by phone number. The phone number is normalized before lookup: spaces and dashes are stripped, '+' is auto-prepended if missing, and the value must contain at least 10 digits.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
phoneNumber |
query | string | yes | Phone number to look up the contact by. '+' is auto-prepended if missing; minimum 10 digits. |
Responses
200: Contact fetched successfully.
{
"id": "contact_xyz789",
"externalId": "crm-1029",
"name": "jane doe",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"message": "Met at trade show",
"status": "status_abc123",
"group": "group_sales",
"source": "tradeshow",
"eventAt": 1740009600000,
"tags": [
"tag_vip",
"tag_newsletter"
],
"createdAt": 1739923200000,
"updatedAt": 1740013200000
}
400: Bad Request. The phoneNumber query parameter is missing or is not a valid phone number.401: Unauthorized. The request did not include a correct authorization token.404: Not Found. No contact matched the provided phone number.500: Internal Server Error. Something went wrong on the server side.
Create a new contact
POST https://server.wylto.com/api/v1/contact
Creates a contact. Pass 'utm' to record which ad or campaign the lead came from. Returns 400 if the contact already exists; use PUT /contact to update it.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
externalId |
string | The unique identifier of the contact in your system. | |
name |
string | The name of the contact. | |
phoneNumber |
string | The phone number of the contact in E.164 format (with leading '+'). For the update-by-phone endpoint, '+' is auto-prepended if missing and the value must contain at least 10 digits. | |
email |
string | The email address of the contact. | |
message |
string | Free-text note attached to the contact. | |
status |
string | Status ID. Takes precedence over 'statusName' when both are provided. | |
statusName |
string | Status name. Resolved to the status ID via a case-insensitive lookup against the app's configured statuses. Ignored when 'status' is provided. Returns 400 if the name does not exist. | |
group |
string | Group ID. Takes precedence over 'groupName' when both are provided. | |
groupName |
string | Group name. Resolved to the group ID via a case-insensitive lookup against the app's configured groups. Ignored when 'group' is provided. Returns 400 if the name does not exist. | |
source |
string | Acquisition source for the contact (e.g. 'web', 'whatsapp', 'partner'). | |
eventAt |
integer (int64) | Unix timestamp in milliseconds tied to the contact's status (used when the status represents a scheduled event). | |
tags |
string[] | List of tag IDs. Replaces the contact's tags. Takes precedence over 'tagNames' when both are provided. | |
tagNames |
string[] | List of tag names. Each is resolved to a tag ID via a case-insensitive lookup against the app's configured tags. Ignored when 'tags' is provided. Returns 400 if any name does not exist. | |
utm |
object | Where the lead came from (first touch). Saved when the contact is created. On update it is saved only if the contact has no UTM yet; it is never overwritten. | |
utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
utm.campaign |
string | utm_campaign. | |
utm.term |
string | utm_term. | |
utm.content |
string | utm_content. | |
utm.adGroup |
string | Ad group name or ID. | |
utm.ad |
string | Ad name or ID. | |
utm.keyword |
string | Search keyword. | |
utm.gclid |
string | Google Ads click ID. | |
utm.fbclid |
string | Facebook click ID. |
Example:
{
"name": "Jane Doe",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"source": "website",
"utm": {
"source": "google",
"medium": "cpc",
"campaign": "diwali_sale",
"term": "running shoes",
"content": "banner_a",
"gclid": "Cj0KCQjw..."
}
}
Responses
200: Contact created successfully.400: Bad Request. The request was unacceptable, often due to missing a required parameter.401: Unauthorized. The request did not include a correct authorization token.500: Internal Server Error. Something went wrong on the server side.
Update a contact by phone number
PUT https://server.wylto.com/api/v1/contact
Looks up a contact by phone number and applies the same partial update semantics as PUT /contact/{contactId}. The phone number is normalized: '+' is auto-prepended if missing and the value must contain at least 10 digits. The lookup value can be passed via the 'phoneNumber' query parameter or the 'phoneNumber' field in the request body. Empty fields are ignored — only fields you provide are updated. Pass createIfContactNotPresent=true to create the contact from the body when no contact matches (same as POST /contact).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
phoneNumber |
query | string | Phone number to look up the contact by. Falls back to the body's 'phoneNumber' field when omitted. '+' is auto-prepended if missing; minimum 10 digits. | |
createIfContactNotPresent |
query | boolean | When true and no contact matches the phone number, a new contact is created from the request body instead of returning 404. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
externalId |
string | The unique identifier of the contact in your system. | |
name |
string | The name of the contact. | |
phoneNumber |
string | The phone number of the contact in E.164 format (with leading '+'). For the update-by-phone endpoint, '+' is auto-prepended if missing and the value must contain at least 10 digits. | |
email |
string | The email address of the contact. | |
message |
string | Free-text note attached to the contact. | |
status |
string | Status ID. Takes precedence over 'statusName' when both are provided. | |
statusName |
string | Status name. Resolved to the status ID via a case-insensitive lookup against the app's configured statuses. Ignored when 'status' is provided. Returns 400 if the name does not exist. | |
group |
string | Group ID. Takes precedence over 'groupName' when both are provided. | |
groupName |
string | Group name. Resolved to the group ID via a case-insensitive lookup against the app's configured groups. Ignored when 'group' is provided. Returns 400 if the name does not exist. | |
source |
string | Acquisition source for the contact (e.g. 'web', 'whatsapp', 'partner'). | |
eventAt |
integer (int64) | Unix timestamp in milliseconds tied to the contact's status (used when the status represents a scheduled event). | |
tags |
string[] | List of tag IDs. Replaces the contact's tags. Takes precedence over 'tagNames' when both are provided. | |
tagNames |
string[] | List of tag names. Each is resolved to a tag ID via a case-insensitive lookup against the app's configured tags. Ignored when 'tags' is provided. Returns 400 if any name does not exist. | |
utm |
object | Where the lead came from (first touch). Saved when the contact is created. On update it is saved only if the contact has no UTM yet; it is never overwritten. | |
utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
utm.campaign |
string | utm_campaign. | |
utm.term |
string | utm_term. | |
utm.content |
string | utm_content. | |
utm.adGroup |
string | Ad group name or ID. | |
utm.ad |
string | Ad name or ID. | |
utm.keyword |
string | Search keyword. | |
utm.gclid |
string | Google Ads click ID. | |
utm.fbclid |
string | Facebook click ID. |
Example:
{
"statusName": "Qualified",
"tagNames": [
"VIP",
"Newsletter"
]
}
Responses
200: Contact updated successfully, or created when createIfContactNotPresent=true and no contact matched.
{
"id": "contact_xyz789",
"externalId": "crm-1029",
"name": "jane doe",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"message": "Met at trade show",
"status": "status_abc123",
"group": "group_sales",
"source": "tradeshow",
"eventAt": 1740009600000,
"tags": [
"tag_vip",
"tag_newsletter"
],
"createdAt": 1739923200000,
"updatedAt": 1740013200000
}
400: Bad Request. Invalid phone number, or a provided statusName / groupName / tagNames value did not match any configured entry.401: Unauthorized. The request did not include a correct authorization token.404: Not Found. No contact matched the provided phone number.500: Internal Server Error. Something went wrong on the server side.
Get a contact by ID
GET https://server.wylto.com/api/v1/contact/:contactId
Fetches a single contact by its Wylto contact ID.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
contactId |
path | string | yes | The ID of the contact to fetch. |
Responses
200: Contact fetched successfully.
{
"id": "contact_xyz789",
"externalId": "crm-1029",
"name": "jane doe",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"message": "Met at trade show",
"status": "status_abc123",
"group": "group_sales",
"source": "tradeshow",
"eventAt": 1740009600000,
"tags": [
"tag_vip",
"tag_newsletter"
],
"createdAt": 1739923200000,
"updatedAt": 1740013200000
}
400: Bad Request. The contactId path parameter is missing or empty.401: Unauthorized. The request did not include a correct authorization token.404: Not Found. No contact matched the provided ID.500: Internal Server Error. Something went wrong on the server side.
Update a contact
PUT https://server.wylto.com/api/v1/contact/:contactId
Partial update for an existing contact identified by ID. Only the fields you include are written; empty strings and empty arrays are ignored. Use 'statusName' / 'groupName' / 'tagNames' to look up IDs by name (case-insensitive); the ID-form fields ('status' / 'group' / 'tags') take precedence when both are provided.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
contactId |
path | string | yes | The ID of the contact to update. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
externalId |
string | The unique identifier of the contact in your system. | |
name |
string | The name of the contact. | |
phoneNumber |
string | The phone number of the contact in E.164 format (with leading '+'). For the update-by-phone endpoint, '+' is auto-prepended if missing and the value must contain at least 10 digits. | |
email |
string | The email address of the contact. | |
message |
string | Free-text note attached to the contact. | |
status |
string | Status ID. Takes precedence over 'statusName' when both are provided. | |
statusName |
string | Status name. Resolved to the status ID via a case-insensitive lookup against the app's configured statuses. Ignored when 'status' is provided. Returns 400 if the name does not exist. | |
group |
string | Group ID. Takes precedence over 'groupName' when both are provided. | |
groupName |
string | Group name. Resolved to the group ID via a case-insensitive lookup against the app's configured groups. Ignored when 'group' is provided. Returns 400 if the name does not exist. | |
source |
string | Acquisition source for the contact (e.g. 'web', 'whatsapp', 'partner'). | |
eventAt |
integer (int64) | Unix timestamp in milliseconds tied to the contact's status (used when the status represents a scheduled event). | |
tags |
string[] | List of tag IDs. Replaces the contact's tags. Takes precedence over 'tagNames' when both are provided. | |
tagNames |
string[] | List of tag names. Each is resolved to a tag ID via a case-insensitive lookup against the app's configured tags. Ignored when 'tags' is provided. Returns 400 if any name does not exist. | |
utm |
object | Where the lead came from (first touch). Saved when the contact is created. On update it is saved only if the contact has no UTM yet; it is never overwritten. | |
utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
utm.campaign |
string | utm_campaign. | |
utm.term |
string | utm_term. | |
utm.content |
string | utm_content. | |
utm.adGroup |
string | Ad group name or ID. | |
utm.ad |
string | Ad name or ID. | |
utm.keyword |
string | Search keyword. | |
utm.gclid |
string | Google Ads click ID. | |
utm.fbclid |
string | Facebook click ID. |
Example:
{
"statusName": "Qualified",
"tagNames": [
"VIP",
"Newsletter"
]
}
Responses
200: Contact updated successfully.
{
"id": "contact_xyz789",
"externalId": "crm-1029",
"name": "jane doe",
"phoneNumber": "+14155551234",
"email": "jane@example.com",
"message": "Met at trade show",
"status": "status_abc123",
"group": "group_sales",
"source": "tradeshow",
"eventAt": 1740009600000,
"tags": [
"tag_vip",
"tag_newsletter"
],
"createdAt": 1739923200000,
"updatedAt": 1740013200000
}
400: Bad Request. A provided statusName / groupName / tagNames value did not match any configured entry, or another field failed validation.401: Unauthorized. The request did not include a correct authorization token.500: Internal Server Error. Something went wrong on the server side.
Delete a contact
DELETE https://server.wylto.com/api/v1/contact/:contactId
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
contactId |
path | string | yes | The ID of the contact to delete. |
Responses
200: Contact deleted successfully.400: Bad Request. The request was unacceptable, often due to missing a required parameter.401: Unauthorized. The request did not include a correct authorization token.500: Internal Server Error. Something went wrong on the server side.
Get activities for a contact
GET https://server.wylto.com/api/v1/contact/:contactId/activities
Returns a contact's activity history (status changes, notes, call logs, chats, broadcasts and more), newest first.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
contactId |
path | string | yes | The ID of the contact. |
limit |
query | integer | Maximum number of activities to return. Default 50, max 200. |
Responses
200: Activities fetched successfully.
{
"activities": [
{
"id": "act_123",
"contactId": "contact_xyz789",
"type": "STATUS_UPDATE",
"from": "status_new",
"to": "status_won",
"author": "member_abc",
"createdAt": 1740013200000
},
{
"id": "act_122",
"contactId": "contact_xyz789",
"type": "NOTE",
"note": "Asked for a demo next week",
"author": "member_abc",
"createdAt": 1740009600000
}
]
}
400: Bad Request. contactId is missing or limit is out of range.401: Unauthorized. The request did not include a correct authorization token.404: Not Found. No contact matched the provided ID.500: Internal Server Error. Something went wrong on the server side.
Campaigns
Create WhatsApp broadcast campaigns and read their reports.
Create and send a campaign
POST https://server.wylto.com/api/v1/campaigns
Creates a campaign and sends messages to the provided contacts. Only CSV type campaigns are supported via API.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
campaignName |
string | yes | The name of the campaign. |
contacts |
object[] | yes | List of contacts to send the campaign to. |
contacts.name |
string | The name of the contact. | |
contacts.phoneNumber |
string | The phone number of the contact. | |
message |
object | yes | The message to send. |
message.type |
string: template |
The type of message. | |
message.template |
object | ||
message.template.templateName |
string | The name of the template. | |
message.template.language |
string | The language code of the template. | |
message.template.header |
object | Header value, needed when the template has a variable or media header. For an image header use type "image" with a public image link. | |
message.template.header.type |
string: text, image |
||
message.template.header.text |
string | Header text. Used when type is "text". | |
message.template.header.image |
object | Used when type is "image". | |
message.template.body |
object[] | ||
message.template.body.type |
string | ||
message.template.body.text |
string |
Example:
{
"campaignName": "January Promo",
"contacts": [
{
"name": "John Doe",
"phoneNumber": "+1234567890"
},
{
"name": "Jane Smith",
"phoneNumber": "+0987654321"
}
],
"message": {
"type": "template",
"template": {
"templateName": "promo_offer",
"language": "en",
"body": [
{
"type": "text",
"text": "20%"
}
]
}
}
}
Responses
200: Campaign created and processing started.400: Bad Request. Invalid campaign data.401: Unauthorized.500: Internal Server Error.
Get campaign report
GET https://server.wylto.com/api/v1/campaigns/:campaignId/report
Returns campaign details and all message delivery statuses.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaignId |
path | string | yes | The ID of the campaign. |
Responses
200: Campaign report retrieved successfully.
{
"campaign": {
"id": "abc123",
"name": "January Promo",
"type": "csv",
"status": "completed",
"channel": "whatsapp",
"count": 100,
"sent": 95,
"delivered": 90,
"read": 50,
"failed": 5,
"createdAt": 1704067200000
},
"messages": [
{
"id": "msg123",
"name": "John Doe",
"phoneNumber": "+1234567890",
"state": "delivered",
"buttonClicks": [],
"createdAt": 1704067200000
},
{
"id": "msg124",
"name": "Jane Smith",
"phoneNumber": "+0987654321",
"state": "failed",
"error": "131026- Message undeliverable",
"buttonClicks": [],
"createdAt": 1704067200000
}
]
}
401: Unauthorized.404: Campaign not found.500: Internal Server Error.
Webhooks
Messages Wylto posts to your server when a WhatsApp message arrives.
on new Message
Webhook: Wylto sends POST to your URL.
Payload
When you subscribe for a message webhook,Wylto sends this payload when a new message is received.
| Field | Type | Required | Description |
|---|---|---|---|
contactId |
string | Unique identifier of the contact in wylto. | |
clientContactId |
string | Unique identifier of the contact in your system. | |
conversationId |
string | Unique identifier of the conversation in wylto. | |
clientMessageId |
string | Unique identifier of the message in your system. You need to provide this as part of the request when sending a message. | |
message |
object | ||
message.context |
object | ||
message.context.from |
string | ||
message.context.id |
string | ||
message.from |
string | ||
message.id |
string | ||
message.timestamp |
string (date-time) | ||
message.type |
enum: text, button, document, location, image, video, audio, order, interactive |
||
message.text |
object | ||
message.text.body |
string | ||
message.button |
object | ||
message.button.payload |
string | ||
message.button.text |
string | ||
message.document |
object | ||
message.document.caption |
string | ||
message.document.fileName |
string | ||
message.document.mimeType |
string | ||
message.document.sha256 |
string | ||
message.document.id |
string | ||
message.location |
object | ||
message.location.latitude |
number (double) | ||
message.location.longitude |
number (double) | ||
message.location.name |
string | ||
message.location.address |
string | ||
message.image |
object | ||
message.image.caption |
string | ||
message.image.mimeType |
string | ||
message.image.sha256 |
string | ||
message.image.id |
string | ||
message.video |
object | ||
message.video.caption |
string | ||
message.video.mimeType |
string | ||
message.video.sha256 |
string | ||
message.video.id |
string | ||
message.audio |
object | ||
message.audio.caption |
string | ||
message.audio.mimeType |
string | ||
message.audio.sha256 |
string | ||
message.audio.id |
string | ||
message.audio.voice |
boolean | ||
message.order |
object | ||
message.order.catalogId |
string | ||
message.order.productItems |
object[] | ||
message.order.productItems.productRetailerId |
string | ||
message.order.productItems.itemPrice |
integer | ||
message.order.productItems.currency |
string | ||
message.order.productItems.quantity |
integer | ||
message.errors |
object[] | ||
message.errors.code |
string | ||
message.errors.message |
string | ||
message.interactive |
object | ||
message.interactive.type |
string | ||
message.interactive.listReply |
object | ||
message.interactive.listReply.id |
string | ||
message.interactive.listReply.title |
string | ||
message.interactive.listReply.description |
string | ||
message.interactive.buttonReply |
object | ||
message.interactive.buttonReply.id |
string | ||
message.interactive.buttonReply.title |
string | ||
message.interactive.nfmReply |
object | ||
message.interactive.nfmReply.name |
string | ||
message.interactive.nfmReply.body |
string | ||
message.interactive.nfmReply.responseJSON |
string |
Responses
200: Your server response after receiving the event
Booking webhooks
Messages Wylto posts to your server when a booking is created, rescheduled, cancelled or completed.
Booking created
Webhook: Wylto sends POST to your URL.
Sent when someone books a slot: from a booking page, an embed, the chatbot or the AI agent.
Set up under Appointments → Settings → Webhooks in Wylto: add an HTTPS URL, optional custom headers and an optional signing secret, pick the events, and choose all booking pages or some. Wylto sends a JSON POST. Reply with any 2xx status within 10 seconds; failed deliveries are not retried and show in the deliveries log.
Headers
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Wylto-Event |
header | string | yes | Same as triggerEvent. |
X-Wylto-Delivery |
header | string | yes | Unique id of this delivery. Use it to ignore duplicates. |
X-Wylto-Signature-256 |
header | string | Only when the webhook has a secret: sha256=<hex>, the HMAC-SHA256 of the raw request body with your secret. Compute it yourself and compare before trusting the request. |
|
User-Agent |
header | string |
Payload
| Field | Type | Required | Description |
|---|---|---|---|
triggerEvent |
string: BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, BOOKING_COMPLETED, PING |
yes | |
createdAt |
string (date-time) | yes | When the webhook was sent, UTC. |
payload |
object | yes | |
payload.bookingId |
string | yes | Booking id in Wylto. |
payload.eventTypeId |
string | Booking page id. | |
payload.eventTypeName |
string | Booking page name, e.g. Product demo. | |
payload.status |
string: scheduled, rescheduled, cancelled, completed |
yes | |
payload.startTime |
string (date-time) | yes | Start, UTC (RFC 3339). |
payload.endTime |
string (date-time) | yes | End, UTC (RFC 3339). |
payload.previousStartTime |
string (date-time) | Only on BOOKING_RESCHEDULED: the start before the change. | |
payload.duration |
integer | yes | Minutes. |
payload.timeZone |
string | Booker's timezone, e.g. Asia/Kolkata. | |
payload.location |
string: video, call, inPerson |
video (Google Meet or a video link), call (phone call) or inPerson. | |
payload.meetingLink |
string (uri) | ||
payload.contact |
object | The person who booked. | |
payload.contact.id |
string | Wylto id. | |
payload.contact.name |
string | ||
payload.contact.email |
string (email) | ||
payload.contact.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.assignedTo |
object | Team member who takes the meeting. | |
payload.assignedTo.id |
string | Wylto id. | |
payload.assignedTo.name |
string | ||
payload.assignedTo.email |
string (email) | ||
payload.assignedTo.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.responses |
object | Booking form answers, keyed by field id. | |
payload.minutesOfMeeting |
string | Only on BOOKING_COMPLETED, when notes were added. | |
payload.source |
string | How it was booked, e.g. event-booking (booking page). | |
payload.utm |
object | UTM tags of the page the booking was made from (the embedding page for booking.js embeds, else the booking link). Only when it had any. | |
payload.utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
payload.utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
payload.utm.campaign |
string | utm_campaign. | |
payload.utm.term |
string | utm_term. | |
payload.utm.content |
string | utm_content. | |
payload.utm.adGroup |
string | Ad group name or ID. | |
payload.utm.ad |
string | Ad name or ID. | |
payload.utm.keyword |
string | Search keyword. | |
payload.utm.gclid |
string | Google Ads click ID. | |
payload.utm.fbclid |
string | Facebook click ID. | |
payload.rescheduleUrl |
string (uri) | Link the booker uses to reschedule. | |
payload.cancelUrl |
string (uri) | Link the booker uses to cancel. |
Example:
{
"triggerEvent": "BOOKING_CREATED",
"createdAt": "2026-11-20T10:02:11Z",
"payload": {
"bookingId": "HRXLZixYtwudTxclPcbE",
"eventTypeId": "yJ08Ls8T4CuOk2ZNhFFc",
"eventTypeName": "Product demo",
"status": "scheduled",
"startTime": "2026-11-24T05:30:00Z",
"endTime": "2026-11-24T06:00:00Z",
"duration": 30,
"timeZone": "Asia/Kolkata",
"location": "video",
"meetingLink": "https://meet.google.com/abc-defg-hij",
"contact": {
"id": "k2VdN8pQ1xYz",
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000"
},
"assignedTo": {
"id": "u7Ty3Lm9",
"name": "Priya",
"email": "priya@yourcompany.com"
},
"responses": {
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000",
"notes": "Interested in the Growth plan"
},
"source": "event-booking",
"utm": {
"source": "facebook",
"medium": "paid_social",
"campaign": "diwali_sale"
},
"rescheduleUrl": "https://app.wylto.com/booking-manage?…",
"cancelUrl": "https://app.wylto.com/booking-manage?…"
}
}
Responses
2XX: Received. Any 2xx counts as delivered.
Booking rescheduled
Webhook: Wylto sends POST to your URL.
Sent when the booker or your team moves a booking. previousStartTime has the old start.
Set up under Appointments → Settings → Webhooks in Wylto: add an HTTPS URL, optional custom headers and an optional signing secret, pick the events, and choose all booking pages or some. Wylto sends a JSON POST. Reply with any 2xx status within 10 seconds; failed deliveries are not retried and show in the deliveries log.
Headers
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Wylto-Event |
header | string | yes | Same as triggerEvent. |
X-Wylto-Delivery |
header | string | yes | Unique id of this delivery. Use it to ignore duplicates. |
X-Wylto-Signature-256 |
header | string | Only when the webhook has a secret: sha256=<hex>, the HMAC-SHA256 of the raw request body with your secret. Compute it yourself and compare before trusting the request. |
|
User-Agent |
header | string |
Payload
| Field | Type | Required | Description |
|---|---|---|---|
triggerEvent |
string: BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, BOOKING_COMPLETED, PING |
yes | |
createdAt |
string (date-time) | yes | When the webhook was sent, UTC. |
payload |
object | yes | |
payload.bookingId |
string | yes | Booking id in Wylto. |
payload.eventTypeId |
string | Booking page id. | |
payload.eventTypeName |
string | Booking page name, e.g. Product demo. | |
payload.status |
string: scheduled, rescheduled, cancelled, completed |
yes | |
payload.startTime |
string (date-time) | yes | Start, UTC (RFC 3339). |
payload.endTime |
string (date-time) | yes | End, UTC (RFC 3339). |
payload.previousStartTime |
string (date-time) | Only on BOOKING_RESCHEDULED: the start before the change. | |
payload.duration |
integer | yes | Minutes. |
payload.timeZone |
string | Booker's timezone, e.g. Asia/Kolkata. | |
payload.location |
string: video, call, inPerson |
video (Google Meet or a video link), call (phone call) or inPerson. | |
payload.meetingLink |
string (uri) | ||
payload.contact |
object | The person who booked. | |
payload.contact.id |
string | Wylto id. | |
payload.contact.name |
string | ||
payload.contact.email |
string (email) | ||
payload.contact.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.assignedTo |
object | Team member who takes the meeting. | |
payload.assignedTo.id |
string | Wylto id. | |
payload.assignedTo.name |
string | ||
payload.assignedTo.email |
string (email) | ||
payload.assignedTo.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.responses |
object | Booking form answers, keyed by field id. | |
payload.minutesOfMeeting |
string | Only on BOOKING_COMPLETED, when notes were added. | |
payload.source |
string | How it was booked, e.g. event-booking (booking page). | |
payload.utm |
object | UTM tags of the page the booking was made from (the embedding page for booking.js embeds, else the booking link). Only when it had any. | |
payload.utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
payload.utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
payload.utm.campaign |
string | utm_campaign. | |
payload.utm.term |
string | utm_term. | |
payload.utm.content |
string | utm_content. | |
payload.utm.adGroup |
string | Ad group name or ID. | |
payload.utm.ad |
string | Ad name or ID. | |
payload.utm.keyword |
string | Search keyword. | |
payload.utm.gclid |
string | Google Ads click ID. | |
payload.utm.fbclid |
string | Facebook click ID. | |
payload.rescheduleUrl |
string (uri) | Link the booker uses to reschedule. | |
payload.cancelUrl |
string (uri) | Link the booker uses to cancel. |
Example:
{
"triggerEvent": "BOOKING_RESCHEDULED",
"createdAt": "2026-11-20T10:02:11Z",
"payload": {
"bookingId": "HRXLZixYtwudTxclPcbE",
"eventTypeId": "yJ08Ls8T4CuOk2ZNhFFc",
"eventTypeName": "Product demo",
"status": "rescheduled",
"startTime": "2026-11-25T09:00:00Z",
"endTime": "2026-11-25T09:30:00Z",
"duration": 30,
"timeZone": "Asia/Kolkata",
"location": "video",
"meetingLink": "https://meet.google.com/abc-defg-hij",
"contact": {
"id": "k2VdN8pQ1xYz",
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000"
},
"assignedTo": {
"id": "u7Ty3Lm9",
"name": "Priya",
"email": "priya@yourcompany.com"
},
"responses": {
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000",
"notes": "Interested in the Growth plan"
},
"source": "event-booking",
"utm": {
"source": "facebook",
"medium": "paid_social",
"campaign": "diwali_sale"
},
"rescheduleUrl": "https://app.wylto.com/booking-manage?…",
"cancelUrl": "https://app.wylto.com/booking-manage?…",
"previousStartTime": "2026-11-24T05:30:00Z"
}
}
Responses
2XX: Received. Any 2xx counts as delivered.
Booking cancelled
Webhook: Wylto sends POST to your URL.
Sent when the booker or your team cancels a booking.
Set up under Appointments → Settings → Webhooks in Wylto: add an HTTPS URL, optional custom headers and an optional signing secret, pick the events, and choose all booking pages or some. Wylto sends a JSON POST. Reply with any 2xx status within 10 seconds; failed deliveries are not retried and show in the deliveries log.
Headers
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Wylto-Event |
header | string | yes | Same as triggerEvent. |
X-Wylto-Delivery |
header | string | yes | Unique id of this delivery. Use it to ignore duplicates. |
X-Wylto-Signature-256 |
header | string | Only when the webhook has a secret: sha256=<hex>, the HMAC-SHA256 of the raw request body with your secret. Compute it yourself and compare before trusting the request. |
|
User-Agent |
header | string |
Payload
| Field | Type | Required | Description |
|---|---|---|---|
triggerEvent |
string: BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, BOOKING_COMPLETED, PING |
yes | |
createdAt |
string (date-time) | yes | When the webhook was sent, UTC. |
payload |
object | yes | |
payload.bookingId |
string | yes | Booking id in Wylto. |
payload.eventTypeId |
string | Booking page id. | |
payload.eventTypeName |
string | Booking page name, e.g. Product demo. | |
payload.status |
string: scheduled, rescheduled, cancelled, completed |
yes | |
payload.startTime |
string (date-time) | yes | Start, UTC (RFC 3339). |
payload.endTime |
string (date-time) | yes | End, UTC (RFC 3339). |
payload.previousStartTime |
string (date-time) | Only on BOOKING_RESCHEDULED: the start before the change. | |
payload.duration |
integer | yes | Minutes. |
payload.timeZone |
string | Booker's timezone, e.g. Asia/Kolkata. | |
payload.location |
string: video, call, inPerson |
video (Google Meet or a video link), call (phone call) or inPerson. | |
payload.meetingLink |
string (uri) | ||
payload.contact |
object | The person who booked. | |
payload.contact.id |
string | Wylto id. | |
payload.contact.name |
string | ||
payload.contact.email |
string (email) | ||
payload.contact.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.assignedTo |
object | Team member who takes the meeting. | |
payload.assignedTo.id |
string | Wylto id. | |
payload.assignedTo.name |
string | ||
payload.assignedTo.email |
string (email) | ||
payload.assignedTo.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.responses |
object | Booking form answers, keyed by field id. | |
payload.minutesOfMeeting |
string | Only on BOOKING_COMPLETED, when notes were added. | |
payload.source |
string | How it was booked, e.g. event-booking (booking page). | |
payload.utm |
object | UTM tags of the page the booking was made from (the embedding page for booking.js embeds, else the booking link). Only when it had any. | |
payload.utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
payload.utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
payload.utm.campaign |
string | utm_campaign. | |
payload.utm.term |
string | utm_term. | |
payload.utm.content |
string | utm_content. | |
payload.utm.adGroup |
string | Ad group name or ID. | |
payload.utm.ad |
string | Ad name or ID. | |
payload.utm.keyword |
string | Search keyword. | |
payload.utm.gclid |
string | Google Ads click ID. | |
payload.utm.fbclid |
string | Facebook click ID. | |
payload.rescheduleUrl |
string (uri) | Link the booker uses to reschedule. | |
payload.cancelUrl |
string (uri) | Link the booker uses to cancel. |
Example:
{
"triggerEvent": "BOOKING_CANCELLED",
"createdAt": "2026-11-20T10:02:11Z",
"payload": {
"bookingId": "HRXLZixYtwudTxclPcbE",
"eventTypeId": "yJ08Ls8T4CuOk2ZNhFFc",
"eventTypeName": "Product demo",
"status": "cancelled",
"startTime": "2026-11-24T05:30:00Z",
"endTime": "2026-11-24T06:00:00Z",
"duration": 30,
"timeZone": "Asia/Kolkata",
"location": "video",
"meetingLink": "https://meet.google.com/abc-defg-hij",
"contact": {
"id": "k2VdN8pQ1xYz",
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000"
},
"assignedTo": {
"id": "u7Ty3Lm9",
"name": "Priya",
"email": "priya@yourcompany.com"
},
"responses": {
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000",
"notes": "Interested in the Growth plan"
},
"source": "event-booking",
"utm": {
"source": "facebook",
"medium": "paid_social",
"campaign": "diwali_sale"
},
"rescheduleUrl": "https://app.wylto.com/booking-manage?…",
"cancelUrl": "https://app.wylto.com/booking-manage?…"
}
}
Responses
2XX: Received. Any 2xx counts as delivered.
Booking completed
Webhook: Wylto sends POST to your URL.
Sent when your team marks a booking as completed. minutesOfMeeting has the notes, if any.
Set up under Appointments → Settings → Webhooks in Wylto: add an HTTPS URL, optional custom headers and an optional signing secret, pick the events, and choose all booking pages or some. Wylto sends a JSON POST. Reply with any 2xx status within 10 seconds; failed deliveries are not retried and show in the deliveries log.
Headers
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Wylto-Event |
header | string | yes | Same as triggerEvent. |
X-Wylto-Delivery |
header | string | yes | Unique id of this delivery. Use it to ignore duplicates. |
X-Wylto-Signature-256 |
header | string | Only when the webhook has a secret: sha256=<hex>, the HMAC-SHA256 of the raw request body with your secret. Compute it yourself and compare before trusting the request. |
|
User-Agent |
header | string |
Payload
| Field | Type | Required | Description |
|---|---|---|---|
triggerEvent |
string: BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, BOOKING_COMPLETED, PING |
yes | |
createdAt |
string (date-time) | yes | When the webhook was sent, UTC. |
payload |
object | yes | |
payload.bookingId |
string | yes | Booking id in Wylto. |
payload.eventTypeId |
string | Booking page id. | |
payload.eventTypeName |
string | Booking page name, e.g. Product demo. | |
payload.status |
string: scheduled, rescheduled, cancelled, completed |
yes | |
payload.startTime |
string (date-time) | yes | Start, UTC (RFC 3339). |
payload.endTime |
string (date-time) | yes | End, UTC (RFC 3339). |
payload.previousStartTime |
string (date-time) | Only on BOOKING_RESCHEDULED: the start before the change. | |
payload.duration |
integer | yes | Minutes. |
payload.timeZone |
string | Booker's timezone, e.g. Asia/Kolkata. | |
payload.location |
string: video, call, inPerson |
video (Google Meet or a video link), call (phone call) or inPerson. | |
payload.meetingLink |
string (uri) | ||
payload.contact |
object | The person who booked. | |
payload.contact.id |
string | Wylto id. | |
payload.contact.name |
string | ||
payload.contact.email |
string (email) | ||
payload.contact.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.assignedTo |
object | Team member who takes the meeting. | |
payload.assignedTo.id |
string | Wylto id. | |
payload.assignedTo.name |
string | ||
payload.assignedTo.email |
string (email) | ||
payload.assignedTo.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.responses |
object | Booking form answers, keyed by field id. | |
payload.minutesOfMeeting |
string | Only on BOOKING_COMPLETED, when notes were added. | |
payload.source |
string | How it was booked, e.g. event-booking (booking page). | |
payload.utm |
object | UTM tags of the page the booking was made from (the embedding page for booking.js embeds, else the booking link). Only when it had any. | |
payload.utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
payload.utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
payload.utm.campaign |
string | utm_campaign. | |
payload.utm.term |
string | utm_term. | |
payload.utm.content |
string | utm_content. | |
payload.utm.adGroup |
string | Ad group name or ID. | |
payload.utm.ad |
string | Ad name or ID. | |
payload.utm.keyword |
string | Search keyword. | |
payload.utm.gclid |
string | Google Ads click ID. | |
payload.utm.fbclid |
string | Facebook click ID. | |
payload.rescheduleUrl |
string (uri) | Link the booker uses to reschedule. | |
payload.cancelUrl |
string (uri) | Link the booker uses to cancel. |
Example:
{
"triggerEvent": "BOOKING_COMPLETED",
"createdAt": "2026-11-20T10:02:11Z",
"payload": {
"bookingId": "HRXLZixYtwudTxclPcbE",
"eventTypeId": "yJ08Ls8T4CuOk2ZNhFFc",
"eventTypeName": "Product demo",
"status": "completed",
"startTime": "2026-11-24T05:30:00Z",
"endTime": "2026-11-24T06:00:00Z",
"duration": 30,
"timeZone": "Asia/Kolkata",
"location": "video",
"meetingLink": "https://meet.google.com/abc-defg-hij",
"contact": {
"id": "k2VdN8pQ1xYz",
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000"
},
"assignedTo": {
"id": "u7Ty3Lm9",
"name": "Priya",
"email": "priya@yourcompany.com"
},
"responses": {
"name": "Asha",
"email": "asha@example.com",
"phone": "919800000000",
"notes": "Interested in the Growth plan"
},
"source": "event-booking",
"utm": {
"source": "facebook",
"medium": "paid_social",
"campaign": "diwali_sale"
},
"rescheduleUrl": "https://app.wylto.com/booking-manage?…",
"cancelUrl": "https://app.wylto.com/booking-manage?…",
"minutesOfMeeting": "Wants the Growth plan. Send pricing by Friday."
}
}
Responses
2XX: Received. Any 2xx counts as delivered.
Test ping
Webhook: Wylto sends POST to your URL.
Sent when you press Send test on a webhook. Sample data only.
Set up under Appointments → Settings → Webhooks in Wylto: add an HTTPS URL, optional custom headers and an optional signing secret, pick the events, and choose all booking pages or some. Wylto sends a JSON POST. Reply with any 2xx status within 10 seconds; failed deliveries are not retried and show in the deliveries log.
Headers
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Wylto-Event |
header | string | yes | Same as triggerEvent. |
X-Wylto-Delivery |
header | string | yes | Unique id of this delivery. Use it to ignore duplicates. |
X-Wylto-Signature-256 |
header | string | Only when the webhook has a secret: sha256=<hex>, the HMAC-SHA256 of the raw request body with your secret. Compute it yourself and compare before trusting the request. |
|
User-Agent |
header | string |
Payload
| Field | Type | Required | Description |
|---|---|---|---|
triggerEvent |
string: BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED, BOOKING_COMPLETED, PING |
yes | |
createdAt |
string (date-time) | yes | When the webhook was sent, UTC. |
payload |
object | yes | |
payload.bookingId |
string | yes | Booking id in Wylto. |
payload.eventTypeId |
string | Booking page id. | |
payload.eventTypeName |
string | Booking page name, e.g. Product demo. | |
payload.status |
string: scheduled, rescheduled, cancelled, completed |
yes | |
payload.startTime |
string (date-time) | yes | Start, UTC (RFC 3339). |
payload.endTime |
string (date-time) | yes | End, UTC (RFC 3339). |
payload.previousStartTime |
string (date-time) | Only on BOOKING_RESCHEDULED: the start before the change. | |
payload.duration |
integer | yes | Minutes. |
payload.timeZone |
string | Booker's timezone, e.g. Asia/Kolkata. | |
payload.location |
string: video, call, inPerson |
video (Google Meet or a video link), call (phone call) or inPerson. | |
payload.meetingLink |
string (uri) | ||
payload.contact |
object | The person who booked. | |
payload.contact.id |
string | Wylto id. | |
payload.contact.name |
string | ||
payload.contact.email |
string (email) | ||
payload.contact.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.assignedTo |
object | Team member who takes the meeting. | |
payload.assignedTo.id |
string | Wylto id. | |
payload.assignedTo.name |
string | ||
payload.assignedTo.email |
string (email) | ||
payload.assignedTo.phone |
string | With country code, digits only, e.g. 919800000000. | |
payload.responses |
object | Booking form answers, keyed by field id. | |
payload.minutesOfMeeting |
string | Only on BOOKING_COMPLETED, when notes were added. | |
payload.source |
string | How it was booked, e.g. event-booking (booking page). | |
payload.utm |
object | UTM tags of the page the booking was made from (the embedding page for booking.js embeds, else the booking link). Only when it had any. | |
payload.utm.source |
string | utm_source, e.g. 'google', 'facebook'. | |
payload.utm.medium |
string | utm_medium, e.g. 'cpc', 'email'. | |
payload.utm.campaign |
string | utm_campaign. | |
payload.utm.term |
string | utm_term. | |
payload.utm.content |
string | utm_content. | |
payload.utm.adGroup |
string | Ad group name or ID. | |
payload.utm.ad |
string | Ad name or ID. | |
payload.utm.keyword |
string | Search keyword. | |
payload.utm.gclid |
string | Google Ads click ID. | |
payload.utm.fbclid |
string | Facebook click ID. | |
payload.rescheduleUrl |
string (uri) | Link the booker uses to reschedule. | |
payload.cancelUrl |
string (uri) | Link the booker uses to cancel. |
Example:
{
"triggerEvent": "PING",
"createdAt": "2026-11-20T10:02:11Z",
"payload": {
"bookingId": "test",
"eventTypeName": "Test booking",
"status": "scheduled",
"startTime": "2026-11-24T05:30:00Z",
"endTime": "2026-11-24T06:00:00Z",
"duration": 30,
"contact": {
"name": "Test contact",
"email": "test@example.com",
"phone": "919999999999"
},
"utm": {
"source": "facebook",
"medium": "paid_social",
"campaign": "test_campaign"
}
}
}
Responses
2XX: Received. Any 2xx counts as delivered.