Messages Webhooks

This guide explains how to receive real-time notifications when a guest sends a message to a host on the Plum Guide platform.

Overview

Webhooks let you know the moment a guest writes to a host, so you do not have to poll for new messages. Plum calls your endpoint with the ids of the conversation and the message; you then fetch the text through the messaging API.

The callback carries no message text. That keeps guest content out of your logs and out of anything that inspects the request on its way to you.



Quick Start

1. Configure Messages Webhook Endpoint

POST /v2/webhooks/config
Authorization: Bearer {your_token}
Content-Type: application/json

{
  "isActive": true,
  "endpoints": {
    "messages": "https://your-domain.com/webhooks/messages"
  }
}

The response includes a signingSecret. It is returned only on create and on rotation — store it now.

2. Receive Message Events

When a guest sends a message, your endpoint receives:

{
  "eventId": "506f6996-2445-499a-abe9-2ca1e1feebcf",
  "timestamp": "2026-08-24T10:15:24.5949004Z",
  "bookingCode": null,
  "listingId": 7,
  "eventType": "GuestMessageReceived",
  "conversationId": 23722,
  "messageId": 325924
}

3. Fetch the Message

Use the conversation id to read the thread:

GET /v2/messaging/conversations/23722/messages
Authorization: Bearer {your_token}

Message Events

EventDescriptionAction Required
Guest Message ReceivedA guest sent a message to the hostFetch the thread and reply if needed

A host's own reply does not produce a callback. You are notified about messages the host needs to see, not about the ones they sent — including the replies you send through this API.


Webhook Payload

{
  "eventId": "506f6996-2445-499a-abe9-2ca1e1feebcf",
  "timestamp": "2026-08-24T10:15:24.5949004Z",
  "bookingCode": null,
  "listingId": 7,
  "eventType": "GuestMessageReceived",
  "conversationId": 23722,
  "messageId": 325924
}
FieldTypeDescription
eventIdUUIDUnique identifier for this event (use for idempotency)
timestampdateTimeWhen the event occurred
bookingCodenullAlways null for message events
listingIdintegerThe listing the conversation is about
eventTypestringGuestMessageReceived
conversationIdintegerThe conversation the message belongs to
messageIdintegerThe message that was sent

eventType distinguishes message events from the booking and listing channels, which share the same payload shape. Treat any value you do not recognize as one to ignore rather than one to reject, so a future event type does not break your handler.


Configuration API

Create Configuration

POST /v2/webhooks/config
Content-Type: application/json
Authorization: Bearer {token}

{
  "isActive": true,
  "endpoints": {
    "messages": "https://your-domain.com/webhooks/messages"
  }
}

POST creates. If a configuration already exists it returns 400 with "Webhook configuration already exists. Use PUT to update." — it does not overwrite.

Update Configuration

PUT /v2/webhooks/config
Content-Type: application/json
Authorization: Bearer {token}

{
  "isActive": true,
  "endpoints": {
    "bookings": "https://your-domain.com/webhooks/bookings",
    "listings": "https://your-domain.com/webhooks/listings",
    "messages": "https://your-domain.com/webhooks/messages"
  }
}

Get Configuration

GET /v2/webhooks/config
Authorization: Bearer {token}

The signing secret is never returned here. Only create and rotation return it.

Toggle On/Off

PATCH /v2/webhooks/config/toggle?isActive=false
Authorization: Bearer {token}

isActive is required — omitting it returns 400, and it sets the state rather than flipping it.

Delete Configuration

DELETE /v2/webhooks/config
Authorization: Bearer {token}
🚧

Note: PUT is the only way to change an existing configuration, and it replaces the whole thing. Any channel you leave out of the body is cleared, so a PUT carrying only bookings silently removes your messages endpoint. Always send every endpoint you want to keep — read the current configuration first if you are unsure. An endpoint cannot be cleared by sending an empty string, which is rejected as an invalid URL; omit it instead.

Requirements:

  • Endpoint URL must use HTTPS
  • Your endpoint must return 200 OK within 30 seconds

Handling Message Events

Guest Message Received

Acknowledge first, then fetch the thread:

app.post('/webhooks/messages', async (req, res) => {
  res.status(200).send('OK');

  const { conversationId } = req.body;
  const { data: messages } = await plumApi.getMessages(conversationId);

  const latest = messages[messages.length - 1];
  if (latest.sender === 'Guest') {
    await notifyHostOfNewMessage(conversationId, latest.body);
  }
});

Messages come back newest first, so the one that just arrived is the first entry. Matching on messageId from the payload is steadier than relying on position, and it is what the fuller example below does.


Complete Example Handler

const processedEvents = new Set();

app.post('/webhooks/messages', async (req, res) => {
  const { eventId, eventType, conversationId, messageId } = req.body;

  // Ignore event types we do not handle yet
  if (eventType !== 'GuestMessageReceived') {
    return res.status(200).send('Ignored');
  }

  // Idempotency check
  if (processedEvents.has(eventId)) {
    return res.status(200).send('Already processed');
  }

  // Acknowledge receipt immediately
  res.status(200).send('OK');
  processedEvents.add(eventId);

  try {
    const { data: messages } = await plumApi.getMessages(conversationId);
    const message = messages.find(m => m.messageId === messageId);

    await deliverToHostInbox(conversationId, message);
  } catch (error) {
    console.error('Failed to process message webhook:', error);
  }
});

Best Practices

  1. Respond quickly - Return 200 OK immediately, process asynchronously
  2. Implement idempotency - Use eventId to avoid processing duplicates
  3. Fetch the text - The payload carries ids only, never message content
  4. Ignore unknown eventType values - Return 200 so new event types do not break you
  5. Do not expect callbacks for host replies - Only guest messages raise an event

Troubleshooting

Not receiving webhooks?

  1. Verify configuration: GET /v2/webhooks/config
  2. Check isActive is true
  3. Ensure endpoints.messages is set — and that an earlier PUT did not clear it
  4. Confirm URL is HTTPS and publicly accessible
  5. Confirm you are expecting a guest message; host replies do not fire

Receiving duplicates?

This is expected. Use eventId for deduplication:

const processedEvents = new Set();

if (processedEvents.has(eventId)) {
  return res.status(200).send('Already processed');
}
processedEvents.add(eventId);

Duplicates also arrive when your endpoint answers with a non-2xx or times out, because the delivery is retried.

Events stop arriving after an outage

A delivery that keeps failing is retried a limited number of times and then given up on. Once your endpoint is healthy again, use GET /v2/messaging/activities?fromDate=... to catch up on anything missed while it was down.


Testing Checklist

  • Endpoint returns 200 OK within 30 seconds

  • Handles duplicate events (same eventId)

  • Ignores unrecognized eventType values with a 200

  • Fetches message text via the messaging API

  • Catches up through the activity feed after downtime