Transactional email for indie hackers

A side project sends a few hundred emails a month, so the only pricing question that matters is what happens below the point where an enterprise plan makes sense. The free tier covers 3,000 emails a month and 100 a day with no credit card, and Pro is $20 a month for 50,000. There is no contract, no minimum commitment, and no call with anyone.

01

Sign up and take a key.

https://emails.sh/dashboard. The key starts with esh_ and works immediately, sending from onboarding@emails.sh.

02

Verify the domain you already own.

Two DNS records at your registrar. Once verified, mail comes from you, which is the difference between landing in the inbox and landing in spam.

03

Install the SDK and write one send function.

`npm install @emails.sh/sdk` or `pip install emailssh`. One module reads EMAILSSH_API_KEY, everything else imports it.

04

Tag your sends so the logs stay useful.

tags: { type: "receipt" } costs nothing now and saves an hour when a customer says a receipt never arrived.

The four emails a small product actually sends

src/email.ts, the whole email layer of a one-person SaaS.

The four emails a small product actually sends
import { Emailssh } from '@emails.sh/sdk';

const emails = new Emailssh(process.env.EMAILSSH_API_KEY!);

const FROM = 'Acme <hello@acme.com>';

// Every send is tagged. Tags are what make the delivery log searchable when a
// customer emails you saying they never got something.
export const sendVerification = (to: string, link: string) =>
	emails.send({
		from: FROM,
		to: [to],
		subject: 'Confirm your email',
		html: `<p><a href="${link}">Confirm your email</a></p>`,
		text: `Confirm your email: ${link}`,
		tags: { type: 'verification' }
	});

export const sendPasswordReset = (to: string, link: string) =>
	emails.send({
		from: FROM,
		to: [to],
		subject: 'Reset your password',
		html: `<p><a href="${link}">Reset your password</a></p><p>Expires in an hour.</p>`,
		text: `Reset your password (expires in an hour): ${link}`,
		tags: { type: 'password-reset' }
	});

export const sendReceipt = (to: string, orderId: string, amount: string) =>
	emails.send({
		from: FROM,
		to: [to],
		replyTo: 'support@acme.com',
		subject: `Receipt for order ${orderId}`,
		html: `<p>Order ${orderId}, ${amount}. Thanks.</p>`,
		text: `Order ${orderId}, ${amount}. Thanks.`,
		tags: { type: 'receipt' },
		idempotencyKey: `receipt:${orderId}`
	});

export const sendTrialEnding = (to: string, days: number) =>
	emails.send({
		from: FROM,
		to: [to],
		subject: `Your trial ends in ${days} days`,
		html: `<p>Your trial ends in ${days} days. Add a card to keep your data.</p>`,
		text: `Your trial ends in ${days} days. Add a card to keep your data.`,
		tags: { type: 'trial-ending' }
	});

Worth knowing

01

Free-tier limits are per day as well as per month

3,000 a month, 100 a day. A launch that sends 400 verification emails in one evening hits the daily limit before the monthly one, which is worth knowing the day before you post.

02

Transactional and marketing should not share a domain

Send campaigns from news.acme.com and receipts from acme.com. One bad campaign then cannot take your password resets down with it.

03

Set idempotencyKey on anything triggered by a webhook

Payment providers retry. Without the key, one retried webhook is two receipts and a confused customer.

04

You can leave without an export ticket

The API is one endpoint and your templates are your own files. Migrating away is changing a base URL and a key, which is the honest reason to try it.

What arrives

One call to POST /v1/emails, and this is the message. The delivery result for it is on GET /v1/emails/:id a second later.

Sent
To:      buyer@example.com
Subject: Receipt for order 3391

Order 3391, $12.00. Thanks.

Questions

What happens if I go over the free tier?

Sends are rejected with an error that says so rather than being silently dropped. Upgrading to Pro at $20 a month takes it to 50,000.

Is there a sales call?

No. Sign up, take a key, send. The enterprise plan is the only one with a human in the loop.

Can I send from more than one product?

Yes. Verify each domain and use a separate API key per product, so revoking one does not affect the other.

Do you charge for delivery logs?

No. GET /v1/emails/:id and the dashboard history are part of every plan including free.

What about a newsletter?

/v1/audiences covers a small contact list. If sending campaigns is the main job, a dedicated marketing tool will serve you better.