A sender is a saved From: the name people see, an address on one of your verified domains, an optional reply-to address, and a short handle. Send with sender: "billing" instead of writing the address in every call, give each template the sender it goes out as, and pick a sender on each automation email. Change a sender once and every automation email and template using it picks up the change.
In the dashboard: Domains › Senders.
Base URL: https://api.groundmail.dev
Auth:
Reading senders needs the domains:read scope; creating, changing and deleting them needs domains:write.
Authorization: Bearer gm_...Send as a sender
Pass sender (a handle or id) on POST /emails or each item of a batch:
The email goes out as the sender's name and address, and replies go to its reply-to address unless the request sets reply_to.
Who an email is from, in order:
from(andreply_to) on the request.senderon the request.- The template's sender, when you send a template that has one.
- Your default sender.
So with a default sender, or a sender on the template, a send can leave out from entirely. A from you pass always wins. A sender your team doesn't have returns 422 with a validation_error.
await groundmail.emails.send({
sender: 'billing',
to: user.email,
template: { id: 'receipt', variables: { amount: '$20.00' } },
})The sender object
| Field | Notes |
|---|---|
name |
The name people see in their inbox. Up to 120 characters |
address |
The address it sends from. Must be on one of your verified domains |
from |
Name and address as the inbox shows them |
reply_to |
Where replies go. null means replies go to address |
handle |
What sends use (sender: "billing"). Lowercase letters, digits and hyphens, unique on your team |
is_default |
Sends with no from, sender or template sender go out as the default, and new automations start with it. At most one per team |
used_by |
How many automations and templates send as it |
{
"id": "4f0c7e2a-6b1d-4c55-9a3e-2d8f6a1b9c10",
"name": "Acme Billing",
"address": "billing@mail.acme.com",
"from": "Acme Billing <billing@mail.acme.com>",
"reply_to": "billing@acme.com",
"handle": "billing",
"is_default": false,
"used_by": { "automations": 2, "templates": 3 },
"created_at": "2026-11-02T10:00:00.000Z",
"updated_at": "2026-11-02T10:00:00.000Z"
}List senders
/sendersThe default sender comes first.
curl https://api.groundmail.dev/senders \
-H "Authorization: Bearer $GROUNDMAIL_API_KEY"{ "data": [ { "id": "…", "handle": "default", "is_default": true, "...": "…" } ] }Add a sender
/senders| Field | Required | Notes |
|---|---|---|
name |
Yes | The name people see |
address |
Yes | On one of your verified domains. Add and verify a domain first to use a new one |
reply_to |
No | Leave it out to get replies at address. Use an inbox someone reads |
handle |
No | Made from name when left out (Acme Billing → acme-billing) |
is_default |
No | Make it the default. Your first sender becomes the default unless this is false; making another sender the default takes it from the previous one |
Returns 201 with { "sender": { … } }. A handle another sender already uses returns 409 conflict; an address on a domain that isn't verified returns 400 validation_error.
curl -X POST https://api.groundmail.dev/senders \
-H "Authorization: Bearer $GROUNDMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Billing", "address": "billing@mail.acme.com", "reply_to": "billing@acme.com", "handle": "billing" }'Retrieve, update, delete
GET /senders/{id}: id or handle. Returns{ "sender": { … } }.PATCH /senders/{id}: any ofname,address,reply_to(nullsends replies toaddress),handle,is_default. Automation emails using the sender pick up the change on their next send. Renaminghandleupdates the automation emails that use it; code that sends with the old handle needs the new one.DELETE /senders/{id}: returns{ "ok": true }.
A sender that an automation email or a template uses can't be deleted. The 409 names them, and details lists them, so you can pick another sender on each first:
Deleting the default sender is allowed: sends with no from then need one again.
{
"error": {
"message": "Sender \"billing\" is used by 1 automation (Receipts) and 1 template (Receipt). Pick another sender on them, then delete it.",
"code": "conflict",
"details": { "automations": ["Receipts"], "templates": ["Receipt"] }
}
}Templates and automations
- Templates: set
sender_id(a sender id or handle) on create or update, or pick Sends as in the template editor. Sends of that template withoutfromorsendergo out as it, and so does the editor's Send a test. - Automations: an email step can carry
email.sender(a handle) instead ofemail.from. It's looked up each time the step sends. See Who an email step is from.
SDK
import { Groundmail } from 'groundmail'
const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
await groundmail.senders.create({
name: 'Acme Billing',
address: 'billing@mail.acme.com',
reply_to: 'billing@acme.com',
handle: 'billing',
})
const { data } = await groundmail.senders.list()
await groundmail.senders.update('billing', { reply_to: 'accounts@acme.com' })
await groundmail.senders.remove('billing')CLI
groundmail senders list
groundmail senders create --name "Acme Billing" --address billing@mail.acme.com --reply-to billing@acme.com --handle billing
groundmail senders get billing
groundmail senders update billing --default
groundmail senders delete billing --yes
groundmail emails send --sender billing --to user@example.com --subject "Your receipt" --html "<p>Thanks!</p>"
groundmail templates update --id receipt --sender billingOpenAPI
Full schemas: openapi.yaml (Senders tag).