Groundmail

Features

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.

Sending

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

Batch send

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>" }
    ]
  }'

Schedule, then cancel

# 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 mode

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

Test a bounce

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": true

Details in the testing docs.

Webhooks

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

Access and data

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.

SDKs

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

Framework packages

Drop-in packages on npm

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
n8n
n8n-nodes-groundmail

More in integrations.

Tooling

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.

FAQ

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