Webhooks
SMS Bridge sends an HTTPS POST to your endpoint when something happens. Add endpoints in Settings → Webhooks or with POST /v1/webhooks (url, events, optional phone_numbers to limit it to some phones).
Events
message.received— an SMS arrived on one of your phones.message.sent— the phone's radio sent an outbound SMS.message.delivered— the carrier confirmed delivery.message.failed— the phone could not send it after all retries.ai.draft.created— an AI reply draft is waiting for your review (the payload is the inbound message).
In test mode, simulated sends fire message.sent and message.delivered too, so you can build your integration before going live.
Payload
{
"event": "message.received",
"message": {
"id": "5d1c2b7a-0e44-4f0b-8c1e-7a9f3d2e6b10",
"direction": "inbound",
"from_number": "+15550100002",
"to_number": "+15550100001",
"content": "Is the store open tomorrow?",
"status": "delivered"
},
"timestamp": "2026-09-26T10:04:12.512+00:00"
}content can be empty on a late retry: message text is removed 30 minutes after a message is sent or received.
Verifying the signature
Each request carries two headers:
X-Signature— HMAC-SHA256 of the raw request body, keyed with your endpoint's signing key, as lowercase hex.X-Timestamp— Unix time (seconds) when it was sent. Reject requests older than 5 minutes.
The signing key is shown once when you create the endpoint. Compute the HMAC over the exact bytes you received, before parsing JSON:
// Node.js (Express): app.use(express.raw({ type: "application/json" }))
import crypto from "node:crypto";
function verify(rawBody, signature, timestamp, signingKey) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac("sha256", signingKey).update(rawBody).digest("hex");
return signature?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}# Python
import hmac, hashlib, time
def verify(raw_body: bytes, signature: str, timestamp: str, signing_key: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(signing_key.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")Retries
- Respond with any
2xxwithin 10 seconds — do slow work after responding. - Timeouts, connection errors and
5xxresponses are retried after 1 minute, 5 minutes, 15 minutes, 1 hour and 6 hours. - A
4xxresponse stops retries for that delivery straight away. - Every attempt is listed under the endpoint in the portal with its status code and error.
Endpoint rules
- Must be
https://and publicly reachable. Private, loopback and internal addresses are refused. - Redirects are not followed — give the final URL.