SendStackDocumentation

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.

Unified Send API
One endpoint, any provider. Switch providers without changing integration code.
Multi-Domain
Route different domains through different providers, all from one dashboard.
Health Monitoring
DNS verification, bounce tracking, and per-domain health scores.
AI Integration
Generate ready-to-paste prompts for Lovable, Claude Code, Cursor, and more.

Quick Start

Get sending in under 5 minutes:

1
Create a project

Go to Projects in the dashboard and create a new project. You'll receive a unique server key (starts with sk_).

2
Add a domain

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.

3
Verify your domain

Click Verify on your domain card. SendStack checks your SPF, DKIM, and DMARC records and computes a health score.

4
Send your first email

Use the test send button, or call the API directly:

cURL
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>"
  }'
Response
{
  "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 TypeScopeSource
Per-project keyCan only send through domains in that projectGenerated when creating a project (sk_...)
Global keyCan send through any project's domainsSENDSTACK_SERVER_KEY env var
Recommendation: Use per-project keys whenever possible. The global key is a fallback for backward compatibility.

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.

API
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.
Warning: Rotating a key is irreversible. Any service using the old key will immediately lose access. Update your environment variables before rotating.
Rotate Key API
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

ProviderRequired CredentialsBest For
Amazon SESregion, accessKeyId, secretAccessKey, fromEmail, dkimSelectorHigh volume, lowest cost per email
SendGridapiKey, fromEmailSimple setup, generous free tier
MailgunapiKey, sendingDomain, fromEmailDeveloper-focused, strong deliverability
ResendapiKey, fromEmailModern 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:

CheckWhat It Looks ForDNS Record
SPFv=spf1 in TXT recordsexample.com TXT
DKIMv=dkim1 in TXT or CNAMEselector._domainkey.example.com
DMARCv=dmarc1 in TXT records_dmarc.example.com
WebhookHTTP 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 AmazonSESFullAccess policy attached.
  • Generate an Access Key for the user. Save the Access Key ID and Secret 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 IdentitiesCreate 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 ConsoleAccount DashboardRequest 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

SES Credentials
{
  "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.
DNS records for SES: Add 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 DashboardSettingsAPI 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 SettingsSender AuthenticationAuthenticate 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

SendGrid Credentials
{
  "apiKey": "SG.xxxxx...",
  "fromEmail": "noreply@mail.example.com"
}

4. Set Up Webhooks

  • Go to SettingsMail SettingsEvent 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_KEY environment 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 SendingDomainsAdd 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

Mailgun Credentials
{
  "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 SendingWebhooks → 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_KEY environment 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

Resend Credentials
{
  "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 your RESEND_WEBHOOK_SIGNING_SECRET environment variable.
Resend uses Svix for webhook delivery. Signatures are verified using the 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 domain field 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.
Send via SES domain
{
  "project_id": "proj_123",
  "domain": "mail.example.com",
  "to": "user@example.com",
  "subject": "Hello",
  "html": "<p>Sent via SES</p>"
}
Send via Resend domain (same project)
{
  "project_id": "proj_123",
  "domain": "notifications.example.com",
  "to": "user@example.com",
  "subject": "Hello",
  "html": "<p>Sent via Resend</p>"
}

Provider Comparison

FeatureAmazon SESSendGridMailgunResend
Pricing~$0.10/1k emailsFree up to 100/dayFree up to 100/dayFree up to 100/day
Setup complexityMedium (IAM + SES console)Low (API key + domain auth)Low (API key + DNS)Low (API key + DNS)
Sandbox modeYes (requires production request)NoNoNo
Webhook setupSNS Topics + SubscriptionsEvent Webhook settingsPer-domain webhook configDashboard webhook config
Webhook signingSNS certificate (RSA-SHA1)ECDSA public keyHMAC-SHA256Svix HMAC-SHA256
Credentials needed5 fields2 fields3 fields2 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.

Inline Content
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" }
}
Template Mode
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

FieldTypeRequiredDescription
project_idstringYesYour project ID
domainstringYesThe sending domain (must be configured)
tostringYesRecipient email address
subjectstringConditionalEmail subject line (required if no template_id)
htmlstringConditionalHTML body (required if no template_id)
textstringNoPlain text fallback
template_idstringNoID of a saved template (replaces subject + html)
variablesobjectNoKey-value pairs for template {{variable}} interpolation
from_namestringNoDisplay name for the sender (max 200 chars)
reply_tostringNoReply-to email address
metadataobjectNoCustom key-value data stored with the message

Response

Success (200)
{
  "message_id": "provider-generated-id",
  "provider": "SES",
  "accepted": true,
  "request_id": "uuid-for-tracing"
}
StatusMeaning
200Email accepted by provider
400Invalid request body or missing template variables
401Missing or invalid server key
403Key doesn't match the specified project
404Domain, credentials, or template not found
422Recipient is suppressed (bounce, complaint, or manual)
429Rate limit exceeded
500Internal server error

Code Examples

Node.js (fetch)
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();
}
Python (requests)
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

Rate limit check (60/min per IP)
Validate server key (global or per-project)
Enforce project isolation
Check suppression list (bounce / complaint / manual)
Resolve template + interpolate variables (if template_id)
Look up domain & decrypt credentials
Route to provider (SES / SendGrid / Mailgun / Resend)
Log message in database
Return message ID + status

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
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.

Missing variables: If any {{placeholder}} in the template has no matching key in the variables object, the send will fail with a 400 error listing the missing variables.
Example
// 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

ActionAPI
List templatesGET /api/templates?projectId=...
Get templateGET /api/templates/:id
Update templatePATCH /api/templates/:id
Delete templateDELETE /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
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

FieldTypeDescription
project_idstringYour project ID
domainstringSending domain
subjectstringDefault subject (overridable per recipient)
htmlstringDefault HTML body (overridable per recipient)
textstringDefault plain text
template_idstringTemplate ID (used if no inline subject/html)
from_namestringDefault sender display name
reply_tostringDefault reply-to address
recipientsarray1–100 recipient objects

Each recipient object can override subject, html, text, from_name, reply_to, and provide unique variables and metadata.

Batch Response

200 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.

ReasonTrigger
BOUNCEHard bounce event from provider
COMPLAINTSpam complaint from recipient
MANUALAdded manually via dashboard or API

Managing Suppressions

Use the Suppressions page in the dashboard, or the API:

Add suppression
POST /api/suppressions
Cookie: sendstack_session=...

{ "projectId": "clxyz...", "email": "user@example.com", "reason": "MANUAL" }
Remove suppression
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/resend

Event Types

EventDescriptionSESSendGridMailgunResend
SENTProvider accepted the emailSendprocessedacceptedemail.sent
DELIVEREDConfirmed delivery to inboxDeliverydelivereddeliveredemail.delivered
BOUNCEDHard or soft bounceBouncebouncefailedemail.bounced
COMPLAINTSpam complaintComplaintspamreportcomplainedemail.complained
OPENEDRecipient opened the emailOpenopenopenedemail.opened
CLICKEDRecipient clicked a linkClickclickclickedemail.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-signature header (requires SENDGRID_WEBHOOK_PUBLIC_KEY env var)
  • Mailgun: HMAC-SHA256 signature verification (requires MAILGUN_WEBHOOK_SIGNING_KEY env var)
  • Resend: Svix HMAC-SHA256 signature verification via svix-signature header (requires RESEND_WEBHOOK_SIGNING_SECRET env 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:

HeaderDescription
X-SendStack-EventEvent type (e.g. DELIVERED, BOUNCED)
X-SendStack-Delivery-IdUnique ID for this delivery attempt
X-SendStack-TimestampUnix timestamp of the delivery
X-SendStack-SignatureHMAC-SHA256 signature for verification

Outbound Signature Verification

Verify the authenticity of forwarded webhooks using HMAC-SHA256:

Node.js Verification
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 .env file
  • Code snippet — a complete server-side send function
  • AI tool prompts — tailored instructions for each tool:
ToolPrompt Style
LovableFull file contents ready to paste into the editor
Claude CodeTerminal commands, file tree, and smoke test curl
CursorConcise patch-style instructions with verification checklist
Raw SDKMinimal Node.js fetch wrapper
Security: All generated code uses server-side calls only. Never expose your server key in browser/client code.

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
Security: The playground uses a server-side proxy route. Your API key is never exposed to the browser — it's injected server-side by an authenticated internal endpoint.

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

Formula
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
ScoreStatusMeaning
80–100HEALTHYGood standing with all providers
50–79WARNINGSome issues need attention
0–49CRITICALDeliverability 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:

EndpointLimitWindowMax Emails
POST /api/send60 requests1 minute60
POST /api/send/batch10 requests1 minute1,000
POST /api/auth/login10 attempts1 minute

When you hit the limit, you'll receive:

429 Response
{
  "error": "Too many requests",
  "retry_after": 42
}

Header: Retry-After: 42

Wait 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:

WavePriceSeats
Wave 1 (Early Adopter)$79First 50
Wave 2$99Next 100
Wave 3$129Next 150
Wave 4 (Final Price)$149Unlimited

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

Required
# 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.com
Optional — Stripe Billing
STRIPE_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
Optional — Webhook Verification
# 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
Optional — Outbound Webhook Signing
# 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-string

Generating Secrets

Terminal
# 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 16

Deploying

Steps
# 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 --prod

The 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

POST
/api/auth/loginSign in with email and password
POST
/api/auth/logoutClear session cookie

Send Email

POST
/api/sendSend a single email (inline or template)
POST
/api/send/batchSend to up to 100 recipients in one call

Templates

GET
/api/templates?projectId=...List templates for a project
POST
/api/templatesCreate a new template
GET
/api/templates/:idGet a template by ID
PATCH
/api/templates/:idUpdate a template
DELETE
/api/templates/:idDelete a template

Suppressions

GET
/api/suppressions?projectId=...&search=...&page=1List suppressions (paginated, searchable)
POST
/api/suppressionsManually suppress an email address
DELETE
/api/suppressions/:idRemove a suppression

Projects

GET
/api/projectsList all projects
POST
/api/projectsCreate a new project
POST
/api/projects/:id/rotate-keyRotate a project's server key
GET
/api/projects/:id/ship-with-aiGet AI integration prompts

Domains

GET
/api/domains?projectId=...List domains (optionally filtered by project)
POST
/api/domainsAdd a domain with encrypted credentials
POST
/api/domains/:id/verifyVerify DNS records and compute health score
POST
/api/domains/:id/test-sendSend a test email through this domain

Webhooks

POST
/api/webhooks/sesReceive SES webhook events (via SNS)
POST
/api/webhooks/sendgridReceive SendGrid webhook events
POST
/api/webhooks/mailgunReceive Mailgun webhook events
POST
/api/webhooks/resendReceive Resend webhook events (via Svix)

Billing

GET
/api/billing/tiersGet pricing tiers and active wave
POST
/api/billing/checkoutCreate a Stripe checkout session
POST
/api/billing/webhookStripe webhook handler

Other

GET
/api/healthHealth check with database ping
GET
/api/dashboard/overviewDashboard metrics (7d/30d)

Common Headers

HeaderPurposeUsed By
x-sendstack-server-keyAPI authentication/api/send, /api/send/batch
x-request-idRequest tracing (optional)All endpoints
Retry-AfterSeconds until rate limit resets429 responses