SendStack Documentation
The unified email control plane for solo builders. One API to send, route, and monitor transactional email across any provider.
Overview
SendStack gives you a single API endpoint to send transactional email through Amazon SES, SendGrid, Mailgun, or Resend — using your own credentials. No vendor lock-in, no per-email fees from us.
Quick Start
Get sending in under 5 minutes:
Go to Projects in the dashboard and create a new project. You'll receive a unique server key (starts with sk_).
Go to Domains, select your project, enter your domain name, choose a provider (SES, SendGrid, Mailgun, or Resend), and paste your provider credentials as JSON.
Click Verify on your domain card. SendStack checks your SPF, DKIM, and DMARC records and computes a health score.
Use the test send button, or call the API directly:
curl -X POST https://your-instance.com/api/send \
-H "Content-Type: application/json" \
-H "x-sendstack-server-key: sk_your_project_key" \
-d '{
"project_id": "your_project_id",
"domain": "mail.example.com",
"to": "user@example.com",
"subject": "Hello from SendStack",
"html": "<h1>It works!</h1><p>Your email is live.</p>"
}'{
"message_id": "abc123...",
"provider": "SES",
"accepted": true,
"request_id": "req_..."
}Authentication
SendStack uses two types of authentication:
Dashboard Access
Sign in at /login with your configured email and password. Sessions last 30 days via a signed, httpOnly cookie.
This is a single-user application — the login credentials are set via environment variables (SINGLE_USER_EMAIL and SINGLE_USER_PASSWORD).
API Authentication
All requests to /api/send must include a server key in the x-sendstack-server-key header.
There are two types of server keys:
| Key Type | Scope | Source |
|---|---|---|
| Per-project key | Can only send through domains in that project | Generated when creating a project (sk_...) |
| Global key | Can send through any project's domains | SENDSTACK_SERVER_KEY env var |
Projects & API Keys
Projects are workspaces that group domains, messages, and API keys together.
Creating a Project
In the dashboard, go to Projects and enter a name (2–80 characters). A unique server key is generated automatically.
POST /api/projects
Content-Type: application/json
Cookie: sendstack_session=...
{ "name": "My SaaS App" }
→ 201 { "project": { "id": "...", "name": "My SaaS App", "serverKey": "sk_..." } }Managing API Keys
Each project has a unique server key visible on the Projects page. You can:
- Copy — click the copy icon to copy the full key to your clipboard
- Rotate — click the rotate icon to generate a new key. The old key stops working immediately.
POST /api/projects/:projectId/rotate-key
Cookie: sendstack_session=...
→ 200 { "serverKey": "sk_new_key_here" }Domains
Domains connect your sending domains to email provider credentials. Each domain belongs to one project and uses one provider. SendStack encrypts all credentials with AES-256-GCM before storage — they are only decrypted at send time.
Supported Providers
| Provider | Required Credentials | Best For |
|---|---|---|
| Amazon SES | region, accessKeyId, secretAccessKey, fromEmail, dkimSelector | High volume, lowest cost per email |
| SendGrid | apiKey, fromEmail | Simple setup, generous free tier |
| Mailgun | apiKey, sendingDomain, fromEmail | Developer-focused, strong deliverability |
| Resend | apiKey, fromEmail | Modern DX, built for developers |
You can mix providers across domains within the same project — for example, use SES for your main app and Resend for onboarding emails. SendStack routes automatically based on the domain in each send request.
Adding a Domain
In the dashboard, click Add Domain on the Domains page and fill in:
- Project — which project this domain belongs to
- Domain name — e.g.
mail.example.com - Provider — SES, SendGrid, Mailgun, or Resend
- Credentials — your provider API keys (encrypted before storage)
- Webhook URL (optional) — where to forward normalized provider events
Verifying a Domain
After adding a domain, click Verify to check your DNS configuration. SendStack checks:
| Check | What It Looks For | DNS Record |
|---|---|---|
| SPF | v=spf1 in TXT records | example.com TXT |
| DKIM | v=dkim1 in TXT or CNAME | selector._domainkey.example.com |
| DMARC | v=dmarc1 in TXT records | _dmarc.example.com |
| Webhook | HTTP HEAD returns status < 500 | — |
| Sandbox (SES only) | SES production access check | — |
Provider Guides
Step-by-step instructions for connecting each supported email provider to SendStack. Each guide covers how to get your credentials, configure DNS, and set up webhooks.
Amazon SES
Amazon Simple Email Service offers the lowest per-email cost and is ideal for high-volume transactional email.
1. Get Your Credentials
- Open the IAM Console in AWS and create a new IAM user (or use an existing one) with the
AmazonSESFullAccesspolicy attached. - Generate an Access Key for the user. Save the
Access Key IDandSecret Access Key— you won't be able to see the secret again. - Note your SES Region (e.g.
us-east-1,eu-west-1). This must match the region where your domain is verified in SES.
2. Verify Your Domain in SES
- In the SES Console, go to Verified Identities → Create Identity → select Domain.
- Enter your domain (e.g.
mail.example.com) and follow the instructions to add DKIM CNAME records to your DNS. - Note the DKIM Selector (usually a hash like
abcdef1234) — you'll need this in SendStack.
3. Move Out of Sandbox
New SES accounts start in sandbox mode, which only allows sending to verified email addresses. To send to any recipient:
- Go to SES Console → Account Dashboard → Request Production Access.
- Fill in your use case (transactional email) and expected volume. Approval usually takes 24 hours.
SendStack detects sandbox mode during domain verification and deducts 20 points from your health score until resolved.
4. Configure in SendStack
{
"region": "us-east-1",
"accessKeyId": "AKIA...",
"secretAccessKey": "wJal...",
"fromEmail": "noreply@mail.example.com",
"dkimSelector": "selector1"
}5. Set Up Webhooks
- In the SES Console, go to Configuration Sets → create a new set or use an existing one.
- Add an SNS Destination for each event type you want to track (Send, Delivery, Bounce, Complaint, Open, Click).
- Create an SNS Topic and add an HTTPS Subscription pointing to:
https://your-instance.com/api/webhooks/ses - SNS will send a confirmation request — SendStack automatically confirms it.
include:amazonses.com to your SPF record, the DKIM CNAME records from the SES console, and a DMARC record (v=DMARC1; p=quarantine).SendGrid
SendGrid (by Twilio) offers a generous free tier and straightforward API key setup.
1. Get Your API Key
- Log in to your SendGrid Dashboard → Settings → API Keys.
- Click Create API Key → select Restricted Access → enable Mail Send permission.
- Copy the generated key (starts with
SG.) — it's only shown once.
2. Authenticate Your Domain
- Go to Settings → Sender Authentication → Authenticate Your Domain.
- Follow the wizard to add the required CNAME records to your DNS for DKIM and SPF authentication.
- SendGrid will verify the records automatically once they propagate.
3. Configure in SendStack
{
"apiKey": "SG.xxxxx...",
"fromEmail": "noreply@mail.example.com"
}4. Set Up Webhooks
- Go to Settings → Mail Settings → Event Webhook.
- Set the HTTP Post URL to:
https://your-instance.com/api/webhooks/sendgrid - Select the events you want to receive: Processed, Delivered, Bounce, Spam Report, Open, Click.
- Enable Signed Event Webhook and copy the Verification Key — set it as your
SENDGRID_WEBHOOK_PUBLIC_KEYenvironment variable.
Mailgun
Mailgun is developer-focused with strong deliverability and a flexible API.
1. Get Your API Key
- Log in to your Mailgun Dashboard → click your profile → API Security.
- Copy your Private API Key (starts with
key-).
2. Add and Verify Your Domain
- Go to Sending → Domains → Add New Domain.
- Enter your sending domain (e.g.
mg.example.com) — Mailgun recommends using a subdomain. - Add the DNS records Mailgun provides: SPF TXT record, DKIM TXT records, and MX records.
- Click Verify DNS Settings to confirm.
3. Configure in SendStack
{
"apiKey": "key-xxxxx...",
"sendingDomain": "mg.example.com",
"fromEmail": "noreply@mg.example.com"
}The sendingDomain should match the domain you verified in Mailgun (typically the subdomain).
4. Set Up Webhooks
- Go to Sending → Webhooks → select your domain.
- Click Add Webhook for each event type and set the URL to:
https://your-instance.com/api/webhooks/mailgun - Copy your Webhook Signing Key from the webhook settings page and set it as your
MAILGUN_WEBHOOK_SIGNING_KEYenvironment variable.
Resend
Resend is a modern email API built for developers, with a clean interface and simple integration.
1. Get Your API Key
- Sign up or log in at resend.com → go to API Keys in the sidebar.
- Click Create API Key → give it a name and select Sending access permission.
- Optionally restrict the key to a specific domain for extra security.
- Copy the generated key (starts with
re_) — it's only shown once.
2. Add and Verify Your Domain
- Go to Domains in the Resend dashboard → click Add Domain.
- Enter your domain (e.g.
mail.example.com). - Resend provides DNS records to add: SPF TXT record, DKIM CNAME records, and optionally a DMARC record.
- Add all records to your DNS provider and click Verify. Propagation usually takes a few minutes.
3. Configure in SendStack
{
"apiKey": "re_xxxxx...",
"fromEmail": "noreply@mail.example.com"
}The fromEmail must use a domain you've verified in Resend.
4. Set Up Webhooks
- Go to Webhooks in the Resend dashboard → click Add Webhook.
- Set the Endpoint URL to:
https://your-instance.com/api/webhooks/resend - Select the events to subscribe to:
email.sent,email.delivered,email.bounced,email.complained,email.opened,email.clicked. - After creating the webhook, copy the Signing Secret (starts with
whsec_) and set it as yourRESEND_WEBHOOK_SIGNING_SECRETenvironment variable.
svix-id, svix-timestamp, and svix-signature headers with HMAC-SHA256.Switching Providers
SendStack makes it straightforward to switch providers or run multiple providers in parallel:
- Per-domain routing — each domain is tied to one provider. Add a new domain with a different provider to start using it alongside existing domains.
- No code changes — your API calls use the
domainfield to route. Change which domain you send through, and SendStack handles the rest. - Gradual migration — run both providers simultaneously during migration. Route new traffic to the new domain while the old one continues to work.
{
"project_id": "proj_123",
"domain": "mail.example.com",
"to": "user@example.com",
"subject": "Hello",
"html": "<p>Sent via SES</p>"
}{
"project_id": "proj_123",
"domain": "notifications.example.com",
"to": "user@example.com",
"subject": "Hello",
"html": "<p>Sent via Resend</p>"
}Provider Comparison
| Feature | Amazon SES | SendGrid | Mailgun | Resend |
|---|---|---|---|---|
| Pricing | ~$0.10/1k emails | Free up to 100/day | Free up to 100/day | Free up to 100/day |
| Setup complexity | Medium (IAM + SES console) | Low (API key + domain auth) | Low (API key + DNS) | Low (API key + DNS) |
| Sandbox mode | Yes (requires production request) | No | No | No |
| Webhook setup | SNS Topics + Subscriptions | Event Webhook settings | Per-domain webhook config | Dashboard webhook config |
| Webhook signing | SNS certificate (RSA-SHA1) | ECDSA public key | HMAC-SHA256 | Svix HMAC-SHA256 |
| Credentials needed | 5 fields | 2 fields | 3 fields | 2 fields |
Sending Email
Send transactional email through the unified API. SendStack routes to the correct provider based on the domain you specify.
Send Endpoint
You can send with inline content (subject + html) or use a saved template (template_id + variables). The two modes are mutually exclusive.
POST /api/send
Content-Type: application/json
x-sendstack-server-key: sk_your_key_here
{
"project_id": "clxyz...",
"domain": "mail.example.com",
"to": "recipient@example.com",
"subject": "Order Confirmation #1234",
"html": "<h1>Thanks for your order!</h1><p>Details below...</p>",
"text": "Thanks for your order! Details below...",
"from_name": "Acme Store",
"reply_to": "support@acme.com",
"metadata": { "order_id": "1234" }
}POST /api/send
Content-Type: application/json
x-sendstack-server-key: sk_your_key_here
{
"project_id": "clxyz...",
"domain": "mail.example.com",
"to": "recipient@example.com",
"template_id": "cltemplate123...",
"variables": {
"name": "Jane",
"order_id": "1234"
}
}Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
project_id | string | Yes | Your project ID |
domain | string | Yes | The sending domain (must be configured) |
to | string | Yes | Recipient email address |
subject | string | Conditional | Email subject line (required if no template_id) |
html | string | Conditional | HTML body (required if no template_id) |
text | string | No | Plain text fallback |
template_id | string | No | ID of a saved template (replaces subject + html) |
variables | object | No | Key-value pairs for template {{variable}} interpolation |
from_name | string | No | Display name for the sender (max 200 chars) |
reply_to | string | No | Reply-to email address |
metadata | object | No | Custom key-value data stored with the message |
Response
{
"message_id": "provider-generated-id",
"provider": "SES",
"accepted": true,
"request_id": "uuid-for-tracing"
}| Status | Meaning |
|---|---|
200 | Email accepted by provider |
400 | Invalid request body or missing template variables |
401 | Missing or invalid server key |
403 | Key doesn't match the specified project |
404 | Domain, credentials, or template not found |
422 | Recipient is suppressed (bounce, complaint, or manual) |
429 | Rate limit exceeded |
500 | Internal server error |
Code Examples
async function sendEmail({ to, subject, html }) {
const res = await fetch(process.env.SENDSTACK_API_BASE + "/api/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-sendstack-server-key": process.env.SENDSTACK_SERVER_KEY,
},
body: JSON.stringify({
project_id: process.env.SENDSTACK_PROJECT_ID,
domain: "mail.example.com",
to,
subject,
html,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error);
}
return res.json();
}import requests, os
def send_email(to, subject, html):
res = requests.post(
f"{os.environ['SENDSTACK_API_BASE']}/api/send",
headers={
"Content-Type": "application/json",
"x-sendstack-server-key": os.environ["SENDSTACK_SERVER_KEY"],
},
json={
"project_id": os.environ["SENDSTACK_PROJECT_ID"],
"domain": "mail.example.com",
"to": to,
"subject": subject,
"html": html,
},
)
res.raise_for_status()
return res.json()What Happens When You Send
Templates
Create reusable email templates with {{variable}} placeholders. Templates are scoped to a project and can be referenced by ID when sending.
Creating a Template
Use the Templates page in the dashboard, or the API:
POST /api/templates
Content-Type: application/json
Cookie: sendstack_session=...
{
"projectId": "clxyz...",
"name": "welcome-email",
"subject": "Welcome, {{name}}!",
"html": "<h1>Hi {{name}}</h1><p>Thanks for joining {{company}}.</p>",
"variables": ["name", "company"]
}Template names must be unique within a project. The variables array documents expected placeholders but is not enforced — the actual interpolation validates at send time.
Variable Interpolation
Variables use double-brace syntax: {{variable_name}}. When you send an email using template_id, all placeholders in the subject and HTML body are replaced with the values from variables.
{{placeholder}} in the template has no matching key in the variables object, the send will fail with a 400 error listing the missing variables.// Template: "Hello {{name}}, your order #{{order_id}} is confirmed."
// Variables: { "name": "Jane", "order_id": "5678" }
// Result: "Hello Jane, your order #5678 is confirmed."Managing Templates
| Action | API |
|---|---|
| List templates | GET /api/templates?projectId=... |
| Get template | GET /api/templates/:id |
| Update template | PATCH /api/templates/:id |
| Delete template | DELETE /api/templates/:id |
Batch Sending
Send to up to 100 recipients in a single API call. The batch endpoint shares domain credentials and template resolution across all recipients, with per-recipient error handling.
Batch Endpoint
POST /api/send/batch
Content-Type: application/json
x-sendstack-server-key: sk_your_key_here
{
"project_id": "clxyz...",
"domain": "mail.example.com",
"template_id": "cltemplate123...",
"from_name": "Acme Store",
"recipients": [
{
"to": "jane@example.com",
"variables": { "name": "Jane", "code": "WELCOME10" }
},
{
"to": "bob@example.com",
"variables": { "name": "Bob", "code": "WELCOME20" },
"from_name": "Bob's Personal Rep"
}
]
}Batch Request Fields
| Field | Type | Description |
|---|---|---|
project_id | string | Your project ID |
domain | string | Sending domain |
subject | string | Default subject (overridable per recipient) |
html | string | Default HTML body (overridable per recipient) |
text | string | Default plain text |
template_id | string | Template ID (used if no inline subject/html) |
from_name | string | Default sender display name |
reply_to | string | Default reply-to address |
recipients | array | 1–100 recipient objects |
Each recipient object can override subject, html, text, from_name, reply_to, and provide unique variables and metadata.
Batch Response
{
"results": [
{ "to": "jane@example.com", "message_id": "abc...", "accepted": true },
{ "to": "bob@example.com", "error": "suppressed (bounce)", "suppressed": true }
],
"total": 2,
"succeeded": 1,
"failed": 1,
"request_id": "req_..."
}The batch endpoint never fails entirely — individual recipients may succeed or fail independently. Suppressed recipients are marked with suppressed: true.
Suppression List
The suppression list prevents sending to email addresses that have bounced, complained, or been manually blocked. Suppressed sends return 422 (single) or are skipped in batch.
Automatic Suppression
When SendStack receives a bounce or complaint webhook event from your provider, the recipient email is automatically added to the project's suppression list. This protects your sender reputation.
| Reason | Trigger |
|---|---|
BOUNCE | Hard bounce event from provider |
COMPLAINT | Spam complaint from recipient |
MANUAL | Added manually via dashboard or API |
Managing Suppressions
Use the Suppressions page in the dashboard, or the API:
POST /api/suppressions
Cookie: sendstack_session=...
{ "projectId": "clxyz...", "email": "user@example.com", "reason": "MANUAL" }DELETE /api/suppressions/:id
Cookie: sendstack_session=...Removing a suppression allows future sends to that address. The suppression list is searchable and paginated in the dashboard.
Webhooks
SendStack receives webhook events from your email providers to track delivery, bounces, complaints, opens, and clicks.
Webhook Endpoint
Point your provider's webhook settings to:
https://your-instance.com/api/webhooks/ses
https://your-instance.com/api/webhooks/sendgrid
https://your-instance.com/api/webhooks/mailgun
https://your-instance.com/api/webhooks/resendEvent Types
| Event | Description | SES | SendGrid | Mailgun | Resend |
|---|---|---|---|---|---|
SENT | Provider accepted the email | Send | processed | accepted | email.sent |
DELIVERED | Confirmed delivery to inbox | Delivery | delivered | delivered | email.delivered |
BOUNCED | Hard or soft bounce | Bounce | bounce | failed | email.bounced |
COMPLAINT | Spam complaint | Complaint | spamreport | complained | email.complained |
OPENED | Recipient opened the email | Open | open | opened | email.opened |
CLICKED | Recipient clicked a link | Click | click | clicked | email.clicked |
Inbound Signature Verification
Each provider's webhooks are verified using their native signature mechanism:
- SES: SNS certificate-based RSA-SHA1 signature verification
- SendGrid: ECDSA signature via
x-twilio-email-event-webhook-signatureheader (requiresSENDGRID_WEBHOOK_PUBLIC_KEYenv var) - Mailgun: HMAC-SHA256 signature verification (requires
MAILGUN_WEBHOOK_SIGNING_KEYenv var) - Resend: Svix HMAC-SHA256 signature verification via
svix-signatureheader (requiresRESEND_WEBHOOK_SIGNING_SECRETenv var)
An optional shared secret (PROVIDER_WEBHOOK_SHARED_SECRET) can be used as a fallback for any provider.
Webhook Forwarding
If you configured a Webhook URL on your domain, SendStack forwards normalized events to your endpoint. Events are delivered with up to 3 retry attempts using exponential backoff (200ms, 800ms, 3.2s).
Each delivery includes these headers:
| Header | Description |
|---|---|
X-SendStack-Event | Event type (e.g. DELIVERED, BOUNCED) |
X-SendStack-Delivery-Id | Unique ID for this delivery attempt |
X-SendStack-Timestamp | Unix timestamp of the delivery |
X-SendStack-Signature | HMAC-SHA256 signature for verification |
Outbound Signature Verification
Verify the authenticity of forwarded webhooks using HMAC-SHA256:
import crypto from "crypto";
function verifyWebhook(body, timestamp, signature, secret) {
const data = timestamp + "." + body;
const expected = crypto
.createHmac("sha256", secret)
.update(data)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
const body = await req.text();
const timestamp = req.headers.get("x-sendstack-timestamp");
const signature = req.headers.get("x-sendstack-signature");
if (!verifyWebhook(body, timestamp, signature, WEBHOOK_SECRET)) {
return new Response("Invalid signature", { status: 401 });
}Set the signing secret via the WEBHOOK_SIGNING_SECRET environment variable. If not set, it falls back to ENCRYPTION_KEY.
Ship With AI
Ship With AI generates ready-to-paste integration prompts for popular AI coding tools. Select a project and get:
- Environment variables — the exact env block for your
.envfile - Code snippet — a complete server-side send function
- AI tool prompts — tailored instructions for each tool:
| Tool | Prompt Style |
|---|---|
| Lovable | Full file contents ready to paste into the editor |
| Claude Code | Terminal commands, file tree, and smoke test curl |
| Cursor | Concise patch-style instructions with verification checklist |
| Raw SDK | Minimal Node.js fetch wrapper |
API Playground
The API Playground in the dashboard lets you test the send API without writing any code. Select a project, domain, and optionally a template, then send a test email directly from the browser.
- Direct HTML mode — enter subject, HTML body, and recipient manually
- Template mode — pick a saved template and fill in the variables
- Response panel — see the full API response, status code, and latency
Dashboard
The dashboard overview shows key metrics at a glance:
- Sent (7d / 30d) — total emails sent in the last 7 and 30 days
- Top Domain — your highest-traffic domain with its health score
- Delivery Rate — percentage of emails confirmed delivered
- Bounce Rate — percentage of emails that bounced
- Complaint Rate — percentage that triggered spam complaints
- Open Rate — percentage of emails opened (when tracking is enabled)
- Suppressions — total suppressed email addresses in your project
An activation checklist guides you through setup: create a project, connect a domain, verify DNS, and send a test email.
The dashboard also shows alert banners when your bounce rate exceeds 5% or complaint rate exceeds 0.1% — helping you catch deliverability issues early.
Domain Health
Each domain has a health score from 0 to 100 that updates daily.
Health Score Calculation
score = 100
score -= bounceRate × 220
score -= complaintRate × 420
score -= (no DKIM) ? 15 : 0
score -= (no DMARC) ? 12 : 0
score -= (webhook inactive) ? 8 : 0
score -= (SES sandbox) ? 20 : 0
Final score: clamped to 0–100| Score | Status | Meaning |
|---|---|---|
| 80–100 | HEALTHY | Good standing with all providers |
| 50–79 | WARNING | Some issues need attention |
| 0–49 | CRITICAL | Deliverability is at risk |
Daily Aggregation
A daily cron job (4 AM UTC) aggregates webhook events by domain and date. It computes delivery, bounce, complaint, open, and click counts, then updates each domain's health score. Events older than 90 days are automatically pruned.
Rate Limiting
SendStack applies rate limiting per IP address:
| Endpoint | Limit | Window | Max Emails |
|---|---|---|---|
POST /api/send | 60 requests | 1 minute | 60 |
POST /api/send/batch | 10 requests | 1 minute | 1,000 |
POST /api/auth/login | 10 attempts | 1 minute | — |
When you hit the limit, you'll receive:
{
"error": "Too many requests",
"retry_after": 42
}
Header: Retry-After: 42Wait for the Retry-After seconds before retrying.
Billing
SendStack uses a lifetime access pricing model — pay once, use forever. No subscriptions, no per-email fees.
Pricing increases in waves as seats fill:
| Wave | Price | Seats |
|---|---|---|
| Wave 1 (Early Adopter) | $79 | First 50 |
| Wave 2 | $99 | Next 100 |
| Wave 3 | $129 | Next 150 |
| Wave 4 (Final Price) | $149 | Unlimited |
Every wave includes:
- Unlimited emails
- All providers (SES, SendGrid, Mailgun, Resend)
- Unlimited domains
- Email templates + batch sending
- Suppression list management
- Webhook forwarding with HMAC signatures
- AI integration prompts
- DNS health monitoring
- API Playground
- All future updates
Self-Hosting
SendStack is designed for self-hosting on Vercel with a Neon (or any PostgreSQL) database.
Environment Variables
# Database (PostgreSQL)
DATABASE_URL=postgresql://user:pass@host:5432/sendstack
# Encryption (32-byte hex string for AES-256-GCM)
ENCRYPTION_KEY=your-64-char-hex-string
# Session signing (32+ random characters)
APP_SESSION_SECRET=your-random-session-secret
# Single-user login credentials
SINGLE_USER_EMAIL=you@example.com
SINGLE_USER_PASSWORD=your-strong-password
# Global API key (fallback for all projects)
SENDSTACK_SERVER_KEY=your-global-api-key
# Cron job authorization
CRON_SECRET=your-cron-secret
# Public URL of your instance
NEXT_PUBLIC_APP_URL=https://your-domain.comSTRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_SUCCESS_URL=https://your-domain.com/billing?success=1
STRIPE_CANCEL_URL=https://your-domain.com/billing?canceled=1# Shared secret for all providers (fallback)
PROVIDER_WEBHOOK_SHARED_SECRET=your-shared-secret
# SendGrid-specific
SENDGRID_WEBHOOK_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----...
# Mailgun-specific
MAILGUN_WEBHOOK_SIGNING_KEY=your-mailgun-key
# Resend-specific
RESEND_WEBHOOK_SIGNING_SECRET=whsec_your-resend-secret# HMAC-SHA256 key for signing forwarded webhooks (min 32 chars)
# Falls back to ENCRYPTION_KEY if not set
WEBHOOK_SIGNING_SECRET=your-64-char-hex-stringGenerating Secrets
# 32-byte hex encryption key
openssl rand -hex 32
# Session secret
openssl rand -base64 32
# Server key
openssl rand -base64 24 | tr '+/' '-_' | sed 's/=//g' | sed 's/^/sk_/'
# Cron secret
openssl rand -base64 16Deploying
# 1. Clone the repository
git clone https://github.com/apwn/sendstack.git
cd sendstack
# 2. Install dependencies
npm install
# 3. Set up your .env file with all required variables
# 4. Push the database schema
npx prisma db push
# 5. Deploy to Vercel
vercel --prodThe vercel.json file includes a cron configuration that runs the daily aggregation job at 4 AM UTC automatically.
API Reference
Complete list of all API endpoints.
Authentication
/api/auth/loginSign in with email and password/api/auth/logoutClear session cookieSend Email
/api/sendSend a single email (inline or template)/api/send/batchSend to up to 100 recipients in one callTemplates
/api/templates?projectId=...List templates for a project/api/templatesCreate a new template/api/templates/:idGet a template by ID/api/templates/:idUpdate a template/api/templates/:idDelete a templateSuppressions
/api/suppressions?projectId=...&search=...&page=1List suppressions (paginated, searchable)/api/suppressionsManually suppress an email address/api/suppressions/:idRemove a suppressionProjects
/api/projectsList all projects/api/projectsCreate a new project/api/projects/:id/rotate-keyRotate a project's server key/api/projects/:id/ship-with-aiGet AI integration promptsDomains
/api/domains?projectId=...List domains (optionally filtered by project)/api/domainsAdd a domain with encrypted credentials/api/domains/:id/verifyVerify DNS records and compute health score/api/domains/:id/test-sendSend a test email through this domainWebhooks
/api/webhooks/sesReceive SES webhook events (via SNS)/api/webhooks/sendgridReceive SendGrid webhook events/api/webhooks/mailgunReceive Mailgun webhook events/api/webhooks/resendReceive Resend webhook events (via Svix)Billing
/api/billing/tiersGet pricing tiers and active wave/api/billing/checkoutCreate a Stripe checkout session/api/billing/webhookStripe webhook handlerOther
/api/healthHealth check with database ping/api/dashboard/overviewDashboard metrics (7d/30d)Common Headers
| Header | Purpose | Used By |
|---|---|---|
x-sendstack-server-key | API authentication | /api/send, /api/send/batch |
x-request-id | Request tracing (optional) | All endpoints |
Retry-After | Seconds until rate limit resets | 429 responses |