Skip to content
SMS SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Webhooks and events

@opencoredev/sms-sdk/webhooks: parseSmsWebhook options per provider, the SmsEvent union, keyword helpers, and signature verify helpers.

import { parseSmsWebhook, type SmsEvent } from "@opencoredev/sms-sdk/webhooks";

parseSmsWebhook(options)

Reads the request body once, verifies the provider’s signature, and returns a normalized SmsEvent. No field is interpreted before verification succeeds.

import { parseSmsWebhook } from "@opencoredev/sms-sdk/webhooks";

export async function POST(request: Request): Promise<Response> {
  const event = await parseSmsWebhook({
    provider: "twilio",
    request,
    publicUrl: "https://example.com/webhooks/sms/twilio",
    credentials: { authToken: process.env.TWILIO_AUTH_TOKEN ?? "" },
  });
  console.log(event.type, event.dedupeKey);
  return new Response(null, { status: 200 });
}

Guides: Receiving overview, Webhook security.

Parameters

Options are discriminated by provider. Every variant accepts the common options.

Option Type Default Meaning
provider "twilio" | "telnyx" | "plivo" | "vonage" required Which scheme to verify and parse.
request Request required The incoming web request. Its body must not have been read.
credentials per provider, below required Verification secret.
unsafeSkipVerification boolean false Skips signature checks. For tests only. The event is not authenticated.
detectKeywords boolean true Match STOP/HELP keywords in inbound text when the provider sends no signal.
now () => number Date.now Clock in epoch ms, for timestamp checks.

Per-provider options:

provider credentials Extra options
"twilio" { authToken: string } publicUrl?: string, trustProxy?: boolean
"telnyx" { publicKey: string } (base64, 32-byte Ed25519) toleranceSec?: number (default 300)
"plivo" { authToken: string } publicUrl?: string, trustProxy?: boolean
"vonage" { signatureSecret: string } toleranceSec?: number (default 300)

publicUrl is the exact URL configured with the provider. If it has no query string, the request’s query string is appended. Without publicUrl, trustProxy: true builds the URL from X-Forwarded-Proto and X-Forwarded-Host. Otherwise request.url is used and forwarded headers are ignored. See Webhook security.

Returns

Promise<SmsEvent>.

Throws

  • WebhookSignatureError (code: "webhook_signature") when the request is not authentic. reason is missing_signature, invalid_signature, stale_timestamp, body_hash_mismatch, malformed_signature, or invalid_credentials. Respond 401 or 403.
  • WebhookPayloadError (code: "webhook_payload") when an authentic payload lacks required fields.

SmsEvent

Every event has these fields.

Field Type Meaning
provider "twilio" | "telnyx" | "plivo" | "vonage" Which provider sent it.
dedupeKey string Stable across repeated deliveries of the same event.
occurredAt Date | undefined When the provider says it happened, if the payload includes it.
raw unknown The verified payload, as a string record of form fields or as parsed JSON. Unknown extra fields are kept here.

message.received

{ type: "message.received"; providerId: string; from: string; to: string; body: string; mediaUrls: readonly string[] }

An inbound message. See Inbound SMS.

Status events

{
  type: "message.queued" | "message.sent" | "message.delivered" | "message.undelivered" | "message.filtered";
  providerId: string;
  providerStatus: string; // the provider's own status value
  errorCode?: string;
  from?: string;
  to?: string;
}

A status update for an outbound message. MessageStatusType is the union of these five types. The mapping per provider is in Delivery status webhooks.

Opt-out and help events

{
  type: "recipient.opted_out" | "recipient.opted_in" | "recipient.help";
  providerId: string;
  from: string;
  to: string;
  body: string; // the original inbound text
  source: "provider" | "keyword";
  keyword: string;
}

source: "provider" comes from Twilio’s OptOutType. source: "keyword" comes from whole-message keyword matching. recipient.opted_in comes only from Twilio OptOutType=START. See STOP, HELP, and opt-outs.

