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
| Setting | Value |
|---|---|
| Host | smtp.avelto.dev |
| Port 587 | STARTTLS. Try this one first. |
| Port 465 | TLS from the first byte (sometimes called SSL). |
| Port 2587 | STARTTLS, for networks that block the standard ports. |
| Port 2465 | TLS from the first byte, for networks that block the standard ports. |
| Username | avelto |
| Password | An API key with the emails:send scope (av_live_... or av_test_...) |
| Encryption | STARTTLS 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. |
| Authentication | PLAIN or LOGIN |
Where the key goes in common tools:
| Tool | Username field | Password field (your API key) |
|---|---|---|
| Nodemailer | auth.user | auth.pass |
Laravel (.env) | MAIL_USERNAME | MAIL_PASSWORD |
Django (settings.py) | EMAIL_HOST_USER | EMAIL_HOST_PASSWORD |
Rails (smtp_settings) | user_name: | password: |
| WordPress (WP Mail SMTP) | SMTP Username | SMTP Password |
| Supabase Auth | Username | Password |
| Anything else | SMTP username / user | SMTP 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.
# 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.
npm install nodemailerimport 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"node send.mjsThe relay answers with the Avelto email id:
250 2.0.0 Ok: queued as 9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f104. 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:
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.devto 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 TOlist, 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, Janestays one recipient. - Other headers pass through when the API's
headersfield 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 asReceived,Return-Path,DKIM-Signature,Message-ID,DateandX-Originating-IP, and headers such asSender,Resent-*,Disposition-Notification-ToandErrors-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.
| Header | Same as | Example |
|---|---|---|
X-Avelto-Tags | tags: comma-separated, up to 10, letters, numbers and _ - : . | welcome, onboarding |
X-Avelto-Scheduled-At | scheduled_at: ISO 8601 with an offset, up to 30 days ahead | 2026-10-08T09:00:00Z |
Idempotency-Key or X-Avelto-Idempotency-Key | The Idempotency-Key header: a resend with the same key answers with the first email's id | order-1234 |
X-Avelto-Tags: welcome, onboardingX-Avelto-Scheduled-At: 2026-10-08T09:00:00ZIdempotency-Key: welcome-user-42Any other X-Avelto-* header, X-Report-Abuse and X-SES-* are reserved and refused.
Limits
| Limit | Value |
|---|---|
Recipients per message (RCPT TO) | 50 |
Message size, advertised with SIZE | 10 MB |
| Attachments | 10 files, 7 MB in total |
| Open connections from one address | 20 |
| Time to sign in after connecting | 30 seconds, then the connection is closed with 421 |
| Idle time between commands once signed in | 60 seconds |
| Messages in progress at once per API key | 5; the next one gets 451 at MAIL FROM and your client retries |
| Failed sign-ins from one address | 10 in 15 minutes blocks the address for 30 minutes |
| Sending rate and monthly volume | The 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.
| Reply | When |
|---|---|
250 2.0.0 | Accepted: Ok: queued as <email id>. The id works with GET /v1/emails/:id and in the dashboard. |
535 5.7.8 | The password is not an API key, the key is revoked, or it lacks the emails:send scope. |
538 5.7.0 | Sign-in was tried before STARTTLS. Turn on STARTTLS, or use a TLS port. |
421 4.7.0 | Too many failed sign-ins from your address. It is blocked for 30 minutes. |
421 4.7.0 | More than 20 open connections from your address. |
421 4.3.2 | The relay has too many connections waiting to sign in. Your client retries. |
421 4.4.2 | No sign-in within 30 seconds of connecting. The connection is closed. |
451 4.7.1 | The API key already has 5 messages in progress. Your client retries. |
452 4.3.1 | The relay is busy receiving other messages. Your client retries. |
452 4.5.3 | More than 50 recipients on one message. |
552 5.3.4 | The message is larger than 10 MB. |
553 5.7.1 | The envelope sender (MAIL FROM) is on a different domain from the From header. |
550 5.7.1 | The message sets a header Avelto reserves: X-Report-Abuse, X-SES-*, or an X-Avelto-* header other than the ones above. |
553 5.1.3 | A RCPT TO address is not a valid email address. |
550 5.6.0 | validation_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.1 | sandbox_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.1 | domain_not_verified: The From domain is not verified on this account. |
550 5.7.1 | recipient_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.1 | account_paused: Sending is paused on the account. The dashboard says why. |
550 5.7.1 | account_suspended: The account is suspended. |
451 4.7.1 | rate_limited: Over the API key's per-second rate limit. Your client retries. |
452 4.7.1 | plan_limit: Over the plan's monthly volume, or the new-account daily cap. Your client retries. |
451 4.3.0 | internal_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
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:
// app/Mail/Welcome.php, inside the Mailableuse Illuminate\Mail\Mailables\Headers;public function headers(): Headers{ return new Headers(text: [ 'X-Avelto-Tags' => 'welcome', ]);}Django
# 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"from django.core.mail import EmailMessageEmailMessage( subject="Welcome", body="Thanks for signing up.", to=["jane@example.com"], headers={"X-Avelto-Tags": "welcome"},).send()Rails
# 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}class UserMailer < ApplicationMailer default from: "hello@mail.acme.com" def welcome(user) headers["X-Avelto-Tags"] = "welcome" mail(to: user.email, subject: "Welcome") endendWordPress
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:
// 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:
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.