New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@webhooks-cc/sdk

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@webhooks-cc/sdk

TypeScript SDK for webhooks.cc — create endpoints, capture requests, assert in tests

Source
npmnpm
Version
1.11.0
Version published
Weekly downloads
1
-98.08%
Maintainers
1
Weekly downloads
 
Created
Source

@webhooks-cc/sdk

TypeScript SDK for webhooks.cc. Create webhook endpoints, capture and search requests, send signed test webhooks, verify provider signatures, and build webhook tests with less boilerplate.

Install

pnpm add @webhooks-cc/sdk

The package also ships a testing entrypoint:

import { captureDuring, assertRequest } from "@webhooks-cc/sdk/testing";

API key setup

The SDK needs an API key in whcc_... format. You can pass the key directly, but most projects load it from WHK_API_KEY so the same code works locally and in CI.

For local development, set the env var in your shell or .env.local:

export WHK_API_KEY=whcc_...

For GitHub Actions, store the key as a repository secret and expose it in the workflow:

# .github/workflows/test.yml
env:
  WHK_API_KEY: ${{ secrets.WHK_API_KEY }}

Quick start

import { WebhooksCC, matchAll, matchHeader, matchMethod } from "@webhooks-cc/sdk";

const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });

const endpoint = await client.endpoints.create({
  name: "stripe-test",
  expiresIn: "1h",
});

await yourApp.registerWebhook(endpoint.url!);
await yourApp.triggerCheckout();

const request = await client.requests.waitFor(endpoint.slug, {
  timeout: "30s",
  match: matchAll(matchMethod("POST"), matchHeader("stripe-signature")),
});

console.log(request.body);

await client.endpoints.delete(endpoint.slug);

Client options

const client = new WebhooksCC({
  apiKey: "whcc_...",
  retry: {
    maxAttempts: 3,
    backoffMs: 500,
  },
  hooks: {
    onRequest: ({ method, url }) => console.log(method, url),
    onResponse: ({ status, durationMs }) => console.log(status, durationMs),
    onError: ({ error }) => console.error(error),
  },
});
OptionTypeDefaultNotes
apiKeystringrequiredAPI key in whcc_... format. Often read from process.env.WHK_API_KEY.
baseUrlstringhttps://webhooks.ccAPI base URL
webhookUrlstringhttps://go.webhooks.ccreceiver base URL used by endpoints.send()
timeoutnumber30000request timeout in milliseconds
retryRetryOptions1 attemptretries transient SDK requests
hooksClientHooksnonelifecycle callbacks for request logging

API overview

  • client.endpoints: create, list, get, update, delete, send, sendTemplate
  • client.requests: list, listPaginated, get, waitFor, waitForAll, subscribe, replay, search, count, clear, export
  • client.templates: listProviders, get
  • client.teams: list, members, share, unshare, invite, invites.list, invites.accept, invites.decline
  • top-level client methods: usage(), sendTo(), buildRequest(), flow(), describe()

Endpoints

Create persistent or ephemeral endpoints. You can also attach a mock response at creation time.

const endpoint = await client.endpoints.create({
  name: "billing-webhooks",
  expiresIn: "12h",
  mockResponse: {
    status: 202,
    body: '{"queued":true}',
    headers: { "x-webhooks-cc": "mock" },
  },
});

const fetched = await client.endpoints.get(endpoint.slug);
console.log(fetched.isEphemeral, fetched.expiresAt);

await client.endpoints.update(endpoint.slug, {
  name: "billing-webhooks-renamed",
  mockResponse: null,
});

Send plain test requests through the hosted receiver:

await client.endpoints.send(endpoint.slug, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: { event: "invoice.paid" },
});

Requests

List, paginate, wait, stream, replay, export, and clear captured requests.

const recent = await client.requests.list(endpoint.slug, {
  limit: 50,
  since: Date.now() - 60_000,
});

const page1 = await client.requests.listPaginated(endpoint.slug, { limit: 100 });
const page2 = page1.cursor
  ? await client.requests.listPaginated(endpoint.slug, { limit: 100, cursor: page1.cursor })
  : { items: [], hasMore: false };

const firstMatch = await client.requests.waitFor(endpoint.slug, {
  timeout: "20s",
  match: matchHeader("stripe-signature"),
});

const allMatches = await client.requests.waitForAll(endpoint.slug, {
  count: 3,
  timeout: "30s",
  match: matchMethod("POST"),
});

for await (const request of client.requests.subscribe(endpoint.slug, { reconnect: true })) {
  console.log(request.method, request.path);
}

