Send email over SMTP

Anything that speaks SMTP can send through Avelto: a mail library you already use, a framework's mailer, a CMS plugin or an auth provider. Mail sent this way goes through the same pipeline as POST /v1/emails, with the same domains, log, webhooks, suppressions and limits.

Five steps: the settings, an API key, a sandbox send, the log, then a verified domain. Setups for common frameworks and tools follow.

1. The settings

SettingValue
Hostsmtp.avelto.dev
Port 587STARTTLS. Try this one first.
Port 465TLS from the first byte (sometimes called SSL).
Port 2587STARTTLS, for networks that block the standard ports.
Port 2465TLS from the first byte, for networks that block the standard ports.
Usernameavelto
PasswordAn API key with the emails:send scope (av_live_... or av_test_...)
EncryptionSTARTTLS on 587 and 2587, TLS on 465 and 2465. Sign-in is only offered, and only accepted, once the connection is encrypted. Set your client to require TLS, not just use it when offered.
AuthenticationPLAIN or LOGIN

Where the key goes in common tools:

ToolUsername fieldPassword field (your API key)
Nodemailerauth.userauth.pass
Laravel (.env)MAIL_USERNAMEMAIL_PASSWORD
Django (settings.py)EMAIL_HOST_USEREMAIL_HOST_PASSWORD
Rails (smtp_settings)user_name:password:
WordPress (WP Mail SMTP)SMTP UsernameSMTP Password
Supabase AuthUsernamePassword
Anything elseSMTP username / userSMTP password / pass

2. Create an API key

Sign in, open API keys in the dashboard and create a test key. It starts with av_test_. A key needs the emails:send scope to send over SMTP, which every key has unless you narrowed it.

shell
# Paste the key you just created. It is shown once, so copy it now.export AVELTO_API_KEY=av_test_...# Windows PowerShell:# $env:AVELTO_API_KEY = "av_test_..."

3. Send your first email

With Nodemailer. Any SMTP client works the same way: host, port, STARTTLS, username and password.

