Agent skill

TextUs Webhooks Skill

Receive and verify TextUs webhooks. Use when setting up TextUs webhook handlers, debugging TextUs signature verification (X-TextUs-Signature, HMAC-SHA256), or handling business texting events like message.received, message.delivered, contact.opted_out, and contact.created.

Install this skill

npx skills add hookdeck/webhook-skills --skill textus-webhooks


TextUs (business texting) POSTs a JSON payload to your HTTPS endpoint whenever a subscribed event happens, such as an inbound text or a contact opting out. Each delivery carries an HMAC-SHA256 signature in the X-TextUs-Signature header that you should verify before trusting the event.

When to Use This Skill

  • How do I receive TextUs webhooks?
  • How do I verify the X-TextUs-Signature header?
  • How do I handle message.received, message.delivered, or contact.opted_out events?
  • Why is my TextUs webhook signature verification failing?
  • How do I register a TextUs webhook with the Integrations API?

How TextUs Signs Webhooks

TextUs uses a plain HMAC scheme (not Standard Webhooks, no timestamp in the signature):

ItemValue
HeaderX-TextUs-Signature
AlgorithmHMAC-SHA256
Signed contentThe raw request body, exactly as received
KeyThe webhook's secret, e.g. textus-HOTh4kXxHIbYst0xutpkdw
EncodingBare lowercase hex (64 characters, no sha256= prefix)

The whole secret string, including the textus- prefix, is the HMAC key (as UTF-8). Do not strip the prefix or base64-decode anything. There is no official TextUs SDK for verification, so every framework verifies manually.

Verification (core)

const crypto = require('crypto');

// rawBody: Buffer of the unparsed request body. header: X-TextUs-Signature.
function verifyTextUsSignature(rawBody, header, secret) {
  // Validate hex first: Buffer.from(x, 'hex') silently truncates bad input.
  if (typeof header !== 'string' || !/^[0-9a-f]{64}$/i.test(header)) return false;

  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(header, 'hex'), expected);
}

In Python, compare bytes so a non-ASCII header yields a failed check (401) rather than a TypeError (500):

expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected.encode(), header.lower().encode())

Use the raw request body. In Express use express.raw({ type: '*/*' }) so any content type is captured as a Buffer; re-serialized JSON changes the bytes.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Payload Structure

Every delivery has an action field naming the event, a timestamp, and a link to the owning webhook, plus an object for the event (message and conversation for message events, contact for contact.created, optOut for opt-out/opt-in events):

{
  "action": "message.received",
  "timestamp": "2026-10-20T18:38:00Z",
  "webHook": "/integrations/123",
  "conversation": { "id": "..." },
  "message": { "id": "...", "body": "Hello" }
}

Common Event Types

EventTriggered When
message.receivedAn incoming message is received
message.deliveredAn outgoing message is delivered
message.failedAn outgoing message failed to deliver
message.unknownAn outgoing message could not be delivered due to an unknown error
phone_call.completedA phone call is completed
contact.opted_outA contact opted out of receiving text messages
contact.opted_inA contact opted in to receiving text messages
contact.createdA contact is created

The web app only sends message.delivered and message.received by default; enable the others through the Integrations API. Branch on action and ignore unknown values. See references/overview.md.

Environment Variables

VariableDescription
TEXTUS_WEBHOOK_SECRETThe webhook's secret (the full textus-... string)

Setup

Register an endpoint in the TextUs web app (Settings > Account Settings > Integrations > Webhooks) or with POST /{account-name}/integrations. See references/setup.md.

Local Development

For local webhook testing, run the Hookdeck CLI via npx — no install required:

npx hookdeck-cli listen 3000 textus --path /webhooks/textus

No account required — the CLI creates a guest account on first run and provides a local tunnel + web UI for inspecting requests.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →