# 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

1. In Wylto, open **CRM → Settings → API Settings** and copy your API token. Only the app owner can see it.
2. Send it as a Bearer token: `Authorization: Bearer <token>`.
3. 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.

## WhatsApp

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:

```json
{
  "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).

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "components": [
    {
      "type": "BODY",
      "text": "Your order #{{1}} is confirmed. It will reach you by {{2}}.",
      "example": {
        "body_text": [
          [
            "1001",
            "Friday"
          ]
        ]
      }
    }
  ]
}
```

**Responses**

- `200`: Template updated.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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:

```json
{
  "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:

```json
{
  "statusName": "Qualified",
  "tagNames": [
    "VIP",
    "Newsletter"
  ]
}
```

**Responses**

- `200`: Contact updated successfully, or created when createIfContactNotPresent=true and no contact matched.

```json
{
  "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.

```json
{
  "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:

```json
{
  "statusName": "Qualified",
  "tagNames": [
    "VIP",
    "Newsletter"
  ]
}
```

**Responses**

- `200`: Contact updated successfully.

```json
{
  "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.

```json
{
  "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:

```json
{
  "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.

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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.