shell
npm install nodemailer
send.mjs
import nodemailer from "nodemailer";const transport = nodemailer.createTransport({  host: "smtp.avelto.dev",  port: 587,  secure: false, // STARTTLS: the connection is encrypted before signing in  requireTLS: true,  auth: {    user: "avelto", // always this    pass: process.env.AVELTO_API_KEY, // your API key, e.g. "av_test_..."  },});const info = await transport.sendMail({  from: "you@sandbox.avelto.dev",  to: "delivered@sandbox.avelto.dev",  subject: "Hello from Avelto",  text: "It works.",  headers: {    "X-Avelto-Tags": "welcome, onboarding",    "Idempotency-Key": "welcome-user-42",  },});console.log(info.response); // "250 2.0.0 Ok: queued as 9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10"
shell
node send.mjs

The relay answers with the Avelto email id:

text
250 2.0.0 Ok: queued as 9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10

4. Check the log

The id in the reply is the same id the API uses. Open the email in the dashboard (it says Sent over SMTP), or fetch it:

shell
curl https://api.avelto.dev/v1/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10 \  -H "Authorization: Bearer $AVELTO_API_KEY"

status moves from queued to sent to delivered. Webhooks fire for SMTP mail exactly as for API mail.

5. Verify a domain

The sandbox sender only reaches you. To send to anyone, verify a domain from Domains in the dashboard or with POST /v1/domains (see Domains), then switch to a live key (av_live_) and send from an address on that domain, such as hello@mail.acme.com. Nothing else changes.

How a message is read

The relay applies the API's rules to every message:

  • From must be on a verified domain, or on sandbox.avelto.dev to the account owner. The envelope sender (MAIL FROM) must be on the same domain as the From header, or on the sandbox domain.
  • Recipients are the envelope RCPT TO list, which is how Bcc works. An address that is also in the To header is sent as To, one in the Cc header as Cc, and the rest as Bcc. At least one recipient must appear in the To header.
  • Subject is required, as on the API. Reply-To becomes reply_to (one address).
  • Attachments keep their filename and content type. Images embedded with cid: are sent as ordinary attachments for now, so link images by https URL instead.
  • Display names are decoded and sent quoted where they need quotes, so a name such as Doe, Jane stays one recipient.
  • Other headers pass through when the API's headers field would accept them: X- headers, threading headers (In-Reply-To, References) and the list and auto-reply headers listed under Send email. Everything else is removed without refusing the message: transport headers such as Received, Return-Path, DKIM-Signature, Message-ID, Date and X-Originating-IP, and headers such as Sender, Resent-*, Disposition-Notification-To and Errors-To. The message is signed with your domain as usual. More than 20 custom headers, or a header value that is not printable ASCII, is refused.

Avelto headers

Set these on the message to use the API's features. The relay reads them and removes them before sending.

HeaderSame asExample
X-Avelto-Tagstags: comma-separated, up to 10, letters, numbers and _ - : .welcome, onboarding
X-Avelto-Scheduled-Atscheduled_at: ISO 8601 with an offset, up to 30 days ahead2026-10-08T09:00:00Z
Idempotency-Key or X-Avelto-Idempotency-KeyThe Idempotency-Key header: a resend with the same key answers with the first email's idorder-1234
headers
X-Avelto-Tags: welcome, onboardingX-Avelto-Scheduled-At: 2026-10-08T09:00:00ZIdempotency-Key: welcome-user-42

Any other X-Avelto-* header, X-Report-Abuse and X-SES-* are reserved and refused.

Limits

LimitValue
Recipients per message (RCPT TO)50
Message size, advertised with SIZE10 MB
Attachments10 files, 7 MB in total
Open connections from one address20
Time to sign in after connecting30 seconds, then the connection is closed with 421
Idle time between commands once signed in60 seconds
Messages in progress at once per API key5; the next one gets 451 at MAIL FROM and your client retries
Failed sign-ins from one address10 in 15 minutes blocks the address for 30 minutes
Sending rate and monthly volumeThe same per-key rate limit and plan limits as the API

Reply codes

A 4xx reply is temporary: your client keeps the message and retries later. A 5xx reply is permanent: retrying the same message gets the same answer. Where a reply comes from a sending rule, the text after the code is the same message the API returns, except that the sandbox and suppression replies leave out addresses: an SMTP reply often ends up in a bounce that other people read.

ReplyWhen
250 2.0.0Accepted: Ok: queued as <email id>. The id works with GET /v1/emails/:id and in the dashboard.
535 5.7.8The password is not an API key, the key is revoked, or it lacks the emails:send scope.
538 5.7.0Sign-in was tried before STARTTLS. Turn on STARTTLS, or use a TLS port.
421 4.7.0Too many failed sign-ins from your address. It is blocked for 30 minutes.
421 4.7.0More than 20 open connections from your address.
421 4.3.2The relay has too many connections waiting to sign in. Your client retries.
421 4.4.2No sign-in within 30 seconds of connecting. The connection is closed.
451 4.7.1The API key already has 5 messages in progress. Your client retries.
452 4.3.1The relay is busy receiving other messages. Your client retries.
452 4.5.3More than 50 recipients on one message.
552 5.3.4The message is larger than 10 MB.
553 5.7.1The envelope sender (MAIL FROM) is on a different domain from the From header.
550 5.7.1The message sets a header Avelto reserves: X-Report-Abuse, X-SES-*, or an X-Avelto-* header other than the ones above.
553 5.1.3A RCPT TO address is not a valid email address.
550 5.6.0validation_error: The message breaks a rule the API applies to POST /v1/emails: no subject, no body, a bad tag, no recipient in the To header. The reply names the rule.
550 5.7.1sandbox_recipient_not_allowed: The sandbox sender only delivers to the account owner and the simulator addresses. The reply does not repeat the owner's address.
553 5.7.1domain_not_verified: The From domain is not verified on this account.
550 5.7.1recipient_suppressed: A recipient bounced or complained before. The reply does not say which; the dashboard's suppression list does. Remove the address from suppressions to send again.
550 5.7.1account_paused: Sending is paused on the account. The dashboard says why.
550 5.7.1account_suspended: The account is suspended.
451 4.7.1rate_limited: Over the API key's per-second rate limit. Your client retries.
452 4.7.1plan_limit: Over the plan's monthly volume, or the new-account daily cap. Your client retries.
451 4.3.0internal_error: Something went wrong on our side. Your client retries.

Framework and tool setups

These are for real mail: a live key and a From address on a verified domain. Keep the key in an environment variable or your platform's secret store, never in the repository.

Laravel

.env
MAIL_MAILER=smtpMAIL_HOST=smtp.avelto.dev# TLS from the first byte, so the key is never sent unencrypted.MAIL_PORT=465MAIL_USERNAME=aveltoMAIL_PASSWORD=av_live_...# Laravel 11 and later. On Laravel 10 and earlier use MAIL_ENCRYPTION=ssl instead.MAIL_SCHEME=smtpsMAIL_FROM_ADDRESS="hello@mail.acme.com"MAIL_FROM_NAME="${APP_NAME}"

To tag a message, add the header in the Mailable:

PHP
// app/Mail/Welcome.php, inside the Mailableuse Illuminate\Mail\Mailables\Headers;public function headers(): Headers{    return new Headers(text: [        'X-Avelto-Tags' => 'welcome',    ]);}

Django

Python
# settings.pyimport osEMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"EMAIL_HOST = "smtp.avelto.dev"EMAIL_PORT = 587EMAIL_USE_TLS = True  # STARTTLSEMAIL_HOST_USER = "avelto"EMAIL_HOST_PASSWORD = os.environ["AVELTO_API_KEY"]DEFAULT_FROM_EMAIL = "hello@mail.acme.com"
Python
from django.core.mail import EmailMessageEmailMessage(    subject="Welcome",    body="Thanks for signing up.",    to=["jane@example.com"],    headers={"X-Avelto-Tags": "welcome"},).send()

Rails

Ruby
# config/environments/production.rbconfig.action_mailer.delivery_method = :smtpconfig.action_mailer.smtp_settings = {  address: "smtp.avelto.dev",  port: 587,  user_name: "avelto",  password: ENV.fetch("AVELTO_API_KEY"),  authentication: :plain,  enable_starttls: true}
Ruby
class UserMailer < ApplicationMailer  default from: "hello@mail.acme.com"  def welcome(user)    headers["X-Avelto-Tags"] = "welcome"    mail(to: user.email, subject: "Welcome")  endend

WordPress

With the WP Mail SMTP plugin, choose Other SMTP as the mailer and fill in the settings from step 1: encryption TLS with port 587 (or SSL with port 465), Auto TLS on, Authentication on, the username and the API key as the password. Set From Email to an address on your verified domain and tick Force From Email.

To keep the key out of the database, define the settings in wp-config.php instead:

PHP
// wp-config.php, above "That's all, stop editing!"define( 'WPMS_ON', true );define( 'WPMS_MAILER', 'smtp' );define( 'WPMS_SMTP_HOST', 'smtp.avelto.dev' );define( 'WPMS_SMTP_PORT', 587 );define( 'WPMS_SSL', 'tls' );define( 'WPMS_SMTP_AUTOTLS', true );define( 'WPMS_SMTP_AUTH', true );define( 'WPMS_SMTP_USER', 'avelto' );define( 'WPMS_SMTP_PASS', 'av_live_...' );define( 'WPMS_MAIL_FROM', 'hello@mail.acme.com' );define( 'WPMS_MAIL_FROM_FORCE', true );

Supabase Auth

In the Supabase dashboard, open Authentication, then the SMTP settings, and turn on custom SMTP. Fill in:

text
Sender email     hello@mail.acme.comSender name      AcmeHost             smtp.avelto.devPort number      587   (or 465)Username         aveltoPassword         av_live_...

Supabase's sign-up, magic link and password reset emails then go through Avelto and appear in your log.

Next

  • Send email: every field the relay maps to, plus idempotency and scheduling.
  • Webhooks: get events pushed to your app.
  • Test mode: test keys, the sandbox sender and the simulator addresses.