Quickstart
From nothing to a delivered email. About ten minutes, most of it waiting for DNS.
1. Create an API key
In the dashboard, API keys → Create. Pick Sending unless the key needs to manage domains and contacts too — a key that lives in your application should only be able to send.
The key is shown once. It is not recoverable afterwards; there is no copy of it to recover.
export MAILSTEIN_API_KEY="ms_..."
2. Add your domain
You can only send from a domain you have proved you control. Add it in Domains → Add domain, or over the API:
curl -X POST https://app.mailstein.com/api/v1/domains \
-H "Authorization: Bearer $MAILSTEIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "yourdomain.com", "region": "us-east-1"}'
The response contains the DNS records to publish. Publish all of them. See Domains for what each one does and which are optional.
Then verify:
curl -X PUT https://app.mailstein.com/api/v1/domains/1/verify \
-H "Authorization: Bearer $MAILSTEIN_API_KEY"
DNS takes minutes to hours to propagate. Until the domain reads verified, sending from it will be refused — which is better than the alternative, where the mail is accepted and then silently discarded by the recipient.
3. Send
TypeScript
import { Mailstein } from "mailstein";
const mailstein = new Mailstein(process.env.MAILSTEIN_API_KEY);
const { data, error } = await mailstein.emails.send({
from: "hello@yourdomain.com",
to: "someone@example.com",
subject: "Hello",
html: "<p>It works.</p>",
});
if (error) throw new Error(error.message);
console.log(data.emailId);
Python
import os
from mailstein import Mailstein
mailstein = Mailstein(os.environ["MAILSTEIN_API_KEY"])
data, error = mailstein.emails.send({
"from": "hello@yourdomain.com",
"to": "someone@example.com",
"subject": "Hello",
"html": "<p>It works.</p>",
})
cURL
curl -X POST https://app.mailstein.com/api/v1/emails \
-H "Authorization: Bearer $MAILSTEIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"hello@yourdomain.com","to":"someone@example.com","subject":"Hello","html":"<p>It works.</p>"}'
CLI
npx -y mailstein-cli emails send \
--from hello@yourdomain.com \
--to someone@example.com \
--subject Hello \
--html "<p>It works.</p>"
Sending later
scheduledAt takes ISO 8601 or plain English:
{ "scheduledAt": "2026-08-11T09:00:00-04:00" }
{ "scheduledAt": "tomorrow 9am" }
Plain English is resolved in the server's timezone, so use ISO 8601 with an offset whenever the exact time matters.
Tag a message on the way out and find it again later:
{ "tags": ["welcome", "onboarding"] }
Then GET /v1/emails?tag=welcome. Repeat the parameter to require several.
4. Find out what happened
The send returns an emailId immediately — that is the message being accepted
for delivery, not delivered. Delivery happens afterwards, and can fail
afterwards too.
curl https://app.mailstein.com/api/v1/emails/$EMAIL_ID \
-H "Authorization: Bearer $MAILSTEIN_API_KEY"
Polling is fine while you are getting started. In production, use webhooks — they tell you about bounces and complaints, which are the two things you actually need to react to.
What to do next
- Read suppressions. Sending to an address that hard-bounced is the fastest way to damage a sending reputation, and mailstein refuses to do it for you.
- Turn on tracking if you want opens and clicks, and read what it costs you first.
- Set up idempotency keys before your first retry, not after.