MeetOne only delivers webhooks to a URL it can reach over the public internet, so testing an integration on your own machine takes a bit of setup.

Expose your local server

MeetOne's servers can't reach localhost directly. Use a tunneling tool to give your local development server a public HTTPS URL:

  • A tunnel, e.g. ngrok — run ngrok http 3000 (or whatever port your app listens on) and use the generated https://*.ngrok.io URL as your webhook endpoint. This is the best option once you're writing real handler code, since requests reach your actual application.
  • A request inspector, e.g. webhook.site — gives you a URL instantly and shows every request it receives, including headers and body, without you running anything locally. Good for a first look at what a delivery looks like before you write any code.

Either way, use the resulting public URL as the endpoint URL when you create a test webhook in the admin.

Set up a test endpoint

  1. In the MeetOne admin, go to Webhooks and create a new endpoint pointing at your tunnel or inspector URL.
  2. Subscribe it to the event(s) you're building against.
  3. Copy the endpoint's signing secret — you'll need it to verify deliveries.

See Webhooks overview if you don't see the Webhooks option in your admin.

Trigger real events

There's no simulated or sandboxed delivery — the way to see a real payload is to cause the underlying event. For room events, create and close a room through the API:

curl -X POST "https://acme.meetone.io/api/rooms" \
  -H "Authorization: Bearer $MEETONE_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test room",
    "meeting_duration": 45,
    "scheduled_at": "2026-07-16T15:19:36.378+02:00"
  }'

This fires room.created. Closing that room (PUT /api/rooms/:token/close) fires room.closed together with room.billing_calculated; deleting it fires room.deleted. See Create your first room for the full request/response shapes and how to obtain an API token.

Verify the signature against your endpoint's secret

Whatever tool you use to receive the delivery, check the signature the same way your production handler will — see Verifying webhook signatures for the algorithm. A request inspector like webhook.site won't verify this for you; you'll need to copy the X-Webhook-Timestamp and X-Webhook-Signature-256 header values and the raw body out of it and run them through your verification code by hand, or point your tunnel at a small receiver that does it automatically (see below).

A minimal example receiver

A tiny handler that verifies the signature and acknowledges the delivery — enough to confirm your endpoint and secret are wired up correctly before you build real processing logic.

Node.js (Express)

const express = require("express")
const crypto = require("crypto")

const SIGNING_SECRET = process.env.WEBHOOK_SIGNING_SECRET
const TOLERANCE_SECONDS = 5 * 60

const app = express()

// Capture the raw body — verification must hash the exact bytes MeetOne sent.
app.use(express.raw({ type: "application/json" }))

app.post("/webhooks/meetone", (req, res) => {
  const timestamp = req.header("X-Webhook-Timestamp")
  const signature = req.header("X-Webhook-Signature-256")
  const rawBody = req.body // Buffer, thanks to express.raw()

  if (!isValid(rawBody, timestamp, signature)) {
    return res.status(401).send("invalid signature")
  }

  const { event, payload } = JSON.parse(rawBody)
  console.log("received", event, payload)

  res.status(200).send("ok")
})

function isValid(rawBody, timestamp, signatureHeader) {
  if (!timestamp || !signatureHeader) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) > TOLERANCE_SECONDS) {
    return false
  }

  const expected = "sha256=" + crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex")

  const a = Buffer.from(expected)
  const b = Buffer.from(signatureHeader)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

app.listen(3000, () => console.log("listening on :3000"))

Ruby (Sinatra)

require "sinatra"
require "openssl"
require "json"

SIGNING_SECRET = ENV.fetch("WEBHOOK_SIGNING_SECRET")
TOLERANCE_SECONDS = 5 * 60

post "/webhooks/meetone" do
  request.body.rewind
  raw_body = request.body.read

  timestamp = request.env["HTTP_X_WEBHOOK_TIMESTAMP"]
  signature = request.env["HTTP_X_WEBHOOK_SIGNATURE_256"]

  halt(401, "invalid signature") unless valid_signature?(raw_body, timestamp, signature)

  body = JSON.parse(raw_body)
  puts "received #{body["event"]} #{body["payload"]}"

  status(200)
  "ok"
end

def valid_signature?(raw_body, timestamp, signature_header)
  return false if timestamp.nil? || signature_header.nil?
  return false if (Time.now.to_i - timestamp.to_i).abs > TOLERANCE_SECONDS

  expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", SIGNING_SECRET, "#{timestamp}.#{raw_body}")}"
  ActiveSupport::SecurityUtils.secure_compare(expected, signature_header.to_s)
rescue LoadError
  # if you don't have ActiveSupport available, use a constant-time
  # comparison of your own, e.g. via `OpenSSL.secure_compare` (Ruby 3.2+).
  false
end

Point your tunnel at whichever of these you run, use its public URL as the webhook endpoint, and set WEBHOOK_SIGNING_SECRET to the secret shown on the endpoint's detail page.

Check the delivery log

After triggering an event, go to Webhook Events in the admin to confirm MeetOne's view of what happened: the payload it sent, the status code and body your endpoint returned, and the retry count if it needed to retry. This is the fastest way to tell whether a problem is on MeetOne's side (event not firing, wrong endpoint subscribed) or your side (signature mismatch, non-2xx response).

Questions? Contact us at support@meetone.io.