SMTP relay

Point any app that already speaks SMTP at emails.sh: swap the host, the port, and the credentials.

Point any app that already speaks SMTP at emails.sh. Nothing in your code changes: you swap the host, the port, and the credentials in your mailer config, and the mail goes out through emails.sh with your verified domain, your delivery log, and your suppression list.

This is the fastest migration path off Postmark, SendGrid, Mailgun, SES, or an old Gmail relay. If your app is Laravel, Rails, Django, WordPress, or anything using PHPMailer or Nodemailer, this is four lines of config.

Settings

SettingValue
Hostsmtp.emails.sh
Port587 with STARTTLS, recommended. 465 with implicit TLS where it is available.
Usernameemailssh, the literal string
Passwordyour API key, the string starting esh_
EncryptionRequired. TLS on 465, STARTTLS on 587.
AuthenticationPLAIN or LOGIN
Max message size40 MB
Max recipients per transaction50
Simultaneous connections10 per source address

The username is always the literal string emailssh. It is not your email address and not your workspace name. Your API key is the password, so there is no second credential to create: make a key at https://emails.sh/dashboard/api-keys and paste it in.

Use port 587 unless something in your stack blocks it. Some hosts, a few PaaS providers and most residential ISPs, block 587 but allow 465. Port 465 only listens where TLS material is configured, so if a connection to it is refused, use 587.

The From address must be on a domain you have verified, exactly as with the API. While you are still testing, send from onboarding@emails.sh.

Nodemailer

Node
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: 'smtp.emails.sh',
  port: 587,
  secure: false, // STARTTLS is negotiated on 587; true only for port 465
  auth: {
    user: 'emailssh',
    pass: process.env.EMAILSSH_API_KEY
  }
});

await transporter.sendMail({
  from: 'Acme <hello@acme.com>',
  to: 'someone@example.com',
  subject: 'Welcome to Acme',
  text: 'Thanks for signing up.',
  html: '<p>Thanks for signing up.</p>'
});

If you would rather not run SMTP at all from Node, npm install @emails.sh/sdk and call mail.send over HTTPS instead. SMTP exists here for the apps that cannot change. See /docs/node.

Django

settings.py
import os

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.emails.sh'
EMAIL_PORT = 587
EMAIL_USE_TLS = True          # STARTTLS on 587
EMAIL_HOST_USER = 'emailssh'
EMAIL_HOST_PASSWORD = os.environ['EMAILSSH_API_KEY']
DEFAULT_FROM_EMAIL = 'Acme <hello@acme.com>'

For port 465 use EMAIL_USE_SSL = True with EMAIL_PORT = 465, and leave EMAIL_USE_TLS unset. Django refuses to start if both are true. Sending is then the ordinary Django call.

An ordinary send
from django.core.mail import send_mail

send_mail(
    subject='Welcome to Acme',
    message='Thanks for signing up.',
    from_email='Acme <hello@acme.com>',
    recipient_list=['someone@example.com'],
)

Laravel

Laravel .env
# .env. The key comes from https://emails.sh/dashboard/api-keys.
MAIL_MAILER=smtp
MAIL_HOST=smtp.emails.sh
MAIL_PORT=587
MAIL_USERNAME=emailssh
MAIL_PASSWORD=esh_your_key_here
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=hello@acme.com
MAIL_FROM_NAME="Acme"

config/mail.php needs no change: those variables are what the shipped config already reads. For port 465, set MAIL_PORT=465.

A send
<?php

use Illuminate\Support\Facades\Mail;

Mail::raw('Thanks for signing up.', function ($message) {
    $message->to('someone@example.com')->subject('Welcome to Acme');
});

Rails

ActionMailer
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address:              'smtp.emails.sh',
  port:                 587,
  user_name:            'emailssh',
  password:             ENV['EMAILSSH_API_KEY'],
  authentication:       :plain,
  enable_starttls_auto: true
}
config.action_mailer.default_options = { from: 'Acme <hello@acme.com>' }

For port 465, use port: 465 and tls: true, and drop enable_starttls_auto.

PHPMailer

PHPMailer
<?php

use PHPMailer\PHPMailer\PHPMailer;

require 'vendor/autoload.php';

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host       = 'smtp.emails.sh';
$mail->Port       = 587;
$mail->SMTPAuth   = true;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // ENCRYPTION_SMTPS for 465
$mail->Username   = 'emailssh';
$mail->Password   = getenv('EMAILSSH_API_KEY');

$mail->setFrom('hello@acme.com', 'Acme');
$mail->addAddress('someone@example.com');
$mail->Subject = 'Welcome to Acme';
$mail->isHTML(true);
$mail->Body    = '<p>Thanks for signing up.</p>';
$mail->AltBody = 'Thanks for signing up.';

$mail->send();

WordPress

Install WP Mail SMTP, choose Other SMTP as the mailer, and fill in smtp.emails.sh, TLS, port 587, authentication on, username emailssh, and your key as the password. The From Email is an address on your verified domain.

Keep the key out of the database by putting it in wp-config.php instead, which the plugin reads in preference to its stored settings.

