# Set Up Webhooks

This tutorial walks you through building a webhook receiver and registering it with the eCommunicate API. You'll have a working server that processes incoming messages and delivery status updates.

## What you'll build

A Node.js Express server that:

1. Receives incoming WhatsApp messages from customers
2. Receives delivery status updates (sent, delivered, read, failed)
3. Handles different content types (text, media, location)
4. Responds quickly to avoid webhook retries


## Prerequisites

- **Node.js 18+** installed
- An eCommunicate account with an API key
- A publicly accessible URL (or [ngrok](https://ngrok.com/) for local development)


## Step 1: Create the project

```bash
mkdir ecomm-webhooks && cd ecomm-webhooks
npm init -y
npm install express
```

## Step 2: Build the webhook server

Create `server.js`:

```javascript
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// ── Health check ──────────────────────────────────────────────────────
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// ── Incoming message webhook ──────────────────────────────────────────
app.post('/webhooks/incoming', (req, res) => {
  const message = req.body;

  console.log('--- Incoming Message ---');
  console.log(`From:    ${message.sender} (${message.profileName})`);
  console.log(`User ID: ${message.senderUserId ?? 'n/a'} | Username: ${message.username ?? 'n/a'}`);
  console.log(`To:      ${message.receiver}`);
  console.log(`Type:    ${message.messageContentType}`);
  console.log(`Text:    ${message.text || '(no text)'}`);
  console.log(`Time:    ${message.sentTime}`);

  // Route by content type
  switch (message.messageContentType) {
    case 'TEXT':
      handleTextMessage(message);
      break;
    case 'MEDIA':
      handleMediaMessage(message);
      break;
    case 'LOCATION':
      handleLocationMessage(message);
      break;
    case 'BUTTON':
    case 'INTERACTIVE':
      handleButtonMessage(message);
      break;
    case 'REACTION':
      handleReactionMessage(message);
      break;
    case 'CONTACT':
      console.log(`Contact shared: ${message.text}`);
      break;
    default:
      console.log(`Unhandled content type: ${message.messageContentType}`);
  }

  // Always respond 200 immediately to avoid retries
  res.status(200).send('OK');
});

// ── Status update webhook ─────────────────────────────────────────────
app.post('/webhooks/status', (req, res) => {
  const update = req.body;

  console.log('--- Status Update ---');
  console.log(`Message ID (gstId): ${update.gstId}`);
  console.log(`Tracking ID (ctId): ${update.ctId}`);
  console.log(`Status:             ${update.status}`);

  switch (update.status) {
    case 'SENT':
      console.log(`-> Sent at: ${update.sendTime}`);
      break;
    case 'DELIVERED':
      console.log(`-> Delivered at: ${update.deliveryTime}`);
      break;
    case 'READ':
      console.log(`-> Read at: ${update.readTime}`);
      break;
    case 'PLAYED':
      console.log(`-> Voice note played at: ${update.playedTime}`);
      break;
    case 'FAILED':
      console.error(`-> Failed: ${update.errorCodeString} (code: ${update.errorCode})`);
      // TODO: Trigger retry logic or alert your team
      break;
  }

  res.status(200).send('OK');
});

// ── Session status webhook (WhatsApp Lite only) ──────────────────────
app.post('/webhooks/session', (req, res) => {
  const session = req.body;
  console.log('--- Session Status ---');
  console.log(`Number: ${session.whatsAppNumber}`);
  console.log(`Status: ${session.status} (${session.statusType})`);
  res.status(200).send('OK');
});

// ── Message handlers ─────────────────────────────────────────────────
function handleTextMessage(message) {
  console.log(`Processing text: "${message.text}"`);
  // Persist senderUserId (BSUID) as the durable customer key — it survives phone-number changes and is present even when sender is withheld.
  // Example: Route to auto-reply, chatbot, or agent queue
}

function handleMediaMessage(message) {
  console.log(`Media URL:   ${message.mediaUrl}`);
  console.log(`Extension:   ${message.fileExtension}`);
  console.log(`Caption:     ${message.caption || 'N/A'}`);
  // Example: Download and store the media file
}

function handleLocationMessage(message) {
  console.log(`Location: ${message.latitude}, ${message.longitude}`);
  console.log(`Label:    ${message.label || 'N/A'}`);
  console.log(`Address:  ${message.address || 'N/A'}`);
}

function handleButtonMessage(message) {
  console.log(`Button text:    ${message.buttonText}`);
  console.log(`Button payload: ${message.buttonPayload}`);
  // Example: Process the user's button selection
}

function handleReactionMessage(message) {
  const emoji = message.text;
  const reactedMessageId = message.gstId;
  if (emoji) {
    console.log(`Reaction "${emoji}" on message ${reactedMessageId}`);
  } else {
    console.log(`Reaction removed from message ${reactedMessageId}`);
  }
}

// ── Start server ─────────────────────────────────────────────────────
app.listen(PORT, () => {
  console.log(`Webhook server running on http://localhost:${PORT}`);
  console.log(`Health check: http://localhost:${PORT}/health`);
});
```

## Step 3: Expose your local server

For local development, use ngrok to create a public HTTPS URL:

```bash
# Terminal 1: Start your server
node server.js

