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

Something here wrong or missing? It is generated from the running API — tell us and we will fix the source.