Webhooks
webhooks.constructEvent() checks a delivery’s Standard Webhooks signature and returns the parsed event; the …Async variants do the same on WebCrypto for edge runtimes. Pass the raw request body: the signature covers the exact bytes MoreVoice sent.
How signing works, and how to verify without the SDK, is on verifying signatures. The events themselves are in the event catalogue.
// A webhook endpoint for any runtime with Fetch API handlers:// Next.js route handlers, Hono, Bun, Deno, Cloudflare Workers.import { webhooks, WebhookVerificationError } from "@morevoice/sdk";
const secret = process.env.MOREVOICE_WEBHOOK_SECRET!; // whsec_…
export async function POST(request: Request): Promise<Response> { const body = await request.text(); // the raw body, exactly as sent try { const event = await webhooks.constructEventAsync(body, request.headers, secret); if (event.type === "call.ended") { console.log(`Call ${event.data.object.id} ended`); } // Acknowledge fast; do slow work afterwards. return new Response(null, { status: 204 }); } catch (err) { if (err instanceof WebhookVerificationError) { return new Response(err.code, { status: 400 }); } throw err; }}Exported from @morevoice/sdk (source: packages/sdk-ts/src/lib/webhooks.ts).
SECRET_PREFIX
Section titled “SECRET_PREFIX”Prefix of a Standard Webhooks secret (whsec_<base64 key>).
const SECRET_PREFIX = "whsec_";DEFAULT_TOLERANCE_SEC
Section titled “DEFAULT_TOLERANCE_SEC”Default replay window: a signature older or newer than this many seconds is refused.
const DEFAULT_TOLERANCE_SEC = 300;WebhookSecret
Section titled “WebhookSecret”A Standard Webhooks secret: "whsec_<base64>" as shown in the dashboard, the same base64 without the prefix, or the
raw key bytes. Pass several (an array) while you rotate your own copy: any one of them may verify.
type WebhookSecret = string | Uint8Array;RawBody
Section titled “RawBody”The request body exactly as received: a string, a Node Buffer, or any Uint8Array / ArrayBuffer.
type RawBody = string | Uint8Array | ArrayBuffer;WebhookVerificationErrorCode
Section titled “WebhookVerificationErrorCode”type WebhookVerificationErrorCode = /** A required header is absent. */ | "missing_header" /** A header is present but malformed (e.g. a non-numeric timestamp). */ | "invalid_header" /** The timestamp is outside the tolerance window (replay protection, or a badly skewed clock). */ | "timestamp_out_of_range" /** No signature matches: wrong secret, or the body / id / timestamp was altered. */ | "bad_signature" /** The secret you passed is not a usable key (empty, or not base64 after "whsec_"). */ | "invalid_secret" /** The signature is valid but the body is not JSON. */ | "invalid_payload";WebhookVerificationError
Section titled “WebhookVerificationError”class WebhookVerificationError extends Error { readonly name = "WebhookVerificationError"; readonly code: WebhookVerificationErrorCode; constructor(code: WebhookVerificationErrorCode, message: string);}VerifyOptions
Section titled “VerifyOptions”interface VerifyOptions { /** Accept timestamps within ± this many seconds of now (default 300). `Infinity` turns the check off. */ toleranceSec?: number; /** The current time in milliseconds since the epoch, like `Date.now` (tests, or a trusted clock). */ now?: () => number;}VerifiedWebhook
Section titled “VerifiedWebhook”A request whose signature checked out.
interface VerifiedWebhook { /** The `webhook-id` (Standard) or `x-webhook-id` (legacy, may be empty). */ id: string; /** Unix seconds from the timestamp header. */ timestamp: number; /** The body as text (UTF-8), not yet parsed. */ payload: string;}StandardWebhookHeaders
Section titled “StandardWebhookHeaders”The three Standard Webhooks headers, as generateTestHeaders returns them (a plain record: pass it to fetch).
type StandardWebhookHeaders = { "webhook-id": string; "webhook-timestamp": string; "webhook-signature": string;};verifySignature()
Section titled “verifySignature()”Check a Standard Webhooks signature and return the verified (unparsed) payload. Throws WebhookVerificationError.
function verifySignature(rawBody: RawBody, headers: HeadersLike, secret: WebhookSecret | readonly WebhookSecret[], opts: VerifyOptions = {}): VerifiedWebhook;verifySignatureAsync()
Section titled “verifySignatureAsync()”verifySignature on WebCrypto, for edge runtimes (Vercel/Next.js edge, Cloudflare Workers, Deno, browsers).
function verifySignatureAsync(rawBody: RawBody, headers: HeadersLike, secret: WebhookSecret | readonly WebhookSecret[], opts: VerifyOptions = {}): Promise<VerifiedWebhook>;constructEvent()
Section titled “constructEvent()”Verify a webhook delivery and return its event. The body is parsed only after the signature checks out.
const event = webhooks.constructEvent(req.body, req.headers, process.env.MOREVOICE_WEBHOOK_SECRET!);Throws WebhookVerificationError (code: missing_header | invalid_header | timestamp_out_of_range | bad_signature |
invalid_secret | invalid_payload); answer 400 and MoreVoice retries.
function constructEvent<T = MoreVoiceEvent>(rawBody: RawBody, headers: HeadersLike, secret: WebhookSecret | readonly WebhookSecret[], opts: VerifyOptions = {}): T;constructEventAsync()
Section titled “constructEventAsync()”constructEvent on WebCrypto, for edge runtimes.
function constructEventAsync<T = MoreVoiceEvent>(rawBody: RawBody, headers: HeadersLike, secret: WebhookSecret | readonly WebhookSecret[], opts: VerifyOptions = {}): Promise<T>;verifyLegacy()
Section titled “verifyLegacy()”Verify a delivery signed with the legacy X-Webhook-Signature: sha256=<hex> scheme (endpoints created before
webhooks v2; kept for one deprecation cycle) and return its body { id, event, createdAt, data }.
function verifyLegacy<T = LegacyWebhookEvent>(rawBody: RawBody, headers: HeadersLike, secret: WebhookSecret | readonly WebhookSecret[], opts: VerifyOptions = {}): T;verifyLegacyAsync()
Section titled “verifyLegacyAsync()”verifyLegacy on WebCrypto, for edge runtimes.
function verifyLegacyAsync<T = LegacyWebhookEvent>(rawBody: RawBody, headers: HeadersLike, secret: WebhookSecret | readonly WebhookSecret[], opts: VerifyOptions = {}): Promise<T>;generateTestHeaders()
Section titled “generateTestHeaders()”Sign a payload the way MoreVoice does, for testing your own webhook or tool endpoint:
const headers = webhooks.generateTestHeaders({ payload: body, secret: "whsec_…" });await fetch("http://localhost:3000/tools", { method: "POST", headers, body });Several secrets produce one “v1,…” entry each (what MoreVoice sends while a secret rotates).
function generateTestHeaders(p: { payload: string | Uint8Array; secret: WebhookSecret | readonly WebhookSecret[]; id?: string; timestamp?: number }): StandardWebhookHeaders;webhooks
Section titled “webhooks”The webhook helpers as one namespace (import { webhooks } from "@morevoice/sdk"; the client exposes mv.webhooks).
const webhooks: { constructEvent: typeof constructEvent; constructEventAsync: typeof constructEventAsync; verifySignature: typeof verifySignature; verifySignatureAsync: typeof verifySignatureAsync; verifyLegacy: typeof verifyLegacy; verifyLegacyAsync: typeof verifyLegacyAsync; generateTestHeaders: typeof generateTestHeaders;};HeadersLike
Section titled “HeadersLike”Anything headers can arrive as.
type HeadersLike = { get(name: string): string | null } | Readonly<Record<string, HeaderValue>> | Iterable<readonly [string, string]>;HeaderValue
Section titled “HeaderValue”type HeaderValue = string | readonly string[] | number | null | undefined;Exported from @morevoice/sdk (source: packages/sdk-ts/src/lib/event-envelope.ts).
EventEnvelope
Section titled “EventEnvelope”One event, as signed and delivered. TObject is the payload (data.object).
interface EventEnvelope<TType extends string = string, TObject = Record<string, unknown>> { /** `evt_…`; the same on every redelivery, so receivers dedupe on it (it is also the `webhook-id` header). */ id: string; object: "event"; type: TType; /** ISO-8601 UTC, second precision. */ created: string; api_version: string; livemode: boolean; /** `org_…` */ org_id: string; data: { object: TObject; previous_attributes?: Record<string, unknown> }; request?: { id: string | null; idempotency_key: string | null };}MoreVoiceEvent
Section titled “MoreVoiceEvent”Every event the API documents, discriminated by type: what webhooks.constructEvent returns.
switch (event.type) { case "call.analyzed": event.data.object.summary … } narrows the payload.
type MoreVoiceEvent = [KnownEvent] extends [never] ? EventEnvelope : KnownEvent;EventOf
Section titled “EventOf”The event of one type: EventOf<"call.ended">.
type EventOf<T extends keyof EventTypeMap> = EventTypeMap[T];UnknownEvent
Section titled “UnknownEvent”An event whose type this SDK does not know yet (a newer server): the envelope with an untyped payload.
type UnknownEvent = EventEnvelope<string, Record<string, unknown>>;AnyEvent
Section titled “AnyEvent”Any event, known or not (for code that stores or forwards events generically).
type AnyEvent = MoreVoiceEvent | UnknownEvent;isKnownEvent()
Section titled “isKnownEvent()”True when this SDK knows the event’s type (its payload is typed); false for types newer than the SDK.
function isKnownEvent(event: { type: string }): event is MoreVoiceEvent;LegacyWebhookEvent
Section titled “LegacyWebhookEvent”The pre-v2 body that endpoints still on api_version: "legacy" receive (signed with X-Webhook-Signature).
interface LegacyWebhookEvent { id: string; event: string; createdAt: string; data: Record<string, unknown>;}