sadasend 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +527 -2
- package/dist/client.d.ts +452 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +266 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +123 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +176 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/retry.d.ts +42 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +30 -0
- package/dist/retry.js.map +1 -0
- package/dist/webhooks.d.ts +71 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +96 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +73 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SadaSend
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,528 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sadasend
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/sadasend)
|
|
4
|
+
[](https://github.com/SadaSend/sadasend-sdk-ts/blob/main/LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/sadasend)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://sadasend.com/docs/sdks/typescript)
|
|
8
|
+
|
|
9
|
+
The official TypeScript and JavaScript SDK for [SadaSend](https://sadasend.com). High-performance transactional email with unbypassable server-enforced guardrails engineered for human engineering teams and autonomous AI agents.
|
|
10
|
+
|
|
11
|
+
**Zero dependencies.** Runs on native web standards (`globalThis.fetch` and `crypto.subtle`) across **Node.js 18+**, **Bun**, **Deno**, **Cloudflare Workers**, **Next.js Edge**, and modern browsers. No transitive supply-chain risks, no bloated bundles (< 8 KB unpacked).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Why SadaSend? The AI Agent Moat](#why-sadasend-the-ai-agent-moat)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Ten-Line Quickstart](#ten-line-quickstart)
|
|
20
|
+
- [Server-Enforced Agent Guardrails](#server-enforced-agent-guardrails)
|
|
21
|
+
- [Comprehensive API Reference](#comprehensive-api-reference)
|
|
22
|
+
- [1. Transactional Sends (`emails.send`)](#1-transactional-sends-emailssend)
|
|
23
|
+
- [2. High-Throughput Batch Sends (`emails.batch`)](#2-high-throughput-batch-sends-emailsbatch)
|
|
24
|
+
- [3. Scheduled Delivery & Atomic Cancellation (`emails.cancel`)](#3-scheduled-delivery--atomic-cancellation-emailscancel)
|
|
25
|
+
- [4. Sending Domains & Live DNS Verification (`domains`)](#4-sending-domains--live-dns-verification-domains)
|
|
26
|
+
- [5. Server-Side Liquid Dynamic Templates (`templates`)](#5-server-side-liquid-dynamic-templates-templates)
|
|
27
|
+
- [6. Automated & Manual Suppressions (`suppressions`)](#6-automated--manual-suppressions-suppressions)
|
|
28
|
+
- [7. Account Deliverability & Telemetry (`account`)](#7-account-deliverability--telemetry-account)
|
|
29
|
+
- [8. API Key Governance & Approvals Queue (`keys`)](#8-api-key-governance--approvals-queue-keys)
|
|
30
|
+
- [Timing-Safe HMAC Webhook Verification](#timing-safe-hmac-webhook-verification)
|
|
31
|
+
- [Error Handling: Discriminated Unions](#error-handling-discriminated-unions)
|
|
32
|
+
- [Native Model Context Protocol (MCP) Integration](#native-model-context-protocol-mcp-integration)
|
|
33
|
+
- [Canonical Documentation & Technical References](#canonical-documentation--technical-references)
|
|
34
|
+
- [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq)
|
|
35
|
+
- [License & Support](#license--support)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Why SadaSend? The AI Agent Moat
|
|
40
|
+
|
|
41
|
+
Traditional transactional email APIs (SendGrid, Mailgun, Postmark, Resend) were created exclusively for deterministic, hardcoded human backend jobs. When an autonomous AI agent (Claude Code, Cursor, LangChain, CrewAI, AutoGen) is given a traditional email API key, a single prompt injection or hallucination can spam thousands of external customers or burn your sending domain reputation.
|
|
42
|
+
|
|
43
|
+
SadaSend introduces **Server-Enforced Guardrails** that no agent prompt can talk its way past:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
┌────────────────────────────────────────────────────────────────────────┐
|
|
47
|
+
│ Human Engineer Mints Key in SadaSend Dashboard │
|
|
48
|
+
│ { │
|
|
49
|
+
│ scopes: ['send', 'read'], // Permitted operations │
|
|
50
|
+
│ allowlist: ['@internal-corp.com'], // Who it may reach │
|
|
51
|
+
│ rateLimit: '100 / 1h', // Token bucket ceiling │
|
|
52
|
+
│ mode: 'approval' // Require human sign-off │
|
|
53
|
+
│ } │
|
|
54
|
+
└───────────────────────────────────┬────────────────────────────────────┘
|
|
55
|
+
│ Mints sada_agent_sk_...
|
|
56
|
+
▼
|
|
57
|
+
┌────────────────────────────────────────────────────────────────────────┐
|
|
58
|
+
│ Autonomous Agent Code (TypeScript / Python / MCP) │
|
|
59
|
+
│ const sadasend = new SadaSend(process.env.AGENT_KEY); │
|
|
60
|
+
│ │
|
|
61
|
+
│ ❌ Agent CANNOT widen allowlist or elevate scopes │
|
|
62
|
+
│ ❌ Agent CANNOT mint new API keys (session-restricted endpoint) │
|
|
63
|
+
│ ❌ Agent CANNOT sign off on its own held messages │
|
|
64
|
+
│ ✅ Server intercepts rogue sends and returns typed remediation │
|
|
65
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Comparison Matrix
|
|
69
|
+
|
|
70
|
+
| Feature | Legacy Email APIs | SadaSend |
|
|
71
|
+
| :--- | :---: | :---: |
|
|
72
|
+
| **Agent Recipient Allowlists** | ❌ None (Sends anywhere) | 🟢 **Server-Enforced** (`@domain.com` or exact addresses) |
|
|
73
|
+
| **Human-in-the-Loop Mode** | ❌ None (Direct to wire) | 🟢 **Built-in Approval Queue** (`mode: 'approval'`) |
|
|
74
|
+
| **Zero-Cost Preflight Dry Runs** | ❌ Burns quota | 🟢 **Native `dryRun: true`** (Validates deliverability free) |
|
|
75
|
+
| **Idempotency Guarantees** | ⚠️ Manual / Inconsistent | 🟢 **Auto UUIDv4** rides retries to prevent duplicate sends |
|
|
76
|
+
| **Result Type Model** | ❌ Throws on every 4xx/5xx | 🟢 **Discriminated Union** (`Result<T>`: `ok: true` / `ok: false`) |
|
|
77
|
+
| **Dependencies** | ❌ Often requires heavy SDKs | 🟢 **Zero Dependencies** (Pure native fetch & WebCrypto) |
|
|
78
|
+
| **Model Context Protocol (MCP)** | ❌ Requires custom wrappers | 🟢 **Native SSE MCP Server** (`https://mcp.sadasend.com/mcp/sse`) |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
Install via your preferred package manager:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# npm
|
|
88
|
+
npm install sadasend
|
|
89
|
+
|
|
90
|
+
# Bun
|
|
91
|
+
bun add sadasend
|
|
92
|
+
|
|
93
|
+
# pnpm
|
|
94
|
+
pnpm add sadasend
|
|
95
|
+
|
|
96
|
+
# yarn
|
|
97
|
+
yarn add sadasend
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Ten-Line Quickstart
|
|
103
|
+
|
|
104
|
+
<!-- example:ten-lines -->
|
|
105
|
+
```ts
|
|
106
|
+
// bun add sadasend
|
|
107
|
+
import { SadaSend } from 'sadasend';
|
|
108
|
+
|
|
109
|
+
const sadasend = new SadaSend(process.env.SADASEND_API_KEY);
|
|
110
|
+
|
|
111
|
+
const result = await sadasend.emails.send({
|
|
112
|
+
from: 'noreply@onboarding.sadasend.com',
|
|
113
|
+
to: 'dana@example.com',
|
|
114
|
+
subject: 'It works.',
|
|
115
|
+
html: '<strong>Sent from the SadaSend SDK.</strong>',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
119
|
+
console.log(result.status, result.id);
|
|
120
|
+
```
|
|
121
|
+
<!-- /example -->
|
|
122
|
+
|
|
123
|
+
*Note: The code above is an active integration test verified against a live server in CI via [`examples/ten-lines.ts`](./examples/ten-lines.ts).*
|
|
124
|
+
|
|
125
|
+
For an executable walkthrough covering all 16 advanced features, inspect [`examples/full-features.ts`](./examples/full-features.ts).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Server-Enforced Agent Guardrails
|
|
130
|
+
|
|
131
|
+
### 1. Recipient Allowlists (`RecipientNotAllowlistedError`)
|
|
132
|
+
When an agent attempts to message an address outside its permitted domain or email list, SadaSend blocks delivery at the API gateway and returns the exact allowlist patterns so the agent can self-correct:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { SadaSend, RecipientNotAllowlistedError } from 'sadasend';
|
|
136
|
+
|
|
137
|
+
const sadasend = new SadaSend(process.env.SADASEND_AGENT_KEY);
|
|
138
|
+
|
|
139
|
+
const result = await sadasend.emails.send({
|
|
140
|
+
from: 'Agent <assistant@yourdomain.com>',
|
|
141
|
+
to: 'unauthorized-stranger@external.com',
|
|
142
|
+
subject: 'Autonomous follow-up',
|
|
143
|
+
text: 'Hello!',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!result.ok && result.error instanceof RecipientNotAllowlistedError) {
|
|
147
|
+
console.error('Delivery prevented by server guardrail:');
|
|
148
|
+
console.log('Violating recipient:', result.error.recipient);
|
|
149
|
+
console.log('Permitted patterns:', result.error.allowlist);
|
|
150
|
+
// Example: ["@yourcompany.com", "partner@authorized.com"]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 2. Human Approval Mode (`mode: 'approval'`)
|
|
155
|
+
Keys minted in approval mode automatically place outgoing messages into `pending_approval` status. The message is queued safely in the [SadaSend Web Dashboard](https://app.sadasend.com/approvals) until a team member approves or cancels it:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const send = await sadasend.emails.send({
|
|
159
|
+
from: 'notifications@yourdomain.com',
|
|
160
|
+
to: 'client@partner.com',
|
|
161
|
+
subject: 'Proposal Agreement Draft',
|
|
162
|
+
html: '<p>Please review the proposal draft.</p>',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (send.ok && send.status === 'pending_approval') {
|
|
166
|
+
console.log(`Held in human approval queue with ID: ${send.id}`);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3. Preflight Dry-Runs (`dryRun: true`)
|
|
171
|
+
Validate template compilation, suppression status, and domain SPF/DKIM verification without sending an email or consuming monthly plan quota:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const preview = await sadasend.emails.send({
|
|
175
|
+
from: 'noreply@yourdomain.com',
|
|
176
|
+
to: 'customer@example.com',
|
|
177
|
+
subject: 'Weekly Digest',
|
|
178
|
+
html: '<h1>Digest</h1>',
|
|
179
|
+
dryRun: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (preview.ok) {
|
|
183
|
+
console.log('Dry run successful. Ready for dispatch.');
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Comprehensive API Reference
|
|
190
|
+
|
|
191
|
+
### 1. Transactional Sends (`emails.send`)
|
|
192
|
+
|
|
193
|
+
Send rich transactional emails with attachments, tags, custom metadata, CC/BCC, and reply-to addresses:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const res = await sadasend.emails.send({
|
|
197
|
+
from: 'Billing Team <billing@yourdomain.com>',
|
|
198
|
+
to: ['customer@example.com'],
|
|
199
|
+
cc: ['finance@yourdomain.com'],
|
|
200
|
+
replyTo: 'support@yourdomain.com',
|
|
201
|
+
subject: 'Invoice #INV-2026-10',
|
|
202
|
+
html: '<h1>Your Invoice is Attached</h1><p>Thank you for your business!</p>',
|
|
203
|
+
text: 'Your Invoice is Attached. Thank you for your business!',
|
|
204
|
+
tags: ['billing', 'october-invoices'],
|
|
205
|
+
headers: {
|
|
206
|
+
'X-Entity-ID': 'cust_98124',
|
|
207
|
+
},
|
|
208
|
+
attachments: [
|
|
209
|
+
{
|
|
210
|
+
filename: 'invoice.pdf',
|
|
211
|
+
content: Buffer.from('%PDF-1.4...').toString('base64'),
|
|
212
|
+
contentType: 'application/pdf',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
idempotencyKey: 'invoice-send-INV-2026-10',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (res.ok) {
|
|
219
|
+
console.log(`Message queued with ID: ${res.id}`);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 2. High-Throughput Batch Sends (`emails.batch`)
|
|
224
|
+
|
|
225
|
+
Send up to 100 individual emails in a single atomic HTTP request. Returns an RFC 4918 207 Multi-Status object detailing per-item delivery outcomes:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const batch = await sadasend.emails.batch([
|
|
229
|
+
{ from: 'alerts@yourdomain.com', to: 'alice@example.com', subject: 'Server Alert', text: 'CPU at 92%' },
|
|
230
|
+
{ from: 'alerts@yourdomain.com', to: 'bob@example.com', subject: 'Server Alert', text: 'CPU at 92%' },
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
if (batch.ok) {
|
|
234
|
+
console.log(`Accepted: ${batch.data.accepted}, Refused: ${batch.data.refused}`);
|
|
235
|
+
for (const item of batch.data.results) {
|
|
236
|
+
if (item.ok) {
|
|
237
|
+
console.log(`Item ${item.index}: Accepted ID ${item.id}`);
|
|
238
|
+
} else {
|
|
239
|
+
console.error(`Item ${item.index}: Refused due to ${item.error}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 3. Scheduled Delivery & Atomic Cancellation (`emails.cancel`)
|
|
246
|
+
|
|
247
|
+
Schedule a future dispatch and cancel it before it leaves the server:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// Schedule dispatch for 2 hours in the future
|
|
251
|
+
const send = await sadasend.emails.send({
|
|
252
|
+
from: 'noreply@yourdomain.com',
|
|
253
|
+
to: 'user@example.com',
|
|
254
|
+
subject: 'Scheduled Notification',
|
|
255
|
+
text: 'This is scheduled.',
|
|
256
|
+
scheduledAt: new Date(Date.now() + 2 * 60 * 60 * 1000),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Cancel if user cancels their action
|
|
260
|
+
if (send.ok) {
|
|
261
|
+
const cancel = await sadasend.emails.cancel(send.id);
|
|
262
|
+
if (cancel.ok) {
|
|
263
|
+
console.log('Scheduled email cancelled successfully.');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 4. Sending Domains & Live DNS Verification (`domains`)
|
|
269
|
+
|
|
270
|
+
Programmatically register domains and retrieve required SPF, DKIM (2048-bit RSA), and DMARC DNS records:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
// List domains
|
|
274
|
+
const domains = await sadasend.domains.list();
|
|
275
|
+
|
|
276
|
+
// Retrieve DNS records for setup
|
|
277
|
+
const records = await sadasend.domains.records('dom_981a2b');
|
|
278
|
+
if (records.ok) {
|
|
279
|
+
for (const record of records.data.records) {
|
|
280
|
+
console.log(`Type: ${record.type} | Host: ${record.host} | Value: ${record.value}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Perform live internet DNS verification
|
|
285
|
+
const verify = await sadasend.domains.verify('dom_981a2b');
|
|
286
|
+
if (verify.ok) {
|
|
287
|
+
console.log(`Verification status: ${verify.data.status}`); // 'verified' | 'unverified'
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 5. Server-Side Liquid Dynamic Templates (`templates`)
|
|
292
|
+
|
|
293
|
+
Render server-side Liquid templates with strict input sanitization:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
// List available templates
|
|
297
|
+
const templates = await sadasend.templates.list();
|
|
298
|
+
|
|
299
|
+
// Test render against test variables
|
|
300
|
+
const preview = await sadasend.templates.render('welcome-email', {
|
|
301
|
+
first_name: 'Sarah',
|
|
302
|
+
login_url: 'https://app.sadasend.com/login',
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (preview.ok) {
|
|
306
|
+
console.log('Rendered HTML:', preview.data.html);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 6. Automated & Manual Suppressions (`suppressions`)
|
|
311
|
+
|
|
312
|
+
Manage suppression lists to prevent contacting hard-bounced or unsubscribed recipients:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// Add manual suppression
|
|
316
|
+
await sadasend.suppressions.add('unsub@example.com', 'unsubscribe');
|
|
317
|
+
|
|
318
|
+
// Query suppression list
|
|
319
|
+
const list = await sadasend.suppressions.list({ q: 'unsub@example.com' });
|
|
320
|
+
if (list.ok && list.data.suppressions.length > 0) {
|
|
321
|
+
console.log('Recipient is suppressed:', list.data.suppressions[0]!.reason);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### 7. Account Deliverability & Telemetry (`account`)
|
|
326
|
+
|
|
327
|
+
Monitor your sending velocity, tier quotas, and deliverability health:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// Quota & tier metrics
|
|
331
|
+
const acc = await sadasend.account.get();
|
|
332
|
+
if (acc.ok) {
|
|
333
|
+
console.log(`Plan: ${acc.data.plan} | Quota: ${acc.data.emailsUsed} / ${acc.data.emailQuota}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 14-day deliverability ratios
|
|
337
|
+
const stats = await sadasend.account.stats({ window: '14d' });
|
|
338
|
+
if (stats.ok) {
|
|
339
|
+
console.log(`Sent: ${stats.data.sent} | Delivered: ${stats.data.delivered} | Bounced: ${stats.data.bounced}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Inspect active agent circuit breakers
|
|
343
|
+
const breakers = await sadasend.account.circuitBreakers();
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### 8. API Key Governance & Approvals Queue (`keys`)
|
|
347
|
+
|
|
348
|
+
Audit active API keys and inspect held emails pending human sign-off:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
// List registered keys
|
|
352
|
+
const keys = await sadasend.keys.list();
|
|
353
|
+
|
|
354
|
+
// List held emails waiting in the approval queue
|
|
355
|
+
const held = await sadasend.keys.approvals();
|
|
356
|
+
if (held.ok) {
|
|
357
|
+
console.log(`Pending approval count: ${held.data.pending.length}`);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Timing-Safe HMAC Webhook Verification
|
|
364
|
+
|
|
365
|
+
SadaSend signs all webhook deliveries with HMAC-SHA256 (`t=timestamp,v1=signature`). Verify webhooks with **zero third-party dependencies** using native WebCrypto (`crypto.subtle`):
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import { parseWebhook, verifyWebhook } from 'sadasend/webhooks';
|
|
369
|
+
|
|
370
|
+
// Example: Next.js App Router (app/api/webhooks/route.ts)
|
|
371
|
+
export async function POST(req: Request) {
|
|
372
|
+
// CRITICAL: Must read as raw string. JSON.parse() changes formatting and breaks HMAC!
|
|
373
|
+
const rawBody = await req.text();
|
|
374
|
+
const signatureHeader = req.headers.get('sadasend-signature');
|
|
375
|
+
|
|
376
|
+
if (!signatureHeader) {
|
|
377
|
+
return new Response('Missing signature header', { status: 400 });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const event = await parseWebhook(
|
|
381
|
+
process.env.SADASEND_WEBHOOK_SECRET!,
|
|
382
|
+
rawBody,
|
|
383
|
+
signatureHeader
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (!event) {
|
|
387
|
+
return new Response('Invalid webhook signature or expired timestamp', { status: 401 });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Handle typed event
|
|
391
|
+
switch (event.type) {
|
|
392
|
+
case 'email.delivered':
|
|
393
|
+
console.log('Delivered message:', event.data.messageId);
|
|
394
|
+
break;
|
|
395
|
+
case 'email.bounced':
|
|
396
|
+
console.warn('Bounced message:', event.data.messageId, event.data.detail);
|
|
397
|
+
break;
|
|
398
|
+
case 'email.complained':
|
|
399
|
+
console.warn('Spam complaint recorded for:', event.data.messageId);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return new Response('OK', { status: 200 });
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
- **Replay Protection**: Timestamps are cryptographically bound to the signed body and rejected if older than 5 minutes.
|
|
408
|
+
- **Timing-Attack Immune**: Evaluated in constant time to prevent side-channel timing analysis.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Error Handling: Discriminated Unions
|
|
413
|
+
|
|
414
|
+
SadaSend returns a discriminated union result (`Result<T>`: `{ ok: true, data } | { ok: false, error }`) instead of throwing exceptions. This ensures that guardrail refusals (such as allowlist violations, suppressions, or unverified domains) are handled as normal business logic:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
const result = await sadasend.emails.send({
|
|
418
|
+
from: 'team@yourdomain.com',
|
|
419
|
+
to: 'partner@example.com',
|
|
420
|
+
subject: 'Project Update',
|
|
421
|
+
text: 'Here is the update.',
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if (!result.ok) {
|
|
425
|
+
if (result.error instanceof DomainNotVerifiedError) {
|
|
426
|
+
// DNS records are attached directly to the error for instant remediation!
|
|
427
|
+
console.error('Domain not verified. Configure these DNS records:');
|
|
428
|
+
for (const rec of result.error.records) {
|
|
429
|
+
console.log(rec.type, rec.host, rec.value);
|
|
430
|
+
}
|
|
431
|
+
} else if (result.error instanceof RecipientNotAllowlistedError) {
|
|
432
|
+
console.error('Allowlist violation:', result.error.recipient);
|
|
433
|
+
} else if (result.error instanceof RecipientSuppressedError) {
|
|
434
|
+
console.error('Recipient suppressed:', result.error.reason);
|
|
435
|
+
} else if (result.error instanceof RateLimitError) {
|
|
436
|
+
console.error(`Rate limit exceeded. Retry in ${result.error.retryAfterSeconds}s`);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Success branch is automatically narrowed by TypeScript
|
|
442
|
+
console.log('Message ID:', result.id);
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Error Hierarchy Table
|
|
446
|
+
|
|
447
|
+
| Error Class | Trigger Scenario | Automatic Retry | Attached Remediation Data |
|
|
448
|
+
| :--- | :--- | :---: | :--- |
|
|
449
|
+
| `AuthenticationError` | Missing, invalid, or revoked API key | No | None |
|
|
450
|
+
| `InsufficientScopeError` | API key lacks required scope (`send`, `read`, `domains`) | No | `required`, `held` |
|
|
451
|
+
| `InvalidFromError` | Sender address is malformed | No | None |
|
|
452
|
+
| `DomainNotFoundError` | Sending domain is not registered on this account | No | None |
|
|
453
|
+
| `DomainNotVerifiedError` | Sending domain has not completed SPF/DKIM verification | No | `records` (Table of required DNS records) |
|
|
454
|
+
| `RecipientNotAllowlistedError` | Agent attempted to email an address not in allowlist | No | `allowlist`, `recipient` |
|
|
455
|
+
| `RecipientSuppressedError` | Recipient previously bounced or unsubscribed | No | `reason`, `recipient` |
|
|
456
|
+
| `IdempotencyKeyReusedError` | Same idempotency key passed with altered payload | No | None |
|
|
457
|
+
| `IdempotencyInProgressError` | Original idempotent request is still processing | **Yes** | None |
|
|
458
|
+
| `RateLimitError` | Per-key or account rate limit ceiling reached (HTTP 429) | **Yes** | `retryAfterSeconds` |
|
|
459
|
+
| `ServerError` | Transient upstream 5xx error | **Yes** | HTTP status |
|
|
460
|
+
| `ConnectionError` | Socket drop or DNS network timeout (**Thrown**) | **Yes** | Root `cause`, `attempts` |
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Native Model Context Protocol (MCP) Integration
|
|
465
|
+
|
|
466
|
+
SadaSend features a native Model Context Protocol (MCP) Server-Sent Events (SSE) server for **Cursor**, **Claude Code**, **Windsurf**, and custom LLM runtimes:
|
|
467
|
+
|
|
468
|
+
Connect your AI agent with zero local software to install:
|
|
469
|
+
|
|
470
|
+
```json
|
|
471
|
+
{
|
|
472
|
+
"mcpServers": {
|
|
473
|
+
"sadasend": {
|
|
474
|
+
"url": "https://mcp.sadasend.com/mcp/sse",
|
|
475
|
+
"headers": {
|
|
476
|
+
"Authorization": "Bearer sada_agent_sk_your_key_here"
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Read the full [Model Context Protocol Guide](https://sadasend.com/docs/mcp-guide) for tool declarations, schemas, and best practices.
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Canonical Documentation & Technical References
|
|
488
|
+
|
|
489
|
+
For exhaustive architectural specifications, REST API contracts, and guides:
|
|
490
|
+
|
|
491
|
+
- 🌐 [Official Website](https://sadasend.com)
|
|
492
|
+
- 📖 [Developer Documentation](https://sadasend.com/docs)
|
|
493
|
+
- 📘 [TypeScript SDK Reference](https://sadasend.com/docs/sdks/typescript)
|
|
494
|
+
- 📡 [REST API Documentation](https://sadasend.com/docs/api-reference)
|
|
495
|
+
- 🤖 [Model Context Protocol (MCP) Guide](https://sadasend.com/docs/mcp-guide)
|
|
496
|
+
- 🛡️ [AI Agent Email Guardrails Overview](https://sadasend.com/docs/agent-guardrails)
|
|
497
|
+
- 🔐 [DKIM, SPF & DMARC Deliverability Guide](https://sadasend.com/docs/domains)
|
|
498
|
+
- 🪝 [Webhooks & Signature Verification](https://sadasend.com/docs/webhooks)
|
|
499
|
+
- 💳 [Plans, Pricing & Quotas](https://sadasend.com/pricing)
|
|
500
|
+
- 🚦 [Platform Live System Status](https://status.sadasend.com)
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Frequently Asked Questions (FAQ)
|
|
505
|
+
|
|
506
|
+
### How is SadaSend different from Resend, SendGrid, or AWS SES?
|
|
507
|
+
Traditional email APIs only provide basic transactional sending with no concept of AI agent safety. SadaSend introduces server-enforced recipient allowlists, human-in-the-loop approval queues, per-key rate limits, and preflight dry-runs. When an autonomous agent hallucinating an email address tries to send, SadaSend refuses the dispatch before real mail is queued.
|
|
508
|
+
|
|
509
|
+
### Can an AI agent bypass its recipient allowlist or edit its scopes?
|
|
510
|
+
**No.** All guardrails are enforced on SadaSend servers. Key management and allowlist modification endpoints require secure browser sessions with CSRF protection and multi-factor authentication (2FA). API keys can never edit themselves or mint other keys.
|
|
511
|
+
|
|
512
|
+
### Does the TypeScript SDK work in Cloudflare Workers and Next.js Edge?
|
|
513
|
+
**Yes.** The SDK uses zero Node.js-only built-in modules. It relies strictly on `globalThis.fetch`, `crypto.subtle`, and `TextEncoder`, running identically in Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge Functions, and modern browser environments.
|
|
514
|
+
|
|
515
|
+
### What happens if an email request fails due to a network drop?
|
|
516
|
+
SadaSend automatically mints a UUIDv4 `Idempotency-Key` on every send. When the SDK retries after a connection timeout or network drop, it reuses the **exact same idempotency key**. The server returns the cached response if the initial request succeeded, preventing duplicate emails.
|
|
517
|
+
|
|
518
|
+
### What is Human Approval Mode?
|
|
519
|
+
When an API key is minted with `mode: 'approval'`, outgoing messages are held in `pending_approval` state rather than dispatched to the network. An operator can review the message body, recipient, and attachments in the [SadaSend Web Console](https://app.sadasend.com/approvals) before clicking "Approve".
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## License & Support
|
|
524
|
+
|
|
525
|
+
- **License:** [MIT License](./LICENSE) © [SadaSend](https://sadasend.com)
|
|
526
|
+
- **General Support:** [support@sadasend.com](mailto:support@sadasend.com)
|
|
527
|
+
- **Security Vulnerabilities:** [security@sadasend.com](mailto:security@sadasend.com)
|
|
528
|
+
- **GitHub Repository:** [SadaSend/sadasend-sdk-ts](https://github.com/SadaSend/sadasend-sdk-ts)
|