subscribe() streams over SSE. The server rotates every stream connection after 30 minutes: it sends a timeout event and closes. With reconnect: true the client follows that rotation transparently, reconnecting immediately, resuming from the last received request, and deduplicating replayed events, so the loop above runs until you break out of it, abort the signal, the timeout expires, or the endpoint is deleted. Rotations do not count against maxReconnectAttempts (default 5), which only limits recovery from unexpected stream ends and transient errors (with exponential backoff from reconnectBackoffMs). Without reconnect, the loop ends at the first rotation, after at most 30 minutes.

An idle watchdog (idleTimeout, default "90s", three server keepalive intervals) detects half-open sockets: if nothing arrives within that window while waiting, the connection is dropped. With reconnect: true it is reconnected (counted as a reconnect attempt, with backoff); without it, the iterator throws TimeoutError. Pass idleTimeout: 0 to disable the watchdog.

for await (const request of client.requests.subscribe(endpoint.slug, {
  reconnect: true,
  maxReconnectAttempts: 10,
  idleTimeout: "2m",
  onReconnect: (attempt) => console.warn(`stream reconnect #${attempt}`),
})) {
  console.log(request.method, request.path);
}

Replay, export, and clear requests:

await client.requests.replay(firstMatch.id, "http://localhost:3001/webhooks");

const curlExport = await client.requests.export(endpoint.slug, {
  format: "curl",
  limit: 10,
});

const harExport = await client.requests.export(endpoint.slug, {
  format: "har",
  since: Date.now() - 3_600_000,
});

await client.requests.clear(endpoint.slug, { before: "24h" });

Search and count use the retained request store rather than the live endpoint request table:

const retained = await client.requests.search({
  slug: endpoint.slug,
  q: "checkout.session.completed",
  from: "7d",
  limit: 20,
});

const total = await client.requests.count({
  slug: endpoint.slug,
  q: "checkout.session.completed",
  from: "7d",
});

search() returns SearchResult[]. Their id field is synthetic and is not valid for requests.get() or requests.replay().

Templates, sendTo, and buildRequest

The SDK can generate provider-shaped webhook payloads (signed where the provider uses a shared secret or public test key flow) for:

  • 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

sendgrid, discord, paypal, and plaid templates are intentionally not shared-secret signed: SendGrid uses IP allowlisting, Discord/PayPal signatures require provider-owned private keys, and Plaid verification uses Plaid JWT/JWK lookup.

Inspect the static provider metadata:

const providers = client.templates.listProviders();
const stripe = client.templates.get("stripe");

console.log(providers);
console.log(stripe.signatureHeader, stripe.templates);

If you prefer a static export, import TEMPLATE_METADATA from @webhooks-cc/sdk.

Send a signed provider template through a hosted endpoint:

await client.endpoints.sendTemplate(endpoint.slug, {
  provider: "slack",
  template: "slash_command",
  secret: process.env.SLACK_SIGNING_SECRET!,
});

Build or send a signed request directly to any URL:

const preview = await client.buildRequest("http://localhost:3001/webhooks", {
  provider: "stripe",
  template: "checkout.session.completed",
  secret: "whsec_test_123",
});

await client.sendTo("http://localhost:3001/webhooks", {
  provider: "github",
  template: "push",
  secret: "github_secret",
});

Signature verification

The SDK includes provider-specific verification helpers and a provider-agnostic verifySignature().

Provider-specific helpers such as verifyStripeSignature() and verifyDiscordSignature() are also exported.

Supported verification providers:

  • stripe
  • github
  • shopify
  • twilio
  • slack
  • paddle
  • linear
  • 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
  • resend
  • workos

sendgrid and plaid are template-only for verification: SendGrid uses IP allowlisting, and Plaid JWT/JWK verification requires Plaid API credentials.

import { isDiscordWebhook, verifySignature } from "@webhooks-cc/sdk";

if (isDiscordWebhook(request)) {
  const result = await verifySignature(request, {
    provider: "discord",
    publicKey: process.env.DISCORD_PUBLIC_KEY!,
  });

  console.log(result.valid);
}

For Twilio, Square, and HubSpot, pass the original signed URL. HubSpot v3 also signs the HTTP method, so pass method too:

const result = await verifySignature(request, {
  provider: "twilio",
  secret: process.env.TWILIO_AUTH_TOKEN!,
  url: "https://example.com/webhooks/twilio",
});

