Transactional email for agencies
An agency ships twenty projects a year and inherits the support burden for each one. The setup that survives that is one sending domain per client, one API key per project, and message ids stored in the client application, so a question about a missing email is answered without rebuilding anyone context.
Add each client domain separately.
mail.clientone.com, mail.clienttwo.com. Verified independently, with reputation tracked per domain, so one client sending badly never affects another.
Issue one API key per project.
Never share a key between two clients. Revoking a key at handover or after a contractor leaves then affects exactly one project.
Tag every send with the client.
tags: { client: "clientone", type: "receipt" } gives you one filter for everything a single client sent, which is what an invoice or an incident review needs.
Hand over by rotating, not migrating.
The client creates their own workspace, verifies their domain there, and you swap EMAILSSH_API_KEY in their environment. The application code does not change.
One client configuration, repeated per project
src/email/client.ts, where each deployment carries its own key and domain.
import { Emailssh } from '@emails.sh/sdk';
// Both values come from the environment, so the same code deploys for every
// client and nothing client-specific is committed to the repository.
const apiKey = process.env.EMAILSSH_API_KEY;
const fromAddress = process.env.EMAIL_FROM; // e.g. "Client One <hello@mail.clientone.com>"
if (!apiKey) throw new Error('EMAILSSH_API_KEY is not set');
if (!fromAddress) throw new Error('EMAIL_FROM is not set');
const emails = new Emailssh(apiKey);
const client = process.env.CLIENT_SLUG ?? 'unknown';
export async function sendOrderConfirmation(to: string, orderId: string, total: string) {
const { id } = await emails.send({
from: fromAddress,
to: [to],
subject: `Order ${orderId} confirmed`,
html: `<p>Your order ${orderId} is confirmed. Total ${total}.</p>`,
text: `Your order ${orderId} is confirmed. Total ${total}.`,
// The client tag is what makes the log answer per-client questions.
tags: { client, type: 'order-confirmation' },
idempotencyKey: `order:${orderId}`
});
return id;
}Worth knowing
Never send client mail from your agency domain
It puts your reputation behind their volume and makes handover a migration instead of a key swap. Verify a subdomain of theirs and send from it.
DNS access is the item that delays every project
Ask for it in week one. Two records, and the client often needs to find who controls their registrar, which takes longer than the integration does.
A shared key is a shared incident
One key across five clients means one leak revokes all five. One key per project keeps the blast radius at one.
Write the message id into the client application
When a client asks about an email from four months ago, an id in their orders table beats searching by recipient and hoping.
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.
To: shopper@example.com
Subject: Order 88214 confirmed
Your order 88214 is confirmed. Total $64.50.Questions
Can one workspace hold many client domains?
Yes. Add each domain, verify each separately, and use a key per project. Reputation is tracked per sending domain.
How do we hand a project over cleanly?
The client verifies the domain in their own workspace, you swap the value of EMAILSSH_API_KEY in their environment, and you revoke the old key. No code change.
Can we bill clients for their own usage?
Tag every send with the client slug and read the totals per tag. If a client wants their own bill, give them their own workspace from the start.
What if a client already uses SendGrid?
Verify their domain here, move one email type across, compare delivery for a week, then move the rest. Both can run at once during the switch.