API Reference
Complete method reference for the TypeScript SDK. Endpoints, requests, emails, forwarding, streaming, matchers, and error handling with code examples.
Updated Nov 2026
Constructor
import { WebhooksCC } from "@webhooks-cc/sdk";
const client = new WebhooksCC({
apiKey: string, // Your API key. Required unless getAccessToken is given.
getAccessToken?: (o?: { forceRefresh?: boolean }) => Promise<string>,
// Optional. Supplies an expiring token instead (agents).
baseUrl?: string, // Optional. Override API base URL.
webhookUrl?: string, // Optional. Override webhook receiver URL.
timeout?: number, // Optional. Request timeout in ms (default: 30000).
retry?: RetryOptions, // Optional. Automatic retry configuration.
hooks?: ClientHooks, // Optional. Lifecycle hooks for observability.
emailDomain?: string, // Optional. Domain for emails.address() (default: mailhooks.cc).
fetch?: typeof fetch, // Optional. The fetch every call goes through (proxies, tests).
userAgent?: string, // Optional. Your product token, like "my-tool/1.2", sent before
// the SDK's (webhooks-cc-sdk/<version>) in the User-Agent.
});Retries are off by default (maxAttempts: 1). With retry, GET, PUT and DELETE calls are repeated after a network error, a timeout or a status in retryOn (429, 500, 502, 503 and 504 by default), and any call after a 429, which the API sends before doing anything. A POST or PATCH that failed any other way is not repeated: it may have gone through, and a second endpoints.create() would leave an extra endpoint. Nor is requests.clear() without before, whose repeat would also delete what arrived in between.
Endpoints
/endpointsclient.endpoints.create
Create a new webhook endpoint. The slug is auto-generated.
create(options?: CreateEndpointOptions): Promise<Endpoint>| Param | Type | Required | Description |
|---|---|---|---|
name | string | no | Display name |
ephemeral | boolean | no | Auto-delete after TTL |
expiresIn | string | number | no | Custom expiry duration, like "12h" or "7d"; implies ephemeral |
mockResponse | MockResponse | no | Mock response config |
responseRules | ResponseRule[] | no | Response rules, first match wins |
notificationUrl | string | no | Adds a Notifications destination: a chat message, at most one a second |
site | EndpointSite | no | Which site's dashboard lists it: "webhooks", "mailhooks" or "both" (the default). Listing only; both addresses work either way |
MockResponse fields:
| Field | Type | Required | Description |
|---|---|---|---|
status | number | yes | HTTP status code (100-599) |
body | string | no | Response body |
headers | Record<string, string> | no | Response headers |
delay | number | no | Response delay in ms (0-30000) |
/endpointsclient.endpoints.list
List the endpoints you own (each with sharedWith) and those shared with you through a subscribed team (each with fromTeam, the oldest share, and fromTeams, every one of your teams it is shared with), as one array. Each carries favorite, whether you marked it as a favourite.
list(options?: ListEndpointsOptions): Promise<Endpoint[]>| Param | Type | Required | Description |
|---|---|---|---|
team | string | no | Keep only endpoints tied to this team, matched via fromTeams, fromTeam, or sharedWith. A team id or a unique name (case-insensitive), resolved through teams.list(); an ambiguous name throws |
/endpoints/:slugclient.endpoints.get
Get a single endpoint by slug.
get(slug: string): Promise<Endpoint>/endpoints/:slugclient.endpoints.update
Update an endpoint's settings. Only the options you pass change.
update(slug: string, options: UpdateEndpointOptions): Promise<Endpoint>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
name | string | no | New display name |
mockResponse | MockResponse | null | no | Default response (see fields above), or null to clear |
responseRules | ResponseRule[] | null | no | Response rules, or null to clear |
notificationUrl | string | null | no | Sets the Notifications destination, or null removes it |
showEmailExtracts | boolean | no | Pick codes and links out of emails (owner only) |
site | EndpointSite | no | Which site's dashboard lists it (owner only) |
emailFields | EmailField[] | null | no | Named fields to pick out of every email, at most 20; replaces the list (owner only) |
layouts | Layout[] | null | no | Layouts that read the endpoint's emails before delivery, at most 50; replaces the list (owner only) |
retentionDays | number | null | no | Keep requests this many days (1 up to the plan's), or null for the plan's (owner only) |
deleteAfterDelivery | boolean | no | Delete each request once every delivery is settled (owner only) |
signingProvider | string | null | no | Provider for server-side signature verification, or null to turn it off |
signingSecret | string | null | no | The provider's signing secret (stored encrypted, never returned) |
signingHeader | string | null | no | The header name for the generic-hmac provider |
Mock response bodies and header values can hold placeholders such as {{ json.id }}, filled in from each request.
/endpoints/:slug/responses/previewclient.endpoints.previewResponse
A dry run: which response rule answers a captured request, and the response with its placeholders filled in, exactly as the receiver would send it. Nothing is sent.
previewResponse(slug: string, options?: PreviewResponseOptions): Promise<ResponsePreview>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
requestId | string | no | A captured request on the endpoint (default: the newest HTTP request) |
mockResponse | MockResponse | null | no | Try an unsaved default response; left out, the saved one applies |
responseRules | ResponseRule[] | null | no | Try unsaved rules; left out, the saved ones apply |
Returns rule ({ index, name } of the rule that answers, or null for the default response), response ({ status, headers, body, delay }, or null for the plain 200 OK), unknownPlaceholders (sent as written), email (true when the request is an email, which never gets a mock response) and request (the request it ran against; sample when none was captured yet).
const preview = await client.endpoints.previewResponse("payments", {
responseRules: [
{
conditions: [{ field: "body_path", op: "gt", path: "data.amount", value: "10000" }],
response: { status: 422, body: '{"amount": {{ json.data.amount }}}', headers: {} },
},
],
});The SDK also exports the rule language itself: evaluateConditions(group, subject) checks conditions against a request and renderTemplate(template, context) fills in placeholders, with the same results as the receiver.
/endpoints/:slug/layouts/previewclient.endpoints.previewLayouts
A dry run of the endpoint's layouts on a captured email: which layout matches and what it finds. Uses the endpoint's newest email unless requestId is given, and the saved layouts unless layouts are. Nothing is saved or sent and no parsed pages are used: attachments show an earlier parse, never a new one. Owner only.
previewLayouts(slug: string, options?: { layouts?: Layout[]; requestId?: string }): Promise<LayoutsPreview>
// Returns: { problems, layoutsCap, request, parsed }/endpoints/:slug/replayclient.endpoints.redeliver
Send the endpoint's requests received in a time range through its destinations again, at most 500, oldest first: after a pattern was fixed or a destination came back. Owner only.
redeliver(
slug: string,
range?: { since?: number; until?: number; kind?: "http" | "email" }
): Promise<{ requests: number; queued: number }>/endpoints/:slugclient.endpoints.delete
Delete an endpoint and all its captured requests.
delete(slug: string): Promise<void>/endpoints/:slug/favoriteclient.endpoints.favorite
Mark an endpoint as one of your favourites, and unfavorite to unmark it. A favourite is your own choice, never the endpoint's, so you can mark endpoints shared with you through a team too. Both are idempotent and return the new state; list() and get() report it as favorite. They need an account: sandbox tokens are refused.
favorite(slug: string): Promise<{ favorite: boolean }> // PUT /endpoints/:slug/favorite
unfavorite(slug: string): Promise<{ favorite: boolean }> // DELETE /endpoints/:slug/favoriteawait client.endpoints.favorite("payments");
const favourites = (await client.endpoints.list()).filter((e) => e.favorite);/w/:slugclient.endpoints.send
Send a test webhook to an endpoint. Sends directly to the receiver, does not go through the API.
send(slug: string, options?: SendOptions): Promise<Response>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
method | string | no | HTTP method (default: "POST") |
headers | Record<string, string> | no | HTTP headers to include |
body | unknown | no | Request body (JSON-serialized) |
/w/:slugclient.endpoints.sendTemplate
Send a provider template webhook with generated signature headers.
sendTemplate(slug: string, options: SendTemplateOptions): Promise<Response>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
provider | TemplateProvider | yes | Provider template to generate |
template | string | no | Provider-specific template preset |
secret | string | yes | Shared secret for signature generation; not needed for unsigned templates (secretRequired: false, such as sendgrid, discord, plaid) |
event | string | no | Provider event/topic override |
method | string | no | HTTP method override (defaults to POST) |
timestamp | number | no | Unix timestamp override for signature generation |
messageId | string | no | Message id for standard-webhooks, clerk and resend signatures (random by default); not in SDK 1.15.0 or earlier |
headers | Record<string, string> | no | Additional headers |
body | unknown | no | Template body override |
Direct Delivery
(any URL)client.sendTo
Send a webhook directly to any URL with optional provider signing. Use it for local integration testing: send properly signed webhooks to localhost handlers without routing through webhooks.cc.
sendTo(url: string, options?: SendToOptions): Promise<Response>| Param | Type | Required | Description |
|---|---|---|---|
url | string | yes | Target URL (http or https) |
provider | TemplateProvider | no | Provider template for signing |
template | string | no | Provider-specific template preset, like "checkout.session.completed"; its sample body is sent when body is left out |
secret | string | no | Secret for signature generation (required when provider is set) |
body | unknown | no | Request body (JSON-serialized if not a string) |
headers | Record<string, string> | no | Additional headers |
method | string | no | HTTP method (default: "POST") |
event | string | no | Event name |
timestamp | number | no | Unix timestamp override for deterministic signatures in tests |
messageId | string | no | Message id for standard-webhooks, clerk and resend signatures (random by default); with timestamp, makes them deterministic. Not in SDK 1.15.0 or earlier |
(build only)client.buildRequest
Build a signed webhook request without sending it. Returns the computed URL, method, headers, and body.
buildRequest(url: string, options?: SendToOptions): Promise<{url, method, headers, body?}>Requests
/endpoints/:slug/requestsclient.requests.list
List captured requests for an endpoint.
list(endpointSlug: string, options?: ListRequestsOptions): Promise<Request[]>| Param | Type | Required | Description |
|---|---|---|---|
endpointSlug | string | yes | Endpoint slug |
limit | number | no | Max results (default: 50) |
since | number | no | Only return requests after this timestamp (ms) |
kind | "http" | "email" | no | Only HTTP requests, or only emails |
/endpoints/:slug/requestsclient.requests.listPaginated
List captured requests with cursor-based pagination.
listPaginated(slug: string, options?: ListPaginatedRequestsOptions): Promise<PaginatedResult<Request>>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
limit | number | no | Max results per page |
cursor | string | no | Cursor from previous page |
kind | "http" | "email" | no | Only HTTP requests, or only emails |
/requests/:idclient.requests.get
Get a single captured request by ID. For a captured email, kind is "email" and email holds the parsed message; client.emails.get() returns the same request typed as an EmailRequest.
get(requestId: string): Promise<Request>When the endpoint has layouts, parsed holds what they found once the parse is done.
/requests/:id/rawclient.requests.raw
A captured email's exact message (RFC 5322 bytes), and attachment() one of its attachments by its index in email.attachments, from 0 to 99. Both work for emails kept in storage (email.stored).
raw(requestId: string): Promise<Uint8Array>
attachment(requestId: string, index: number): Promise<AttachmentDownload> // { bytes, filename, contentType }/requests/:id/replayclient.requests.redeliver
Send a captured request through its endpoint's destinations again, with their current settings. True when at least one delivery was queued. To send it to one destination only, use forwarding.redeliver().
redeliver(requestId: string): Promise<boolean>/endpoints/:slug/requestsclient.requests.waitFor
Poll for incoming requests until one matches or timeout expires. Accepts human-readable duration strings.
waitFor(endpointSlug: string, options?: WaitForOptions): Promise<Request>| Param | Type | Required | Description |
|---|---|---|---|
endpointSlug | string | yes | Endpoint slug to monitor |
timeout | number | string | no | Max wait time: ms or "30s", "5m", "1h" (default: 30000) |
pollInterval | number | string | no | Interval between polls (default: 500) |
match | (request) => boolean | no | Filter function |
since | number | no | Only consider requests after this timestamp in ms (default: 5 minutes ago) |
The five-minute lookback means a request that arrived just before the call is still found. Pass since: Date.now() to wait only for new requests.
/endpoints/:slug/requestsclient.requests.waitForAll
Wait for multiple matching requests to arrive.
waitForAll(slug: string, options: WaitForAllOptions): Promise<Request[]>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
count | number | yes | Number of requests to collect |
timeout | number | string | no | Max wait time |
match | (request) => boolean | no | Filter function |
since | number | no | Only consider requests after this timestamp (ms) |
/api/stream/:slugclient.requests.subscribe
Stream incoming requests via SSE as an async iterator.
subscribe(slug: string, options?: SubscribeOptions): AsyncIterable<Request>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
signal | AbortSignal | no | Signal to cancel the subscription |
timeout | number | string | no | Max stream duration |
reconnect | boolean | no | Auto-reconnect on disconnect |
maxReconnectAttempts | number | no | Max reconnection attempts (default: 5) |
reconnectBackoffMs | number | string | no | Base backoff between reconnects (default: 1000) |
idleTimeout | number | string | no | Treat the stream as dead after this long without data (default: "90s"; 0 turns it off) |
onReconnect | (attempt) => void | no | Called before each reconnect attempt |
Streamed requests carry kind, so you can tell emails apart, but not the parsed email. Read it with client.emails.get(request.id).
/requests/:id/replayclient.requests.replay
Replay a captured request to a target URL. Sends the original method, headers, and body. Hop-by-hop headers are stripped. Throws for a captured email, which is not an HTTP request; to post an email to a handler, send client.emails.toJson(email) with sendTo().
replay(requestId: string, targetUrl: string): Promise<Response>| Param | Type | Required | Description |
|---|---|---|---|
requestId | string | yes | ID of the captured request |
targetUrl | string | yes | URL to send the replayed request to |
/requests/searchclient.requests.search
Search captured webhook requests across endpoints with full-text search.
search(filters?: SearchFilters): Promise<SearchResult[]>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | no | Filter by endpoint slug |
method | string | no | Filter by HTTP method |
kind | "http" | "email" | no | Only HTTP requests, or only emails |
q | string | no | Free-text search |
from | string | number | no | Start time (duration or timestamp) |
to | string | number | no | End time (duration or timestamp) |
limit | number | no | Max results (default: 50) |
offset | number | no | Pagination offset |
order | "asc" | "desc" | no | Sort order (default: "desc") |
/requests/countclient.requests.count
Count captured webhook requests matching the given filters.
count(filters?: SearchFilters): Promise<number>/endpoints/:slug/requestsclient.requests.clear
Delete captured requests for an endpoint without deleting the endpoint itself.
clear(slug: string, options?: ClearRequestsOptions): Promise<void>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
before | string | number | no | Delete requests before this duration/timestamp |
/endpoints/:slug/requests/exportclient.requests.export
Export captured requests as HAR 1.2 or cURL commands. Emails are skipped: they have no HTTP request to export.
export(slug: string, options: ExportRequestsOptions): Promise<RequestsExport>| Param | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Endpoint slug |
format | "har" | "curl" | yes | Export format |
limit | number | no | Max requests to export |
since | number | no | Only export requests after this timestamp |
Emails
Every endpoint on an account receives email at <slug>@mailhooks.cc, and at <slug>+<tag>@mailhooks.cc for any tag. Guest endpoints receive none. A captured email is a request with kind: "email", method: "EMAIL", the recipient address as path, the raw message as body, and the parsed message as email; the SDK types it as EmailRequest. Each email counts as one request against your quota. See Email capture for what is stored.
Use a fresh tag per test run, so each run finds its own email:
import { extractCode } from "@webhooks-cc/sdk";
const runId = `signup-${Date.now()}`;
await signUp(client.emails.address(slug, runId)); // your app sends the email
const email = await client.emails.waitFor(slug, { tag: runId });
const code = extractCode(email); // "482913"list, latest, waitFor and waitForAll filter by the same criteria. Every criterion you pass must match:
| 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 |
(local)client.emails.address
The address an endpoint receives email at, with an optional tag. Pass the endpoint object (from endpoints.create() or endpoints.get()) to use the address the server reports; a slug builds the address with the client's emailDomain. Throws on an invalid slug or tag. A tag is printable ASCII without spaces, quotes, brackets, @, ,, ; or :, and the slug, the + and the tag together are at most 64 characters.
address(endpoint: string | Pick<Endpoint, "slug" | "emailAddress">, tag?: string): stringclient.emails.address("my-app"); // "my-app@mailhooks.cc"
client.emails.address("my-app", "run-42"); // "my-app+run-42@mailhooks.cc"/endpoints/:slug/requests?kind=emailclient.emails.list
Emails an endpoint captured, newest first, filtered by the criteria above.
list(endpointSlug: string, options?: ListEmailsOptions): Promise<EmailRequest[]>| Param | Type | Required | Description |
|---|---|---|---|
endpointSlug | string | yes | Endpoint slug |
tag, subject, from, to | criteria | no | See the criteria table |
limit | number | no | How many of the newest emails to fetch before filtering (default: 50) |
since | number | no | Only emails received after this timestamp (ms) |
/requests/:idclient.emails.get
One captured email by request ID, with the endpoint's named fields as emailFields and the reply text as replyText. Throws NotFoundError when the request is not an email.
get(requestId: string): Promise<EmailRequest>/endpoints/:slug/requests?kind=emailclient.emails.latest
The newest email matching the criteria, or null. Looks at the 100 newest emails.
latest(endpointSlug: string, criteria?: EmailCriteria): Promise<EmailRequest | null>/endpoints/:slug/requests?kind=emailclient.emails.waitFor
Poll until an email matching the criteria arrives. Throws TimeoutError when none does.
waitFor(endpointSlug: string, options?: WaitForEmailOptions): Promise<EmailRequest>| Param | Type | Required | Description |
|---|---|---|---|
endpointSlug | string | yes | Endpoint slug |
tag, subject, from, to | criteria | no | See the criteria table |
timeout | number | string | no | Max wait time, ms or a duration like "30s" (default: 60000) |
pollInterval | number | string | no | Interval between polls (default: 1000) |
since | number | no | Only emails received after this timestamp in ms (default: 5 minutes ago) |
match | (email: EmailRequest) => boolean | no | Extra filter on top of the criteria |
The five-minute lookback finds an email that arrived just before the call. Use a fresh tag, or pass since: Date.now() before triggering the email, so an older email cannot match.
/endpoints/:slug/requests?kind=emailclient.emails.waitForAll
Poll until count matching emails have arrived. Returns them oldest first. Takes the same options as waitFor, plus count.
waitForAll(endpointSlug: string, options: WaitForEmailsOptions): Promise<EmailRequest[]>/send-test-emailclient.emails.sendTest
Deliver a sample email with a six-digit code and a link to an endpoint, optionally to a tagged address. It is captured and counted like any other email, but it does not travel over SMTP, so no sender checks run on it and email.smtp.test is true. Rate limited to 10 per minute.
sendTest(endpointSlug: string, options?: { tag?: string }): Promise<SendTestEmailResult>
// Returns: { status: "captured", requestId: string | null }(local)client.emails.toJson
The email.received JSON that forwarding would post for an email: an EmailReceivedEvent. No API call is made. Pass endpoint to fill in data.endpoint.name; without it the slug is read from the address and the name is null. Set includeExtracts: false to leave out codes and links, as forwarding does when the endpoint turned them off.
toJson(
email: EmailRequest,
options?: {
endpoint?: { slug: string; name?: string | null };
includeExtracts?: boolean;
fields?: Record<string, string | null> | null; // default: email.emailFields
}
): EmailReceivedEvent(local)client.emails.toInbound
The email in the shape an inbound mail service posts it, to test the handler you wrote for that service: postmark, sendgrid (a multipart form), mailgun (a form, signed when you pass your Mailgun signing key) or cloudmailin. No API call is made. Post body with contentType to your handler. See In a mail provider's shape.
toInbound(
email: EmailRequest,
format: "postmark" | "sendgrid" | "mailgun" | "cloudmailin",
options?: { mailgunSigningKey?: string }
): Promise<{ contentType: string; body: string }>/requests/:id/renderclient.emails.render
The same, rendered by webhooks.cc (what whk tunnel and the Python and Go SDKs use), or the email.received JSON with format: "json".
render(
requestId: string,
options: { format: "json" | "postmark" | "sendgrid" | "mailgun" | "cloudmailin"; mailgunSigningKey?: string }
): Promise<{ format: string; method: "POST"; headers: Record<string, string>; body: string }>Codes and links
extractCode, extractLink and extractFromEmail run the same finder as the dashboard's "Found in this email" strip and the codes and links of forwarded JSON. extractCode and extractLink take an EmailRequest, any Request (they return null for HTTP requests), or the parsed email; extractFromEmail takes the parsed email. None of them makes an API call.
import { extractCode, extractLink, extractFromEmail } from "@webhooks-cc/sdk";
extractCode(email); // "482913", or null
extractLink(email); // the confirm, verify, reset or sign-in link, else the first link worth keeping
extractLink(email, { actionOnly: true }); // only an action link, or null
extractFromEmail(email.email); // { codes: string[], links: { url, label, action }[] }A code is only taken when a word like "code", "verification" or "sign in" sits next to it, so order numbers, prices and years stay out. See Matchers & Helpers for the rules.
The email entry point
@webhooks-cc/sdk/email exports the email types, the code and link finder (extractCode, extractLink, extractFromEmail, htmlToText), buildEmailJson, emailAddress and isValidEmailTag. It has no network code and imports no other SDK module, so it is small enough for a browser bundle.
import { emailAddress, extractCode, type EmailReceivedEvent } from "@webhooks-cc/sdk/email";
emailAddress("my-app", "run-42"); // "my-app+run-42@mailhooks.cc"Forwarding
Forwarding sends what an endpoint captures to up to 10 destinations, each with its own kinds, conditions, format, timing and signing secret, queued, retried and logged. client.forwarding manages the endpoint's primary destination (the one its forwardUrl stands for) and reads the delivery log of all of them; client.destinations manages each one. Only the endpoint's owner can manage forwarding: for anyone else these methods throw NotFoundError, and configure() fails with a 403 for team members. In your handler, verify relayed requests with verifyRelayedRequest(), signed JSON and your own JSON with verifyForwardedRequest() or verifyForwardedEmail().
await client.forwarding.configure(slug, {
url: "https://api.example.com/hooks",
http: true,
headers: [{ name: "Authorization", value: `Bearer ${process.env.HOOK_TOKEN}` }],
sentField: "publishedAt",
});
const secret = await client.forwarding.secret(slug); // put it in your handler
const result = await client.forwarding.test(slug, { kind: "http" });
if (result.delivered) {
await client.forwarding.configure(slug, { enabled: true });
}/endpoints/:slugclient.forwarding.configure
Change the primary destination's settings and return the updated endpoint. Only the options you pass change. Saving the first URL creates the endpoint's signing secret, so you can send a test delivery before turning forwarding on. Turning forwarding, or one kind of it, off marks the deliveries still waiting as failed. Throws when no option is given.
configure(endpointSlug: string, options: ConfigureForwardingOptions): Promise<Endpoint>| Param | Type | Required | Description |
|---|---|---|---|
endpointSlug | string | yes | Endpoint slug |
url | string | null | no | An https URL on a public host name, or null to remove it (forwarding must be off) |
enabled | boolean | no | Turn forwarding on or off. Turning it on needs a URL, sent now or saved before |
http | boolean | no | Forward HTTP requests |
email | boolean | no | Forward emails |
format | "auto" | "as_received" | "json" | "chat" | no | How deliveries go out; "auto" picks chat for Slack and Discord URLs and as received otherwise. See Formats |
headers | { name: string; value: string | null }[] | no | Your own headers, at most 10, for relayed and signed JSON deliveries. Replaces the whole list; null keeps the value stored under that name |
appendPath | boolean | no | Add the path after the slug to the URL for relayed requests (default true) |
retrySeconds | 0 | 3600 | 86400 | no | Retry failed deliveries for a day (default), an hour, or not at all |
keepOrder | boolean | no | One delivery at a time, in capture order (default false) |
sentField | string | null | no | A JSON body field with the sender's own timestamp, such as "publishedAt" or "data.publishedAt". See Timing |
/endpoints/:slug/forwardingclient.forwarding.secret
The endpoint's signing secret (whsec_...). Throws NotFoundError until a URL has been saved.
secret(endpointSlug: string): Promise<string>/endpoints/:slug/forwardingclient.forwarding.rotateSecret
Replace the signing secret and return the new one. Every delivery from then on, retries included, is signed with it, so update your handler right away.
rotateSecret(endpointSlug: string): Promise<string>/endpoints/:slug/forwarding/testclient.forwarding.test
Send the newest captured request of a kind (or a sample, when none has arrived) to the saved URL once, exactly as a delivery would go out, and report the answer. kind defaults to "http" when HTTP requests are forwarded, otherwise "email". Works with forwarding on or off; nothing is retried or logged. Fails with a 400 when no URL is saved. Rate limited to 10 per minute.
test(endpointSlug: string, options?: { kind?: "http" | "email" }): Promise<ForwardingTestResult>
// Returns: { status, durationMs, excerpt, error, delivered, sample, kind, format, url, target, request, sender }/endpoints/:slug/deliveriesclient.forwarding.deliveries
The endpoint's delivery log, newest first. Each row has its destination ({ id, name, url }), the status, your server's last answer and time, the format, the request's method and path (or an email's subject), when it was received, and senderAt when the request carried the sender's own timestamp.
deliveries(endpointSlug: string, options?: ListDeliveriesOptions): Promise<DeliveryLogEntry[]>| Param | Type | Required | Description |
|---|---|---|---|
endpointSlug | string | yes | Endpoint slug |
limit | number | no | 1 to 100 (default: 5) |
status | "all" | "pending" | "failed" | "skipped" | no | "pending" is queued or retrying, "skipped" filtered out (default: all) |
destinationId | string | no | Only this destination's deliveries |
before | string | no | A row's cursor, to list the rows after it |
/endpoints/:slug/deliveries/summaryclient.forwarding.summary
Delivered and failed among the last 24 hours, waiting now, and the failed and total counts the log's filters show. Each count stops at 100,000.
summary(endpointSlug: string): Promise<DeliverySummary>
// Returns: { last24h: { delivered, failed }, pending, failed, total }/requests/:id/deliveriesclient.forwarding.requestDeliveries
Every delivery of one captured request or email, with each try in attemptLog. Anyone who can read the request can call this; team members get the destination's host without its path. emailDeliveries() is the older name for the same call.
requestDeliveries(requestId: string): Promise<ForwardingDelivery[]>/requests/:id/deliveriesclient.forwarding.redeliver
Queue a request or email for delivery again with the current settings, to every destination that takes it, and return the new delivery's id. It also sends one that was never forwarded. With destinationId, it goes to that destination only, whatever its filter says (the dashboard's Send anyway). Fails with a 409 when no destination takes it. Rate limited to 30 per minute.
redeliver(requestId: string, options?: { destinationId?: string }): Promise<{ id: string }>/endpoints/:slug/deliveries/redeliver-failedclient.forwarding.redeliverFailed
Queue every request whose latest delivery failed again, oldest first, with each destination's current settings; with destinationId, only that destination's. At most 1,000 deliveries wait for a destination at a time, so queued can be lower than the failed count; call it again once those are through. Fails with a 409 when no destination is on. Rate limited to 10 per minute.
redeliverFailed(endpointSlug: string, options?: { destinationId?: string }): Promise<{ queued: number }>Destinations
Each destination of an endpoint decides what it gets (http, email, a filter), how it is sent (format: "as_received", "json", "template" with your bodyTemplate, or "chat" with an optional messageTemplate, "sheets" rows in a Google Sheet, or "file_drop" files on your own server) and when (throttleSeconds, delaySeconds). See Forwarding for what each does.
const zap = await client.destinations.create(slug, {
url: "https://hooks.zapier.com/hooks/catch/123/abc",
email: false,
filter: { conditions: [{ field: "body_path", op: "eq", path: "type", value: "invoice.paid" }] },
format: "template",
bodyTemplate: { invoice: "{{ json.data.object.id }}", amount: "{{ json.data.object.amount_paid }}" },
});
const preview = await client.destinations.preview(slug, { destinationId: zap.id });
// preview.filterPasses, preview.delivery?.body/endpoints/:slug/destinationsclient.destinations.list
The endpoint's destinations in order, header values masked. With summary: true, each has summary: its last 24 hours (delivered, failed, skipped) and what is waiting.
list(endpointSlug: string, options?: { summary?: boolean }): Promise<Destination[]>
get(endpointSlug: string, destinationId: string): Promise<Destination>/endpoints/:slug/destinationsclient.destinations.create
Add a destination, up to 10 per endpoint. It is on unless enabled: false, and gets a signing secret of its own. Templates and filters are checked before the request is sent, so a mistake throws with the field it is in.
create(endpointSlug: string, input: DestinationCreateInput): Promise<Destination>
// DestinationCreateInput: DestinationInput with a url, or a file drop's format and config| Param | Type | Required | Description |
|---|---|---|---|
url | string | yes | An https URL on a public host name, or a Slack or Discord webhook (not for a file drop) |
name | string | null | no | For the list and the log |
enabled | boolean | no | Send to it (default true) |
http, email | boolean | no | Which kinds it takes (default both) |
filter | { logic?: "and" | "or"; conditions: Condition[] } | null | no | Send only matching captures. See Send only some captures |
format | DestinationFormat | no | Default "auto": chat for Slack and Discord URLs, as received otherwise. Also "as_received", "json", "template", "chat", "sheets" (Google Sheets), "file_drop" (File drops, Business), and for emails a mail provider's shape: "postmark", "sendgrid", "mailgun", "cloudmailin" |
mailgunSigningKey | string | null | no | With "mailgun": your Mailgun signing key, so signature verifies (write-only; hasMailgunSigningKey says one is saved) |
bodyTemplate | JSON | no | With "template": the body; its strings hold {{ placeholders }} (Your own JSON) |
messageTemplate | string | null | no | With "chat": your own message text; null sends the standard message |
headers | { name: string; value: string | null }[] | no | Your own headers, at most 10 (not for chat). Replaces the list; null keeps a stored value |
appendPath | boolean | no | Add the captured path to the URL for relayed requests (default true) |
retrySeconds | 0 | 3600 | 86400 | no | Retry failed deliveries for a day (default), an hour, or not at all |
keepOrder | boolean | no | One delivery at a time, in capture order |
sentField | string | null | no | A JSON body field with the sender's own timestamp |
throttleSeconds | number | no | At most one delivery every so many seconds (0 to 3,600); the rest are not sent |
delaySeconds | number | no | Wait this long after the capture before sending (0 to 3,600) |
attachments | "links" | "inline" | "none" | no | How a stored email's attachments travel: signed links (default), inline base64, or names only |
config | SheetsConfig | FileDropConfig | null | no | With "sheets": the spreadsheet, tab and columns; with "file_drop": the server, folder and file layout |
dropSecret | { password?, privateKey?, secretAccessKey? } | null | no | With "file_drop": the login secret, write-only, needed again when the server or login changes |
position | number | no | Its place in the list |
/endpoints/:slug/destinations/:idclient.destinations.update
Change a destination; only the fields you pass change. Switching it off or taking a kind away marks what is waiting for it as failed.
update(endpointSlug: string, destinationId: string, input: DestinationInput): Promise<Destination>/endpoints/:slug/destinations/:idclient.destinations.delete
Remove a destination. Deliveries still waiting for it are marked failed; its log stays.
delete(endpointSlug: string, destinationId: string): Promise<void>/endpoints/:slug/destinations/orderclient.destinations.reorder
Put the destinations in this order (every id, once) and return them. Captures are queued for destinations in this order.
reorder(endpointSlug: string, destinationIds: string[]): Promise<Destination[]>/endpoints/:slug/destinations/:id/secretclient.destinations.secret
The destination's signing secret (whsec_...); rotateSecret() replaces it and returns the new one, used by every delivery from then on.
secret(endpointSlug: string, destinationId: string): Promise<string>
rotateSecret(endpointSlug: string, destinationId: string): Promise<string>/endpoints/:slug/destinations/:id/testclient.destinations.test
Send one delivery now, exactly as a real one would go out: requestId, or the newest request of kind, or a sample. Its filter is reported in filterPasses, not applied; nothing is retried or logged. Rate limited to 10 per minute.
test(
endpointSlug: string,
destinationId: string,
options?: { kind?: "http" | "email"; requestId?: string }
): Promise<DestinationTestResult>
// Returns what forwarding.test() does, plus filterPasses/endpoints/:slug/destinations/previewclient.destinations.preview
A dry run against a captured request: whether the destination takes its kind (takesKind), whether its filter lets it through (filterPasses), and the exact request it would send (delivery: method, URL, headers with stored values masked, body), or an error such as a template that cannot be filled in. Pass a saved destinationId, unsaved fields in destination (on top of the saved ones, or with a url on their own), or both. Nothing is sent.
preview(endpointSlug: string, options: PreviewDestinationOptions): Promise<DestinationPreview>Alerts
/endpoints/:slug/alertsclient.alerts
An endpoint's alerts: when its email fields stop finding values, mail stops arriving, or a delivery fails for good. get() returns the settings and the recent alerts, open (resolvedAt: null) or resolved; update() changes only the settings you pass and returns them.
get(slug: string): Promise<{ settings: AlertSettings; events: AlertEvent[] }>
update(slug: string, settings: Partial<AlertSettings>): Promise<AlertSettings>
// AlertSettings: { enabled, fieldFailures, fieldFailureShare, requiredFields, silenceHours,
// destinationFailures, notifyEmail, notifyDestinationIds }await client.alerts.update(slug, { enabled: true, silenceHours: 24, notifyEmail: true });Templates
(local)client.templates.listProviders
List all supported webhook provider templates. No API call made.
listProviders(): TemplateProvider[]
// Returns: ["stripe", "github", "shopify", "twilio", "slack", "paddle", "linear", "sendgrid", "clerk", "discord", "vercel", "gitlab", "typeform", "standard-webhooks", "meta", "lemonsqueezy", "coinbase-commerce", "razorpay", "cal", "intercom", "telegram", "square", "hubspot", "mailgun", "calendly", "mux", "sentry", "bitbucket", "docusign", "adyen", "paypal", "plaid", "resend", "workos"](local)client.templates.get
Get metadata for a specific provider template.
get(provider: TemplateProvider): TemplateProviderInfoUsage
/usageclient.usage
Get current request quota and usage.
usage(): Promise<UsageInfo>
// Returns: { used, limit, remaining, plan, periodEnd }Teams
A key has exactly the team access of the account it belongs to. Endpoints shared with you already show up in endpoints.list() and work with every request method by slug except requests.clear, which stays with the owner; this namespace manages the teams themselves.
/teamsclient.teams.list
List the teams you own or belong to, with role, seats, pooled usage, and subscription state.
list(): Promise<Team[]>
// Team: { id, name, role, seats, memberCount, suspended, subscriptionStatus,
// requestsUsed, requestLimit, periodEnd, cancelAtPeriodEnd, ... }/teams/:teamId/membersclient.teams.members
List a team's members and pending invites. Any member may call this.
members(teamId: string): Promise<TeamMembers>
// Returns: { members: TeamMember[], pendingInvites: TeamInvite[] }/teams/:teamId/endpointsclient.teams.share
Share an endpoint you own with a team. Resolves the slug to an endpoint id for you. The team needs an active subscription; the endpoint's requests then draw from the team's pooled quota.
share(teamId: string, slug: string): Promise<void>/teams/:teamId/endpoints/:endpointIdclient.teams.unshare
Stop sharing an endpoint you own with a team. Works on suspended teams too.
unshare(teamId: string, slug: string): Promise<void>/teams/:teamId/inviteclient.teams.invite
Invite an email address to a team you own. Sends an invite email; accepting claims one of the team's paid seats.
invite(teamId: string, email: string): Promise<TeamInvite>/invitesclient.teams.invites
Invites waiting for you, and the calls to answer them.
invites.list(): Promise<TeamInvite[]>
invites.accept(inviteId: string): Promise<void>
invites.decline(inviteId: string): Promise<void>Flow Builder
(chained)client.flow
Create a fluent workflow builder for common webhook capture, verify, and replay workflows.
const result = await client
.flow()
.createEndpoint({ name: "test" })
.send({ body: { event: "test" } })
.waitForCapture({ timeout: "10s" })
.verifySignature({ provider: "stripe", secret: "whsec_..." })
.replayTo("http://localhost:3000/webhooks")
.cleanup()
.run();Sandbox and Agent Registration
AI agents can use webhooks.cc without an API key. WebhooksCC.sandbox() registers through auth.md, solves the proof-of-work challenge (about 3 seconds), and returns a sandbox client: up to 3 endpoints, 25 captured requests each and 100 in all, deleted 24 hours after registration. See AI Agents for the limits and how a human connects an agent.
/api/agent/identityWebhooksCC.sandbox
const sandbox = await WebhooksCC.sandbox({ clientName: "my-agent" });
const endpoint = await sandbox.endpoints.create(); // endpoint.url captures webhooks
const request = await sandbox.requests.waitFor(endpoint.slug, { timeout: "60s" });
await sandbox.endpoints.list();
await sandbox.requests.list(endpoint.slug, { limit: 10 });
await sandbox.endpoints.delete(endpoint.slug); // frees a slot; the budget is not refundedSandbox endpoints only capture: no mock responses, notifications, signature verification, email or forwarding. Refusals throw SandboxError with the server's code: sandbox_endpoint_limit at 3 endpoints, sandbox_full when the shared sandbox is full. The access token renews itself.
/api/agent/identity/claimsandbox.claim / sandbox.waitForClaim
Connects the agent to a human's account. Show them the link and the code together; only someone signed in with that email can complete it.
const { verificationUri, userCode } = await sandbox.claim({ email: "dev@example.com" });
console.log(`Open ${verificationUri}, sign in, and enter ${userCode}`);
const client = await sandbox.waitForClaim(); // a WebhooksCC acting for that account
const endpoints = await client.endpoints.list(); // includes the sandbox endpointswaitForClaim() rejects with AgentAuthError expired_token when the 15-minute code runs out (call claim() again for a new one), or access_denied when the human declines. After the claim the sandbox client stops working; the returned client renews its token from the claimed assertion for 90 days.
(credential file)WebhooksCC.connected / WebhooksCC.connect
The connection is saved in the agent credential file that the MCP server, the CLI and the Python and Go SDKs share (~/.config/whk/agent.json, or WHK_AGENT_FILE), so a later process picks it up. connected() returns a client for the connected account, or null when there is none or the human has disconnected the agent; fromSavedConnection() is the synchronous form that does not check first. connect() connects an agent to a human's account without a sandbox.
const client = (await WebhooksCC.connected()) ?? (await WebhooksCC.sandbox());
const pending = await WebhooksCC.connect({ email: "dev@example.com" });
console.log(`Open ${pending.verificationUri}, sign in, and enter ${pending.userCode}`);
const account = await pending.wait();(protocol)WebhooksCC.agent
The protocol steps one by one, for callers who drive auth.md themselves:
WebhooksCC.agent.discover({ baseUrl? })
WebhooksCC.agent.challenge() // a proof-of-work challenge
WebhooksCC.agent.solveChallenge(challenge, { signal? })
WebhooksCC.agent.registerAnonymous({ clientName? }) // challenge + solve + register
WebhooksCC.agent.registerServiceAuth({ email, clientName? }) // no sandbox; a human must connect it
WebhooksCC.agent.exchange({ assertion }) // one-hour access token
WebhooksCC.agent.startClaim({ claimToken, email? })
WebhooksCC.agent.pollClaim({ claimToken })
WebhooksCC.agent.waitForClaim({ claimToken, timeout? })
WebhooksCC.agent.revoke({ token })The solver refuses challenges above 2^26 hashes, yields to the event loop about every 50 ms, and works in Node and browsers.
WebhooksCC.register.* (auth.md 0.1) is deprecated: anonymous and ID-JAG registration throw AgentRegisterError with code endpoint_moved, and the email flow stops working on November 30, 2026.
Introspection
(local)client.describe
Returns a static description of all SDK operations and their parameters. No API call is made. Useful for AI agents and tool discovery.
describe(): SDKDescriptionMatchers
Composable functions that return (request) => boolean. Use with waitFor or filter logic.
import {
matchMethod,
matchHeader,
matchPath,
matchQueryParam,
matchBodyPath,
matchBodySubset,
matchContentType,
matchJsonField,
matchAll,
matchAny,
matchEmail,
isEmailRequest,
} from "@webhooks-cc/sdk";
matchMethod("POST"); // match by HTTP method
matchHeader("x-github-event"); // match header presence
matchHeader("x-github-event", "push"); // match header value
matchPath("/webhooks/**"); // match path with glob wildcards
matchQueryParam("token"); // match query param presence
matchQueryParam("token", "abc"); // match query param value
matchBodyPath("data.object.id", "obj_123"); // match nested JSON path
matchBodySubset({ type: "checkout" }); // match deep partial subset
matchContentType("application/json"); // match content-type ignoring charset
matchJsonField("type", "checkout"); // match top-level JSON field
matchAll(matchMethod("POST"), matchHeader("stripe-signature"));
matchAny(matchMethod("GET"), matchMethod("POST"));
matchEmail({ tag: "run-42", subject: "Confirm" }); // match captured emails (see Emails)
isEmailRequest(request); // true for a captured email with its parsed messagematchEmail takes the email criteria and works as the match of requests.waitFor() and captureDuring(). It needs the parsed email, so it never matches requests from subscribe(); check request.kind === "email" there.
Provider Detection
Use detectWebhookInfo when you want the provider plus an event/topic label, or the is*Webhook helpers when you only need a boolean branch.
import { detectWebhookInfo, detectWebhookProvider, isTypeformWebhook } from "@webhooks-cc/sdk";
const info = detectWebhookInfo(request);
console.log(info?.provider, info?.event);
// "typeform", "form_response"
console.log(detectWebhookProvider(request));
// "typeform"
if (isTypeformWebhook(request)) {
// Handle Typeform-specific fields
}detectWebhookInfo returns:
type DetectedWebhookInfo = {
provider:
| "stripe"
| "github"
| "shopify"
| "twilio"
| "slack"
| "paddle"
| "linear"
| "sendgrid"
| "clerk"
| "discord"
| "vercel"
| "gitlab"
| "typeform"
| "standard-webhooks"
| "meta"
| "lemonsqueezy"
| "coinbase-commerce"
| "razorpay"
| "cal"
| "intercom"
| "telegram"
| "square"
| "hubspot"
| "mailgun"
| "calendly"
| "mux"
| "sentry"
| "bitbucket"
| "docusign"
| "adyen"
| "paypal"
| "plaid"
| "resend"
| "workos";
event: string | null;
via: "header" | "body";
matchedOn?: string;
};Body Parsing
import { parseJsonBody, parseFormBody, parseBody, extractJsonField } from "@webhooks-cc/sdk";
// Parse JSON body safely (returns undefined on failure)
const json = parseJsonBody(request);
// Parse form-encoded body
const form = parseFormBody(request);
// Auto-detect and parse by content-type
const parsed = parseBody(request);
// Extract nested JSON field by dot notation
const email = extractJsonField<string>(request, "data.customer.email");Payload Schemas
Generate types from captured requests: TypeScript interfaces, Zod schemas, Pydantic models, or JSON Schema. One schema is learned from every sample, so pass several requests of the same event to find optional fields (missing from some samples) and nullable ones (sometimes null).
import { generateRequestSchema, generatePayloadSchema } from "@webhooks-cc/sdk";
const requests = await client.requests.list("acme-stripe", { limit: 20 });
const { code, samples, skipped } = generateRequestSchema(requests, {
format: "zod", // "typescript" (default), "zod", "pydantic", or "json-schema"
name: "InvoicePaidEvent",
});
// From parsed values instead of requests
const ts = generatePayloadSchema([{ id: "evt_1", amount: 100 }], { name: "Payment" });generateRequestSchema() reads JSON bodies, and form bodies as string fields; it skips emails and other bodies and counts them in skipped. payloadSample(request) returns the value it would use. Both run without network access, and @webhooks-cc/sdk/schema exports them without the client, for browser bundles.
Request Diffing
import { diffRequests } from "@webhooks-cc/sdk";
const diff = diffRequests(requestA, requestB);
console.log(diff.matches); // boolean
console.log(diff.differences); // { method?, path?, headers?, body? }Signature Verification
import {
verifySignature,
verifyStripeSignature,
verifyGitHubSignature,
verifyShopifySignature,
verifyTwilioSignature,
verifySlackSignature,
verifyPaddleSignature,
verifyLinearSignature,
verifyDiscordSignature,
verifyClerkSignature,
verifyVercelSignature,
verifyGitLabSignature,
verifyTypeformSignature,
verifyStandardWebhookSignature,
verifyMetaSignature,
verifyLemonSqueezySignature,
verifyCoinbaseCommerceSignature,
verifyRazorpaySignature,
verifyCalSignature,
verifyIntercomSignature,
verifyTelegramSignature,
verifySquareSignature,
verifyHubSpotSignature,
verifyMailgunSignature,
verifyCalendlySignature,
verifyMuxSignature,
verifySentrySignature,
verifyBitbucketSignature,
verifyDocuSignSignature,
verifyAdyenSignature,
verifyPayPalSignature,
verifyResendSignature,
verifyWorkOSSignature,
verifyForwardedEmail,
verifyForwardedRequest,
verifyForwardedJson,
verifyRelayedRequest,
} from "@webhooks-cc/sdk";
// Universal verifier (dispatches by provider)
const result = await verifySignature(request, {
provider: "stripe",
secret: "whsec_...",
});
console.log(result.valid); // boolean
// Provider-specific verifiers
const valid = await verifyStripeSignature(body, signatureHeader, secret);
const valid = await verifyGitHubSignature(body, signatureHeader, secret);verifyForwardedEmail checks an email forwarded to your server: the Standard Webhooks signature against the endpoint's whsec_ secret and a timestamp within five minutes. It returns the typed email.received event, or throws WebhookVerificationError with a code. Pass the raw body (a string, Uint8Array or ArrayBuffer) before any JSON parsing.
verifyForwardedEmail(
body: string | Uint8Array | ArrayBuffer,
headers: HeadersInput,
secret: string,
options?: { toleranceSeconds?: number; now?: number } // defaults: 300, Date.now()
): Promise<EmailReceivedEvent>
// HeadersInput: a Fetch Headers, a plain object (Node and Express req.headers,
// array values included) or name and value pairs
type HeadersInput =
| Headers
| Record<string, string | string[] | undefined>
| Iterable<readonly [string, string]>;verifyStandardWebhookSignature(body, headers, secret) accepts the same HeadersInput, including a Fetch Headers. It checks the signature only, not the timestamp. See Signature Verification for a full handler.
Duration Strings
timeout and pollInterval accept human-readable strings alongside milliseconds.
"500ms" → 500
"30s" → 30000
"5m" → 300000
"1h" → 3600000
500 → 500 // numbers passed through
Types
interface Endpoint {
id: string;
slug: string;
name?: string;
url?: string;
isEphemeral?: boolean;
expiresAt?: number;
createdAt: number;
favorite?: boolean; // your own favourite; present on list() and get()
emailAddress?: string | null; // null for guest endpoints, which receive no email
showEmailExtracts?: boolean;
site?: "webhooks" | "mailhooks" | "both";
emailFields?: EmailField[]; // { name, from, header?, pattern }
layouts?: Layout[]; // owner only
retentionDays?: number | null; // owner only; null keeps the plan's
deleteAfterDelivery?: boolean; // owner only
sharedWith?: TeamShare[]; // { teamId, teamName }; when you own it
fromTeams?: TeamShare[]; // when it is shared with you
forwardEnabled?: boolean;
forwardUrl?: string | null; // only returned to the owner
hasForwardSecret?: boolean;
}
interface Request {
id: string;
endpointId: string;
method: string;
path: string;
headers: Record<string, string>;
body?: string;
bodyRaw?: string; // base64 raw bytes, only for non-UTF-8 payloads
queryParams: Record<string, string>;
contentType?: string;
ip: string;
size: number;
receivedAt: number;
signatureVerified?: boolean | null;
signatureError?: string | null;
signingProvider?: string | null;
kind?: "http" | "email";
email?: EmailCapture | null; // the parsed message, for a captured email
parsed?: ParsedEmail | null; // what the endpoint's layouts found, once parsed
}
// A captured email: method "EMAIL", path is the recipient address, body the raw message
interface EmailRequest extends Request {
kind: "email";
email: EmailCapture;
emailFields?: Record<string, string | null> | null; // named fields, from emails.get()
replyText?: string | null; // the new text of a reply, from emails.get()
}
interface EmailCapture {
subject: string | null;
from: EmailAddress[]; // EmailAddress: { name: string | null; address: string | null }
to: EmailAddress[];
cc: EmailAddress[];
replyTo: EmailAddress[];
sender: EmailAddress[];
date: string | null;
messageId: string | null;
inReplyTo: string[];
tag: string | null; // the +tag of the address it was sent to
text: string | null; // up to 256 KB
html: string | null; // up to 256 KB
textFromHtml: boolean;
attachments: {
filename: string | null;
contentType: string;
size: number;
contentId: string | null;
inline: boolean;
}[];
auth: EmailAuth | null; // SPF, DKIM, DMARC and reverse DNS results
spam?: EmailSpam | null; // the spam score
smtp: EmailSmtp | null; // client IP, HELO, TLS, envelope; smtp.test marks sample emails
parseError: boolean;
stored?: boolean; // the raw message and attachments are kept: requests.raw(), requests.attachment()
truncated: {
raw: boolean;
text: boolean;
html: boolean;
headers: boolean;
addresses: boolean;
attachments: boolean;
};
}
// What extractFromEmail() returns
interface EmailExtracts {
codes: string[]; // in order of appearance, subject first; spaces and dashes removed
links: { url: string; label: string | null; action: boolean }[]; // action links first
}
interface EmailCriteria {
tag?: string | null;
subject?: string | RegExp;
from?: string | RegExp;
to?: string | RegExp;
}
interface ListEmailsOptions extends EmailCriteria {
limit?: number; // emails fetched before filtering (default: 50)
since?: number;
}
interface WaitForEmailOptions extends EmailCriteria {
timeout?: number | string; // default: 60000
pollInterval?: number | string; // default: 1000
since?: number; // default: five minutes ago
match?: (email: EmailRequest) => boolean;
}
interface WaitForEmailsOptions extends WaitForEmailOptions {
count: number;
}
interface SendTestEmailResult {
status: "captured";
requestId: string | null;
}
interface ConfigureForwardingOptions {
url?: string | null;
enabled?: boolean;
}
interface ForwardingTestResult {
status: number | null; // null when no answer arrived
durationMs: number;
excerpt: string | null; // the start of your server's response body
error: string | null; // timeout, refused connection, TLS problem
delivered: boolean; // your server answered 2xx
sample: boolean; // no email had arrived, so a sample was sent
}
interface EmailDelivery {
id: string;
requestId: string;
status: "pending" | "succeeded" | "failed";
attempts: number;
createdAt: number;
finishedAt: number | null;
nextAttemptAt: number | null;
lastStatus: number | null;
lastError: string | null;
attemptLog: EmailDeliveryAttempt[];
}
interface EmailDeliveryAttempt {
attemptedAt: number;
status: number | null;
durationMs: number;
error: string | null;
responseExcerpt: string | null;
}
interface RecentEmailDelivery {
id: string;
requestId: string;
status: "pending" | "succeeded" | "failed";
attempts: number;
createdAt: number;
lastStatus: number | null;
lastError: string | null;
subject: string | null;
}
// The body forwarding posts; field by field in /docs/forwarding#the-request
interface EmailReceivedEvent {
type: "email.received";
timestamp: string;
data: EmailReceivedData;
}
interface MockResponse {
status: number; // 100-599
body: string;
headers: Record<string, string>;
delay?: number; // Response delay in ms (0-30000)
}
interface UsageInfo {
used: number;
limit: number;
remaining: number;
plan: "free" | "pro" | "business";
periodEnd: number | null;
}
interface PaginatedResult<T> {
items: T[];
cursor?: string;
hasMore: boolean;
}
type TemplateProvider =
| "stripe"
| "github"
| "shopify"
| "twilio"
| "slack"
| "paddle"
| "linear"
| "sendgrid"
| "clerk"
| "discord"
| "vercel"
| "gitlab"
| "typeform"
| "standard-webhooks"
| "meta"
| "lemonsqueezy"
| "coinbase-commerce"
| "razorpay"
| "cal"
| "intercom"
| "telegram"
| "square"
| "hubspot"
| "mailgun"
| "calendly"
| "mux"
| "sentry"
| "bitbucket"
| "docusign"
| "adyen"
| "paypal"
| "plaid"
| "resend"
| "workos";
type VerifyProvider = TemplateProvider;Error Classes
Errors include actionable recovery hints when the server message is generic.
import {
WebhooksCCError, // Base error (has statusCode)
UnauthorizedError, // 401: includes link to get API key
NotFoundError, // 404: suggests using endpoints.list()
RateLimitError, // 429: includes retryAfter seconds
TimeoutError, // Request timeout
WebhookVerificationError, // Thrown by verifyForwardedEmail(); has a code
} from "@webhooks-cc/sdk";WebhookVerificationError.code is "missing_headers", "timestamp_out_of_range", "invalid_signature" or "invalid_payload". Answer the first three with 401 and invalid_payload with 400.