An email API that's easy to build on
and safe to test.
One REST API with SDKs in five languages, signed webhooks, test keys that deliver nothing, and simulator addresses for every bounce and complaint your code needs to handle.
The calls a real app needs. Everything below works the same from the REST API, every SDK and the CLI.
Batch send
Up to 100 emails in one POST /emails/batch call. Results are per email, so one bad item doesn't fail the rest.
Scheduled send
Set scheduled_at on a send. Reschedule or cancel it with PATCH /emails/{id} before it goes.
Idempotency keys
Send an Idempotency-Key header and retries never send twice. Keys are kept for 24 hours.
Attachments and inline images
Up to 10 files of 10 MB each, 20 MB per email. Give an attachment a content_id and reference it as cid: in your HTML.
Templates by alias
Send a published template by its alias with variables, from any SDK. Templates.
Errors with a fix
Every error has a stable code, a hint and a docs link, so your code and your agent know what to do next.
curl -X POST https://api.groundmail.dev/emails/batch \
-H "Authorization: Bearer $GROUNDMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
{ "from": "receipts@yourdomain.com", "to": "ada@example.com",
"subject": "Your receipt", "html": "<p>Thanks, Ada.</p>" },
{ "from": "receipts@yourdomain.com", "to": "lin@example.com",
"subject": "Your receipt", "html": "<p>Thanks, Lin.</p>" }
]
}'# Send tomorrow morning, retry-safe
curl -X POST https://api.groundmail.dev/emails \
-H "Authorization: Bearer $GROUNDMAIL_API_KEY" \
-H "Idempotency-Key: trial-ending-8841" \
-H "Content-Type: application/json" \
-d '{
"from": "billing@yourdomain.com",
"to": "ada@example.com",
"subject": "Your trial ends tomorrow",
"html": "<p>…</p>",
"scheduled_at": "2026-11-02T09:00:00Z"
}'
# Changed your mind? Cancel it.
curl -X PATCH https://api.groundmail.dev/emails/msg_abc123 \
-H "Authorization: Bearer $GROUNDMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "cancel": true }'Test keys and simulator addresses.
A test key (gm_test_…) runs the whole pipeline, fires the same webhooks a real delivery would, and delivers nothing. Test sends don't count toward your plan, the sending ramp, your stats or your bill.
Send to these addresses at simulator.groundmail.dev for a specific outcome, with real webhook payloads and signatures. Plus addressing works, so bounce+signup-42@ tells your tests apart.
- delivered@
- Delivered
- bounce@
- Hard bounce: the mailbox doesn't exist
- block@
- Block bounce: the receiving server refused the sender
- complaint@
- Delivered, then marked as spam
- suppressed@
- Treated as a suppressed recipient
- delayed@
- The receiving server deferred delivery
import { Groundmail, simulatorAddress } from 'groundmail'
// A gm_test_ key runs the whole pipeline but delivers nothing
const groundmail = new Groundmail(process.env.GROUNDMAIL_TEST_KEY!)
await groundmail.emails.send({
from: 'you@yourdomain.com',
to: simulatorAddress('bounce', 'signup-42'),
subject: 'Bounce handling test',
html: '<p>Simulated.</p>',
})
// → your webhook gets email.bounced with "test": trueDetails in the testing docs.
Signed events for everything that happens.
Subscribe an endpoint to the events you care about. Every event is signed with your endpoint's secret, and the Node SDK checks the signature for you. Fire a test event at any endpoint with POST /emails/test.
- email.sent
- email.delivered
- email.delivery_delayed
- email.held
- email.bounced
- email.complained
- email.opened
- email.clicked
- email.failed
- email.suppressed
- email.scheduled
- email.received
- contact.unsubscribed
- automation.started
- automation.step_completed
- automation.completed
- automation.failed
Keys you can scope, data you can take with you.
Scoped API keys
Scopes from full down to emails:send, emails:read, domains:write, webhooks:write, templates:write and more, with Send only and Read only presets.
Domain-restricted keys
Limit a key to the domains it may send from, so a leaked key can't send as your other brands.
CSV export
Export sent emails, received emails and suppressions from the dashboard, up to 100,000 rows per export.
Official clients for five languages.
Each wraps the same REST API, so anything an SDK doesn't cover is one HTTP call away.
- Node.js / TypeScript
npm install groundmail- Python
pip install groundmail- Go
go get github.com/Groundmail/groundmail-sdks/go- PHP
composer require groundmail/groundmail- Laravel
composer require groundmail/laravel- Symfony Mailer
composer require groundmail/symfony-mailer- Ruby
gem install groundmail
Drop-in packages on npm.
For frameworks with their own mail or notification layer, a package saves the setup.
- Better Auth
groundmail-better-auth- Auth.js
groundmail-authjs- Clerk
groundmail-clerk- Payload CMS
groundmail-payload- Nodemailer
groundmail-nodemailer- NestJS
nestjs-groundmail- AdonisJS
adonisjs-groundmail- Strapi
strapi-provider-email-groundmail- Medusa
medusa-notification-groundmail- Vendure
vendure-plugin-groundmail- n8n
n8n-nodes-groundmail
More in integrations.
CLI, MCP and OpenAPI.
CLI
npm install -g groundmail-cli. Send email and manage domains, keys, webhooks, templates and tenants from your terminal, CI or a coding agent.
MCP server
Hosted at mcp.groundmail.dev/mcp with OAuth, or npx -y groundmail-mcp locally. MCP & AI agents.
OpenAPI
The whole API as an OpenAPI spec, plus an interactive API explorer.
Questions
How do I test without sending real email?
Create a test key (it starts with gm_test_). The API works exactly as with a live key, including templates, idempotency, scheduling, batch and webhooks, but nothing is delivered and nothing counts toward your limits or bill. To see a bounce or complaint, send to an address at simulator.groundmail.dev; those work with live keys too.
How many emails can one batch request send?
Up to 100 per call to POST /emails/batch. Each email gets its own result, so one bad item doesn't fail the rest.
How long is an idempotency key remembered?
24 hours, per account. Sending the same key with the same body returns the original result; the same key with a different body returns 409 idempotency_conflict.
Can I limit what an API key can do?
Yes. Pick scopes such as emails:send, emails:read, domains:write or webhooks:write, or the Send only and Read only presets, and restrict a key to the domains it may send from.
Which languages have an SDK?
Node.js and TypeScript, Python, Go, PHP (with Laravel and Symfony Mailer packages) and Ruby. They all wrap the same REST API, so any other language can send over HTTP or SMTP.
Put your email
on solid ground.
Start free on our transactional pool. Move to dedicated IPs or a dedicated server when you need the isolation. Same API either way.