A Story About Getting Stuck on Signature Verification for Twilio ConversationRelay WebSocket Communication

A Story About Getting Stuck on Signature Verification for Twilio ConversationRelay WebSocket Communication

When I implemented my own signature verification for Twilio ConversationRelay's WebSocket connection, only the validation of the Upgrade request kept failing, causing calls to repeatedly disconnect with errors. The cause was that the signature target URL needed to be calculated using the wss scheme as specified in the TwiML, without converting it.
2026.08.15

This page has been translated by machine translation. View original

Introduction

While working on validating a voice AI bot using Twilio ConversationRelay with my own WebSocket server, I encountered an issue where only the signature validation for WebSocket Upgrade requests kept failing, causing calls to repeatedly disconnect with errors. The root cause was that I had reused the same URL construction method I had built for POST webhook signature validation, applying it directly to Upgrade request signature validation.

When I calculated the signature by converting the target URL to https://, it did not match the actual signature received from Twilio. It was necessary to calculate using the wss:// scheme exactly as specified in the TwiML <ConversationRelay url="wss://...">.

What is ConversationRelay WebSocket Signature Validation?

ConversationRelay is a mechanism that connects Twilio voice calls to your own server via WebSocket. On the server side, it accepts WebSocket Upgrade requests sent by Twilio in response to TwiML <Connect><ConversationRelay>. Twilio advises validating the X-Twilio-Signature on these Upgrade requests as well, in order to verify the source of the connection.

Validation Environment

  • WebSocket server: Plain Node.js + ws + twilio package
  • Node.js runtime: v24.18.0
  • Deployment target: Vercel Functions

Target Audience

  • Those trying to receive Twilio ConversationRelay on their own server
  • Those trying to implement X-Twilio-Signature validation on WebSocket Upgrade requests
  • Those struggling with signature validation errors of unknown cause

References

Issue: Calls Disconnecting Due to Signature Validation Errors

I implemented signature validation for WebSocket Upgrade requests. I reused the same URL construction method I had been using for POST webhooks: requestUrl = PUBLIC_BASE_URL + req.url (where PUBLIC_BASE_URL starts with https://).

When I actually made a call, it disconnected with error 64102 (Unable to connect to websocket URL). The ringtone would play, but the connection would not be established after the TwiML response.

Looking at the server-side logs, /voice (POST) was passing signature validation and proceeding to normal processing, but only the Upgrade request to /ws was returning a 403 with invalid_signature. The fact that the POST side was passing through while using the same TWILIO_AUTH_TOKEN became the clue for my investigation.

Investigation: Comparing the Actual Signature

I temporarily added debug logging and compared the actual signature value received from Twilio against the expected value calculated on the server side — they did not match.

So I wrote a Node.js script locally, calculated signatures using HMAC-SHA1 for multiple URL candidates, and compared them against the actually received signature.

const crypto = require('crypto');

function sign(url, params = {}) {
  const data = Object.keys(params).sort().reduce((acc, key) => acc + key + params[key], url);
  return crypto.createHmac('sha1', AUTH_TOKEN).update(Buffer.from(data, 'utf-8')).digest('base64');
}

const candidates = [
  'https://example.vercel.app/ws',
  'wss://example.vercel.app/ws',
  'https://example.vercel.app/ws/',
  'https://example.vercel.app:443/ws',
];

for (const url of candidates) {
  console.log(url, sign(url) === RECEIVED_SIGNATURE ? 'MATCH' : 'no match');
}

Only the candidate calculated with the wss:// scheme matched the actually received signature.

Root Cause: The Scheme of the Signature Target URL

The URL specified in TwiML <ConversationRelay url="wss://..."> was being used as-is as the target for signature calculation. The conversion to https:// used in POST webhook validation did not apply to WebSocket Upgrade requests.

Fix: Updated Code

I changed only the scheme of the signature target URL from https:// to wss:// and redeployed.

api/index.js
function validateGetRequest(req) {
  const signature = req.headers['x-twilio-signature'];
  const requestUrl = PUBLIC_BASE_URL.replace(/^http/, 'ws') + req.url;
  return twilio.validateRequest(AUTH_TOKEN, signature, requestUrl, {});
}

I confirmed that the setup event was successfully received in an actual call.

Summary

When implementing my own signature validation for Twilio ConversationRelay WebSocket connections, I encountered an issue where only the Upgrade request validation kept failing and calls repeatedly disconnected with errors, because I had reused the URL construction method intended for POST webhooks. The root cause was that the signature target URL needed to be calculated using the wss:// scheme exactly as specified in the TwiML.

When implementing X-Twilio-Signature validation yourself for ConversationRelay WebSocket connections, I recommend not reusing POST webhook code as-is, and instead verifying the scheme of the signature target URL.


Twilioの導入支援はクラスメソッドにお任せください!

クラスメソッドでは、Twilioの導入から運用までしっかりサポートいたします。
コミュニケーションツールの導入や最適化をお考えの方は、ぜひお気軽にご相談ください。

Twilioの詳細を見る

Share this article