Skip to content

Matchers & Helpers

Filter, detect, and parse webhook requests and captured emails with built-in matchers for method, header, body, and email matching. Includes provider detection, JSON body parsing, and one-time code and link extraction.

Updated Oct 2026

Matchers

Matchers are predicate functions that return (request) => boolean. Use them with waitFor, waitForAll, and subscribe to filter incoming requests by method, headers, path, body content, and more.

import {
  matchMethod,
  matchHeader,
  matchPath,
  matchQueryParam,
  matchBodyPath,
  matchBodySubset,
  matchContentType,
  matchAll,
  matchAny,
  matchVerified,
  matchUnverified,
  matchEmail,
  isEmailRequest,
} from "@webhooks-cc/sdk";

matchMethod

Match by HTTP method. Comparison is case-insensitive.

matchMethod(method: string): (request: Request) => boolean
const req = await client.requests.waitFor(slug, {
  match: matchMethod("POST"),
});

matchHeader

Match by header presence, or by header name and exact value. Header names are matched case-insensitively. Values are matched with exact (case-sensitive) equality.

matchHeader(name: string, value?: string): (request: Request) => boolean
// Header exists (any value)
matchHeader("stripe-signature");
 
// Header exists with exact value
matchHeader("x-github-event", "push");

matchPath

Match request paths using glob-style wildcards. * matches a single path segment, ** matches multiple segments.

matchPath(pattern: string): (request: Request) => boolean
matchPath("/webhooks/*"); // matches /webhooks/stripe, not /webhooks/stripe/events
matchPath("/webhooks/**"); // matches /webhooks/stripe/events/123
matchPath("/api/*/webhooks"); // matches /api/v1/webhooks

matchQueryParam

Match query parameter presence or a specific value.

matchQueryParam(key: string, value?: string): (request: Request) => boolean
// Parameter exists (any value)
matchQueryParam("token");
 
// Parameter exists with exact value
matchQueryParam("token", "abc123");

matchBodyPath

Match a value at a dot-notation path into the parsed JSON body. Supports array indexing with numeric path segments.

matchBodyPath(path: string, value: unknown): (request: Request) => boolean
matchBodyPath("type", "checkout.session.completed");
matchBodyPath("data.object.id", "obj_123");
matchBodyPath("items.0.name", "Widget");

matchBodySubset

Deep partial match against the parsed JSON body. Returns true if every key/value in the subset exists in the body, recursively.

matchBodySubset(subset: Record<string, unknown>): (request: Request) => boolean
matchBodySubset({
  type: "checkout.session.completed",
  data: { object: { status: "complete" } },
});

matchContentType

Match the Content-Type header, ignoring charset and other parameters.

matchContentType(type: string): (request: Request) => boolean
matchContentType("application/json");
// matches "application/json; charset=utf-8"

matchAll

Logical AND -- returns true only when every matcher matches. Requires at least one matcher.

matchAll(...matchers: Array<(request: Request) => boolean>): (request: Request) => boolean
matchAll(matchMethod("POST"), matchHeader("content-type", "application/json"));

matchAny

Logical OR -- returns true when at least one matcher matches. Requires at least one matcher.

matchAny(...matchers: Array<(request: Request) => boolean>): (request: Request) => boolean
matchAny(matchMethod("PUT"), matchMethod("PATCH"));

matchVerified

Match requests whose server-side signature verification succeeded.

matchVerified(): (request: Request) => boolean
const req = await client.requests.waitFor(slug, {
  match: matchAll(matchMethod("POST"), matchVerified()),
  timeout: "30s",
});
// req.signatureVerified === true

Requires signature verification to be configured on the endpoint. Returns false for requests where verification was not configured (signatureVerified is null).

matchUnverified

Match requests whose server-side signature verification failed.

matchUnverified(): (request: Request) => boolean
const failed = await client.requests.waitFor(slug, {
  match: matchUnverified(),
  timeout: "10s",
});
console.log(failed.signatureError); // structured error JSON

matchEmail

Match captured emails by tag, subject, sender, or recipient. Every criterion you pass must match, and the matcher never matches an HTTP request.

matchEmail(criteria?: EmailCriteria): (request: Request) => boolean
CriterionTypeMatches
tagstring | nullThe address's +tag, exactly (tags are case-sensitive). null matches untagged mail only
subjectstring | RegExpA string matches when the subject contains it; a RegExp is tested against the subject
fromstring | RegExpAny From address. A string must equal the address (case-insensitive); a RegExp is tested against Name <address>
tostring | RegExpAny To or Cc address, or the address the email was delivered to, with the same rules as from
const request = await client.requests.waitFor(slug, {
  match: matchEmail({ tag: runId, subject: "Confirm your email" }),
});
 
// Composes with the other matchers
matchAll(matchEmail({ from: "[email protected]" }), matchEmail({ subject: /reset/i }));

client.emails.waitFor() and client.emails.list() take the same criteria directly and return typed EmailRequests, so reach for matchEmail when you wait through requests.waitFor(), captureDuring(), or your own filter.