# Terminal 2: Expose it via ngrok
ngrok http 3000
```

Copy the HTTPS URL from ngrok output (e.g., `https://abc123.ngrok.io`).

HTTPS required
Webhook URLs must use HTTPS with a valid SSL certificate. ngrok provides this automatically for local development.

## Step 4: Register your webhooks

cURL
```bash
curl -X POST \
  "https://api.payless4messaging.com/payless4messaging-service/WhatsApp/WhatsAppClientWebHook" \
  -H "Authorization: $ECOMM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "incomingMessageURL": "https://abc123.ngrok.io/webhooks/incoming",
    "statusUpdateURL": "https://abc123.ngrok.io/webhooks/status",
    "sessionStatusURL": "https://abc123.ngrok.io/webhooks/session",
    "eventWebhookURL": "https://abc123.ngrok.io/webhooks/events"
  }'
```

Node.js
```javascript
const API_KEY = process.env.ECOMM_API_KEY;
const BASE_URL = 'https://api.payless4messaging.com';
const NGROK_URL = 'https://abc123.ngrok.io'; // Replace with your ngrok URL

const response = await fetch(
  `${BASE_URL}/payless4messaging-service/WhatsApp/WhatsAppClientWebHook`,
  {
    method: 'POST',
    headers: {
      'Authorization': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      incomingMessageURL: `${NGROK_URL}/webhooks/incoming`,
      statusUpdateURL: `${NGROK_URL}/webhooks/status`,
      sessionStatusURL: `${NGROK_URL}/webhooks/session`,
      eventWebhookURL: `${NGROK_URL}/webhooks/events`,
    }),
  }
);

console.log(await response.json());
```

## Step 5: Verify configuration

Confirm your webhooks are registered:

```bash
curl -s -X GET \
  "https://api.payless4messaging.com/payless4messaging-service/WhatsApp/WhatsAppClientWebHook/getForCurrentLoginUser" \
  -H "Authorization: $ECOMM_API_KEY" | python3 -m json.tool
```

**Response:**

```json
{
  "id": "webhook-uuid",
  "incomingMessageURL": "https://abc123.ngrok.io/webhooks/incoming",
  "statusUpdateURL": "https://abc123.ngrok.io/webhooks/status",
  "sessionStatusURL": "https://abc123.ngrok.io/webhooks/session",
  "eventWebhookURL": "https://abc123.ngrok.io/webhooks/events"
}
```

## Step 6: Test it

Send a test message (see [Send Your First Message](/tutorials/send-first-message)) and watch your terminal for live status updates:

```
--- Status Update ---
Message ID (gstId): a1b2c3d4-5678-90ab-cdef-1234567890ab
Tracking ID (ctId): customer-tracking-001
Status:             SENT
-> Sent at: 2026-01-15T10:30:02.000+00:00

--- Status Update ---
Message ID (gstId): a1b2c3d4-5678-90ab-cdef-1234567890ab
Tracking ID (ctId): customer-tracking-001
Status:             DELIVERED
-> Delivered at: 2026-01-15T10:30:05.000+00:00
```

## Production checklist

Before going live, address these items:

Production requirements
- **Use a permanent URL** — Replace ngrok with your production server URL and update the webhook configuration
- **Add error handling** — Wrap webhook handlers in try/catch blocks and log errors
- **Process asynchronously** — Use a message queue (Redis, RabbitMQ, SQS) for heavy processing instead of handling inline
- **Respond within 5 seconds** — Return `200 OK` immediately; do all processing in the background
- **Deduplicate events** — Use `gstId` to detect and skip duplicate webhook deliveries
- **Key your customer records on `senderUserId` (BSUID)**, not the phone number — the phone may change or be withheld, while the BSUID is stable per business.
- **Monitor uptime** — If your webhook endpoint is down, you'll miss events with no way to recover them
- **Log everything** — Log all payloads with timestamps; redact sensitive customer data before storing


## Next steps

| Guide | What you'll learn |
|  --- | --- |
| [Webhooks Guide](/guides/webhooks) | Full webhook configuration reference and payload schemas |
| [WhatsApp Messaging](/guides/whatsapp-messaging) | Template types, media, and components |
| [Error Handling](/guides/error-handling) | Delivery failure codes and troubleshooting |