RevoplyAIDocs
Guides

Receive messages with webhooks

Build a receiver that gets every customer message as it arrives, verifies it, deduplicates it and answers in time.

This guide builds an endpoint that receives message.received every time a customer writes to you on any channel, and logs who wrote what. You need the Business plan and an Owner or Admin login.

1. Write the receiver

The receiver must do four things, in this order: verify the signature against the raw body, skip an event id it has already handled, answer 2xx within 10 seconds, then do the work.

// A RevoplyAI webhook receiver on Express 4 or 5.
//
//   npm install express
//   REVOPLY_WEBHOOK_SECRET=whsec_… node express.mjs

import express from 'express';
import { verifyRevoplySignature } from '../verify.node.mjs';

const secret = process.env.REVOPLY_WEBHOOK_SECRET;
const app = express();

// Stands in for your database: remember the event ids you have handled.
const handled = new Set();

// #region route
// express.raw() on this route, registered before any app.use(express.json()): the signature
// covers the exact bytes we sent, and a parsed-then-re-serialised body is not those bytes.
app.post('/webhooks/revoply', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyRevoplySignature(req.body, req.get('X-Revoply-Signature'), secret)) {
    return res.status(400).send('Invalid signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));

  // The same event can arrive more than once: acknowledge a repeat and do nothing.
  if (handled.has(event.id)) return res.sendStatus(200);
  handled.add(event.id);

  // Answer within 10 seconds, then do the work.
  res.sendStatus(200);
  setImmediate(() => handleEvent(event));
});
// #endregion

// #region handle
function handleEvent(event) {
  // "Send test" posts sample data about nobody real, marked "test": true.
  if (event.test) {
    console.log(`test ${event.type} received`);
    return;
  }

  switch (event.type) {
    case 'message.received': {
      const { conversation, contact, message } = event.data;
      console.log(
        `${contact?.name ?? contact?.phone ?? 'A visitor'} wrote on ${conversation.channelType}:`,
      );
      console.log(message.text ?? `[${message.type}]`);
      break;
    }
    default:
      // A type you did not expect is still acknowledged; new events may be added.
      console.log(`ignored ${event.type}`);
  }
}
// #endregion

// The rest of the app may parse JSON as usual, registered after the webhook route.
app.use(express.json());

app.listen(3000, () => console.log('Listening on port 3000, path /webhooks/revoply'));

Each one imports the verifier from Signatures. The id set is a stand-in: in production, record handled ids in your database, in the same transaction as the work they trigger.

2. Make it reachable

We deliver only to public https:// addresses. Deploy the receiver, or expose it from your computer with a tunnel such as ngrok http 3000; see Testing safely.

3. Add the endpoint

  1. Open Integrations → Webhooks → Add endpoint.
  2. Enter your receiver's URL, such as https://example.com/webhooks/revoply, and choose Message received.
  3. Save, and copy the signing secret it shows once into your receiver's configuration as REVOPLY_WEBHOOK_SECRET.

4. Send a test

Choose Send test on the endpoint, pick Message received, and send. Your receiver logs test message.received received, and the dashboard shows the 200 it answered. The example is marked "test": true; the receiver above skips such events before doing any work.

If the test fails, the dashboard shows why; a 400 usually means the signature check failed, see Verify webhook signatures.

5. Read the message

Real events carry "test": false. The fields you will use most:

FieldHolds
data.conversation.idThe conversation; opaque, store it as it is
data.conversation.channelTypewhatsapp, instagram, web_widget… treat unknown values as "other"
data.contact.nameThe customer's name, when known, or null
data.contact.phoneE.164 or null; see Identifiers
data.message.typetext, image, audio, document…
data.message.textThe text, or a media message's caption; at most 4,096 characters, or null

The event fires once per customer message, on every channel, whoever is handling the conversation. It does not fire for messages your business sends: the assistant's replies, flows, or your team's.

Before going live

  • Keep the secret in your server's configuration, never in the code.
  • Record handled event ids durably, for at least as long as a delivery can be retried or resent (30 days covers both).
  • Move slow work (CRM calls, emails) to a queue so every answer is fast.
  • Watch the endpoint's delivery log for failures after deploys. An endpoint that fails 100 times in a row, or for 3 days, is switched off.

On this page