Receive real-time HTTP POST notifications when an SMS arrives at your private number.
When you configure a webhook URL in your account settings, every inbound SMS to your private number triggers an HTTP POST to your endpoint. The request includes a JSON payload with the message details and an HMAC-SHA256 signature for verification.
Every webhook request is an HTTP POST with Content-Type: application/json.
{
"event": "sms.received",
"version": "1",
"data": {
"messageId": 42,
"from": "+14155551234",
"to": "+12025550100",
"body": "Hello, this is a test message",
"receivedAt": "2026-03-01T15:30:00.000Z"
}
}| Field | Type | Description |
|---|---|---|
| event | string | Always sms.received |
| version | string | Payload version. Currently 1 |
| data.messageId | number | Unique message identifier |
| data.from | string | Sender phone number in E.164 format |
| data.to | string | Your private number in E.164 format |
| data.body | string | SMS message content |
| data.receivedAt | string | ISO 8601 timestamp (UTC) |
Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the request body, prefixed with sha256=.
To verify a webhook request:
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('SMS received:', event.data.from, event.data.body);
res.status(200).send('OK');
});
app.listen(3000);import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET']
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature', '')
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
print(f"SMS received: {event['data']['from']} - {event['data']['body']}")
return 'OK', 200package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
var webhookSecret = os.Getenv("WEBHOOK_SECRET")
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
signature := r.Header.Get("X-Webhook-Signature")
if !hmac.Equal([]byte(signature), []byte(expected)) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var event struct {
Event string `json:"event"`
Data struct {
From string `json:"from"`
Body string `json:"body"`
} `json:"data"`
}
json.Unmarshal(body, &event)
fmt.Printf("SMS received: %s - %s\n", event.Data.From, event.Data.Body)
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
http.ListenAndServe(":3000", nil)
}require 'sinatra'
require 'openssl'
require 'json'
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']
post '/webhook' do
payload = request.body.read
signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, payload)
unless Rack::Utils.secure_compare(signature, expected)
halt 401, 'Invalid signature'
end
event = JSON.parse(payload)
puts "SMS received: #{event['data']['from']} - #{event['data']['body']}"
status 200
'OK'
endNeed help? Contact us at [email protected]