unrecognized

{ type: "unrecognized"; providerEventType: string }

A verified event SMS SDK does not map, such as Twilio read or a new Telnyx event type. Inspect raw. A provider’s new statuses arrive here instead of breaking your handler.

Dedupe keys

Provider dedupeKey
Twilio twilio:{MessageSid}:{MessageStatus} or twilio:{MessageSid}:received
Telnyx telnyx:{data.id}
Plivo plivo:{MessageUUID}:{Status} or plivo:{MessageUUID}:received
Vonage vonage:{message_uuid}:{status} or vonage:{message_uuid}:received

Keyword helpers

detectKeyword(body)

Matches a whole message against the keyword lists, ignoring case, surrounding whitespace, and trailing ., !, or ?.

import { detectKeyword } from "@opencoredev/sms-sdk/webhooks";

console.log(detectKeyword("Stop!")); // { kind: "opted_out", keyword: "STOP" }
console.log(detectKeyword("help")); // { kind: "help", keyword: "HELP" }
console.log(detectKeyword("Please stop texting me")); // undefined

Returns KeywordMatch | undefined, where KeywordMatch is { kind: "opted_out" | "help"; keyword: string }.

OPT_OUT_KEYWORDS and HELP_KEYWORDS

  • OPT_OUT_KEYWORDS: ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"]
  • HELP_KEYWORDS: ["HELP", "INFO"]

Per-provider parsers

parseTwilioWebhook, parseTelnyxWebhook, parsePlivoWebhook, and parseVonageWebhook take the same options as parseSmsWebhook without provider, with option types TwilioWebhookOptions, TelnyxWebhookOptions, PlivoWebhookOptions, and VonageWebhookOptions.

Verify helpers

These check a signature without parsing, for example in middleware.

verifyTwilioSignature({ authToken, url, params, signature })

Returns Promise<boolean>. params is a list of [name, value] pairs from the form body, empty for JSON bodies and GET requests. Tries the URL with and without the default port.

computeTwilioSignature({ authToken, url, params }) returns the expected X-Twilio-Signature as Promise<string>.

verifyTelnyxSignature({ publicKey, rawBody, signature, timestamp, toleranceSec?, now? })

Returns Promise<WebhookVerification>, either { valid: true } or { valid: false, reason }. signature and timestamp are the telnyx-signature-ed25519 and telnyx-timestamp header values, or null. now is epoch ms. TELNYX_DEFAULT_TOLERANCE_SEC is 300.

verifyPlivoSignatureV2({ authToken, url, nonce, signature })

Returns Promise<boolean>. url may include a query string, which is removed before signing. computePlivoSignatureV2({ authToken, url, nonce }) returns the expected signature.

verifyVonageSignature({ signatureSecret, authorization, rawBody, toleranceSec?, now? })

Returns Promise<WebhookVerification>. authorization is the Authorization header value, or null. VONAGE_DEFAULT_TOLERANCE_SEC is 300.

import { verifyTelnyxSignature } from "@opencoredev/sms-sdk/webhooks";

export async function isAuthentic(request: Request): Promise<boolean> {
  const result = await verifyTelnyxSignature({
    publicKey: process.env.TELNYX_PUBLIC_KEY ?? "",
    rawBody: await request.clone().text(),
    signature: request.headers.get("telnyx-signature-ed25519"),
    timestamp: request.headers.get("telnyx-timestamp"),
  });
  return result.valid;
}

Types

Exported from @opencoredev/sms-sdk/webhooks: ParseSmsWebhookOptions, SmsEvent, SmsEventBase, MessageReceivedEvent, MessageStatusEvent, MessageStatusType, RecipientKeywordEvent, UnrecognizedEvent, WebhookProvider, KeywordMatch, WebhookCommonOptions, SignedUrlOptions, WebhookVerification, the four per-provider option types, and WebhookFailureReason.