// HubSpot v3 signs `method + url + body + timestamp` and rejects stale timestamps
const hubspot = await verifySignature(request, {
  provider: "hubspot",
  secret: process.env.HUBSPOT_CLIENT_SECRET!,
  url: "https://example.com/webhooks/hubspot",
  method: "POST",
});

Mailgun is the exception with no signature header — it embeds signature.{timestamp,token,signature} in the request body, so verifyMailgunSignature reads the body directly and never throws on malformed input.

SendGrid uses IP allowlisting rather than cryptographic signature verification.

Request detection helpers are exported for every supported provider:

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

Matchers, parsing, and diffing

Use matchers with waitFor() or waitForAll():

import {
  matchAll,
  matchBodySubset,
  matchContentType,
  matchHeader,
  matchPath,
  matchQueryParam,
} from "@webhooks-cc/sdk";

const request = await client.requests.waitFor(endpoint.slug, {
  match: matchAll(
    matchPath("/webhooks/stripe"),
    matchHeader("stripe-signature"),
    matchContentType("application/json"),
    matchQueryParam("tenant", "acme"),
    matchBodySubset({ type: "checkout.session.completed" })
  ),
});

matchAny(), matchBodyPath(), and matchJsonField() are available when you need looser matching.

Parse request bodies and diff captures:

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

const parsed = parseBody(request);
const form = parseFormBody(request);
const eventType = extractJsonField<string>(request, "type");

const diff = diffRequests(previousRequest, request, {
  ignoreHeaders: ["date", "x-request-id"],
});

console.log(parsed, form, eventType, diff.matches);

Testing helpers

@webhooks-cc/sdk/testing adds a small test-oriented layer:

  • withEndpoint()
  • withEphemeralEndpoint()
  • captureDuring()
  • assertRequest()
import { matchHeader, WebhooksCC } from "@webhooks-cc/sdk";
import { assertRequest, captureDuring } from "@webhooks-cc/sdk/testing";

const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });

const [request] = await captureDuring(
  client,
  async (endpoint) => {
    await yourApp.registerWebhook(endpoint.url!);
    await yourApp.triggerCheckout();
  },
  {
    expiresIn: "1h",
    timeout: "20s",
    match: matchHeader("stripe-signature"),
  }
);

assertRequest(
  request,
  {
    method: "POST",
    bodyJson: { type: "checkout.session.completed" },
  },
  { throwOnFailure: true }
);

Flow builder

client.flow() composes the common test sequence into one chain: create endpoint, optionally set a mock, send a request, wait for capture, verify the signature, replay the request, and clean up.

const result = await client
  .flow()
  .createEndpoint({ expiresIn: "1h" })
  .sendTemplate({
    provider: "github",
    template: "push",
    secret: "github_secret",
  })
  .waitForCapture({ timeout: "15s" })
  .verifySignature({
    provider: "github",
    secret: "github_secret",
  })
  .cleanup()
  .run();

console.log(result.request?.id, result.verification?.valid, result.cleanedUp);

Teams

A key has exactly the team access of the account it belongs to. Endpoints shared with you through a subscribed team appear in endpoints.list() with fromTeam set and work with every request method by slug except requests.clear, which stays with the owner. The teams namespace manages the teams themselves:

const teams = await client.teams.list();
// [{ id, name, role, seats, requestsUsed, requestLimit, periodEnd, suspended, ... }]

// Only endpoints tied to one team (id or name), shared either way
const payments = await client.endpoints.list({ team: "Payments" });

// Owner-only: share an endpoint you own, by slug. Its requests then bill the team pool.
await client.teams.share(teams[0].id, "my-stripe");
await client.teams.unshare(teams[0].id, "my-stripe");

const { members, pendingInvites } = await client.teams.members(teams[0].id);

// Invites: sending one emails the address; accepting claims a paid seat.
await client.teams.invite(teams[0].id, "dev@example.com");
const invites = await client.teams.invites.list();
await client.teams.invites.accept(invites[0].id);

Usage and self-description

Check quota state from code:

const usage = await client.usage();
console.log(usage.used, usage.limit, usage.remaining, usage.plan);

Ask the client what it supports without making an API call:

const description = client.describe();
console.log(description.requests.waitForAll);

Errors

API failures throw typed errors:

  • WebhooksCCError
  • UnauthorizedError
  • NotFoundError
  • TimeoutError (also thrown by requests.subscribe() when the idle watchdog trips without reconnect)
  • RateLimitError

ApiError is still exported as a legacy alias of WebhooksCCError.

License

MIT

Keywords

webhook

FAQs

Package last updated on 03 Oct 2026

Related posts