← Files InsForgeARCHIVED FILE
skills/insforge/email/sdk-integration.md
6.34 KB · Oct 3, 2026 · 06:30 UTC
# Email SDK Integration
Send custom transactional HTML emails via `insforge.emails.send`. Routes through AWS SES under per-project tenant identities, with automatic List-Unsubscribe headers and unsubscribe filtering.
> **🛑 No SMTP, no third-party packages.** `insforge.emails.send()` works on every paid project — no `nodemailer` / `resend` / `sendgrid` / `mailgun` / `postmark`, no `SMTP_HOST`, no API keys. The platform manages the SES sender. Custom sender domain → dashboard, not `package.json`.
>
> **Scope.** This module sends **custom** transactional emails (welcome, receipt, newsletter, alerts). For auth flows (signup verification, password reset, invites), use `insforge.auth.*` — those ship on **every plan**, also no SMTP.
> **⚠️ Private preview.** Custom email is in private preview. The API may change; pin to a tested SDK version. Custom email requires a **paid plan** — free-tier projects can only use the built-in auth emails.
## Setup
Ensure your `.env` is configured with your InsForge URL and anon key. Get the anon key with `npx @insforge/cli secrets get ANON_KEY`. See the main [SKILL.md](../SKILL.md) for framework-specific variable names.
```javascript
import { createClient } from '@insforge/sdk'
const insforge = createClient({
baseUrl: process.env.NEXT_PUBLIC_INSFORGE_URL, // adjust prefix for your framework
anonKey: process.env.NEXT_PUBLIC_INSFORGE_ANON_KEY // adjust prefix for your framework
})
```
The anon JWT is sufficient — **no user sign-in required** to call `emails.send`. A signed-in user JWT also works.
## Send an Email
```javascript
const { data, error } = await insforge.emails.send({
to: 'user@example.com',
subject: 'Welcome to Acme',
html: '<h1>Hi!</h1><p>Thanks for joining Acme.</p>',
from: 'Acme Updates', // optional display name
replyTo: 'support@acme.com', // optional
});
if (error) {
console.error('Email failed:', error.message);
return;
}
console.log('Sent:', data?.id);
```
## Send to Multiple Recipients
```javascript
const { data, error } = await insforge.emails.send({
to: ['alex@example.com', 'sam@example.com'],
cc: 'manager@example.com',
bcc: ['archive@example.com'],
subject: 'April update',
html: '<h1>This month at Acme</h1><p>...</p>',
from: 'Acme Updates',
});
// data.skipped lists any recipients that have unsubscribed and were silently dropped
if (data?.skipped?.length) {
console.log('Skipped (unsubscribed):', data.skipped);
}
```
## API Reference
```typescript
emails.send(options: {
to: string | string[]; // required, 1–50 addresses
subject: string; // required, 1–500 chars
html: string; // required, HTML body
cc?: string | string[]; // optional
bcc?: string | string[]; // optional
from?: string; // optional display name only (NOT email address)
replyTo?: string; // optional email address
}): Promise<{
data: { id: string; skipped?: string[] } | null;
error: Error | null;
}>
```
### Sender Address Rules
- **`from` is a display name only.** The actual sending address is always `noreply@<appkey>.send.insforge.dev` (your project's SES tenant subdomain). You cannot send from `hello@yourdomain.com`.
- Use `replyTo` to route replies to a real inbox you control.
- The backend appends a per-recipient `List-Unsubscribe` footer and headers automatically — do not add your own.
### Limits
| Limit | Value |
|---|---|
| Max recipients per request | 50 (across `to` + `cc` + `bcc`) |
| Hourly send rate | 10–50 emails depending on plan |
| Subject length | 1–500 chars |
| Plan requirement | Paid plan (free tier returns `On-demand email service is only available for paid plans`) |
Each address in `to`/`cc`/`bcc` counts as one delivery (per SES billing) — a single send to 30 addresses is 30 emails against your quota.
## REST Fallback
If you can't use the SDK (e.g. cURL, server-side calls without the JS SDK):
```bash
curl -X POST "$INSFORGE_URL/api/email/send-raw" \
-H "Authorization: Bearer $INSFORGE_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Welcome",
"html": "<h1>Hello</h1>",
"from": "Acme",
"replyTo": "support@acme.com"
}'
```
Success: `HTTP 200` with body `{}` (REST endpoint omits the wrapping; SDK adds `{ data, error }`).
## Common Errors
| Error message | Cause | Fix |
|---|---|---|
| `On-demand email service is only available for paid plans` | Free tier project | Upgrade billing in the InsForge dashboard |
| `Rate limit exceeded. Try again in N minutes` | Hourly send cap reached | Wait `rateLimit.resetIn` seconds and retry, or upgrade plan |
| `Invalid ARN provided` | Backend infra config issue (missing `AWS_ACCOUNT_ID` env on the cloud backend) | Not a client bug — file with InsForge support |
| `401 Unauthorized` | Bad/missing `Authorization` header | Re-init SDK with correct `anonKey` |
| Email lands in spam | Cold SES tenant reputation | Warm up with a few low-volume sends; use a recognizable display name; avoid spammy HTML and link-heavy bodies |
## Common Mistakes
- **Installing `nodemailer` / `resend` / `sendgrid` / `mailgun` / `postmark` or asking for SMTP credentials.** The built-in service is already wired in — `insforge.emails.send()` is all you need.
- **Trying to set `from` to an email address.** Only the display-name part is honored. The address is fixed.
- **Adding your own unsubscribe link.** The backend already injects one — adding another duplicates UX and confuses ESPs.
- **Treating skipped recipients as failures.** `data.skipped` recipients have unsubscribed; the request still returns success.
- **Sending one big request to 30 unrelated users without per-recipient personalization.** The backend already sends individual messages internally for personalized unsubscribe links — you just pass the array.
- **Calling from the free tier and reporting an "API bug".** Check `subscriptionPlan` first; the rejection is by design.
## Quick Reference
| Task | Code |
|------|------|
| Send to one recipient | `insforge.emails.send({ to, subject, html })` |
| Send to many | `insforge.emails.send({ to: [a, b], cc, bcc, subject, html })` |
| Set sender display name | `from: 'Acme Updates'` |
| Route replies | `replyTo: 'support@acme.com'` |
| Inspect skipped (unsubscribed) | `data.skipped` |
| REST endpoint | `POST {baseUrl}/api/email/send-raw` |
SHA-256: 42d2440b40a93eb53140fb57b07b06d9dd61a41966a9fda082bf66978e76abe7