@zyphr-dev/mcp-server 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2315 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+
9
+ // src/config.ts
10
+ function loadToolGuards() {
11
+ const readOnly = process.env.ZYPHR_READ_ONLY === "true" || process.env.ZYPHR_READ_ONLY === "1";
12
+ const raw = process.env.ZYPHR_ALLOWED_TOOLS;
13
+ const allowedTools = raw ? new Set(
14
+ raw.split(",").map((s) => s.trim()).filter(Boolean)
15
+ ) : null;
16
+ return { readOnly, allowedTools };
17
+ }
18
+ function isToolEnabled(tool, guards) {
19
+ if (guards.allowedTools) {
20
+ return guards.allowedTools.has(tool.name);
21
+ }
22
+ if (guards.readOnly && tool.mutates) {
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+
28
+ // src/client.ts
29
+ import { Zyphr } from "@zyphr-dev/node-sdk";
30
+ var DEFAULT_BASE_URL = "https://api.zyphr.dev/v1";
31
+ var cached;
32
+ function getZyphrClient() {
33
+ if (cached) return cached;
34
+ const apiKey = process.env.ZYPHR_API_KEY;
35
+ if (!apiKey) {
36
+ process.stderr.write(
37
+ "[zyphr-mcp] ZYPHR_API_KEY is not set. Provide a zy_live_* or zy_test_* key via the `env` block of your MCP client config.\n"
38
+ );
39
+ process.exit(1);
40
+ }
41
+ const baseUrl = process.env.ZYPHR_BASE_URL || DEFAULT_BASE_URL;
42
+ cached = new Zyphr({ apiKey, baseUrl });
43
+ return cached;
44
+ }
45
+ function getBaseUrl() {
46
+ return process.env.ZYPHR_BASE_URL || DEFAULT_BASE_URL;
47
+ }
48
+
49
+ // src/result.ts
50
+ import { ZyphrError, ZyphrRateLimitError } from "@zyphr-dev/node-sdk";
51
+ function toolResult(data) {
52
+ return {
53
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
54
+ };
55
+ }
56
+ async function runTool(fn) {
57
+ try {
58
+ const data = await fn();
59
+ return toolResult(data);
60
+ } catch (err) {
61
+ return await renderApiError(err);
62
+ }
63
+ }
64
+ function errorPayload(content) {
65
+ return {
66
+ isError: true,
67
+ content: [{ type: "text", text: JSON.stringify(content, null, 2) }]
68
+ };
69
+ }
70
+ async function renderApiError(err) {
71
+ if (err instanceof ZyphrError) {
72
+ const payload = {
73
+ name: err.name,
74
+ message: err.message,
75
+ status: err.status
76
+ };
77
+ if (err.code) payload.code = err.code;
78
+ if (err.requestId) payload.requestId = err.requestId;
79
+ if (err.details) payload.details = err.details;
80
+ if (err instanceof ZyphrRateLimitError && err.retryAfter !== void 0) {
81
+ payload.retryAfter = err.retryAfter;
82
+ }
83
+ return errorPayload(payload);
84
+ }
85
+ if (err && typeof err === "object" && "response" in err) {
86
+ const response = err.response;
87
+ if (response && typeof response.text === "function") {
88
+ try {
89
+ const text = await response.text();
90
+ let parsed = text;
91
+ try {
92
+ parsed = JSON.parse(text);
93
+ } catch {
94
+ }
95
+ return errorPayload({ status: response.status, body: parsed });
96
+ } catch {
97
+ }
98
+ }
99
+ }
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ const name = err instanceof Error ? err.name : "Error";
102
+ return errorPayload({ name, message });
103
+ }
104
+
105
+ // src/schemas.ts
106
+ import { z } from "zod";
107
+ var emailAddress = z.object({
108
+ email: z.string().email(),
109
+ name: z.string().optional()
110
+ });
111
+ var recipient = z.union([z.string().email(), emailAddress]);
112
+ var sendEmailShape = {
113
+ to: z.union([recipient, z.array(recipient).min(1)]).describe("Recipient email address (string or {email,name}) or an array of them"),
114
+ from: z.union([z.string().email(), emailAddress]).optional().describe('Sender address. Defaults to the account-level "from" address.'),
115
+ replyTo: z.union([z.string().email(), emailAddress]).optional(),
116
+ cc: z.array(z.string().email()).optional(),
117
+ bcc: z.array(z.string().email()).optional(),
118
+ subject: z.string().min(1),
119
+ html: z.string().optional().describe("Rendered HTML body. Mutually exclusive with templateId."),
120
+ text: z.string().optional().describe("Plain-text body. Mutually exclusive with templateId."),
121
+ templateId: z.string().optional().describe("Template ID. When set, html/text are ignored."),
122
+ templateData: z.record(z.unknown()).optional().describe("Variables to interpolate into the template"),
123
+ tags: z.array(z.string()).optional(),
124
+ metadata: z.record(z.unknown()).optional(),
125
+ subscriberId: z.string().optional(),
126
+ category: z.string().optional(),
127
+ scheduledAt: z.string().datetime().optional().describe("ISO 8601 timestamp for scheduled delivery")
128
+ };
129
+ var sendPushShape = {
130
+ userId: z.string().optional().describe("Send to all devices for this user/subscriber"),
131
+ deviceId: z.string().optional().describe("Send to a specific device only"),
132
+ title: z.string().optional(),
133
+ body: z.string().optional(),
134
+ data: z.record(z.unknown()).optional().describe("Custom data payload delivered to the device"),
135
+ badge: z.number().int().nonnegative().optional(),
136
+ sound: z.string().optional(),
137
+ imageUrl: z.string().url().optional(),
138
+ contentAvailable: z.boolean().optional().describe("Silent/background push"),
139
+ tags: z.array(z.string()).optional(),
140
+ metadata: z.record(z.unknown()).optional(),
141
+ collapseKey: z.string().optional(),
142
+ subscriberId: z.string().optional(),
143
+ subscriberExternalId: z.string().optional(),
144
+ category: z.string().optional(),
145
+ force: z.boolean().optional().describe("Skip subscriber preference checks"),
146
+ sendAt: z.string().datetime().optional(),
147
+ delay: z.number().int().nonnegative().optional()
148
+ };
149
+ var sendSmsShape = {
150
+ to: z.string().min(1).describe("Recipient phone number in E.164 format (e.g. +14155551234)"),
151
+ from: z.string().optional().describe("Sender phone number or sender ID"),
152
+ body: z.string().min(1),
153
+ subscriberId: z.string().optional(),
154
+ scheduledAt: z.string().datetime().optional(),
155
+ metadata: z.record(z.unknown()).optional()
156
+ };
157
+ var sdkLanguages = ["node", "python", "ruby", "go", "php", "csharp"];
158
+ var quickstartChannels = ["email", "push", "sms", "inbox", "webhook"];
159
+ var getQuickstartShape = {
160
+ channel: z.enum(quickstartChannels).describe("Which Zyphr channel to wire up"),
161
+ language: z.enum(["node", "python", "ruby", "go", "php", "csharp"]).describe("Target language for the integration"),
162
+ framework: z.string().optional().describe(
163
+ 'Optional framework hint (e.g. "express", "nextjs", "flask", "fastapi", "rails", "gin", "laravel", "aspnetcore"). Falls back to plain SDK code when unrecognized.'
164
+ )
165
+ };
166
+ var getSdkInstallShape = {
167
+ language: z.enum(sdkLanguages).describe("Target language for the integration"),
168
+ packageManager: z.string().optional().describe(
169
+ 'Optional package manager override (e.g. "yarn" instead of "npm"). When recognized, only that manager is returned; otherwise the full list is returned.'
170
+ )
171
+ };
172
+ var listTemplatesShape = {
173
+ limit: z.number().int().positive().max(200).optional(),
174
+ offset: z.number().int().nonnegative().optional()
175
+ };
176
+ var getTemplateShape = {
177
+ id: z.string().min(1).describe("Template ID")
178
+ };
179
+ var renderTemplateShape = {
180
+ id: z.string().min(1).describe("Template ID"),
181
+ variables: z.record(z.unknown()).describe("Key/value variables to interpolate into the template")
182
+ };
183
+ var createTemplateShape = {
184
+ name: z.string().min(1),
185
+ description: z.string().optional(),
186
+ subject: z.string().optional().describe("Default subject (email templates)"),
187
+ html: z.string().optional(),
188
+ text: z.string().optional()
189
+ };
190
+ var findSubscriberShape = {
191
+ externalId: z.string().min(1).describe("Subscriber external ID (your application user/customer ID)")
192
+ };
193
+ var listSubscribersShape = {
194
+ status: z.enum(["active", "inactive"]).optional(),
195
+ email: z.string().email().optional(),
196
+ limit: z.number().int().positive().max(200).optional(),
197
+ offset: z.number().int().nonnegative().optional()
198
+ };
199
+ var createSubscriberShape = {
200
+ externalId: z.string().min(1).describe("Your application user/customer ID"),
201
+ email: z.string().email().optional(),
202
+ phone: z.string().optional(),
203
+ name: z.string().optional(),
204
+ avatarUrl: z.string().url().optional(),
205
+ timezone: z.string().optional(),
206
+ locale: z.string().optional(),
207
+ metadata: z.record(z.unknown()).optional()
208
+ };
209
+ var updateSubscriberShape = {
210
+ id: z.string().min(1).describe("Zyphr subscriber ID"),
211
+ email: z.string().email().nullable().optional(),
212
+ phone: z.string().nullable().optional(),
213
+ name: z.string().nullable().optional(),
214
+ avatarUrl: z.string().url().nullable().optional(),
215
+ timezone: z.string().optional(),
216
+ locale: z.string().optional(),
217
+ metadata: z.record(z.unknown()).optional(),
218
+ status: z.enum(["active", "inactive"]).optional()
219
+ };
220
+ var setSubscriberPreferencesShape = {
221
+ id: z.string().min(1).describe("Zyphr subscriber ID"),
222
+ preferences: z.array(
223
+ z.object({
224
+ categoryId: z.string().optional(),
225
+ channel: z.string().optional().describe("email | push | sms | in_app"),
226
+ enabled: z.boolean().optional()
227
+ })
228
+ ).min(1)
229
+ };
230
+ var listWebhooksShape = {
231
+ limit: z.number().int().positive().max(200).optional(),
232
+ offset: z.number().int().nonnegative().optional()
233
+ };
234
+ var createWebhookShape = {
235
+ url: z.string().url().describe("Receiver URL that Zyphr will POST events to"),
236
+ events: z.array(z.string().min(1)).min(1).describe('Event types to subscribe to (e.g. ["email.*", "subscriber.created"])'),
237
+ description: z.string().optional(),
238
+ secret: z.string().optional().describe("Optional secret used to sign payloads. If omitted, Zyphr generates one."),
239
+ metadata: z.record(z.unknown()).optional(),
240
+ headers: z.record(z.string()).optional().describe("Custom headers to send with every delivery"),
241
+ version: z.string().optional(),
242
+ rateLimit: z.number().int().positive().optional()
243
+ };
244
+ var getWebhookDeliveriesShape = {
245
+ webhookId: z.string().min(1).describe("Webhook endpoint ID"),
246
+ status: z.enum(["pending", "delivering", "delivered", "failed", "exhausted"]).optional(),
247
+ eventType: z.string().optional(),
248
+ search: z.string().optional(),
249
+ startDate: z.string().datetime().optional(),
250
+ endDate: z.string().datetime().optional(),
251
+ limit: z.number().int().positive().max(200).optional(),
252
+ offset: z.number().int().nonnegative().optional()
253
+ };
254
+ var sendInboxMessageShape = {
255
+ subscriberId: z.string().min(1).describe("Subscriber to deliver the in-app message to"),
256
+ title: z.string().min(1),
257
+ body: z.string().optional(),
258
+ actionUrl: z.string().url().optional(),
259
+ actionLabel: z.string().optional(),
260
+ imageUrl: z.string().url().optional(),
261
+ icon: z.string().optional(),
262
+ category: z.string().optional(),
263
+ priority: z.enum(["low", "normal", "high", "urgent"]).optional(),
264
+ data: z.record(z.unknown()).optional(),
265
+ tags: z.array(z.string()).optional(),
266
+ expiresAt: z.string().datetime().optional()
267
+ };
268
+
269
+ // src/tools/send.ts
270
+ function normalizeEmailAddress(value) {
271
+ if (typeof value === "string") return { email: value };
272
+ if (value && typeof value === "object" && "email" in value) {
273
+ return value;
274
+ }
275
+ return void 0;
276
+ }
277
+ function normalizeRecipients(to) {
278
+ if (Array.isArray(to)) {
279
+ return to.map(normalizeEmailAddress).filter((v) => Boolean(v));
280
+ }
281
+ const one = normalizeEmailAddress(to);
282
+ return one ? [one] : [];
283
+ }
284
+ function registerSendTools(server, guards) {
285
+ if (isToolEnabled({ name: "send_email", mutates: true }, guards)) {
286
+ server.registerTool(
287
+ "send_email",
288
+ {
289
+ title: "Send email",
290
+ description: "Send a transactional email via Zyphr. Use either html/text OR templateId+templateData, not both.",
291
+ inputSchema: sendEmailShape
292
+ },
293
+ async (args) => {
294
+ return runTool(async () => {
295
+ const zyphr = getZyphrClient();
296
+ return await zyphr.emails.sendEmail({
297
+ to: normalizeRecipients(args.to),
298
+ from: normalizeEmailAddress(args.from),
299
+ replyTo: normalizeEmailAddress(args.replyTo),
300
+ cc: args.cc,
301
+ bcc: args.bcc,
302
+ subject: args.subject,
303
+ html: args.html,
304
+ text: args.text,
305
+ templateId: args.templateId,
306
+ templateData: args.templateData,
307
+ tags: args.tags,
308
+ metadata: args.metadata,
309
+ subscriberId: args.subscriberId,
310
+ category: args.category,
311
+ scheduledAt: args.scheduledAt ? new Date(args.scheduledAt) : void 0
312
+ });
313
+ });
314
+ }
315
+ );
316
+ }
317
+ if (isToolEnabled({ name: "send_push", mutates: true }, guards)) {
318
+ server.registerTool(
319
+ "send_push",
320
+ {
321
+ title: "Send push notification",
322
+ description: "Send a push notification. Target one of: userId (all devices for a user), deviceId (specific device), subscriberId, or subscriberExternalId.",
323
+ inputSchema: sendPushShape
324
+ },
325
+ async (args) => {
326
+ return runTool(async () => {
327
+ const zyphr = getZyphrClient();
328
+ return await zyphr.push.sendPush({
329
+ userId: args.userId,
330
+ deviceId: args.deviceId,
331
+ title: args.title,
332
+ body: args.body,
333
+ data: args.data,
334
+ badge: args.badge,
335
+ sound: args.sound,
336
+ imageUrl: args.imageUrl,
337
+ contentAvailable: args.contentAvailable,
338
+ tags: args.tags,
339
+ metadata: args.metadata,
340
+ collapseKey: args.collapseKey,
341
+ subscriberId: args.subscriberId,
342
+ subscriberExternalId: args.subscriberExternalId,
343
+ category: args.category,
344
+ force: args.force,
345
+ sendAt: args.sendAt ? new Date(args.sendAt) : void 0,
346
+ delay: args.delay
347
+ });
348
+ });
349
+ }
350
+ );
351
+ }
352
+ if (isToolEnabled({ name: "send_sms", mutates: true }, guards)) {
353
+ server.registerTool(
354
+ "send_sms",
355
+ {
356
+ title: "Send SMS",
357
+ description: "Send an SMS message via Zyphr. The recipient must be in E.164 format.",
358
+ inputSchema: sendSmsShape
359
+ },
360
+ async (args) => {
361
+ return runTool(async () => {
362
+ const zyphr = getZyphrClient();
363
+ return await zyphr.sms.sendSms({
364
+ to: args.to,
365
+ from: args.from,
366
+ body: args.body,
367
+ subscriberId: args.subscriberId,
368
+ scheduledAt: args.scheduledAt ? new Date(args.scheduledAt) : void 0,
369
+ metadata: args.metadata
370
+ });
371
+ });
372
+ }
373
+ );
374
+ }
375
+ if (isToolEnabled({ name: "send_inbox_message", mutates: true }, guards)) {
376
+ server.registerTool(
377
+ "send_inbox_message",
378
+ {
379
+ title: "Send in-app inbox message",
380
+ description: "Deliver an in-app inbox notification to a Zyphr subscriber.",
381
+ inputSchema: sendInboxMessageShape
382
+ },
383
+ async (args) => {
384
+ return runTool(async () => {
385
+ const zyphr = getZyphrClient();
386
+ return await zyphr.inbox.sendInApp({
387
+ subscriberId: args.subscriberId,
388
+ title: args.title,
389
+ body: args.body,
390
+ actionUrl: args.actionUrl,
391
+ actionLabel: args.actionLabel,
392
+ imageUrl: args.imageUrl,
393
+ icon: args.icon,
394
+ category: args.category,
395
+ priority: args.priority,
396
+ data: args.data,
397
+ tags: args.tags,
398
+ expiresAt: args.expiresAt ? new Date(args.expiresAt) : void 0
399
+ });
400
+ });
401
+ }
402
+ );
403
+ }
404
+ }
405
+
406
+ // src/tools/subscribers.ts
407
+ function registerSubscriberTools(server, guards) {
408
+ if (isToolEnabled({ name: "find_subscriber", mutates: false }, guards)) {
409
+ server.registerTool(
410
+ "find_subscriber",
411
+ {
412
+ title: "Find subscriber by external ID",
413
+ description: "Look up a subscriber by the external ID you assigned (typically your application user/customer ID).",
414
+ inputSchema: findSubscriberShape
415
+ },
416
+ async (args) => {
417
+ return runTool(async () => {
418
+ const zyphr = getZyphrClient();
419
+ return await zyphr.subscribers.getSubscriberByExternalId(args.externalId);
420
+ });
421
+ }
422
+ );
423
+ }
424
+ if (isToolEnabled({ name: "list_subscribers", mutates: false }, guards)) {
425
+ server.registerTool(
426
+ "list_subscribers",
427
+ {
428
+ title: "List subscribers",
429
+ description: "List subscribers, optionally filtered by status or email.",
430
+ inputSchema: listSubscribersShape
431
+ },
432
+ async (args) => {
433
+ return runTool(async () => {
434
+ const zyphr = getZyphrClient();
435
+ return await zyphr.subscribers.listSubscribers(
436
+ args.status,
437
+ args.email,
438
+ args.limit,
439
+ args.offset
440
+ );
441
+ });
442
+ }
443
+ );
444
+ }
445
+ if (isToolEnabled({ name: "create_subscriber", mutates: true }, guards)) {
446
+ server.registerTool(
447
+ "create_subscriber",
448
+ {
449
+ title: "Create subscriber",
450
+ description: "Create a new subscriber. `externalId` must be unique within your account.",
451
+ inputSchema: createSubscriberShape
452
+ },
453
+ async (args) => {
454
+ return runTool(async () => {
455
+ const zyphr = getZyphrClient();
456
+ return await zyphr.subscribers.createSubscriber({
457
+ externalId: args.externalId,
458
+ email: args.email,
459
+ phone: args.phone,
460
+ name: args.name,
461
+ avatarUrl: args.avatarUrl,
462
+ timezone: args.timezone,
463
+ locale: args.locale,
464
+ metadata: args.metadata
465
+ });
466
+ });
467
+ }
468
+ );
469
+ }
470
+ if (isToolEnabled({ name: "update_subscriber", mutates: true }, guards)) {
471
+ server.registerTool(
472
+ "update_subscriber",
473
+ {
474
+ title: "Update subscriber",
475
+ description: "Update a subscriber by Zyphr ID. Pass null for email/phone/name/avatarUrl to clear those fields.",
476
+ inputSchema: updateSubscriberShape
477
+ },
478
+ async (args) => {
479
+ return runTool(async () => {
480
+ const zyphr = getZyphrClient();
481
+ return await zyphr.subscribers.updateSubscriber(args.id, {
482
+ email: args.email ?? void 0,
483
+ phone: args.phone ?? void 0,
484
+ name: args.name ?? void 0,
485
+ avatarUrl: args.avatarUrl ?? void 0,
486
+ timezone: args.timezone,
487
+ locale: args.locale,
488
+ metadata: args.metadata,
489
+ status: args.status
490
+ });
491
+ });
492
+ }
493
+ );
494
+ }
495
+ if (isToolEnabled({ name: "set_subscriber_preferences", mutates: true }, guards)) {
496
+ server.registerTool(
497
+ "set_subscriber_preferences",
498
+ {
499
+ title: "Set subscriber preferences",
500
+ description: "Set notification preferences for a subscriber. Each preference targets a category and/or channel and toggles `enabled`.",
501
+ inputSchema: setSubscriberPreferencesShape
502
+ },
503
+ async (args) => {
504
+ return runTool(async () => {
505
+ const zyphr = getZyphrClient();
506
+ return await zyphr.subscribers.setSubscriberPreferences(args.id, {
507
+ preferences: args.preferences
508
+ });
509
+ });
510
+ }
511
+ );
512
+ }
513
+ }
514
+
515
+ // src/tools/templates.ts
516
+ function registerTemplateTools(server, guards) {
517
+ if (isToolEnabled({ name: "list_templates", mutates: false }, guards)) {
518
+ server.registerTool(
519
+ "list_templates",
520
+ {
521
+ title: "List templates",
522
+ description: "List notification templates in the account.",
523
+ inputSchema: listTemplatesShape
524
+ },
525
+ async (args) => {
526
+ return runTool(async () => {
527
+ const zyphr = getZyphrClient();
528
+ return await zyphr.templates.listTemplates(args.limit, args.offset);
529
+ });
530
+ }
531
+ );
532
+ }
533
+ if (isToolEnabled({ name: "get_template", mutates: false }, guards)) {
534
+ server.registerTool(
535
+ "get_template",
536
+ {
537
+ title: "Get template",
538
+ description: "Fetch a single template by ID.",
539
+ inputSchema: getTemplateShape
540
+ },
541
+ async (args) => {
542
+ return runTool(async () => {
543
+ const zyphr = getZyphrClient();
544
+ return await zyphr.templates.getTemplate(args.id);
545
+ });
546
+ }
547
+ );
548
+ }
549
+ if (isToolEnabled({ name: "render_template", mutates: false }, guards)) {
550
+ server.registerTool(
551
+ "render_template",
552
+ {
553
+ title: "Render template",
554
+ description: "Preview a template with the given variables WITHOUT sending. Returns the rendered subject/html/text so the AI can show the user what would be sent.",
555
+ inputSchema: renderTemplateShape
556
+ },
557
+ async (args) => {
558
+ return runTool(async () => {
559
+ const zyphr = getZyphrClient();
560
+ return await zyphr.templates.renderTemplate(args.id, { variables: args.variables });
561
+ });
562
+ }
563
+ );
564
+ }
565
+ if (isToolEnabled({ name: "create_template", mutates: true }, guards)) {
566
+ server.registerTool(
567
+ "create_template",
568
+ {
569
+ title: "Create template",
570
+ description: "Create a new notification template.",
571
+ inputSchema: createTemplateShape
572
+ },
573
+ async (args) => {
574
+ return runTool(async () => {
575
+ const zyphr = getZyphrClient();
576
+ return await zyphr.templates.createTemplate({
577
+ name: args.name,
578
+ description: args.description,
579
+ subject: args.subject,
580
+ html: args.html,
581
+ text: args.text
582
+ });
583
+ });
584
+ }
585
+ );
586
+ }
587
+ }
588
+
589
+ // src/integration/quickstart/email.ts
590
+ var DOCS = "https://docs.zyphr.dev/channels/email";
591
+ var ENV = ["ZYPHR_API_KEY"];
592
+ var NEXT_STEPS = [
593
+ "Add ZYPHR_API_KEY to your .env file (run get_sdk_install_for_language to confirm the install).",
594
+ "Wire the new service into your app entrypoint.",
595
+ "Verify your sender domain in the Zyphr dashboard before sending to real recipients."
596
+ ];
597
+ var emailChannel = {
598
+ node: {
599
+ sdk: {
600
+ channel: "email",
601
+ language: "node",
602
+ framework: null,
603
+ variant: "sdk",
604
+ files: [
605
+ {
606
+ path: "src/lib/zyphr.ts",
607
+ purpose: "Zyphr SDK client singleton",
608
+ contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nexport const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n",
609
+ overwrite: false
610
+ },
611
+ {
612
+ path: "src/services/notify.ts",
613
+ purpose: "Sends a transactional email through Zyphr",
614
+ contents: "import { zyphr } from '../lib/zyphr.js';\n\nexport async function sendWelcomeEmail(to: string, name: string) {\n return await zyphr.emails.sendEmail({\n to: [{ email: to, name }],\n subject: `Welcome, ${name}!`,\n html: `<h1>Welcome aboard, ${name}!</h1><p>Glad you're here.</p>`,\n });\n}\n",
615
+ overwrite: false
616
+ }
617
+ ],
618
+ envVarsNeeded: ENV,
619
+ nextSteps: NEXT_STEPS,
620
+ docsUrl: DOCS
621
+ },
622
+ frameworks: {
623
+ express: {
624
+ channel: "email",
625
+ language: "node",
626
+ framework: "express",
627
+ variant: "sdk",
628
+ files: [
629
+ {
630
+ path: "src/lib/zyphr.ts",
631
+ purpose: "Zyphr SDK client singleton",
632
+ contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nexport const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n",
633
+ overwrite: false
634
+ },
635
+ {
636
+ path: "src/routes/notify.ts",
637
+ purpose: "Express route that sends a welcome email",
638
+ contents: "import { Router } from 'express';\nimport { zyphr } from '../lib/zyphr.js';\n\nexport const notifyRouter = Router();\n\nnotifyRouter.post('/notify', async (req, res, next) => {\n try {\n const { to, name } = req.body as { to: string; name: string };\n const result = await zyphr.emails.sendEmail({\n to: [{ email: to, name }],\n subject: `Welcome, ${name}!`,\n html: `<h1>Welcome, ${name}!</h1>`,\n });\n res.json(result);\n } catch (err) {\n next(err);\n }\n});\n",
639
+ overwrite: false
640
+ }
641
+ ],
642
+ envVarsNeeded: ENV,
643
+ nextSteps: [
644
+ ...NEXT_STEPS,
645
+ "Mount the router in app.ts: app.use('/api', notifyRouter)",
646
+ `Test: curl -X POST http://localhost:3000/api/notify -d '{"to":"you@example.com","name":"You"}' -H "Content-Type: application/json"`
647
+ ],
648
+ docsUrl: DOCS
649
+ },
650
+ nextjs: {
651
+ channel: "email",
652
+ language: "node",
653
+ framework: "nextjs",
654
+ variant: "sdk",
655
+ files: [
656
+ {
657
+ path: "src/lib/zyphr.ts",
658
+ purpose: "Zyphr SDK client singleton (server-only)",
659
+ contents: "import 'server-only';\nimport { Zyphr } from '@zyphr-dev/node-sdk';\n\nexport const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n",
660
+ overwrite: false
661
+ },
662
+ {
663
+ path: "src/app/api/notify/route.ts",
664
+ purpose: "Next.js App Router route handler that sends a welcome email",
665
+ contents: "import { NextResponse } from 'next/server';\nimport { zyphr } from '@/lib/zyphr';\n\nexport async function POST(req: Request) {\n const { to, name } = (await req.json()) as { to: string; name: string };\n const result = await zyphr.emails.sendEmail({\n to: [{ email: to, name }],\n subject: `Welcome, ${name}!`,\n html: `<h1>Welcome, ${name}!</h1>`,\n });\n return NextResponse.json(result);\n}\n",
666
+ overwrite: false
667
+ }
668
+ ],
669
+ envVarsNeeded: ENV,
670
+ nextSteps: [
671
+ ...NEXT_STEPS,
672
+ `Test: curl -X POST http://localhost:3000/api/notify -d '{"to":"you@example.com","name":"You"}' -H "Content-Type: application/json"`
673
+ ],
674
+ docsUrl: DOCS
675
+ }
676
+ }
677
+ },
678
+ python: {
679
+ sdk: {
680
+ channel: "email",
681
+ language: "python",
682
+ framework: null,
683
+ variant: "sdk",
684
+ files: [
685
+ {
686
+ path: "app/zyphr_client.py",
687
+ purpose: "REST helper for the Zyphr API",
688
+ contents: 'import os\nimport requests\n\nZYPHR_API_KEY = os.environ["ZYPHR_API_KEY"]\nBASE_URL = "https://api.zyphr.dev/v1"\n\nheaders = {\n "X-API-Key": ZYPHR_API_KEY,\n "Content-Type": "application/json",\n}\n\ndef zyphr_request(method, path, json=None, params=None):\n response = requests.request(\n method, f"{BASE_URL}{path}", headers=headers, json=json, params=params,\n )\n response.raise_for_status()\n return response.json()\n',
689
+ overwrite: false
690
+ },
691
+ {
692
+ path: "app/notify.py",
693
+ purpose: "Send a welcome email through Zyphr",
694
+ contents: 'from .zyphr_client import zyphr_request\n\ndef send_welcome_email(to: str, name: str) -> dict:\n return zyphr_request("POST", "/emails", json={\n "to": [{"email": to, "name": name}],\n "subject": f"Welcome, {name}!",\n "html": f"<h1>Welcome, {name}!</h1>",\n })\n',
695
+ overwrite: false
696
+ }
697
+ ],
698
+ envVarsNeeded: ENV,
699
+ nextSteps: NEXT_STEPS,
700
+ docsUrl: DOCS
701
+ },
702
+ frameworks: {
703
+ flask: {
704
+ channel: "email",
705
+ language: "python",
706
+ framework: "flask",
707
+ variant: "sdk",
708
+ files: [
709
+ {
710
+ path: "app/zyphr_client.py",
711
+ purpose: "REST helper for the Zyphr API",
712
+ contents: 'import os\nimport requests\n\nBASE_URL = "https://api.zyphr.dev/v1"\n\ndef zyphr_request(method, path, json=None, params=None):\n response = requests.request(\n method, f"{BASE_URL}{path}",\n headers={"X-API-Key": os.environ["ZYPHR_API_KEY"], "Content-Type": "application/json"},\n json=json, params=params,\n )\n response.raise_for_status()\n return response.json()\n',
713
+ overwrite: false
714
+ },
715
+ {
716
+ path: "app/routes/notify.py",
717
+ purpose: "Flask blueprint that sends a welcome email",
718
+ contents: `from flask import Blueprint, request, jsonify
719
+ from ..zyphr_client import zyphr_request
720
+
721
+ notify_bp = Blueprint("notify", __name__)
722
+
723
+ @notify_bp.route("/notify", methods=["POST"])
724
+ def notify():
725
+ body = request.get_json() or {}
726
+ result = zyphr_request("POST", "/emails", json={
727
+ "to": [{"email": body["to"], "name": body.get("name", "")}],
728
+ "subject": f"Welcome, {body.get('name', 'friend')}!",
729
+ "html": f"<h1>Welcome!</h1>",
730
+ })
731
+ return jsonify(result)
732
+ `,
733
+ overwrite: false
734
+ }
735
+ ],
736
+ envVarsNeeded: ENV,
737
+ nextSteps: [
738
+ ...NEXT_STEPS,
739
+ 'Register the blueprint: app.register_blueprint(notify_bp, url_prefix="/api")'
740
+ ],
741
+ docsUrl: DOCS
742
+ },
743
+ fastapi: {
744
+ channel: "email",
745
+ language: "python",
746
+ framework: "fastapi",
747
+ variant: "sdk",
748
+ files: [
749
+ {
750
+ path: "app/zyphr_client.py",
751
+ purpose: "REST helper for the Zyphr API",
752
+ contents: 'import os\nimport httpx\n\nBASE_URL = "https://api.zyphr.dev/v1"\n\nasync def zyphr_request(method: str, path: str, json: dict | None = None) -> dict:\n async with httpx.AsyncClient() as client:\n resp = await client.request(\n method, f"{BASE_URL}{path}",\n headers={"X-API-Key": os.environ["ZYPHR_API_KEY"], "Content-Type": "application/json"},\n json=json,\n )\n resp.raise_for_status()\n return resp.json()\n',
753
+ overwrite: false
754
+ },
755
+ {
756
+ path: "app/routers/notify.py",
757
+ purpose: "FastAPI router that sends a welcome email",
758
+ contents: 'from fastapi import APIRouter\nfrom pydantic import BaseModel, EmailStr\nfrom ..zyphr_client import zyphr_request\n\nrouter = APIRouter()\n\nclass NotifyIn(BaseModel):\n to: EmailStr\n name: str\n\n@router.post("/notify")\nasync def notify(body: NotifyIn):\n return await zyphr_request("POST", "/emails", json={\n "to": [{"email": body.to, "name": body.name}],\n "subject": f"Welcome, {body.name}!",\n "html": f"<h1>Welcome, {body.name}!</h1>",\n })\n',
759
+ overwrite: false
760
+ }
761
+ ],
762
+ envVarsNeeded: ENV,
763
+ nextSteps: [
764
+ ...NEXT_STEPS,
765
+ "Install httpx: `pip install httpx` (or `poetry add httpx`)",
766
+ 'Mount the router: app.include_router(router, prefix="/api")'
767
+ ],
768
+ docsUrl: DOCS
769
+ }
770
+ }
771
+ },
772
+ ruby: {
773
+ sdk: {
774
+ channel: "email",
775
+ language: "ruby",
776
+ framework: null,
777
+ variant: "sdk",
778
+ files: [
779
+ {
780
+ path: "config/initializers/zyphr.rb",
781
+ purpose: "Zyphr SDK configuration",
782
+ contents: "require 'zyphr'\n\nZyphr.configure do |config|\n config.api_key['X-API-Key'] = ENV.fetch('ZYPHR_API_KEY')\nend\n",
783
+ overwrite: false
784
+ },
785
+ {
786
+ path: "app/services/notify_service.rb",
787
+ purpose: "Service object that sends a welcome email",
788
+ contents: 'class NotifyService\n def self.send_welcome_email(to:, name:)\n Zyphr::EmailsApi.new.send_email(\n Zyphr::SendEmailRequest.new(\n to: [{ email: to, name: name }],\n subject: "Welcome, #{name}!",\n html: "<h1>Welcome, #{name}!</h1>"\n )\n )\n end\nend\n',
789
+ overwrite: false
790
+ }
791
+ ],
792
+ envVarsNeeded: ENV,
793
+ nextSteps: NEXT_STEPS,
794
+ docsUrl: DOCS
795
+ },
796
+ frameworks: {
797
+ rails: {
798
+ channel: "email",
799
+ language: "ruby",
800
+ framework: "rails",
801
+ variant: "sdk",
802
+ files: [
803
+ {
804
+ path: "config/initializers/zyphr.rb",
805
+ purpose: "Zyphr SDK configuration",
806
+ contents: "require 'zyphr'\n\nZyphr.configure do |config|\n config.api_key['X-API-Key'] = ENV.fetch('ZYPHR_API_KEY')\nend\n",
807
+ overwrite: false
808
+ },
809
+ {
810
+ path: "app/controllers/notify_controller.rb",
811
+ purpose: "Rails controller that sends a welcome email",
812
+ contents: 'class NotifyController < ApplicationController\n def create\n result = Zyphr::EmailsApi.new.send_email(\n Zyphr::SendEmailRequest.new(\n to: [{ email: params.require(:to), name: params[:name] }],\n subject: "Welcome, #{params[:name]}!",\n html: "<h1>Welcome, #{params[:name]}!</h1>"\n )\n )\n render json: result\n end\nend\n',
813
+ overwrite: false
814
+ }
815
+ ],
816
+ envVarsNeeded: ENV,
817
+ nextSteps: [
818
+ ...NEXT_STEPS,
819
+ "Add a route in config/routes.rb: post '/notify', to: 'notify#create'"
820
+ ],
821
+ docsUrl: DOCS
822
+ }
823
+ }
824
+ },
825
+ go: {
826
+ sdk: {
827
+ channel: "email",
828
+ language: "go",
829
+ framework: null,
830
+ variant: "sdk",
831
+ files: [
832
+ {
833
+ path: "internal/zyphr/client.go",
834
+ purpose: "Thin REST client for the Zyphr API",
835
+ contents: 'package zyphr\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "io"\n "net/http"\n "os"\n)\n\nconst baseURL = "https://api.zyphr.dev/v1"\n\ntype Client struct {\n APIKey string\n HTTPClient *http.Client\n}\n\nfunc NewClient() *Client {\n return &Client{APIKey: os.Getenv("ZYPHR_API_KEY"), HTTPClient: &http.Client{}}\n}\n\nfunc (c *Client) Do(method, path string, body any) ([]byte, error) {\n var buf io.Reader\n if body != nil {\n b, err := json.Marshal(body)\n if err != nil { return nil, fmt.Errorf("marshal: %w", err) }\n buf = bytes.NewReader(b)\n }\n req, _ := http.NewRequest(method, baseURL+path, buf)\n req.Header.Set("X-API-Key", c.APIKey)\n req.Header.Set("Content-Type", "application/json")\n resp, err := c.HTTPClient.Do(req)\n if err != nil { return nil, err }\n defer resp.Body.Close()\n data, _ := io.ReadAll(resp.Body)\n if resp.StatusCode >= 400 { return nil, fmt.Errorf("zyphr %d: %s", resp.StatusCode, data) }\n return data, nil\n}\n',
836
+ overwrite: false
837
+ },
838
+ {
839
+ path: "internal/notify/notify.go",
840
+ purpose: "Send a welcome email through Zyphr",
841
+ contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc SendWelcomeEmail(client *zyphr.Client, to, name string) ([]byte, error) {\n return client.Do("POST", "/emails", map[string]any{\n "to": []map[string]string{{"email": to, "name": name}},\n "subject": "Welcome, " + name + "!",\n "html": "<h1>Welcome, " + name + "!</h1>",\n })\n}\n',
842
+ overwrite: false
843
+ }
844
+ ],
845
+ envVarsNeeded: ENV,
846
+ nextSteps: NEXT_STEPS,
847
+ docsUrl: DOCS
848
+ }
849
+ },
850
+ php: {
851
+ sdk: {
852
+ channel: "email",
853
+ language: "php",
854
+ framework: null,
855
+ variant: "sdk",
856
+ files: [
857
+ {
858
+ path: "app/Services/ZyphrClient.php",
859
+ purpose: "Guzzle-backed Zyphr client",
860
+ contents: `<?php
861
+
862
+ namespace App\\Services;
863
+
864
+ use GuzzleHttp\\Client;
865
+
866
+ class ZyphrClient
867
+ {
868
+ private Client $http;
869
+
870
+ public function __construct()
871
+ {
872
+ $this->http = new Client([
873
+ 'base_uri' => 'https://api.zyphr.dev/v1/',
874
+ 'headers' => [
875
+ 'X-API-Key' => getenv('ZYPHR_API_KEY'),
876
+ 'Content-Type' => 'application/json',
877
+ ],
878
+ ]);
879
+ }
880
+
881
+ public function sendWelcomeEmail(string $to, string $name): array
882
+ {
883
+ $response = $this->http->post('emails', [
884
+ 'json' => [
885
+ 'to' => [['email' => $to, 'name' => $name]],
886
+ 'subject' => "Welcome, {$name}!",
887
+ 'html' => "<h1>Welcome, {$name}!</h1>",
888
+ ],
889
+ ]);
890
+ return json_decode((string) $response->getBody(), true);
891
+ }
892
+ }
893
+ `,
894
+ overwrite: false
895
+ }
896
+ ],
897
+ envVarsNeeded: ENV,
898
+ nextSteps: NEXT_STEPS,
899
+ docsUrl: DOCS
900
+ },
901
+ frameworks: {
902
+ laravel: {
903
+ channel: "email",
904
+ language: "php",
905
+ framework: "laravel",
906
+ variant: "sdk",
907
+ files: [
908
+ {
909
+ path: "app/Services/ZyphrClient.php",
910
+ purpose: "Laravel-friendly Zyphr client",
911
+ contents: `<?php
912
+
913
+ namespace App\\Services;
914
+
915
+ use Illuminate\\Support\\Facades\\Http;
916
+
917
+ class ZyphrClient
918
+ {
919
+ public function sendWelcomeEmail(string $to, string $name): array
920
+ {
921
+ $response = Http::withHeaders([
922
+ 'X-API-Key' => config('services.zyphr.api_key'),
923
+ 'Content-Type' => 'application/json',
924
+ ])->post('https://api.zyphr.dev/v1/emails', [
925
+ 'to' => [['email' => $to, 'name' => $name]],
926
+ 'subject' => "Welcome, {$name}!",
927
+ 'html' => "<h1>Welcome, {$name}!</h1>",
928
+ ]);
929
+ $response->throw();
930
+ return $response->json();
931
+ }
932
+ }
933
+ `,
934
+ overwrite: false
935
+ },
936
+ {
937
+ path: "config/services.php (snippet)",
938
+ purpose: "Register the Zyphr API key under services config",
939
+ contents: "'zyphr' => [\n 'api_key' => env('ZYPHR_API_KEY'),\n],\n",
940
+ overwrite: false
941
+ }
942
+ ],
943
+ envVarsNeeded: ENV,
944
+ nextSteps: NEXT_STEPS,
945
+ docsUrl: DOCS
946
+ }
947
+ }
948
+ },
949
+ csharp: {
950
+ sdk: {
951
+ channel: "email",
952
+ language: "csharp",
953
+ framework: null,
954
+ variant: "sdk",
955
+ files: [
956
+ {
957
+ path: "Services/ZyphrClient.cs",
958
+ purpose: "Singleton-style Zyphr client wrapper",
959
+ contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class ZyphrClient\n{\n private readonly EmailsApi _emails;\n\n public ZyphrClient()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _emails = new EmailsApi(config);\n }\n\n public async Task<SendEmailResponse> SendWelcomeEmailAsync(string to, string name)\n {\n return await _emails.SendEmailAsync(new SendEmailRequest(\n to: new List<EmailAddress> { new() { Email = to, Name = name } },\n subject: $"Welcome, {name}!",\n html: $"<h1>Welcome, {name}!</h1>"\n ));\n }\n}\n',
960
+ overwrite: false
961
+ }
962
+ ],
963
+ envVarsNeeded: ENV,
964
+ nextSteps: NEXT_STEPS,
965
+ docsUrl: DOCS
966
+ },
967
+ frameworks: {
968
+ aspnetcore: {
969
+ channel: "email",
970
+ language: "csharp",
971
+ framework: "aspnetcore",
972
+ variant: "sdk",
973
+ files: [
974
+ {
975
+ path: "Services/ZyphrClient.cs",
976
+ purpose: "DI-friendly Zyphr client",
977
+ contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class ZyphrClient\n{\n public EmailsApi Emails { get; }\n\n public ZyphrClient(IConfiguration cfg)\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", cfg["Zyphr:ApiKey"]! }\n }\n };\n Emails = new EmailsApi(config);\n }\n}\n',
978
+ overwrite: false
979
+ },
980
+ {
981
+ path: "Controllers/NotifyController.cs",
982
+ purpose: "ASP.NET Core controller that sends a welcome email",
983
+ contents: 'using Microsoft.AspNetCore.Mvc;\nusing YourApp.Services;\nusing ZyphrDev.SDK.Model;\n\n[ApiController]\n[Route("api/[controller]")]\npublic class NotifyController : ControllerBase\n{\n private readonly ZyphrClient _zyphr;\n public NotifyController(ZyphrClient zyphr) => _zyphr = zyphr;\n\n public record NotifyIn(string To, string Name);\n\n [HttpPost]\n public async Task<IActionResult> Post([FromBody] NotifyIn body)\n {\n var result = await _zyphr.Emails.SendEmailAsync(new SendEmailRequest(\n to: new List<EmailAddress> { new() { Email = body.To, Name = body.Name } },\n subject: $"Welcome, {body.Name}!",\n html: $"<h1>Welcome, {body.Name}!</h1>"\n ));\n return Ok(result);\n }\n}\n',
984
+ overwrite: false
985
+ }
986
+ ],
987
+ envVarsNeeded: ENV,
988
+ nextSteps: [
989
+ ...NEXT_STEPS,
990
+ "Register the client in Program.cs: builder.Services.AddSingleton<ZyphrClient>();"
991
+ ],
992
+ docsUrl: DOCS
993
+ }
994
+ }
995
+ }
996
+ };
997
+
998
+ // src/integration/quickstart/inbox.ts
999
+ var DOCS2 = "https://docs.zyphr.dev/channels/in-app-messaging";
1000
+ var ENV2 = ["ZYPHR_API_KEY"];
1001
+ var NEXT_STEPS2 = [
1002
+ "Add ZYPHR_API_KEY to your .env file.",
1003
+ "Use the matching subscriberId on the client (e.g. @zyphr-dev/inbox-react) to display the message."
1004
+ ];
1005
+ var inboxChannel = {
1006
+ node: {
1007
+ sdk: {
1008
+ channel: "inbox",
1009
+ language: "node",
1010
+ framework: null,
1011
+ variant: "sdk",
1012
+ files: [
1013
+ {
1014
+ path: "src/services/inbox.ts",
1015
+ purpose: "Send an in-app inbox message through Zyphr",
1016
+ contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nconst zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\nexport async function notifyReportReady(subscriberId: string, reportId: string) {\n return await zyphr.inbox.sendInApp({\n subscriberId,\n title: 'New report ready',\n body: 'Your report finished processing \u2014 click to view.',\n actionUrl: `/reports/${reportId}`,\n actionLabel: 'View report',\n });\n}\n",
1017
+ overwrite: false
1018
+ }
1019
+ ],
1020
+ envVarsNeeded: ENV2,
1021
+ nextSteps: NEXT_STEPS2,
1022
+ docsUrl: DOCS2
1023
+ }
1024
+ },
1025
+ python: {
1026
+ sdk: {
1027
+ channel: "inbox",
1028
+ language: "python",
1029
+ framework: null,
1030
+ variant: "sdk",
1031
+ files: [
1032
+ {
1033
+ path: "app/inbox.py",
1034
+ purpose: "Send an in-app inbox message through Zyphr",
1035
+ contents: 'from .zyphr_client import zyphr_request\n\ndef notify_report_ready(subscriber_id: str, report_id: str) -> dict:\n return zyphr_request("POST", "/inbox", json={\n "subscriberId": subscriber_id,\n "title": "New report ready",\n "body": "Your report finished processing \u2014 click to view.",\n "actionUrl": f"/reports/{report_id}",\n "actionLabel": "View report",\n })\n',
1036
+ overwrite: false
1037
+ }
1038
+ ],
1039
+ envVarsNeeded: ENV2,
1040
+ nextSteps: NEXT_STEPS2,
1041
+ docsUrl: DOCS2
1042
+ }
1043
+ },
1044
+ ruby: {
1045
+ sdk: {
1046
+ channel: "inbox",
1047
+ language: "ruby",
1048
+ framework: null,
1049
+ variant: "sdk",
1050
+ files: [
1051
+ {
1052
+ path: "app/services/inbox_service.rb",
1053
+ purpose: "Send an in-app inbox message through Zyphr",
1054
+ contents: `class InboxService
1055
+ def self.notify_report_ready(subscriber_id:, report_id:)
1056
+ Zyphr::InboxApi.new.send_in_app(
1057
+ Zyphr::SendInAppRequest.new(
1058
+ subscriber_id: subscriber_id,
1059
+ title: 'New report ready',
1060
+ body: 'Your report finished processing \u2014 click to view.',
1061
+ action_url: "/reports/#{report_id}",
1062
+ action_label: 'View report'
1063
+ )
1064
+ )
1065
+ end
1066
+ end
1067
+ `,
1068
+ overwrite: false
1069
+ }
1070
+ ],
1071
+ envVarsNeeded: ENV2,
1072
+ nextSteps: NEXT_STEPS2,
1073
+ docsUrl: DOCS2
1074
+ }
1075
+ },
1076
+ go: {
1077
+ sdk: {
1078
+ channel: "inbox",
1079
+ language: "go",
1080
+ framework: null,
1081
+ variant: "sdk",
1082
+ files: [
1083
+ {
1084
+ path: "internal/notify/inbox.go",
1085
+ purpose: "Send an in-app inbox message through Zyphr",
1086
+ contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc NotifyReportReady(client *zyphr.Client, subscriberID, reportID string) ([]byte, error) {\n return client.Do("POST", "/inbox", map[string]any{\n "subscriberId": subscriberID,\n "title": "New report ready",\n "body": "Your report finished processing \u2014 click to view.",\n "actionUrl": "/reports/" + reportID,\n "actionLabel": "View report",\n })\n}\n',
1087
+ overwrite: false
1088
+ }
1089
+ ],
1090
+ envVarsNeeded: ENV2,
1091
+ nextSteps: NEXT_STEPS2,
1092
+ docsUrl: DOCS2
1093
+ }
1094
+ },
1095
+ php: {
1096
+ sdk: {
1097
+ channel: "inbox",
1098
+ language: "php",
1099
+ framework: null,
1100
+ variant: "sdk",
1101
+ files: [
1102
+ {
1103
+ path: "app/Services/InboxService.php",
1104
+ purpose: "Send an in-app inbox message through Zyphr",
1105
+ contents: `<?php
1106
+
1107
+ namespace App\\Services;
1108
+
1109
+ use GuzzleHttp\\Client;
1110
+
1111
+ class InboxService
1112
+ {
1113
+ public function notifyReportReady(string $subscriberId, string $reportId): array
1114
+ {
1115
+ $http = new Client([
1116
+ 'base_uri' => 'https://api.zyphr.dev/v1/',
1117
+ 'headers' => [
1118
+ 'X-API-Key' => getenv('ZYPHR_API_KEY'),
1119
+ 'Content-Type' => 'application/json',
1120
+ ],
1121
+ ]);
1122
+ $r = $http->post('inbox', ['json' => [
1123
+ 'subscriberId' => $subscriberId,
1124
+ 'title' => 'New report ready',
1125
+ 'body' => 'Your report finished processing \u2014 click to view.',
1126
+ 'actionUrl' => "/reports/{$reportId}",
1127
+ 'actionLabel' => 'View report',
1128
+ ]]);
1129
+ return json_decode((string) $r->getBody(), true);
1130
+ }
1131
+ }
1132
+ `,
1133
+ overwrite: false
1134
+ }
1135
+ ],
1136
+ envVarsNeeded: ENV2,
1137
+ nextSteps: NEXT_STEPS2,
1138
+ docsUrl: DOCS2
1139
+ }
1140
+ },
1141
+ csharp: {
1142
+ sdk: {
1143
+ channel: "inbox",
1144
+ language: "csharp",
1145
+ framework: null,
1146
+ variant: "sdk",
1147
+ files: [
1148
+ {
1149
+ path: "Services/InboxService.cs",
1150
+ purpose: "Send an in-app inbox message through Zyphr",
1151
+ contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class InboxService\n{\n private readonly InboxApi _inbox;\n public InboxService()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _inbox = new InboxApi(config);\n }\n\n public Task<SendInAppResponse> NotifyReportReadyAsync(string subscriberId, string reportId)\n {\n return _inbox.SendInAppAsync(new SendInAppRequest(\n subscriberId: subscriberId,\n title: "New report ready",\n body: "Your report finished processing \u2014 click to view.",\n actionUrl: $"/reports/{reportId}",\n actionLabel: "View report"\n ));\n }\n}\n',
1152
+ overwrite: false
1153
+ }
1154
+ ],
1155
+ envVarsNeeded: ENV2,
1156
+ nextSteps: NEXT_STEPS2,
1157
+ docsUrl: DOCS2
1158
+ }
1159
+ }
1160
+ };
1161
+
1162
+ // src/integration/quickstart/push.ts
1163
+ var DOCS3 = "https://docs.zyphr.dev/channels/push-notifications";
1164
+ var ENV3 = ["ZYPHR_API_KEY"];
1165
+ var NEXT_STEPS3 = [
1166
+ "Add ZYPHR_API_KEY to your .env file.",
1167
+ "Register at least one device for the target subscriber (via the SDK or dashboard) before sending.",
1168
+ "Verify your push provider credentials (APNs/FCM) are configured in the Zyphr dashboard."
1169
+ ];
1170
+ var pushChannel = {
1171
+ node: {
1172
+ sdk: {
1173
+ channel: "push",
1174
+ language: "node",
1175
+ framework: null,
1176
+ variant: "sdk",
1177
+ files: [
1178
+ {
1179
+ path: "src/services/push.ts",
1180
+ purpose: "Send a push notification through Zyphr",
1181
+ contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nconst zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\nexport async function pushOrderShipped(subscriberId: string, orderId: string) {\n return await zyphr.push.sendPush({\n subscriberId,\n title: 'Order shipped',\n body: `Order ${orderId} is on its way.`,\n data: { orderId },\n });\n}\n",
1182
+ overwrite: false
1183
+ }
1184
+ ],
1185
+ envVarsNeeded: ENV3,
1186
+ nextSteps: NEXT_STEPS3,
1187
+ docsUrl: DOCS3
1188
+ }
1189
+ },
1190
+ python: {
1191
+ sdk: {
1192
+ channel: "push",
1193
+ language: "python",
1194
+ framework: null,
1195
+ variant: "sdk",
1196
+ files: [
1197
+ {
1198
+ path: "app/push.py",
1199
+ purpose: "Send a push notification through Zyphr",
1200
+ contents: 'from .zyphr_client import zyphr_request\n\ndef push_order_shipped(subscriber_id: str, order_id: str) -> dict:\n return zyphr_request("POST", "/push", json={\n "subscriberId": subscriber_id,\n "title": "Order shipped",\n "body": f"Order {order_id} is on its way.",\n "data": {"orderId": order_id},\n })\n',
1201
+ overwrite: false
1202
+ }
1203
+ ],
1204
+ envVarsNeeded: ENV3,
1205
+ nextSteps: NEXT_STEPS3,
1206
+ docsUrl: DOCS3
1207
+ }
1208
+ },
1209
+ ruby: {
1210
+ sdk: {
1211
+ channel: "push",
1212
+ language: "ruby",
1213
+ framework: null,
1214
+ variant: "sdk",
1215
+ files: [
1216
+ {
1217
+ path: "app/services/push_service.rb",
1218
+ purpose: "Send a push notification through Zyphr",
1219
+ contents: `class PushService
1220
+ def self.order_shipped(subscriber_id:, order_id:)
1221
+ Zyphr::PushApi.new.send_push(
1222
+ Zyphr::SendPushRequest.new(
1223
+ subscriber_id: subscriber_id,
1224
+ title: 'Order shipped',
1225
+ body: "Order #{order_id} is on its way.",
1226
+ data: { orderId: order_id }
1227
+ )
1228
+ )
1229
+ end
1230
+ end
1231
+ `,
1232
+ overwrite: false
1233
+ }
1234
+ ],
1235
+ envVarsNeeded: ENV3,
1236
+ nextSteps: NEXT_STEPS3,
1237
+ docsUrl: DOCS3
1238
+ }
1239
+ },
1240
+ go: {
1241
+ sdk: {
1242
+ channel: "push",
1243
+ language: "go",
1244
+ framework: null,
1245
+ variant: "sdk",
1246
+ files: [
1247
+ {
1248
+ path: "internal/notify/push.go",
1249
+ purpose: "Send a push notification through Zyphr",
1250
+ contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc PushOrderShipped(client *zyphr.Client, subscriberID, orderID string) ([]byte, error) {\n return client.Do("POST", "/push", map[string]any{\n "subscriberId": subscriberID,\n "title": "Order shipped",\n "body": "Order " + orderID + " is on its way.",\n "data": map[string]string{"orderId": orderID},\n })\n}\n',
1251
+ overwrite: false
1252
+ }
1253
+ ],
1254
+ envVarsNeeded: ENV3,
1255
+ nextSteps: NEXT_STEPS3,
1256
+ docsUrl: DOCS3
1257
+ }
1258
+ },
1259
+ php: {
1260
+ sdk: {
1261
+ channel: "push",
1262
+ language: "php",
1263
+ framework: null,
1264
+ variant: "sdk",
1265
+ files: [
1266
+ {
1267
+ path: "app/Services/PushService.php",
1268
+ purpose: "Send a push notification through Zyphr",
1269
+ contents: `<?php
1270
+
1271
+ namespace App\\Services;
1272
+
1273
+ use GuzzleHttp\\Client;
1274
+
1275
+ class PushService
1276
+ {
1277
+ public function orderShipped(string $subscriberId, string $orderId): array
1278
+ {
1279
+ $http = new Client([
1280
+ 'base_uri' => 'https://api.zyphr.dev/v1/',
1281
+ 'headers' => [
1282
+ 'X-API-Key' => getenv('ZYPHR_API_KEY'),
1283
+ 'Content-Type' => 'application/json',
1284
+ ],
1285
+ ]);
1286
+ $r = $http->post('push', ['json' => [
1287
+ 'subscriberId' => $subscriberId,
1288
+ 'title' => 'Order shipped',
1289
+ 'body' => "Order {$orderId} is on its way.",
1290
+ 'data' => ['orderId' => $orderId],
1291
+ ]]);
1292
+ return json_decode((string) $r->getBody(), true);
1293
+ }
1294
+ }
1295
+ `,
1296
+ overwrite: false
1297
+ }
1298
+ ],
1299
+ envVarsNeeded: ENV3,
1300
+ nextSteps: NEXT_STEPS3,
1301
+ docsUrl: DOCS3
1302
+ }
1303
+ },
1304
+ csharp: {
1305
+ sdk: {
1306
+ channel: "push",
1307
+ language: "csharp",
1308
+ framework: null,
1309
+ variant: "sdk",
1310
+ files: [
1311
+ {
1312
+ path: "Services/PushService.cs",
1313
+ purpose: "Send a push notification through Zyphr",
1314
+ contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class PushService\n{\n private readonly PushApi _push;\n public PushService()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _push = new PushApi(config);\n }\n\n public Task<SendPushResponse> OrderShippedAsync(string subscriberId, string orderId)\n {\n return _push.SendPushAsync(new SendPushRequest(\n subscriberId: subscriberId,\n title: "Order shipped",\n body: $"Order {orderId} is on its way."\n ));\n }\n}\n',
1315
+ overwrite: false
1316
+ }
1317
+ ],
1318
+ envVarsNeeded: ENV3,
1319
+ nextSteps: NEXT_STEPS3,
1320
+ docsUrl: DOCS3
1321
+ }
1322
+ }
1323
+ };
1324
+
1325
+ // src/integration/quickstart/sms.ts
1326
+ var DOCS4 = "https://docs.zyphr.dev/channels/sms";
1327
+ var ENV4 = ["ZYPHR_API_KEY"];
1328
+ var NEXT_STEPS4 = [
1329
+ "Add ZYPHR_API_KEY to your .env file.",
1330
+ "Recipients MUST be in E.164 format (e.g. +14155551234).",
1331
+ "Provision your SMS sender (phone number or alphanumeric sender ID) in the Zyphr dashboard."
1332
+ ];
1333
+ var smsChannel = {
1334
+ node: {
1335
+ sdk: {
1336
+ channel: "sms",
1337
+ language: "node",
1338
+ framework: null,
1339
+ variant: "sdk",
1340
+ files: [
1341
+ {
1342
+ path: "src/services/sms.ts",
1343
+ purpose: "Send an SMS through Zyphr",
1344
+ contents: "import { Zyphr } from '@zyphr-dev/node-sdk';\n\nconst zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\nexport async function sendOtp(to: string, code: string) {\n return await zyphr.sms.sendSms({\n to,\n body: `Your verification code is ${code}. Expires in 5 minutes.`,\n });\n}\n",
1345
+ overwrite: false
1346
+ }
1347
+ ],
1348
+ envVarsNeeded: ENV4,
1349
+ nextSteps: NEXT_STEPS4,
1350
+ docsUrl: DOCS4
1351
+ }
1352
+ },
1353
+ python: {
1354
+ sdk: {
1355
+ channel: "sms",
1356
+ language: "python",
1357
+ framework: null,
1358
+ variant: "sdk",
1359
+ files: [
1360
+ {
1361
+ path: "app/sms.py",
1362
+ purpose: "Send an SMS through Zyphr",
1363
+ contents: 'from .zyphr_client import zyphr_request\n\ndef send_otp(to: str, code: str) -> dict:\n return zyphr_request("POST", "/sms", json={\n "to": to,\n "body": f"Your verification code is {code}. Expires in 5 minutes.",\n })\n',
1364
+ overwrite: false
1365
+ }
1366
+ ],
1367
+ envVarsNeeded: ENV4,
1368
+ nextSteps: NEXT_STEPS4,
1369
+ docsUrl: DOCS4
1370
+ }
1371
+ },
1372
+ ruby: {
1373
+ sdk: {
1374
+ channel: "sms",
1375
+ language: "ruby",
1376
+ framework: null,
1377
+ variant: "sdk",
1378
+ files: [
1379
+ {
1380
+ path: "app/services/sms_service.rb",
1381
+ purpose: "Send an SMS through Zyphr",
1382
+ contents: 'class SmsService\n def self.send_otp(to:, code:)\n Zyphr::SMSApi.new.send_sms(\n Zyphr::SendSmsRequest.new(\n to: to,\n body: "Your verification code is #{code}. Expires in 5 minutes."\n )\n )\n end\nend\n',
1383
+ overwrite: false
1384
+ }
1385
+ ],
1386
+ envVarsNeeded: ENV4,
1387
+ nextSteps: NEXT_STEPS4,
1388
+ docsUrl: DOCS4
1389
+ }
1390
+ },
1391
+ go: {
1392
+ sdk: {
1393
+ channel: "sms",
1394
+ language: "go",
1395
+ framework: null,
1396
+ variant: "sdk",
1397
+ files: [
1398
+ {
1399
+ path: "internal/notify/sms.go",
1400
+ purpose: "Send an SMS through Zyphr",
1401
+ contents: 'package notify\n\nimport "yourapp/internal/zyphr"\n\nfunc SendOtp(client *zyphr.Client, to, code string) ([]byte, error) {\n return client.Do("POST", "/sms", map[string]any{\n "to": to,\n "body": "Your verification code is " + code + ". Expires in 5 minutes.",\n })\n}\n',
1402
+ overwrite: false
1403
+ }
1404
+ ],
1405
+ envVarsNeeded: ENV4,
1406
+ nextSteps: NEXT_STEPS4,
1407
+ docsUrl: DOCS4
1408
+ }
1409
+ },
1410
+ php: {
1411
+ sdk: {
1412
+ channel: "sms",
1413
+ language: "php",
1414
+ framework: null,
1415
+ variant: "sdk",
1416
+ files: [
1417
+ {
1418
+ path: "app/Services/SmsService.php",
1419
+ purpose: "Send an SMS through Zyphr",
1420
+ contents: `<?php
1421
+
1422
+ namespace App\\Services;
1423
+
1424
+ use GuzzleHttp\\Client;
1425
+
1426
+ class SmsService
1427
+ {
1428
+ public function sendOtp(string $to, string $code): array
1429
+ {
1430
+ $http = new Client([
1431
+ 'base_uri' => 'https://api.zyphr.dev/v1/',
1432
+ 'headers' => [
1433
+ 'X-API-Key' => getenv('ZYPHR_API_KEY'),
1434
+ 'Content-Type' => 'application/json',
1435
+ ],
1436
+ ]);
1437
+ $r = $http->post('sms', ['json' => [
1438
+ 'to' => $to,
1439
+ 'body' => "Your verification code is {$code}. Expires in 5 minutes.",
1440
+ ]]);
1441
+ return json_decode((string) $r->getBody(), true);
1442
+ }
1443
+ }
1444
+ `,
1445
+ overwrite: false
1446
+ }
1447
+ ],
1448
+ envVarsNeeded: ENV4,
1449
+ nextSteps: NEXT_STEPS4,
1450
+ docsUrl: DOCS4
1451
+ }
1452
+ },
1453
+ csharp: {
1454
+ sdk: {
1455
+ channel: "sms",
1456
+ language: "csharp",
1457
+ framework: null,
1458
+ variant: "sdk",
1459
+ files: [
1460
+ {
1461
+ path: "Services/SmsService.cs",
1462
+ purpose: "Send an SMS through Zyphr",
1463
+ contents: 'using ZyphrDev.SDK.Api;\nusing ZyphrDev.SDK.Client;\nusing ZyphrDev.SDK.Model;\n\nnamespace YourApp.Services;\n\npublic class SmsService\n{\n private readonly SMSApi _sms;\n public SmsService()\n {\n var config = new Configuration\n {\n ApiKey = new Dictionary<string, string>\n {\n { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n }\n };\n _sms = new SMSApi(config);\n }\n\n public Task<SendSmsResponse> SendOtpAsync(string to, string code)\n {\n return _sms.SendSmsAsync(new SendSmsRequest(\n to: to,\n body: $"Your verification code is {code}. Expires in 5 minutes."\n ));\n }\n}\n',
1464
+ overwrite: false
1465
+ }
1466
+ ],
1467
+ envVarsNeeded: ENV4,
1468
+ nextSteps: NEXT_STEPS4,
1469
+ docsUrl: DOCS4
1470
+ }
1471
+ }
1472
+ };
1473
+
1474
+ // src/integration/quickstart/webhook.ts
1475
+ var DOCS5 = "https://docs.zyphr.dev/features/webhooks-security";
1476
+ var ENV5 = ["ZYPHR_WEBHOOK_SECRET"];
1477
+ var NEXT_STEPS5 = [
1478
+ "Add ZYPHR_WEBHOOK_SECRET to your .env file \u2014 get it from `zyphr.webhooks.rotateWebhookSecret(id)` or the Zyphr dashboard.",
1479
+ "Configure the webhook endpoint URL in the Zyphr dashboard or via `create_webhook`.",
1480
+ "ALWAYS verify signatures before processing payloads \u2014 never trust an unverified webhook.",
1481
+ "Reject deliveries whose timestamp is more than 5 minutes from now to prevent replay attacks."
1482
+ ];
1483
+ var webhookChannel = {
1484
+ node: {
1485
+ sdk: {
1486
+ channel: "webhook",
1487
+ language: "node",
1488
+ framework: null,
1489
+ variant: "webhook-handler",
1490
+ files: [
1491
+ {
1492
+ path: "src/lib/verifyZyphrWebhook.ts",
1493
+ purpose: "Standard Webhooks (HMAC-SHA256) signature + timestamp verification. Mirrors the canonical snippet in apps/docs/docs/features/webhooks-security.md.",
1494
+ contents: "import crypto from 'crypto';\n\nexport function verifyZyphrWebhook(\n payload: string,\n headers: { 'webhook-id': string; 'webhook-timestamp': string; 'webhook-signature': string },\n secret: string,\n): boolean {\n const msgId = headers['webhook-id'];\n const timestamp = parseInt(headers['webhook-timestamp'], 10);\n const signatures = headers['webhook-signature'];\n\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - timestamp) > 300) return false;\n\n const signedContent = `${msgId}.${timestamp}.${payload}`;\n const secretBytes = Buffer.from(\n secret.startsWith('whsec_') ? secret.slice(6) : secret,\n 'hex',\n );\n const expected = crypto\n .createHmac('sha256', secretBytes)\n .update(signedContent)\n .digest('base64');\n\n for (const sig of signatures.split(' ')) {\n const sigValue = sig.slice(3);\n if (\n sigValue.length === expected.length &&\n crypto.timingSafeEqual(Buffer.from(sigValue), Buffer.from(expected))\n ) {\n return true;\n }\n }\n return false;\n}\n",
1495
+ overwrite: false
1496
+ }
1497
+ ],
1498
+ envVarsNeeded: ENV5,
1499
+ nextSteps: NEXT_STEPS5,
1500
+ docsUrl: DOCS5
1501
+ },
1502
+ frameworks: {
1503
+ express: {
1504
+ channel: "webhook",
1505
+ language: "node",
1506
+ framework: "express",
1507
+ variant: "webhook-handler",
1508
+ files: [
1509
+ {
1510
+ path: "src/lib/verifyZyphrWebhook.ts",
1511
+ purpose: "Standard Webhooks signature verification helper",
1512
+ contents: "import crypto from 'crypto';\n\nexport function verifyZyphrWebhook(payload: string, headers: Record<string,string>, secret: string): boolean {\n const msgId = headers['webhook-id'];\n const timestamp = parseInt(headers['webhook-timestamp'], 10);\n const signatures = headers['webhook-signature'] || '';\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - timestamp) > 300) return false;\n const signedContent = `${msgId}.${timestamp}.${payload}`;\n const secretBytes = Buffer.from(secret.startsWith('whsec_') ? secret.slice(6) : secret, 'hex');\n const expected = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');\n return signatures.split(' ').some((sig) => {\n const v = sig.slice(3);\n return v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected));\n });\n}\n",
1513
+ overwrite: false
1514
+ },
1515
+ {
1516
+ path: "src/routes/zyphrWebhook.ts",
1517
+ purpose: "Express route that VERIFIES the signature before processing the webhook. Uses express.raw() so we can hash the exact bytes.",
1518
+ contents: "import { Router, raw } from 'express';\nimport { verifyZyphrWebhook } from '../lib/verifyZyphrWebhook.js';\n\nexport const zyphrWebhookRouter = Router();\n\nzyphrWebhookRouter.post('/zyphr', raw({ type: 'application/json' }), (req, res) => {\n const payload = (req.body as Buffer).toString();\n const headers = req.headers as Record<string, string>;\n if (!verifyZyphrWebhook(payload, headers, process.env.ZYPHR_WEBHOOK_SECRET!)) {\n return res.status(401).send('invalid signature');\n }\n const event = JSON.parse(payload) as { type: string; data: unknown };\n console.log('zyphr event', event.type);\n res.sendStatus(204);\n});\n",
1519
+ overwrite: false
1520
+ }
1521
+ ],
1522
+ envVarsNeeded: ENV5,
1523
+ nextSteps: [
1524
+ ...NEXT_STEPS5,
1525
+ "Mount the router BEFORE express.json(): app.use(zyphrWebhookRouter)"
1526
+ ],
1527
+ docsUrl: DOCS5
1528
+ },
1529
+ nextjs: {
1530
+ channel: "webhook",
1531
+ language: "node",
1532
+ framework: "nextjs",
1533
+ variant: "webhook-handler",
1534
+ files: [
1535
+ {
1536
+ path: "src/lib/verifyZyphrWebhook.ts",
1537
+ purpose: "Standard Webhooks signature verification helper",
1538
+ contents: "import crypto from 'crypto';\n\nexport function verifyZyphrWebhook(payload: string, headers: Headers, secret: string): boolean {\n const msgId = headers.get('webhook-id') || '';\n const timestamp = parseInt(headers.get('webhook-timestamp') || '0', 10);\n const signatures = headers.get('webhook-signature') || '';\n const now = Math.floor(Date.now() / 1000);\n if (Math.abs(now - timestamp) > 300) return false;\n const signedContent = `${msgId}.${timestamp}.${payload}`;\n const secretBytes = Buffer.from(secret.startsWith('whsec_') ? secret.slice(6) : secret, 'hex');\n const expected = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');\n return signatures.split(' ').some((sig) => {\n const v = sig.slice(3);\n return v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected));\n });\n}\n",
1539
+ overwrite: false
1540
+ },
1541
+ {
1542
+ path: "src/app/api/zyphr/webhook/route.ts",
1543
+ purpose: "Next.js App Router webhook handler with signature verification",
1544
+ contents: "import { NextResponse } from 'next/server';\nimport { verifyZyphrWebhook } from '@/lib/verifyZyphrWebhook';\n\nexport async function POST(req: Request) {\n const payload = await req.text();\n if (!verifyZyphrWebhook(payload, req.headers, process.env.ZYPHR_WEBHOOK_SECRET!)) {\n return new NextResponse('invalid signature', { status: 401 });\n }\n const event = JSON.parse(payload) as { type: string; data: unknown };\n console.log('zyphr event', event.type);\n return new NextResponse(null, { status: 204 });\n}\n",
1545
+ overwrite: false
1546
+ }
1547
+ ],
1548
+ envVarsNeeded: ENV5,
1549
+ nextSteps: NEXT_STEPS5,
1550
+ docsUrl: DOCS5
1551
+ }
1552
+ }
1553
+ },
1554
+ python: {
1555
+ sdk: {
1556
+ channel: "webhook",
1557
+ language: "python",
1558
+ framework: null,
1559
+ variant: "webhook-handler",
1560
+ files: [
1561
+ {
1562
+ path: "app/zyphr_webhook.py",
1563
+ purpose: "Standard Webhooks signature verification helper (verbatim from docs)",
1564
+ contents: 'import hashlib\nimport hmac\nimport base64\nimport time\n\ndef verify_zyphr_webhook(payload: str, headers: dict, secret: str) -> bool:\n msg_id = headers.get("webhook-id", "")\n timestamp = headers.get("webhook-timestamp", "")\n signature = headers.get("webhook-signature", "")\n\n now = int(time.time())\n if abs(now - int(timestamp)) > 300:\n return False\n\n signed_content = f"{msg_id}.{timestamp}.{payload}"\n secret_hex = secret.removeprefix("whsec_")\n secret_bytes = bytes.fromhex(secret_hex)\n expected = base64.b64encode(\n hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest()\n ).decode()\n\n for sig in signature.split(" "):\n sig_value = sig.removeprefix("v1,")\n if hmac.compare_digest(sig_value, expected):\n return True\n return False\n',
1565
+ overwrite: false
1566
+ }
1567
+ ],
1568
+ envVarsNeeded: ENV5,
1569
+ nextSteps: NEXT_STEPS5,
1570
+ docsUrl: DOCS5
1571
+ },
1572
+ frameworks: {
1573
+ flask: {
1574
+ channel: "webhook",
1575
+ language: "python",
1576
+ framework: "flask",
1577
+ variant: "webhook-handler",
1578
+ files: [
1579
+ {
1580
+ path: "app/zyphr_webhook.py",
1581
+ purpose: "Standard Webhooks signature verification helper",
1582
+ contents: 'import hashlib, hmac, base64, time\n\ndef verify_zyphr_webhook(payload: str, headers, secret: str) -> bool:\n msg_id = headers.get("webhook-id", "")\n timestamp = headers.get("webhook-timestamp", "")\n signature = headers.get("webhook-signature", "")\n if abs(int(time.time()) - int(timestamp)) > 300:\n return False\n signed = f"{msg_id}.{timestamp}.{payload}"\n secret_bytes = bytes.fromhex(secret.removeprefix("whsec_"))\n expected = base64.b64encode(hmac.new(secret_bytes, signed.encode(), hashlib.sha256).digest()).decode()\n return any(hmac.compare_digest(sig.removeprefix("v1,"), expected) for sig in signature.split(" "))\n',
1583
+ overwrite: false
1584
+ },
1585
+ {
1586
+ path: "app/routes/zyphr_webhook.py",
1587
+ purpose: "Flask blueprint that verifies the webhook signature before processing",
1588
+ contents: 'import os, json\nfrom flask import Blueprint, request, abort\nfrom ..zyphr_webhook import verify_zyphr_webhook\n\nzyphr_webhook_bp = Blueprint("zyphr_webhook", __name__)\n\n@zyphr_webhook_bp.route("/webhooks/zyphr", methods=["POST"])\ndef handle():\n payload = request.get_data(as_text=True)\n if not verify_zyphr_webhook(payload, request.headers, os.environ["ZYPHR_WEBHOOK_SECRET"]):\n abort(401, "invalid signature")\n event = json.loads(payload)\n print("zyphr event", event.get("type"))\n return "", 204\n',
1589
+ overwrite: false
1590
+ }
1591
+ ],
1592
+ envVarsNeeded: ENV5,
1593
+ nextSteps: NEXT_STEPS5,
1594
+ docsUrl: DOCS5
1595
+ },
1596
+ fastapi: {
1597
+ channel: "webhook",
1598
+ language: "python",
1599
+ framework: "fastapi",
1600
+ variant: "webhook-handler",
1601
+ files: [
1602
+ {
1603
+ path: "app/routers/zyphr_webhook.py",
1604
+ purpose: "FastAPI router that verifies the webhook signature before processing",
1605
+ contents: 'import os, json, hashlib, hmac, base64, time\nfrom fastapi import APIRouter, Request, HTTPException\n\nrouter = APIRouter()\n\ndef verify_zyphr_webhook(payload: str, headers, secret: str) -> bool:\n msg_id = headers.get("webhook-id", "")\n timestamp = headers.get("webhook-timestamp", "")\n signature = headers.get("webhook-signature", "")\n if abs(int(time.time()) - int(timestamp)) > 300:\n return False\n signed = f"{msg_id}.{timestamp}.{payload}"\n secret_bytes = bytes.fromhex(secret.removeprefix("whsec_"))\n expected = base64.b64encode(hmac.new(secret_bytes, signed.encode(), hashlib.sha256).digest()).decode()\n return any(hmac.compare_digest(sig.removeprefix("v1,"), expected) for sig in signature.split(" "))\n\n@router.post("/webhooks/zyphr")\nasync def handle(request: Request):\n body = (await request.body()).decode()\n if not verify_zyphr_webhook(body, request.headers, os.environ["ZYPHR_WEBHOOK_SECRET"]):\n raise HTTPException(status_code=401, detail="invalid signature")\n event = json.loads(body)\n print("zyphr event", event.get("type"))\n return {"ok": True}\n',
1606
+ overwrite: false
1607
+ }
1608
+ ],
1609
+ envVarsNeeded: ENV5,
1610
+ nextSteps: NEXT_STEPS5,
1611
+ docsUrl: DOCS5
1612
+ }
1613
+ }
1614
+ },
1615
+ ruby: {
1616
+ sdk: {
1617
+ channel: "webhook",
1618
+ language: "ruby",
1619
+ framework: null,
1620
+ variant: "webhook-handler",
1621
+ files: [
1622
+ {
1623
+ path: "app/services/zyphr_webhook.rb",
1624
+ purpose: "Standard Webhooks signature verification helper",
1625
+ contents: `require 'openssl'
1626
+ require 'base64'
1627
+
1628
+ class ZyphrWebhook
1629
+ def self.verify(payload:, headers:, secret:)
1630
+ msg_id = headers['webhook-id'].to_s
1631
+ timestamp = headers['webhook-timestamp'].to_i
1632
+ signatures = headers['webhook-signature'].to_s
1633
+
1634
+ return false if (Time.now.to_i - timestamp).abs > 300
1635
+
1636
+ signed = "#{msg_id}.#{timestamp}.#{payload}"
1637
+ hex = secret.start_with?('whsec_') ? secret[6..] : secret
1638
+ secret_bytes = [hex].pack('H*')
1639
+ expected = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret_bytes, signed))
1640
+
1641
+ signatures.split(' ').any? do |sig|
1642
+ v = sig[3..]
1643
+ v && Rack::Utils.secure_compare(v, expected)
1644
+ end
1645
+ end
1646
+ end
1647
+ `,
1648
+ overwrite: false
1649
+ }
1650
+ ],
1651
+ envVarsNeeded: ENV5,
1652
+ nextSteps: NEXT_STEPS5,
1653
+ docsUrl: DOCS5
1654
+ },
1655
+ frameworks: {
1656
+ rails: {
1657
+ channel: "webhook",
1658
+ language: "ruby",
1659
+ framework: "rails",
1660
+ variant: "webhook-handler",
1661
+ files: [
1662
+ {
1663
+ path: "app/services/zyphr_webhook.rb",
1664
+ purpose: "Standard Webhooks signature verification helper",
1665
+ contents: `require 'openssl'
1666
+ require 'base64'
1667
+
1668
+ class ZyphrWebhook
1669
+ def self.verify(payload:, headers:, secret:)
1670
+ msg_id = headers['webhook-id'].to_s
1671
+ timestamp = headers['webhook-timestamp'].to_i
1672
+ signatures = headers['webhook-signature'].to_s
1673
+ return false if (Time.now.to_i - timestamp).abs > 300
1674
+ signed = "#{msg_id}.#{timestamp}.#{payload}"
1675
+ hex = secret.start_with?('whsec_') ? secret[6..] : secret
1676
+ secret_bytes = [hex].pack('H*')
1677
+ expected = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret_bytes, signed))
1678
+ signatures.split(' ').any? { |sig| sig[3..] && ActiveSupport::SecurityUtils.secure_compare(sig[3..], expected) }
1679
+ end
1680
+ end
1681
+ `,
1682
+ overwrite: false
1683
+ },
1684
+ {
1685
+ path: "app/controllers/zyphr_webhooks_controller.rb",
1686
+ purpose: "Rails controller that verifies the webhook signature before processing",
1687
+ contents: `class ZyphrWebhooksController < ApplicationController
1688
+ skip_before_action :verify_authenticity_token
1689
+
1690
+ def create
1691
+ payload = request.raw_post
1692
+ secret = ENV.fetch('ZYPHR_WEBHOOK_SECRET')
1693
+ unless ZyphrWebhook.verify(payload: payload, headers: request.headers, secret: secret)
1694
+ head :unauthorized and return
1695
+ end
1696
+ event = JSON.parse(payload)
1697
+ Rails.logger.info("zyphr event #{event['type']}")
1698
+ head :no_content
1699
+ end
1700
+ end
1701
+ `,
1702
+ overwrite: false
1703
+ }
1704
+ ],
1705
+ envVarsNeeded: ENV5,
1706
+ nextSteps: [
1707
+ ...NEXT_STEPS5,
1708
+ "Add route: post '/webhooks/zyphr', to: 'zyphr_webhooks#create'"
1709
+ ],
1710
+ docsUrl: DOCS5
1711
+ }
1712
+ }
1713
+ },
1714
+ go: {
1715
+ sdk: {
1716
+ channel: "webhook",
1717
+ language: "go",
1718
+ framework: null,
1719
+ variant: "webhook-handler",
1720
+ files: [
1721
+ {
1722
+ path: "internal/zyphr/verify.go",
1723
+ purpose: "Standard Webhooks signature verification helper (verbatim from docs).",
1724
+ contents: 'package zyphr\n\nimport (\n "crypto/hmac"\n "crypto/sha256"\n "encoding/base64"\n "encoding/hex"\n "math"\n "strconv"\n "strings"\n "time"\n)\n\nfunc VerifyWebhook(payload, msgID, timestamp, signature, secret string) bool {\n ts, err := strconv.ParseInt(timestamp, 10, 64)\n if err != nil { return false }\n if math.Abs(float64(time.Now().Unix()-ts)) > 300 { return false }\n\n signed := msgID + "." + timestamp + "." + payload\n secretHex := strings.TrimPrefix(secret, "whsec_")\n secretBytes, err := hex.DecodeString(secretHex)\n if err != nil { return false }\n mac := hmac.New(sha256.New, secretBytes)\n mac.Write([]byte(signed))\n expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))\n\n for _, sig := range strings.Split(signature, " ") {\n v := strings.TrimPrefix(sig, "v1,")\n if hmac.Equal([]byte(v), []byte(expected)) { return true }\n }\n return false\n}\n',
1725
+ overwrite: false
1726
+ }
1727
+ ],
1728
+ envVarsNeeded: ENV5,
1729
+ nextSteps: NEXT_STEPS5,
1730
+ docsUrl: DOCS5
1731
+ }
1732
+ },
1733
+ php: {
1734
+ sdk: {
1735
+ channel: "webhook",
1736
+ language: "php",
1737
+ framework: null,
1738
+ variant: "webhook-handler",
1739
+ files: [
1740
+ {
1741
+ path: "app/Webhooks/ZyphrWebhook.php",
1742
+ purpose: "Standard Webhooks signature verification helper (verbatim from docs)",
1743
+ contents: `<?php
1744
+
1745
+ namespace App\\Webhooks;
1746
+
1747
+ class ZyphrWebhook
1748
+ {
1749
+ public static function verify(string $payload, array $headers, string $secret): bool
1750
+ {
1751
+ $msgId = $headers['webhook-id'] ?? '';
1752
+ $timestamp = $headers['webhook-timestamp'] ?? '';
1753
+ $signature = $headers['webhook-signature'] ?? '';
1754
+ if (abs(time() - intval($timestamp)) > 300) {
1755
+ return false;
1756
+ }
1757
+ $signed = "{$msgId}.{$timestamp}.{$payload}";
1758
+ $hex = str_starts_with($secret, 'whsec_') ? substr($secret, 6) : $secret;
1759
+ $expected = base64_encode(hash_hmac('sha256', $signed, hex2bin($hex), true));
1760
+ foreach (explode(' ', $signature) as $sig) {
1761
+ if (hash_equals(substr($sig, 3), $expected)) {
1762
+ return true;
1763
+ }
1764
+ }
1765
+ return false;
1766
+ }
1767
+ }
1768
+ `,
1769
+ overwrite: false
1770
+ }
1771
+ ],
1772
+ envVarsNeeded: ENV5,
1773
+ nextSteps: NEXT_STEPS5,
1774
+ docsUrl: DOCS5
1775
+ },
1776
+ frameworks: {
1777
+ laravel: {
1778
+ channel: "webhook",
1779
+ language: "php",
1780
+ framework: "laravel",
1781
+ variant: "webhook-handler",
1782
+ files: [
1783
+ {
1784
+ path: "app/Webhooks/ZyphrWebhook.php",
1785
+ purpose: "Standard Webhooks signature verification helper",
1786
+ contents: `<?php
1787
+
1788
+ namespace App\\Webhooks;
1789
+
1790
+ class ZyphrWebhook
1791
+ {
1792
+ public static function verify(string $payload, array $headers, string $secret): bool
1793
+ {
1794
+ $msgId = $headers['webhook-id'][0] ?? '';
1795
+ $timestamp = $headers['webhook-timestamp'][0] ?? '';
1796
+ $signature = $headers['webhook-signature'][0] ?? '';
1797
+ if (abs(time() - intval($timestamp)) > 300) {
1798
+ return false;
1799
+ }
1800
+ $signed = "{$msgId}.{$timestamp}.{$payload}";
1801
+ $hex = str_starts_with($secret, 'whsec_') ? substr($secret, 6) : $secret;
1802
+ $expected = base64_encode(hash_hmac('sha256', $signed, hex2bin($hex), true));
1803
+ foreach (explode(' ', $signature) as $sig) {
1804
+ if (hash_equals(substr($sig, 3), $expected)) {
1805
+ return true;
1806
+ }
1807
+ }
1808
+ return false;
1809
+ }
1810
+ }
1811
+ `,
1812
+ overwrite: false
1813
+ },
1814
+ {
1815
+ path: "app/Http/Controllers/ZyphrWebhookController.php",
1816
+ purpose: "Laravel controller that verifies the webhook signature before processing",
1817
+ contents: "<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\Webhooks\\ZyphrWebhook;\nuse Illuminate\\Http\\Request;\n\nclass ZyphrWebhookController extends Controller\n{\n public function handle(Request $request)\n {\n $payload = $request->getContent();\n $secret = config('services.zyphr.webhook_secret');\n if (! ZyphrWebhook::verify($payload, $request->headers->all(), $secret)) {\n return response('invalid signature', 401);\n }\n $event = json_decode($payload, true);\n \\Log::info('zyphr event ' . ($event['type'] ?? 'unknown'));\n return response()->noContent();\n }\n}\n",
1818
+ overwrite: false
1819
+ }
1820
+ ],
1821
+ envVarsNeeded: ENV5,
1822
+ nextSteps: [
1823
+ ...NEXT_STEPS5,
1824
+ "Register route in routes/api.php: Route::post('/webhooks/zyphr', [ZyphrWebhookController::class, 'handle']);",
1825
+ "Exclude this route from CSRF (VerifyCsrfToken::$except)."
1826
+ ],
1827
+ docsUrl: DOCS5
1828
+ }
1829
+ }
1830
+ },
1831
+ csharp: {
1832
+ sdk: {
1833
+ channel: "webhook",
1834
+ language: "csharp",
1835
+ framework: null,
1836
+ variant: "webhook-handler",
1837
+ files: [
1838
+ {
1839
+ path: "Webhooks/ZyphrWebhookVerifier.cs",
1840
+ purpose: "Standard Webhooks signature verification helper",
1841
+ contents: `using System.Security.Cryptography;
1842
+ using System.Text;
1843
+
1844
+ namespace YourApp.Webhooks;
1845
+
1846
+ public static class ZyphrWebhookVerifier
1847
+ {
1848
+ public static bool Verify(string payload, IDictionary<string,string> headers, string secret)
1849
+ {
1850
+ var msgId = headers.TryGetValue("webhook-id", out var id) ? id : "";
1851
+ var timestamp = headers.TryGetValue("webhook-timestamp", out var ts) ? long.Parse(ts) : 0;
1852
+ var signatures = headers.TryGetValue("webhook-signature", out var sig) ? sig : "";
1853
+
1854
+ var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
1855
+ if (Math.Abs(now - timestamp) > 300) return false;
1856
+
1857
+ var signed = $"{msgId}.{timestamp}.{payload}";
1858
+ var hex = secret.StartsWith("whsec_") ? secret[6..] : secret;
1859
+ var secretBytes = Convert.FromHexString(hex);
1860
+ using var hmac = new HMACSHA256(secretBytes);
1861
+ var expected = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signed)));
1862
+
1863
+ foreach (var s in signatures.Split(' '))
1864
+ {
1865
+ var v = s.Length > 3 ? s[3..] : "";
1866
+ if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(v), Encoding.UTF8.GetBytes(expected)))
1867
+ return true;
1868
+ }
1869
+ return false;
1870
+ }
1871
+ }
1872
+ `,
1873
+ overwrite: false
1874
+ }
1875
+ ],
1876
+ envVarsNeeded: ENV5,
1877
+ nextSteps: NEXT_STEPS5,
1878
+ docsUrl: DOCS5
1879
+ },
1880
+ frameworks: {
1881
+ aspnetcore: {
1882
+ channel: "webhook",
1883
+ language: "csharp",
1884
+ framework: "aspnetcore",
1885
+ variant: "webhook-handler",
1886
+ files: [
1887
+ {
1888
+ path: "Webhooks/ZyphrWebhookVerifier.cs",
1889
+ purpose: "Standard Webhooks signature verification helper",
1890
+ contents: `using System.Security.Cryptography;
1891
+ using System.Text;
1892
+
1893
+ namespace YourApp.Webhooks;
1894
+
1895
+ public static class ZyphrWebhookVerifier
1896
+ {
1897
+ public static bool Verify(string payload, IHeaderDictionary headers, string secret)
1898
+ {
1899
+ string msgId = headers["webhook-id"].ToString();
1900
+ long timestamp = long.TryParse(headers["webhook-timestamp"], out var t) ? t : 0;
1901
+ string signatures = headers["webhook-signature"].ToString();
1902
+ if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > 300) return false;
1903
+ var signed = $"{msgId}.{timestamp}.{payload}";
1904
+ var hex = secret.StartsWith("whsec_") ? secret[6..] : secret;
1905
+ using var hmac = new HMACSHA256(Convert.FromHexString(hex));
1906
+ var expected = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signed)));
1907
+ foreach (var s in signatures.Split(' '))
1908
+ {
1909
+ var v = s.Length > 3 ? s[3..] : "";
1910
+ if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(v), Encoding.UTF8.GetBytes(expected)))
1911
+ return true;
1912
+ }
1913
+ return false;
1914
+ }
1915
+ }
1916
+ `,
1917
+ overwrite: false
1918
+ },
1919
+ {
1920
+ path: "Controllers/ZyphrWebhookController.cs",
1921
+ purpose: "ASP.NET Core controller that verifies the webhook signature before processing",
1922
+ contents: 'using Microsoft.AspNetCore.Mvc;\nusing YourApp.Webhooks;\n\n[ApiController]\n[Route("webhooks/zyphr")]\npublic class ZyphrWebhookController : ControllerBase\n{\n [HttpPost]\n public async Task<IActionResult> Handle()\n {\n using var reader = new StreamReader(Request.Body);\n var payload = await reader.ReadToEndAsync();\n var secret = Environment.GetEnvironmentVariable("ZYPHR_WEBHOOK_SECRET")!;\n if (!ZyphrWebhookVerifier.Verify(payload, Request.Headers, secret))\n return Unauthorized("invalid signature");\n Console.WriteLine($"zyphr event {payload[..Math.Min(80, payload.Length)]}");\n return NoContent();\n }\n}\n',
1923
+ overwrite: false
1924
+ }
1925
+ ],
1926
+ envVarsNeeded: ENV5,
1927
+ nextSteps: NEXT_STEPS5,
1928
+ docsUrl: DOCS5
1929
+ }
1930
+ }
1931
+ }
1932
+ };
1933
+
1934
+ // src/integration/quickstart/index.ts
1935
+ var REGISTRY = {
1936
+ email: emailChannel,
1937
+ push: pushChannel,
1938
+ sms: smsChannel,
1939
+ inbox: inboxChannel,
1940
+ webhook: webhookChannel
1941
+ };
1942
+ function resolveQuickstart(args) {
1943
+ const langMap = REGISTRY[args.channel]?.[args.language];
1944
+ if (!langMap) return null;
1945
+ if (args.framework) {
1946
+ const key = args.framework.trim().toLowerCase();
1947
+ const fw = langMap.frameworks?.[key];
1948
+ if (fw) return { result: fw, frameworkRecognized: true };
1949
+ return { result: langMap.sdk, frameworkRecognized: false };
1950
+ }
1951
+ return { result: langMap.sdk, frameworkRecognized: true };
1952
+ }
1953
+
1954
+ // src/integration/sdk-snippets.ts
1955
+ var DOCS6 = "https://docs.zyphr.dev/sdks";
1956
+ var SDK_INSTALL_TABLE = {
1957
+ node: {
1958
+ language: "node",
1959
+ kind: "sdk",
1960
+ packageName: "@zyphr-dev/node-sdk",
1961
+ registry: "npm",
1962
+ registryUrl: "https://www.npmjs.com/package/@zyphr-dev/node-sdk",
1963
+ installCommands: [
1964
+ { manager: "npm", command: "npm install @zyphr-dev/node-sdk" },
1965
+ { manager: "yarn", command: "yarn add @zyphr-dev/node-sdk" },
1966
+ { manager: "pnpm", command: "pnpm add @zyphr-dev/node-sdk" }
1967
+ ],
1968
+ initSnippet: {
1969
+ imports: "import { Zyphr } from '@zyphr-dev/node-sdk';",
1970
+ init: "export const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });",
1971
+ fileExample: "src/lib/zyphr.ts"
1972
+ },
1973
+ envVarsNeeded: ["ZYPHR_API_KEY"],
1974
+ docsUrl: `${DOCS6}/node`
1975
+ },
1976
+ csharp: {
1977
+ language: "csharp",
1978
+ kind: "sdk",
1979
+ packageName: "ZyphrDev.SDK",
1980
+ registry: "NuGet",
1981
+ registryUrl: "https://www.nuget.org/packages/ZyphrDev.SDK",
1982
+ installCommands: [
1983
+ { manager: "dotnet", command: "dotnet add package ZyphrDev.SDK" },
1984
+ { manager: "nuget", command: "Install-Package ZyphrDev.SDK" }
1985
+ ],
1986
+ initSnippet: {
1987
+ imports: [
1988
+ "using ZyphrDev.SDK.Api;",
1989
+ "using ZyphrDev.SDK.Client;",
1990
+ "using ZyphrDev.SDK.Model;"
1991
+ ].join("\n"),
1992
+ init: [
1993
+ "var config = new Configuration",
1994
+ "{",
1995
+ " ApiKey = new Dictionary<string, string>",
1996
+ " {",
1997
+ ' { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }',
1998
+ " }",
1999
+ "};",
2000
+ "var emails = new EmailsApi(config);"
2001
+ ].join("\n"),
2002
+ fileExample: "Services/ZyphrClient.cs"
2003
+ },
2004
+ envVarsNeeded: ["ZYPHR_API_KEY"],
2005
+ docsUrl: `${DOCS6}/csharp`
2006
+ },
2007
+ ruby: {
2008
+ language: "ruby",
2009
+ kind: "sdk",
2010
+ packageName: "zyphr",
2011
+ registry: "RubyGems",
2012
+ registryUrl: "https://rubygems.org/gems/zyphr",
2013
+ installCommands: [
2014
+ { manager: "gem", command: "gem install zyphr" },
2015
+ { manager: "bundler", command: "bundle add zyphr" }
2016
+ ],
2017
+ initSnippet: {
2018
+ imports: "require 'zyphr'",
2019
+ init: [
2020
+ "Zyphr.configure do |config|",
2021
+ " config.api_key['X-API-Key'] = ENV.fetch('ZYPHR_API_KEY')",
2022
+ "end"
2023
+ ].join("\n"),
2024
+ fileExample: "config/initializers/zyphr.rb"
2025
+ },
2026
+ envVarsNeeded: ["ZYPHR_API_KEY"],
2027
+ docsUrl: `${DOCS6}/ruby`
2028
+ },
2029
+ python: {
2030
+ language: "python",
2031
+ kind: "rest-client",
2032
+ packageName: "requests",
2033
+ registry: "PyPI",
2034
+ registryUrl: "https://pypi.org/project/requests/",
2035
+ installCommands: [
2036
+ { manager: "pip", command: "pip install requests" },
2037
+ { manager: "poetry", command: "poetry add requests" },
2038
+ { manager: "uv", command: "uv add requests" }
2039
+ ],
2040
+ initSnippet: {
2041
+ imports: "import os\nimport requests",
2042
+ init: [
2043
+ 'ZYPHR_API_KEY = os.environ["ZYPHR_API_KEY"]',
2044
+ 'BASE_URL = "https://api.zyphr.dev/v1"',
2045
+ "",
2046
+ "headers = {",
2047
+ ' "X-API-Key": ZYPHR_API_KEY,',
2048
+ ' "Content-Type": "application/json",',
2049
+ "}",
2050
+ "",
2051
+ "def zyphr_request(method, path, json=None, params=None):",
2052
+ " response = requests.request(",
2053
+ ' method, f"{BASE_URL}{path}", headers=headers, json=json, params=params,',
2054
+ " )",
2055
+ " response.raise_for_status()",
2056
+ " return response.json()"
2057
+ ].join("\n"),
2058
+ fileExample: "app/zyphr_client.py"
2059
+ },
2060
+ envVarsNeeded: ["ZYPHR_API_KEY"],
2061
+ docsUrl: `${DOCS6}/python`,
2062
+ notes: "There is no official Zyphr Python SDK yet \u2014 the canonical integration is a thin REST wrapper around the requests library."
2063
+ },
2064
+ go: {
2065
+ language: "go",
2066
+ kind: "rest-client",
2067
+ packageName: "net/http (stdlib)",
2068
+ registry: "stdlib",
2069
+ registryUrl: "https://pkg.go.dev/net/http",
2070
+ installCommands: [
2071
+ { manager: "go", command: "# No install needed \u2014 net/http ships with Go." }
2072
+ ],
2073
+ initSnippet: {
2074
+ imports: [
2075
+ "package zyphr",
2076
+ "",
2077
+ "import (",
2078
+ ' "bytes"',
2079
+ ' "encoding/json"',
2080
+ ' "fmt"',
2081
+ ' "io"',
2082
+ ' "net/http"',
2083
+ ' "os"',
2084
+ ")"
2085
+ ].join("\n"),
2086
+ init: [
2087
+ 'const baseURL = "https://api.zyphr.dev/v1"',
2088
+ "",
2089
+ "type Client struct {",
2090
+ " APIKey string",
2091
+ " HTTPClient *http.Client",
2092
+ "}",
2093
+ "",
2094
+ "func NewClient() *Client {",
2095
+ ' return &Client{APIKey: os.Getenv("ZYPHR_API_KEY"), HTTPClient: &http.Client{}}',
2096
+ "}",
2097
+ "",
2098
+ "func (c *Client) Do(method, path string, body any) ([]byte, error) {",
2099
+ " var buf io.Reader",
2100
+ " if body != nil {",
2101
+ " b, err := json.Marshal(body)",
2102
+ ' if err != nil { return nil, fmt.Errorf("marshal: %w", err) }',
2103
+ " buf = bytes.NewReader(b)",
2104
+ " }",
2105
+ " req, err := http.NewRequest(method, baseURL+path, buf)",
2106
+ " if err != nil { return nil, err }",
2107
+ ' req.Header.Set("X-API-Key", c.APIKey)',
2108
+ ' req.Header.Set("Content-Type", "application/json")',
2109
+ " resp, err := c.HTTPClient.Do(req)",
2110
+ " if err != nil { return nil, err }",
2111
+ " defer resp.Body.Close()",
2112
+ " data, _ := io.ReadAll(resp.Body)",
2113
+ ' if resp.StatusCode >= 400 { return nil, fmt.Errorf("zyphr %d: %s", resp.StatusCode, data) }',
2114
+ " return data, nil",
2115
+ "}"
2116
+ ].join("\n"),
2117
+ fileExample: "internal/zyphr/client.go"
2118
+ },
2119
+ envVarsNeeded: ["ZYPHR_API_KEY"],
2120
+ docsUrl: `${DOCS6}/go`,
2121
+ notes: "There is no official Zyphr Go SDK yet \u2014 the canonical integration is a thin REST wrapper around net/http."
2122
+ },
2123
+ php: {
2124
+ language: "php",
2125
+ kind: "rest-client",
2126
+ packageName: "guzzlehttp/guzzle",
2127
+ registry: "Packagist",
2128
+ registryUrl: "https://packagist.org/packages/guzzlehttp/guzzle",
2129
+ installCommands: [
2130
+ { manager: "composer", command: "composer require guzzlehttp/guzzle" }
2131
+ ],
2132
+ initSnippet: {
2133
+ imports: [
2134
+ "<?php",
2135
+ "",
2136
+ "use GuzzleHttp\\Client;"
2137
+ ].join("\n"),
2138
+ init: [
2139
+ "$client = new Client([",
2140
+ " 'base_uri' => 'https://api.zyphr.dev/v1/',",
2141
+ " 'headers' => [",
2142
+ " 'X-API-Key' => getenv('ZYPHR_API_KEY'),",
2143
+ " 'Content-Type' => 'application/json',",
2144
+ " ],",
2145
+ "]);"
2146
+ ].join("\n"),
2147
+ fileExample: "app/Services/ZyphrClient.php"
2148
+ },
2149
+ envVarsNeeded: ["ZYPHR_API_KEY"],
2150
+ docsUrl: `${DOCS6}/php`,
2151
+ notes: "There is no official Zyphr PHP SDK yet \u2014 the canonical integration uses Guzzle (or raw cURL)."
2152
+ }
2153
+ };
2154
+ function resolveInstallEntry(language, packageManager) {
2155
+ const entry = SDK_INSTALL_TABLE[language];
2156
+ if (!packageManager) return entry;
2157
+ const normalized = packageManager.trim().toLowerCase();
2158
+ const match = entry.installCommands.find((c) => c.manager.toLowerCase() === normalized);
2159
+ if (!match) return entry;
2160
+ return { ...entry, installCommands: [match] };
2161
+ }
2162
+
2163
+ // src/tools/integration.ts
2164
+ function registerIntegrationTools(server, guards) {
2165
+ if (isToolEnabled({ name: "get_sdk_install_for_language", mutates: false }, guards)) {
2166
+ server.registerTool(
2167
+ "get_sdk_install_for_language",
2168
+ {
2169
+ title: "Get SDK install instructions for a language",
2170
+ description: "Returns install commands, init snippet, env vars, and docs URL for the chosen language. Use this when wiring Zyphr into a new project so the AI can drop the correct package + client init code.",
2171
+ inputSchema: getSdkInstallShape
2172
+ },
2173
+ async (args) => {
2174
+ const entry = resolveInstallEntry(args.language, args.packageManager);
2175
+ return toolResult(entry);
2176
+ }
2177
+ );
2178
+ }
2179
+ if (isToolEnabled({ name: "get_quickstart_for_channel", mutates: false }, guards)) {
2180
+ server.registerTool(
2181
+ "get_quickstart_for_channel",
2182
+ {
2183
+ title: "Get quickstart code for a channel + language",
2184
+ description: "Returns drop-in service file(s) for the chosen Zyphr channel (email/push/sms/inbox/webhook), language, and optional framework (express, nextjs, flask, fastapi, rails, laravel, aspnetcore). Webhook handlers ALWAYS verify HMAC signatures. Unknown frameworks fall back to plain SDK code.",
2185
+ inputSchema: getQuickstartShape
2186
+ },
2187
+ async (args) => {
2188
+ const resolved = resolveQuickstart({
2189
+ channel: args.channel,
2190
+ language: args.language,
2191
+ framework: args.framework
2192
+ });
2193
+ if (!resolved) {
2194
+ return toolResult({
2195
+ error: `No quickstart available for channel=${args.channel} language=${args.language}`
2196
+ });
2197
+ }
2198
+ const { result, frameworkRecognized } = resolved;
2199
+ return toolResult({
2200
+ ...result,
2201
+ frameworkRecognized,
2202
+ requestedFramework: args.framework ?? null
2203
+ });
2204
+ }
2205
+ );
2206
+ }
2207
+ }
2208
+
2209
+ // src/tools/webhooks.ts
2210
+ function registerWebhookTools(server, guards) {
2211
+ if (isToolEnabled({ name: "list_webhooks", mutates: false }, guards)) {
2212
+ server.registerTool(
2213
+ "list_webhooks",
2214
+ {
2215
+ title: "List webhooks",
2216
+ description: "List webhook endpoints configured for the account.",
2217
+ inputSchema: listWebhooksShape
2218
+ },
2219
+ async (args) => {
2220
+ return runTool(async () => {
2221
+ const zyphr = getZyphrClient();
2222
+ return await zyphr.webhooks.listWebhooks(args.limit, args.offset);
2223
+ });
2224
+ }
2225
+ );
2226
+ }
2227
+ if (isToolEnabled({ name: "create_webhook", mutates: true }, guards)) {
2228
+ server.registerTool(
2229
+ "create_webhook",
2230
+ {
2231
+ title: "Create webhook",
2232
+ description: 'Register a new webhook endpoint. Subscribe to event types like "email.*", "subscriber.created", or "*".',
2233
+ inputSchema: createWebhookShape
2234
+ },
2235
+ async (args) => {
2236
+ return runTool(async () => {
2237
+ const zyphr = getZyphrClient();
2238
+ return await zyphr.webhooks.createWebhook({
2239
+ url: args.url,
2240
+ events: args.events,
2241
+ description: args.description,
2242
+ secret: args.secret,
2243
+ metadata: args.metadata,
2244
+ headers: args.headers,
2245
+ version: args.version,
2246
+ rateLimit: args.rateLimit
2247
+ });
2248
+ });
2249
+ }
2250
+ );
2251
+ }
2252
+ if (isToolEnabled({ name: "get_webhook_deliveries", mutates: false }, guards)) {
2253
+ server.registerTool(
2254
+ "get_webhook_deliveries",
2255
+ {
2256
+ title: "List webhook deliveries",
2257
+ description: "Inspect delivery history for a webhook endpoint. Filter by status (pending/delivering/delivered/failed/exhausted), event type, or date range. Useful for debugging why a webhook is not firing.",
2258
+ inputSchema: getWebhookDeliveriesShape
2259
+ },
2260
+ async (args) => {
2261
+ return runTool(async () => {
2262
+ const zyphr = getZyphrClient();
2263
+ return await zyphr.webhooks.listWebhookDeliveries(
2264
+ args.webhookId,
2265
+ args.status,
2266
+ args.eventType,
2267
+ args.search,
2268
+ args.startDate ? new Date(args.startDate) : void 0,
2269
+ args.endDate ? new Date(args.endDate) : void 0,
2270
+ args.limit,
2271
+ args.offset
2272
+ );
2273
+ });
2274
+ }
2275
+ );
2276
+ }
2277
+ }
2278
+
2279
+ // src/server.ts
2280
+ var SERVER_NAME = "zyphr";
2281
+ var SERVER_VERSION = "0.1.0";
2282
+ function createServer() {
2283
+ const server = new McpServer(
2284
+ { name: SERVER_NAME, version: SERVER_VERSION },
2285
+ { capabilities: { tools: {} } }
2286
+ );
2287
+ const guards = loadToolGuards();
2288
+ registerSendTools(server, guards);
2289
+ registerTemplateTools(server, guards);
2290
+ registerSubscriberTools(server, guards);
2291
+ registerWebhookTools(server, guards);
2292
+ registerIntegrationTools(server, guards);
2293
+ return server;
2294
+ }
2295
+
2296
+ // src/index.ts
2297
+ async function main() {
2298
+ if (!process.env.ZYPHR_API_KEY) {
2299
+ process.stderr.write(
2300
+ "[zyphr-mcp] ZYPHR_API_KEY is not set. Provide a zy_live_* or zy_test_* key via the `env` block of your MCP client config.\n"
2301
+ );
2302
+ process.exit(1);
2303
+ }
2304
+ const server = createServer();
2305
+ const transport = new StdioServerTransport();
2306
+ await server.connect(transport);
2307
+ process.stderr.write(`[zyphr-mcp] connected (base=${getBaseUrl()})
2308
+ `);
2309
+ }
2310
+ main().catch((err) => {
2311
+ process.stderr.write(`[zyphr-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}
2312
+ `);
2313
+ process.exit(1);
2314
+ });
2315
+ //# sourceMappingURL=index.js.map