matchEmail reads the parsed email, which requests from subscribe() do not carry. Streamed requests have kind, so check request.kind === "email" there and fetch the message with client.emails.get(request.id).

isEmailRequest

A type guard: true for a captured email that carries its parsed message, which narrows the request to EmailRequest.

isEmailRequest(request: Request): request is EmailRequest
const requests = await client.requests.list(slug);
const emails = requests.filter(isEmailRequest);
console.log(emails.map((email) => email.email.subject));

Combining matchers

Compose matchers to build precise filters for waitFor:

const req = await client.requests.waitFor(slug, {
  timeout: "10s",
  match: matchAll(
    matchMethod("POST"),
    matchHeader("stripe-signature"),
    matchBodyPath("type", "checkout.session.completed")
  ),
});

Nest matchAll and matchAny for complex logic:

const req = await client.requests.waitFor(slug, {
  timeout: "30s",
  match: matchAll(
    matchMethod("POST"),
    matchAny(matchBodyPath("type", "invoice.paid"), matchBodyPath("type", "invoice.payment_failed"))
  ),
});

Email helpers

Captured emails come with helpers that pick out what signup and login tests usually need: the one-time code and the link to click. They run the same finder as the dashboard's "Found in this email" strip and the codes and links of forwarded JSON, and they make no API call.

import { extractCode, extractLink, extractFromEmail, htmlToText } from "@webhooks-cc/sdk";

The same functions, the email types, and buildEmailJson are also exported from @webhooks-cc/sdk/email, an entry point with no network code that is small enough for a browser bundle.

extractCode

The first one-time code in an email, or null. Takes a captured email request, any request (null for HTTP requests), or the parsed email.

extractCode(input: Request | EmailCapture): string | null
const email = await client.emails.waitFor(slug, { tag: runId });
const code = extractCode(email); // "482913"

A code is 4 to 8 digits (also split in two, like 123 456, which comes back as "123456") or a 6 to 8 character mix of capital letters and digits. It only counts when a word such as "code", "verification", "PIN", "OTP", or "sign in" sits within a few words of it on the same line, or, for a code alone on its line, on the line just above. Years, order numbers, prices, and phone numbers stay out.

The link the email asks you to click (confirm, verify, activate, reset, magic link, sign in, accept an invite) when it has one, otherwise the first link worth keeping, or null when there is none: the same link the dashboard shows first. With actionOnly: true, only an action link counts.

extractLink(input: Request | EmailCapture, options?: { actionOnly?: boolean }): string | null
const link = extractLink(email);
if (link) await page.goto(link);
 
// Only a confirm, verify, reset or sign-in link, or null
const actionLink = extractLink(email, { actionOnly: true });

Unsubscribe, preference, privacy, terms, help and support, tracking, and social media links are never returned.

extractFromEmail

Every code and link found in a parsed email, best first: action links come before the rest, in the order they appear, up to 20 links.

extractFromEmail(email: Pick<EmailCapture, "subject" | "text" | "html">): EmailExtracts
// EmailExtracts: { codes: string[]; links: { url: string; label: string | null; action: boolean }[] }
const { codes, links } = extractFromEmail(email.email);

htmlToText

The HTML part as plain text: tags removed, block ends turned into line breaks. Useful for assertions on HTML-only emails.

htmlToText(html: string): string

Provider detection

Detector functions identify common providers from headers and, for providers without a distinctive signature header (SendGrid, Meta, Lemon Squeezy, Mailgun, Adyen), body shape. Captured emails are never detected as a provider, even when their headers look like one. Use detectWebhookInfo when you want the provider and event/topic, or the boolean helpers when you only need to route one provider.

