@webhooks-cc/sdk 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,250 +1,426 @@
1
1
  # @webhooks-cc/sdk
2
2
 
3
- TypeScript SDK for [webhooks.cc](https://webhooks.cc). Create webhook endpoints, capture requests, match and replay them, and stream events in real time.
3
+ TypeScript SDK for [webhooks.cc](https://webhooks.cc). Create webhook endpoints, capture and search requests, send signed test webhooks, verify provider signatures, and build webhook tests with less boilerplate.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @webhooks-cc/sdk
9
- # or: pnpm add / yarn add / bun add
8
+ pnpm add @webhooks-cc/sdk
9
+ ```
10
+
11
+ The package also ships a testing entrypoint:
12
+
13
+ ```typescript
14
+ import { captureDuring, assertRequest } from "@webhooks-cc/sdk/testing";
15
+ ```
16
+
17
+ ## API key setup
18
+
19
+ 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.
20
+
21
+ For local development, set the env var in your shell or `.env.local`:
22
+
23
+ ```bash
24
+ export WHK_API_KEY=whcc_...
25
+ ```
26
+
27
+ For GitHub Actions, store the key as a repository secret and expose it in the workflow:
28
+
29
+ ```yaml
30
+ # .github/workflows/test.yml
31
+ env:
32
+ WHK_API_KEY: ${{ secrets.WHK_API_KEY }}
10
33
  ```
11
34
 
12
35
  ## Quick start
13
36
 
14
37
  ```typescript
15
- import { WebhooksCC, matchAll, matchMethod, matchHeader } from "@webhooks-cc/sdk";
38
+ import { WebhooksCC, matchAll, matchHeader, matchMethod } from "@webhooks-cc/sdk";
16
39
 
17
- const client = new WebhooksCC({ apiKey: "whcc_..." });
40
+ const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
41
+
42
+ const endpoint = await client.endpoints.create({
43
+ name: "stripe-test",
44
+ expiresIn: "1h",
45
+ });
18
46
 
19
- // Create an endpoint
20
- const endpoint = await client.endpoints.create({ name: "stripe-test" });
21
- console.log(endpoint.url); // https://go.webhooks.cc/w/abc123
47
+ await yourApp.registerWebhook(endpoint.url!);
48
+ await yourApp.triggerCheckout();
22
49
 
23
- // Point your service at endpoint.url, then wait for the webhook
24
50
  const request = await client.requests.waitFor(endpoint.slug, {
25
51
  timeout: "30s",
26
52
  match: matchAll(matchMethod("POST"), matchHeader("stripe-signature")),
27
53
  });
28
54
 
29
- console.log(request.body); // '{"type":"checkout.session.completed",...}'
55
+ console.log(request.body);
30
56
 
31
- // Clean up
32
57
  await client.endpoints.delete(endpoint.slug);
33
58
  ```
34
59
 
35
60
  ## Client options
36
61
 
37
- ```typescript
38
- new WebhooksCC(options);
39
- ```
40
-
41
- | Option | Type | Default | Description |
42
- | ------------ | ------------- | ------------------------ | ------------------------- |
43
- | `apiKey` | `string` | _required_ | API key (`whcc_...`) |
44
- | `baseUrl` | `string` | `https://webhooks.cc` | API base URL |
45
- | `webhookUrl` | `string` | `https://go.webhooks.cc` | Webhook receiver URL |
46
- | `timeout` | `number` | `30000` | HTTP request timeout (ms) |
47
- | `hooks` | `ClientHooks` | — | Lifecycle callbacks |
48
-
49
- ### Hooks
50
-
51
62
  ```typescript
52
63
  const client = new WebhooksCC({
53
64
  apiKey: "whcc_...",
65
+ retry: {
66
+ maxAttempts: 3,
67
+ backoffMs: 500,
68
+ },
54
69
  hooks: {
55
- onRequest: (info) => console.log(info.method, info.url),
56
- onResponse: (info) => console.log(info.status),
57
- onError: (info) => console.error(info.error),
70
+ onRequest: ({ method, url }) => console.log(method, url),
71
+ onResponse: ({ status, durationMs }) => console.log(status, durationMs),
72
+ onError: ({ error }) => console.error(error),
58
73
  },
59
74
  });
60
75
  ```
61
76
 
77
+ | Option | Type | Default | Notes |
78
+ | ------------ | -------------- | ------------------------ | ------------------------------------------------------------------------ |
79
+ | `apiKey` | `string` | required | API key in `whcc_...` format. Often read from `process.env.WHK_API_KEY`. |
80
+ | `baseUrl` | `string` | `https://webhooks.cc` | API base URL |
81
+ | `webhookUrl` | `string` | `https://go.webhooks.cc` | receiver base URL used by `endpoints.send()` |
82
+ | `timeout` | `number` | `30000` | request timeout in milliseconds |
83
+ | `retry` | `RetryOptions` | `1` attempt | retries transient SDK requests |
84
+ | `hooks` | `ClientHooks` | none | lifecycle callbacks for request logging |
85
+
86
+ ## API overview
87
+
88
+ - `client.endpoints`: `create`, `list`, `get`, `update`, `delete`, `send`, `sendTemplate`
89
+ - `client.requests`: `list`, `listPaginated`, `get`, `waitFor`, `waitForAll`, `subscribe`, `replay`, `search`, `count`, `clear`, `export`
90
+ - `client.templates`: `listProviders`, `get`
91
+ - top-level client methods: `usage()`, `sendTo()`, `buildRequest()`, `flow()`, `describe()`
92
+
62
93
  ## Endpoints
63
94
 
64
- ```typescript
65
- // Create
66
- const endpoint = await client.endpoints.create({ name: "my-test" });
95
+ Create persistent or ephemeral endpoints. You can also attach a mock response at creation time.
67
96
 
68
- // List all
69
- const endpoints = await client.endpoints.list();
97
+ ```typescript
98
+ const endpoint = await client.endpoints.create({
99
+ name: "billing-webhooks",
100
+ expiresIn: "12h",
101
+ mockResponse: {
102
+ status: 202,
103
+ body: '{"queued":true}',
104
+ headers: { "x-webhooks-cc": "mock" },
105
+ },
106
+ });
70
107
 
71
- // Get by slug
72
- const endpoint = await client.endpoints.get("abc123");
108
+ const fetched = await client.endpoints.get(endpoint.slug);
109
+ console.log(fetched.isEphemeral, fetched.expiresAt);
73
110
 
74
- // Update name or mock response
75
- await client.endpoints.update("abc123", {
76
- name: "New Name",
77
- mockResponse: { status: 201, body: '{"ok":true}', headers: {} },
111
+ await client.endpoints.update(endpoint.slug, {
112
+ name: "billing-webhooks-renamed",
113
+ mockResponse: null,
78
114
  });
115
+ ```
79
116
 
80
- // Clear mock response
81
- await client.endpoints.update("abc123", { mockResponse: null });
117
+ Send plain test requests through the hosted receiver:
82
118
 
83
- // Send a test webhook
84
- const res = await client.endpoints.send("abc123", {
119
+ ```typescript
120
+ await client.endpoints.send(endpoint.slug, {
85
121
  method: "POST",
86
122
  headers: { "content-type": "application/json" },
87
- body: { event: "test" },
123
+ body: { event: "invoice.paid" },
88
124
  });
89
-
90
- // Delete
91
- await client.endpoints.delete("abc123");
92
125
  ```
93
126
 
94
127
  ## Requests
95
128
 
129
+ List, paginate, wait, stream, replay, export, and clear captured requests.
130
+
96
131
  ```typescript
97
- // List captured requests
98
- const requests = await client.requests.list("endpoint-slug", {
132
+ const recent = await client.requests.list(endpoint.slug, {
99
133
  limit: 50,
100
- since: Date.now() - 60000,
134
+ since: Date.now() - 60_000,
101
135
  });
102
136
 
103
- // Get a single request by ID
104
- const request = await client.requests.get("request-id");
137
+ const page1 = await client.requests.listPaginated(endpoint.slug, { limit: 100 });
138
+ const page2 = page1.cursor
139
+ ? await client.requests.listPaginated(endpoint.slug, { limit: 100, cursor: page1.cursor })
140
+ : { items: [], hasMore: false };
105
141
 
106
- // Poll until a matching request arrives
107
- const request = await client.requests.waitFor("endpoint-slug", {
108
- timeout: "30s", // human-readable or milliseconds
109
- pollInterval: "500ms",
142
+ const firstMatch = await client.requests.waitFor(endpoint.slug, {
143
+ timeout: "20s",
110
144
  match: matchHeader("stripe-signature"),
111
145
  });
112
146
 
113
- // Replay a captured request to a target URL
114
- const res = await client.requests.replay(request.id, "http://localhost:3000/webhooks");
147
+ const allMatches = await client.requests.waitForAll(endpoint.slug, {
148
+ count: 3,
149
+ timeout: "30s",
150
+ match: matchMethod("POST"),
151
+ });
115
152
 
116
- // Stream requests in real time via SSE
117
- for await (const req of client.requests.subscribe("endpoint-slug")) {
118
- console.log(req.method, req.body);
153
+ for await (const request of client.requests.subscribe(endpoint.slug, { reconnect: true })) {
154
+ console.log(request.method, request.path);
119
155
  }
120
156
  ```
121
157
 
122
- ## Matchers
123
-
124
- Composable functions for `waitFor`'s `match` option:
158
+ Replay, export, and clear requests:
125
159
 
126
160
  ```typescript
127
- import { matchMethod, matchHeader, matchBodyPath, matchAll, matchAny } from "@webhooks-cc/sdk";
161
+ await client.requests.replay(firstMatch.id, "http://localhost:3001/webhooks");
162
+
163
+ const curlExport = await client.requests.export(endpoint.slug, {
164
+ format: "curl",
165
+ limit: 10,
166
+ });
167
+
168
+ const harExport = await client.requests.export(endpoint.slug, {
169
+ format: "har",
170
+ since: Date.now() - 3_600_000,
171
+ });
128
172
 
129
- // Match POST requests with a specific header
130
- matchAll(matchMethod("POST"), matchHeader("x-event-type", "payment.success"));
173
+ await client.requests.clear(endpoint.slug, { before: "24h" });
174
+ ```
131
175
 
132
- // Match header presence (any value)
133
- matchHeader("stripe-signature");
176
+ Search and count use the retained request store rather than the live endpoint request table:
134
177
 
135
- // Match a nested JSON body field
136
- matchBodyPath("data.object.id", "sub_123");
178
+ ```typescript
179
+ const retained = await client.requests.search({
180
+ slug: endpoint.slug,
181
+ q: "checkout.session.completed",
182
+ from: "7d",
183
+ limit: 20,
184
+ });
137
185
 
138
- // Match any of several conditions
139
- matchAny(matchHeader("stripe-signature"), matchHeader("x-github-event"));
186
+ const total = await client.requests.count({
187
+ slug: endpoint.slug,
188
+ q: "checkout.session.completed",
189
+ from: "7d",
190
+ });
140
191
  ```
141
192
 
142
- ## Provider helpers
193
+ `search()` returns `SearchResult[]`. Their `id` field is synthetic and is not valid for `requests.get()` or `requests.replay()`.
194
+
195
+ ## Templates, sendTo, and buildRequest
196
+
197
+ The SDK can generate signed webhook payloads for:
143
198
 
144
- Detect webhook sources by their signature headers:
199
+ - `stripe`
200
+ - `github`
201
+ - `shopify`
202
+ - `twilio`
203
+ - `slack`
204
+ - `paddle`
205
+ - `linear`
206
+ - `standard-webhooks`
207
+
208
+ Inspect the static provider metadata:
145
209
 
146
210
  ```typescript
147
- import { isStripeWebhook, isGitHubWebhook } from "@webhooks-cc/sdk";
211
+ const providers = client.templates.listProviders();
212
+ const stripe = client.templates.get("stripe");
148
213
 
149
- if (isStripeWebhook(request)) {
150
- // has stripe-signature header
151
- }
214
+ console.log(providers);
215
+ console.log(stripe.signatureHeader, stripe.templates);
152
216
  ```
153
217
 
154
- Available: `isStripeWebhook`, `isGitHubWebhook`, `isShopifyWebhook`, `isSlackWebhook`, `isTwilioWebhook`, `isPaddleWebhook`, `isLinearWebhook`.
218
+ If you prefer a static export, import `TEMPLATE_METADATA` from `@webhooks-cc/sdk`.
219
+
220
+ Send a signed provider template through a hosted endpoint:
155
221
 
156
- ## Self-description
222
+ ```typescript
223
+ await client.endpoints.sendTemplate(endpoint.slug, {
224
+ provider: "slack",
225
+ template: "slash_command",
226
+ secret: process.env.SLACK_SIGNING_SECRET!,
227
+ });
228
+ ```
157
229
 
158
- AI agents can call `client.describe()` to get a structured summary of all SDK operations, parameters, and return types — no API call required.
230
+ Build or send a signed request directly to any URL:
159
231
 
160
232
  ```typescript
161
- const desc = client.describe();
162
- // { version: "0.3.0", endpoints: { create: { ... }, ... }, requests: { ... } }
233
+ const preview = await client.buildRequest("http://localhost:3001/webhooks", {
234
+ provider: "stripe",
235
+ template: "checkout.session.completed",
236
+ secret: "whsec_test_123",
237
+ });
238
+
239
+ await client.sendTo("http://localhost:3001/webhooks", {
240
+ provider: "github",
241
+ template: "push",
242
+ secret: "github_secret",
243
+ });
163
244
  ```
164
245
 
165
- ## Errors
246
+ ## Signature verification
247
+
248
+ The SDK includes provider-specific verification helpers and a provider-agnostic `verifySignature()`.
249
+
250
+ Provider-specific helpers such as `verifyStripeSignature()` and `verifyDiscordSignature()` are also exported.
251
+
252
+ Supported verification providers:
166
253
 
167
- All API errors extend `WebhooksCCError` and include actionable recovery hints:
254
+ - `stripe`
255
+ - `github`
256
+ - `shopify`
257
+ - `twilio`
258
+ - `slack`
259
+ - `paddle`
260
+ - `linear`
261
+ - `discord`
262
+ - `standard-webhooks`
168
263
 
169
264
  ```typescript
170
- import { WebhooksCC, WebhooksCCError, NotFoundError } from "@webhooks-cc/sdk";
265
+ import { isDiscordWebhook, verifySignature } from "@webhooks-cc/sdk";
171
266
 
172
- try {
173
- await client.endpoints.get("nonexistent");
174
- } catch (error) {
175
- if (error instanceof WebhooksCCError) {
176
- console.log(error.statusCode); // 404
177
- console.log(error.message); // includes what went wrong and how to fix it
178
- }
267
+ if (isDiscordWebhook(request)) {
268
+ const result = await verifySignature(request, {
269
+ provider: "discord",
270
+ publicKey: process.env.DISCORD_PUBLIC_KEY!,
271
+ });
272
+
273
+ console.log(result.valid);
179
274
  }
180
275
  ```
181
276
 
182
- Error classes: `WebhooksCCError`, `UnauthorizedError`, `NotFoundError`, `TimeoutError`, `RateLimitError`. The legacy `ApiError` alias is still exported for backward compatibility.
277
+ For Twilio, pass the original signed URL:
183
278
 
184
- ## GitHub Actions
279
+ ```typescript
280
+ const result = await verifySignature(request, {
281
+ provider: "twilio",
282
+ secret: process.env.TWILIO_AUTH_TOKEN!,
283
+ url: "https://example.com/webhooks/twilio",
284
+ });
285
+ ```
185
286
 
186
- Add your API key as a repository secret named `WHK_API_KEY`:
287
+ Discord support is verification-only. It is not part of the template generation API.
187
288
 
188
- ```yaml
189
- - name: Run webhook tests
190
- env:
191
- WHK_API_KEY: ${{ secrets.WHK_API_KEY }}
192
- run: npx vitest run
289
+ Request detection helpers are exported too: `isStripeWebhook()`, `isGitHubWebhook()`, `isShopifyWebhook()`, `isSlackWebhook()`, `isTwilioWebhook()`, `isPaddleWebhook()`, `isLinearWebhook()`, `isDiscordWebhook()`, and `isStandardWebhook()`.
290
+
291
+ ## Matchers, parsing, and diffing
292
+
293
+ Use matchers with `waitFor()` or `waitForAll()`:
294
+
295
+ ```typescript
296
+ import {
297
+ matchAll,
298
+ matchBodySubset,
299
+ matchContentType,
300
+ matchHeader,
301
+ matchPath,
302
+ matchQueryParam,
303
+ } from "@webhooks-cc/sdk";
304
+
305
+ const request = await client.requests.waitFor(endpoint.slug, {
306
+ match: matchAll(
307
+ matchPath("/webhooks/stripe"),
308
+ matchHeader("stripe-signature"),
309
+ matchContentType("application/json"),
310
+ matchQueryParam("tenant", "acme"),
311
+ matchBodySubset({ type: "checkout.session.completed" })
312
+ ),
313
+ });
193
314
  ```
194
315
 
316
+ `matchAny()`, `matchBodyPath()`, and `matchJsonField()` are available when you need looser matching.
317
+
318
+ Parse request bodies and diff captures:
319
+
195
320
  ```typescript
196
- // webhook.test.ts
197
- import { describe, it, expect, afterAll } from "vitest";
198
- import { WebhooksCC, matchHeader } from "@webhooks-cc/sdk";
321
+ import { diffRequests, extractJsonField, parseBody, parseFormBody } from "@webhooks-cc/sdk";
199
322
 
200
- const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
323
+ const parsed = parseBody(request);
324
+ const form = parseFormBody(request);
325
+ const eventType = extractJsonField<string>(request, "type");
201
326
 
202
- describe("webhook integration", () => {
203
- let slug: string;
327
+ const diff = diffRequests(previousRequest, request, {
328
+ ignoreHeaders: ["date", "x-request-id"],
329
+ });
204
330
 
205
- it("receives Stripe webhook", async () => {
206
- const endpoint = await client.endpoints.create({ name: "CI Test" });
207
- slug = endpoint.slug;
331
+ console.log(parsed, form, eventType, diff.matches);
332
+ ```
208
333
 
209
- // Trigger your service to send a webhook to endpoint.url
210
- await yourService.registerWebhook(endpoint.url!);
211
- await yourService.createOrder();
334
+ ## Testing helpers
212
335
 
213
- const req = await client.requests.waitFor(slug, {
214
- timeout: "15s",
215
- match: matchHeader("stripe-signature"),
216
- });
336
+ `@webhooks-cc/sdk/testing` adds a small test-oriented layer:
217
337
 
218
- const body = JSON.parse(req.body!);
219
- expect(body.type).toBe("checkout.session.completed");
220
- });
338
+ - `withEndpoint()`
339
+ - `withEphemeralEndpoint()`
340
+ - `captureDuring()`
341
+ - `assertRequest()`
221
342
 
222
- afterAll(async () => {
223
- if (slug) await client.endpoints.delete(slug);
224
- });
225
- });
343
+ ```typescript
344
+ import { matchHeader, WebhooksCC } from "@webhooks-cc/sdk";
345
+ import { assertRequest, captureDuring } from "@webhooks-cc/sdk/testing";
346
+
347
+ const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
348
+
349
+ const [request] = await captureDuring(
350
+ client,
351
+ async (endpoint) => {
352
+ await yourApp.registerWebhook(endpoint.url!);
353
+ await yourApp.triggerCheckout();
354
+ },
355
+ {
356
+ expiresIn: "1h",
357
+ timeout: "20s",
358
+ match: matchHeader("stripe-signature"),
359
+ }
360
+ );
361
+
362
+ assertRequest(
363
+ request,
364
+ {
365
+ method: "POST",
366
+ bodyJson: { type: "checkout.session.completed" },
367
+ },
368
+ { throwOnFailure: true }
369
+ );
226
370
  ```
227
371
 
228
- ## Types
372
+ ## Flow builder
229
373
 
230
- All types are exported:
374
+ `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.
231
375
 
232
376
  ```typescript
233
- import type {
234
- ClientOptions,
235
- ClientHooks,
236
- Endpoint,
237
- Request,
238
- CreateEndpointOptions,
239
- UpdateEndpointOptions,
240
- SendOptions,
241
- ListRequestsOptions,
242
- WaitForOptions,
243
- SubscribeOptions,
244
- SDKDescription,
245
- } from "@webhooks-cc/sdk";
377
+ const result = await client
378
+ .flow()
379
+ .createEndpoint({ expiresIn: "1h" })
380
+ .sendTemplate({
381
+ provider: "github",
382
+ template: "push",
383
+ secret: "github_secret",
384
+ })
385
+ .waitForCapture({ timeout: "15s" })
386
+ .verifySignature({
387
+ provider: "github",
388
+ secret: "github_secret",
389
+ })
390
+ .cleanup()
391
+ .run();
392
+
393
+ console.log(result.request?.id, result.verification?.valid, result.cleanedUp);
394
+ ```
395
+
396
+ ## Usage and self-description
397
+
398
+ Check quota state from code:
399
+
400
+ ```typescript
401
+ const usage = await client.usage();
402
+ console.log(usage.used, usage.limit, usage.remaining, usage.plan);
246
403
  ```
247
404
 
405
+ Ask the client what it supports without making an API call:
406
+
407
+ ```typescript
408
+ const description = client.describe();
409
+ console.log(description.requests.waitForAll);
410
+ ```
411
+
412
+ ## Errors
413
+
414
+ API failures throw typed errors:
415
+
416
+ - `WebhooksCCError`
417
+ - `UnauthorizedError`
418
+ - `NotFoundError`
419
+ - `TimeoutError`
420
+ - `RateLimitError`
421
+
422
+ `ApiError` is still exported as a legacy alias of `WebhooksCCError`.
423
+
248
424
  ## License
249
425
 
250
426
  MIT