Skip to content

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 2xx within 10 seconds — do slow work after responding.
  • Timeouts, connection errors and 5xx responses are retried after 1 minute, 5 minutes, 15 minutes, 1 hour and 6 hours.
  • A 4xx response 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.