import {
  detectWebhookInfo,
  detectWebhookProvider,
  isStripeWebhook,
  isGitHubWebhook,
  isShopifyWebhook,
  isSlackWebhook,
  isTwilioWebhook,
  isPaddleWebhook,
  isLinearWebhook,
  isSendGridWebhook,
  isClerkWebhook,
  isDiscordWebhook,
  isVercelWebhook,
  isGitLabWebhook,
  isTypeformWebhook,
  isStandardWebhook,
  isMetaWebhook,
  isLemonSqueezyWebhook,
  isCoinbaseCommerceWebhook,
  isRazorpayWebhook,
  isCalWebhook,
  isIntercomWebhook,
  isTelegramWebhook,
  isSquareWebhook,
  isHubSpotWebhook,
  isMailgunWebhook,
  isCalendlyWebhook,
  isMuxWebhook,
  isSentryWebhook,
  isBitbucketWebhook,
  isDocuSignWebhook,
  isAdyenWebhook,
  isPayPalWebhook,
  isPlaidWebhook,
  isResendWebhook,
  isWorkOSWebhook,
} from "@webhooks-cc/sdk";
FunctionMatch sourceEvent source
isStripeWebhook(request)stripe-signatureJSON type
isGitHubWebhook(request)x-github-event or x-hub-signature-256x-github-event
isShopifyWebhook(request)x-shopify-hmac-sha256 or x-shopify-topicx-shopify-topic
isSlackWebhook(request)x-slack-signatureJSON type or form command
isTwilioWebhook(request)x-twilio-signaturenone
isPaddleWebhook(request)paddle-signatureJSON event_type
isLinearWebhook(request)linear-signatureJSON action + type
isSendGridWebhook(request)JSON array with sg_event_idfirst array item's event
isClerkWebhook(request)svix-id (unless the body has the Resend shape)JSON type
isDiscordWebhook(request)x-signature-ed25519 AND x-signature-timestampDiscord interaction type
isVercelWebhook(request)x-vercel-signatureJSON type
isGitLabWebhook(request)x-gitlab-event or x-gitlab-tokennormalized x-gitlab-event
isTypeformWebhook(request)typeform-signatureJSON event_type
isStandardWebhook(request)webhook-id AND webhook-timestamp AND webhook-signatureJSON type
isMetaWebhook(request)JSON object fieldJSON object
isLemonSqueezyWebhook(request)JSON meta.event_nameJSON meta.event_name
isCoinbaseCommerceWebhook(request)x-cc-webhook-signatureJSON event.type
isRazorpayWebhook(request)x-razorpay-signatureJSON event
isCalWebhook(request)x-cal-signature-256JSON triggerEvent
isIntercomWebhook(request)x-hub-signature (sha1=)JSON topic
isTelegramWebhook(request)x-telegram-bot-api-secret-tokennone
isSquareWebhook(request)x-square-hmacsha256-signatureJSON type
isHubSpotWebhook(request)x-hubspot-signature-v3JSON 0.subscriptionType
isMailgunWebhook(request)JSON signature.tokenJSON event-data.event
isCalendlyWebhook(request)calendly-webhook-signatureJSON event
isMuxWebhook(request)mux-signatureJSON type
isSentryWebhook(request)sentry-hook-signaturesentry-hook-resource
isBitbucketWebhook(request)x-event-keyx-event-key
isDocuSignWebhook(request)x-docusign-signature-1JSON event
isAdyenWebhook(request)JSON notificationItems.0.NotificationRequestItem.eventCodeJSON notificationItems.0.NotificationRequestItem.eventCode
isPayPalWebhook(request)paypal-transmission-sigJSON event_type
isPlaidWebhook(request)plaid-verificationJSON webhook_code
isResendWebhook(request)svix-id AND JSON { type, created_at, data } with an email., contact., domain., suppression., topic., or inbox. typeJSON type
isWorkOSWebhook(request)workos-signatureJSON event

All header comparisons are case-insensitive. Discord and Standard Webhooks require multiple headers to be present simultaneously.

Provider + event example

const info = detectWebhookInfo(request);
 
if (info?.provider === "typeform") {
  console.log(info.event); // "form_response"
}
 
console.log(detectWebhookProvider(request)); // "typeform"

Router example

const request = await client.requests.waitFor(slug, { timeout: "10s" });
 
if (isStripeWebhook(request)) {
  // Verify Stripe signature and handle event
} else if (isGitHubWebhook(request)) {
  // Verify GitHub signature and handle event
} else if (isStandardWebhook(request)) {
  // Standard Webhooks request (Polar, Svix, Clerk, Resend)
} else {
  console.log("Unknown provider");
}

Body parsing

Four utilities for extracting structured data from captured request bodies.

import { parseJsonBody, parseFormBody, parseBody, extractJsonField } from "@webhooks-cc/sdk";

parseJsonBody

Safely parse a JSON request body. Returns undefined if the body is empty or not valid JSON. Never throws.

parseJsonBody(request: Request): unknown | undefined
const body = parseJsonBody(request);
if (body) {
  console.log(body);
}

parseFormBody

Parse an application/x-www-form-urlencoded body. Returns undefined if the content type does not match. Repeated keys are collected into arrays.

parseFormBody(request: Request): Record<string, string | string[]> | undefined
const form = parseFormBody(request);
if (form) {
  console.log(form.email); // "[email protected]"
  console.log(form.tags); // ["a", "b"] if repeated
}

parseBody

Auto-detect content type and parse accordingly. JSON and form-encoded bodies are decoded; other content types are returned as raw text.

parseBody(request: Request): unknown | Record<string, string | string[]> | string | undefined
const parsed = parseBody(request);

extractJsonField

Extract a nested field from the JSON body using dot-notation. Supports array indexing. Returns undefined if the body is missing, invalid JSON, or the path does not exist.

extractJsonField<T>(request: Request, path: string): T | undefined
const email = extractJsonField<string>(request, "data.customer.email");
const firstItem = extractJsonField<string>(request, "items.0.name");

matchJsonField

A convenience matcher that checks whether a top-level JSON field equals an expected value. Works like matchBodyPath but limited to top-level fields.

import { matchJsonField } from "@webhooks-cc/sdk";
 
const req = await client.requests.waitFor(slug, {
  match: matchJsonField("type", "checkout.session.completed"),
});

Learn more

API Reference

Complete method reference for the SDK.

Signature Verification

Verify webhook signatures for 32 signed providers.

Testing

CI/CD integration and testing patterns.