> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.meetstream.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server.

# MeetStream Guide: Test Webhooks Locally with ngrok or Cloudflare Tunnel

When you’re building a MeetStream integration, you’ll often want to test **webhooks locally** (on your laptop) before deploying a server.

This guide shows how to:
- run a local webhook server
- expose it to the internet using **ngrok** or **Cloudflare Tunnel (cloudflared)**
- paste the public URL into MeetStream as your `callback_url`

---

<div style="position: relative; padding-bottom: 56.25%; height: 0;"><iframe src="https://www.loom.com/embed/375df4ba5da64553b9174ba1bf696741" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div>

---

## What you’re building

1. You run a local HTTP server (example: `http://localhost:3000/webhooks/meetstream`)
2. You create a tunnel that gives you a public URL (example: `https://abc123.ngrok-free.app/webhooks/meetstream`)
3. You use that public URL as MeetStream’s webhook `callback_url`

---

## 0) Create a simple local webhook endpoint

Pick any stack. Here are two minimal examples.

### A) Node.js (Express)

```bash
mkdir meetstream-webhook-test && cd meetstream-webhook-test
npm init -y
npm i express
```

Create `server.js`:

```js
import express from "express";

const app = express();
app.use(express.json({ type: "*/*" }));

app.post("/webhooks/meetstream", (req, res) => {
  console.log("✅ Webhook received");
  console.log("Headers:", req.headers);
  console.log("Body:", req.body);
  res.status(200).send("ok");
});

app.listen(3000, () => console.log("Listening on http://localhost:3000"));
```

Run it:

```bash
node server.js
```

Your local endpoint is now:
- `http://localhost:3000/webhooks/meetstream`

---

### B) Python (FastAPI)

```bash
pip install fastapi uvicorn
```

Create `app.py`:

```py
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhooks/meetstream")
async def webhook(req: Request):
    body = await req.json()
    print("✅ Webhook received")
    print("Headers:", dict(req.headers))
    print("Body:", body)
    return {"ok": True}
```

Run it:

```bash
uvicorn app:app --port 3000 --reload
```

Endpoint:
- `http://localhost:3000/webhooks/meetstream`

---

## 1) Option A — Use ngrok (quickest)

### Step 1: Install ngrok

- macOS (Homebrew):

```bash
brew install ngrok/ngrok/ngrok
```

Or download from ngrok website and follow install steps.

### Step 2: Authenticate ngrok

```bash
ngrok config add-authtoken <YOUR_NGROK_TOKEN>
```

### Step 3: Start a tunnel to your local port

If your local server is running on `localhost:3000`:

```bash
ngrok http 3000
```

ngrok will print a public HTTPS URL like:

- `https://abc123.ngrok-free.app`

### Step 4: Use the public URL as callback_url

If your route is `/webhooks/meetstream`, your final webhook URL becomes:

- `https://abc123.ngrok-free.app/webhooks/meetstream`

Use this in your Create Bot payload:

```json
{
  "callback_url": "https://abc123.ngrok-free.app/webhooks/meetstream"
}
```

---

## 2) Option B — Use Cloudflare Tunnel (cloudflared)

Cloudflare Tunnel is great if you already use Cloudflare or want a stable tunnel.

### Step 1: Install cloudflared

- macOS (Homebrew):

```bash
brew install cloudflare/cloudflare/cloudflared
```

### Step 2: Start a quick (temporary) tunnel

If your local server is on port `3000`:

```bash
cloudflared tunnel --url http://localhost:3000
```

cloudflared will output a public URL like:

- `https://something.trycloudflare.com`

### Step 3: Use it as callback_url

Final webhook URL:

- `https://something.trycloudflare.com/webhooks/meetstream`

---

## 3) Verify your local webhook is receiving events

### A) Add the URL while creating the bot

Include in Create Bot payload:

```json
{
  "callback_url": "https://<PUBLIC_TUNNEL_URL>/webhooks/meetstream"
}
```

### B) Trigger a bot lifecycle

You should see incoming webhook payloads for events like:
- `bot.joining`
- `bot.inmeeting`
- `bot.stopped`

---

## 4) Common issues & fixes

### Webhook not arriving?
- Confirm your local server is running and the endpoint path matches exactly.
- Ensure your tunnel is pointing to the right port (e.g., 3000).
- Make sure your webhook handler returns **2xx** quickly.

### “502 Bad Gateway” in ngrok / Cloudflare
- Your local server is not reachable on that port.
- Try opening locally:
  - `curl -X POST http://localhost:3000/webhooks/meetstream -H "Content-Type: application/json" -d '{}'`

### Missing JSON body
- Ensure you have JSON parsing enabled:
  - Express: `app.use(express.json({ type: "*/*" }))`
  - FastAPI already parses JSON with `await req.json()`

---

## 5) Production best practices (when you deploy)

- Make webhook handling **idempotent** (avoid double-processing).
- Verify webhook signatures (if configured).
- Log webhook events + store minimal event history for debugging.
- Don’t run tunnels in production — use a hosted endpoint.

---