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) => booleanconst 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) => booleanmatchPath("/webhooks/*"); // matches /webhooks/stripe, not /webhooks/stripe/events
matchPath("/webhooks/**"); // matches /webhooks/stripe/events/123
matchPath("/api/*/webhooks"); // matches /api/v1/webhooksmatchQueryParam
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) => booleanmatchBodyPath("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) => booleanmatchBodySubset({
type: "checkout.session.completed",
data: { object: { status: "complete" } },
});matchContentType
Match the Content-Type header, ignoring charset and other parameters.
matchContentType(type: string): (request: Request) => booleanmatchContentType("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) => booleanmatchAll(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) => booleanmatchAny(matchMethod("PUT"), matchMethod("PATCH"));matchVerified
Match requests whose server-side signature verification succeeded.
matchVerified(): (request: Request) => booleanconst req = await client.requests.waitFor(slug, {
match: matchAll(matchMethod("POST"), matchVerified()),
timeout: "30s",
});
// req.signatureVerified === trueRequires 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) => booleanconst failed = await client.requests.waitFor(slug, {
match: matchUnverified(),
timeout: "10s",
});
console.log(failed.signatureError); // structured error JSONmatchEmail
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| Criterion | Type | Matches |
|---|---|---|
tag | string | null | The address's +tag, exactly (tags are case-sensitive). null matches untagged mail only |
subject | string | RegExp | A string matches when the subject contains it; a RegExp is tested against the subject |
from | string | RegExp | Any From address. A string must equal the address (case-insensitive); a RegExp is tested against Name <address> |
to | string | RegExp | Any 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 EmailRequestconst 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 | nullconst 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.
extractLink
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 | nullconst 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): stringProvider 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";| Function | Match source | Event source |
|---|---|---|
isStripeWebhook(request) | stripe-signature | JSON type |
isGitHubWebhook(request) | x-github-event or x-hub-signature-256 | x-github-event |
isShopifyWebhook(request) | x-shopify-hmac-sha256 or x-shopify-topic | x-shopify-topic |
isSlackWebhook(request) | x-slack-signature | JSON type or form command |
isTwilioWebhook(request) | x-twilio-signature | none |
isPaddleWebhook(request) | paddle-signature | JSON event_type |
isLinearWebhook(request) | linear-signature | JSON action + type |
isSendGridWebhook(request) | JSON array with sg_event_id | first 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-timestamp | Discord interaction type |
isVercelWebhook(request) | x-vercel-signature | JSON type |
isGitLabWebhook(request) | x-gitlab-event or x-gitlab-token | normalized x-gitlab-event |
isTypeformWebhook(request) | typeform-signature | JSON event_type |
isStandardWebhook(request) | webhook-id AND webhook-timestamp AND webhook-signature | JSON type |
isMetaWebhook(request) | JSON object field | JSON object |
isLemonSqueezyWebhook(request) | JSON meta.event_name | JSON meta.event_name |
isCoinbaseCommerceWebhook(request) | x-cc-webhook-signature | JSON event.type |
isRazorpayWebhook(request) | x-razorpay-signature | JSON event |
isCalWebhook(request) | x-cal-signature-256 | JSON triggerEvent |
isIntercomWebhook(request) | x-hub-signature (sha1=) | JSON topic |
isTelegramWebhook(request) | x-telegram-bot-api-secret-token | none |
isSquareWebhook(request) | x-square-hmacsha256-signature | JSON type |
isHubSpotWebhook(request) | x-hubspot-signature-v3 | JSON 0.subscriptionType |
isMailgunWebhook(request) | JSON signature.token | JSON event-data.event |
isCalendlyWebhook(request) | calendly-webhook-signature | JSON event |
isMuxWebhook(request) | mux-signature | JSON type |
isSentryWebhook(request) | sentry-hook-signature | sentry-hook-resource |
isBitbucketWebhook(request) | x-event-key | x-event-key |
isDocuSignWebhook(request) | x-docusign-signature-1 | JSON event |
isAdyenWebhook(request) | JSON notificationItems.0.NotificationRequestItem.eventCode | JSON notificationItems.0.NotificationRequestItem.eventCode |
isPayPalWebhook(request) | paypal-transmission-sig | JSON event_type |
isPlaidWebhook(request) | plaid-verification | JSON webhook_code |
isResendWebhook(request) | svix-id AND JSON { type, created_at, data } with an email., contact., domain., suppression., topic., or inbox. type | JSON type |
isWorkOSWebhook(request) | workos-signature | JSON 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 | undefinedconst 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[]> | undefinedconst 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 | undefinedconst 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 | undefinedconst 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.