wp-config.php
<?php
// wp-config.php. The key comes from https://emails.sh/dashboard/api-keys.
define( 'WPMS_ON', true );
define( 'WPMS_MAILER', 'smtp' );
define( 'WPMS_SMTP_HOST', 'smtp.emails.sh' );
define( 'WPMS_SMTP_PORT', 587 );
define( 'WPMS_SSL', 'tls' );
define( 'WPMS_SMTP_AUTH', true );
define( 'WPMS_SMTP_USER', 'emailssh' );
define( 'WPMS_SMTP_PASS', 'esh_your_key_here' );

Anything else

msmtp, ssmtp, Postfix relayhost, a printer, a CI job
host:       smtp.emails.sh
port:       587    (or 465)
security:   STARTTLS on 587, TLS on 465
auth:       PLAIN or LOGIN
username:   emailssh
password:   an esh_ key from https://emails.sh/dashboard/api-keys

A one-line check from a shell, which is what to run when a framework is failing and you want to know whether the credentials or the framework is the problem.

swaks
swaks --server smtp.emails.sh:587 --tls \
  --auth-user emailssh --auth-password "$EMAILSSH_API_KEY" \
  --from hello@acme.com --to someone@example.com \
  --header 'Subject: SMTP test' --body 'It works.'

What happens to your message

The relay parses your MIME message and sends it through the same POST /v1/emails endpoint the API and the SDKs use, so an SMTP send is not a second-class send. It gets the same domain verification, the same suppression list, the same delivery log, the same webhooks, and the same quota. The gateway holds no database and no sending logic of its own.

The envelope decides delivery
Every address your client issues a RCPT TO for is delivered to, and the To and Cc headers only decide how each one is labelled. Bcc works as you expect: an address in the envelope and in no header is delivered to and shown to nobody.
The From header decides which domain must be verified
The envelope MAIL FROM is only the bounce path.
Reply-To, Cc, Bcc, text and HTML parts, and attachments carry through
They arrive as the equivalent fields on the API call.
The 250 reply carries the message id
250 2.0.0 Ok: queued as 3f9c1e07-42b8-4d65-9a10-7c53e8b2f491. It is a bare UUID with no prefix, and it is the id GET /v1/emails/:id takes, so your SMTP log lines are enough to look a message up later.

Four things the relay does that will surprise you

These are consequences of translating MIME into a JSON API, and each one is a thing somebody has been caught by.

Inline attachments are dropped
An attachment referenced from your HTML with cid: does not survive, and the image shows as broken. Host the image and reference it with an https URL. This is the one that most often turns a good-looking template into a broken one.
Only string-valued X- headers survive
Custom X- headers pass through when their value is a string. Anything structured is dropped. X-Emailssh-* names are reserved and always dropped.
Recipients can become mutually visible
If no envelope address matches any header, every recipient is promoted into the To header and they can all see each other. That happens when a client issues RCPT TO for addresses that appear in no To or Cc header at all. If you are sending to a list, send one transaction per person, or use POST /v1/emails/batch.
Ten connections per address
A source address opening an eleventh simultaneous connection gets 421. A pool of workers that each hold a connection open needs to be smaller than ten, or to share.

Reading the reply codes

Your mail library will surface these. What matters is that a 4xx means try again and a 5xx means the message will never be accepted as sent.

ReplyWhat it meansWhat to do
250 2.0.0Accepted. The id is in the reply.Nothing.
421 4.7.0Too many connections from your address, or too many failed logins. Five auth failures in fifteen minutes locks you out.Open fewer connections. Fix the credentials, then wait for the lockout to pass.
451 4.3.0emails.sh could not be reached.Retry. Your client will.
451 4.7.1Rate limited, or the workspace is paused.Retry. Slow down if it repeats.
452 4.3.1Your quota, daily cap, or spend cap is used up.Retry after it resets, or raise the plan.
452 4.5.3More than 50 recipients in one transaction.Split it, or use POST /v1/emails/batch for up to 100 in one call.
454 4.7.0Credentials could not be checked right now.Retry.
535 5.7.8The username or the API key is wrong.Username is emailssh. Password is an esh_ key that has not been revoked.
550 5.1.1The recipient is suppressed after an earlier bounce or complaint.Do not retry. See /docs/suppressions.
550 5.1.3A recipient address is not valid.Fix the address.
550 5.6.0The message is missing a subject or a body, or a header is not allowed.Fix the message.
550 5.7.1The sending domain is not verified, or the recipient is blocked.Verify the domain at https://emails.sh/dashboard/domains, or send from onboarding@emails.sh while testing.
552 5.3.4The message is over 40 MB.Link to the file instead of attaching it.

Limits

40 MB per message
Including base64 attachments. The relay advertises SIZE, so a well-behaved client refuses before sending the bytes.
50 recipients per transaction
Counting To, Cc, and Bcc. Past that you get 452 4.5.3, which is temporary: split the send.
Ten simultaneous connections per source address
An eleventh gets 421.
Port 25 is not offered
And never will be. There is no unauthenticated relay.