# Mujib webhook events: every event and its fields

> Mujib webhook events in detail, meaning when each event is sent, which fields it carries and their possible values, with a full JSON example of each event.

Mujib has 5 webhook events: contact.created, booking.created, booking.cancelled, conversation.handoff and message.received. Every event arrives in the same envelope, and the fields inside `data` depend on its type. This page is the reference for every field; setup, retries and what your server must answer are in [WhatsApp webhook](https://docs.mujib.ai/en/developers/webhooks/).

A value that does not apply to an event arrives as `null`; it is never left out of the body. Write your receiver so it ignores any field it does not know.

## Which fields do all webhook events share?

Every event is a JSON object with five fixed fields, and all the details sit inside `data`.

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string (UUID) | The event id, the same in every webhook that receives this event |
| `type` | string | The event type, matching the `X-Mujib-Event` header |
| `createdAt` | string | When the event happened, in UTC, for example `2026-01-01T09:30:00.000Z` |
| `workspaceId` | string (UUID) | Your business's id in Mujib, the same in all your events |
| `data` | object | The event's details, as in the sections below |

Mujib reads the data once, moments after the event, and every retry of a delivery carries that same body. If what the event is about was deleted before then, nothing is sent.

## Which fields does `contact.created` carry?

**New customer** is sent once per new customer: on the first message from a new sender on WhatsApp, Instagram or Messenger, when you message a number that is not yet a customer from your business phone, or when someone on the team adds a customer in the dashboard.

| Field | Type | Meaning |
| --- | --- | --- |
| `data.contact.id` | string (UUID) | The customer's id in Mujib |
| `data.contact.name` | string or `null` | The customer's saved name |
| `data.contact.phone` | string or `null` | Their number as saved, for example `962790000000`. Usually `null` for Messenger and Instagram customers |
| `data.contact.channel` | string | Where they came from: `whatsapp`, `instagram`, `facebook` for Messenger, or `manual` when the team added them |
| `data.contact.stage` | string | The customer's stage: `new`, `engaged`, `qualified`, `won` or `lost` |
| `data.contact.createdAt` | string | When the customer was first seen, in UTC |

```json
{
  "id": "5d7c1e2a-3b4f-4c6d-9e8f-0a1b2c3d4e5f",
  "type": "contact.created",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "workspaceId": "7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d",
  "data": {
    "contact": {
      "id": "c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8",
      "name": "سارة",
      "phone": "962790000000",
      "channel": "whatsapp",
      "stage": "new",
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  }
}
```

## Which fields do `booking.created` and `booking.cancelled` carry?

Both carry the same fields in `data.booking`: **New booking** is sent when the agent books an appointment or the team adds one in the calendar, and **Booking cancelled** when an appointment is cancelled. A recurring booking sends one event per appointment in it.

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string (UUID) | The booking id |
| `status` | string | `requested`, `confirmed`, `cancelled`, `completed` or `no_show` |
| `source` | string | Who made it: `agent` or `staff` |
| `service` | string | The visit type code your business defined |
| `startsAt` | string | The start instant in UTC, for example `2026-11-02T14:30:00Z` |
| `localTime` | string | The start in the branch's local time, with no zone, for example `2026-11-02T17:30:00` |
| `timezone` | string | The branch's time zone, for example `Asia/Amman` |
| `durationMinutes` | number or `null` | The length in minutes; `null` means your business's default length |
| `branch` | object or `null` | The branch: `id` and `name` |
| `specialist` | string or `null` | The specialist's name, when one is assigned |
| `customer` | object | `contactId`, `name` and `phone`. `contactId` is `null` when the booking is not linked to a customer in Mujib |
| `createdAt` | string | When the booking was made, in UTC |

```json
{
  "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "type": "booking.created",
  "createdAt": "2026-11-01T08:15:02.441Z",
  "workspaceId": "7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d",
  "data": {
    "booking": {
      "id": "b7e6d5c4-b3a2-4910-8f7e-6d5c4b3a2910",
      "status": "confirmed",
      "source": "agent",
      "service": "consultation",
      "startsAt": "2026-11-02T14:30:00Z",
      "localTime": "2026-11-02T17:30:00",
      "timezone": "Asia/Amman",
      "durationMinutes": 30,
      "branch": { "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", "name": "الفرع الرئيسي" },
      "specialist": "ليلى",
      "customer": { "contactId": "c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8", "name": "سارة", "phone": "962790000000" },
      "createdAt": "2026-11-01T08:15:01.982Z"
    }
  }
}
```

> **Which time to show?**
>
> Show `localTime` to branch staff as it is, and use `startsAt` to calculate and to compare across branches.
## Which fields does `conversation.handoff` carry?

**Handed to staff** is sent once per handoff to staff, including a handoff that happens because the agent could not reply.

| Field | Type | Meaning |
| --- | --- | --- |
| `data.conversation.id` | string (UUID) | The conversation id |
| `data.conversation.channel` | string | `whatsapp`, `instagram` or `facebook` |
| `data.conversation.reason` | string or `null` | Why it was handed over: a short code, or a sentence the agent wrote in the conversation's language. Show it as it is; do not build fixed rules on it |
| `data.contact` | object or `null` | The customer: `id`, `name` and `phone` |

```json
{
  "id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
  "type": "conversation.handoff",
  "createdAt": "2026-11-01T10:02:47.120Z",
  "workspaceId": "7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d",
  "data": {
    "conversation": {
      "id": "f0e1d2c3-b4a5-4697-8879-6a5b4c3d2e1f",
      "channel": "whatsapp",
      "reason": "العميلة تطلب عرض سعر خاص لمجموعة"
    },
    "contact": { "id": "c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8", "name": "سارة", "phone": "962790000000" }
  }
}
```

## Which fields does `message.received` carry?

**New message** is sent with every message a customer sends on any channel, and carries its text only, never a file.

| Field | Type | Meaning |
| --- | --- | --- |
| `data.message.id` | string (UUID) | The message id |
| `data.message.conversationId` | string (UUID) | The conversation id |
| `data.message.channel` | string | `whatsapp`, `instagram` or `facebook` |
| `data.message.type` | string | The kind of message, such as `text`, `image`, `audio`, `video`, `document` or `location` |
| `data.message.text` | string or `null` | The message text, or the transcript of a voice note when one is available, cut after 4000 characters. `null` for the other kinds |
| `data.message.createdAt` | string | When the message was received, in UTC |
| `data.contact` | object or `null` | The customer: `id`, `name` and `phone` |

```json
{
  "id": "8e9f0a1b-2c3d-4e5f-9a6b-7c8d9e0f1a2b",
  "type": "message.received",
  "createdAt": "2026-11-01T18:40:11.305Z",
  "workspaceId": "7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d",
  "data": {
    "message": {
      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "conversationId": "f0e1d2c3-b4a5-4697-8879-6a5b4c3d2e1f",
      "channel": "instagram",
      "type": "text",
      "text": "هل يوجد موعد غدًا مساءً؟",
      "createdAt": "2026-11-01T18:40:11.298Z"
    },
    "contact": { "id": "d2e3f4a5-b6c7-4d8e-9f0a-1b2c3d4e5f6a", "name": "سارة", "phone": null }
  }
}
```

> **Customer text is personal data**
>
> The message event carries your customers' words as they wrote them. Subscribe to it only if your system needs it, and store it with the care you give your conversations.
## What does the `webhook.test` event look like?

The test event arrives when you press **Send a test**, signed with the same secret as real events, so it is the right way to test your signature check.

- It goes only to the webhook whose button you pressed, with a single attempt and no retries.
- It is not in the list of events you subscribe to, and shows in the log as **Test delivery**.

```json
{
  "id": "0f1e2d3c-4b5a-4968-8776-5a4b3c2d1e0f",
  "type": "webhook.test",
  "createdAt": "2026-11-01T12:00:00.000Z",
  "workspaceId": "7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d",
  "data": {
    "test": true,
    "message": "This is a test delivery from Mujib. Your endpoint received and can verify it."
  }
}
```

## Frequently asked questions

### Is there an event for a rescheduled booking?

No. Changing the time or the specialist sends no event. To get a booking's latest state, read it by its id from the Mujib API.

### Why is the phone number sometimes empty?

Messenger and Instagram do not share the customer's phone number, so phone stays null unless a number was saved for them in Mujib.

### Do the agent's replies arrive as message.received?

No. The event covers incoming customer messages only; replies from the agent and the team are not sent.

### What is the difference between the body's id and the X-Mujib-Delivery header?

id identifies the event and is the same in every webhook that receives it. X-Mujib-Delivery identifies one delivery to one webhook and stays the same across retries.

---

Source: https://docs.mujib.ai/en/developers/webhook-events/ · Last updated: 2026-11-04
