Channels
Email with SMTP
Send email through any SMTP relay with Ray, such as Google Workspace, SendGrid, Mailgun or your own server. Setup, config fields, recipient, per-send from and reply-to, custom headers and errors.
The smtp_email channel sends email through any SMTP server you have credentials for: a transactional provider's SMTP endpoint, Google Workspace, or your own relay. It uses the same email_html templates and recipient shape as Amazon SES, so a template works over either. API calls go to https://ray-api.gege.mn with Authorization: Bearer $RAY_API_KEY.
| Channel type | smtp_email |
| Template kind | email_html (shared with SES) |
| Recipient | { "email", "name"?, "cc"?, "bcc"?, "from"?, "replyTo"?, "headers"?, "attachments"? } |
| Pacing | ratePerMinute, default 840, max 1600 |
| Suppression | Not populated (SMTP has no bounce callback) |
| Click tracking | Yes (Starter plan and above) |
What you need from your provider
Collect five values: host, port, TLS mode, username and password, plus a from address the server allows you to send as.
| Provider | Host | Port | Username | Password |
|---|---|---|---|---|
| SendGrid | smtp.sendgrid.net |
587 | apikey (literally) |
A SendGrid API key with Mail Send permission |
| Mailgun | smtp.mailgun.org (or smtp.eu.mailgun.org) |
587 | The domain's SMTP login | That login's SMTP password |
| Google Workspace or Gmail | smtp.gmail.com |
587 | Your full address | An app password (requires 2-Step Verification) |
| Amazon SES SMTP interface | email-smtp.<region>.amazonaws.com |
587 | SES SMTP username | SES SMTP password (created under SES, SMTP settings) |
| Your own relay | Your host | 587 or 465 | As configured | As configured |
Generic steps:
- In your provider's dashboard, verify the domain or address you will send from (SPF and DKIM records).
- Create SMTP credentials (often an API key or a dedicated SMTP user).
- Note your provider's sending limit to set
ratePerMinute.
Channel config
Create the config in the dashboard: Channels, New channel, SMTP relay. Credentials are stored encrypted.
| Field | Required | Notes |
|---|---|---|
| Name | yes | Internal label. |
SMTP host (host) |
yes | Hostname or IP, max 255 characters. |
Port (port) |
no | Default 587. |
Use implicit TLS (secure) |
no | Default off. Turn on for port 465 (TLS from connect). Off means the connection must upgrade with STARTTLS (ports 587 and 25); a server that doesn't offer STARTTLS fails the send. Mail is never sent unencrypted. |
Username (username) |
yes | Max 255 characters. |
Password (password) |
yes | Max 1024 characters. Leave blank when editing to keep the stored password. |
From address (fromAddress) |
yes | A valid email address the server lets you send as. The default; a send can override it with from. |
From name (fromName) |
no | Display name. No line breaks. The default; a send can override it. |
Rate per minute (ratePerMinute) |
no | Default 840. Max 1600. Match your provider's limit. |
Host restrictions: Ray refuses to connect to loopback (127.0.0.0/8, ::1), 0.0.0.0/8 and link-local addresses (169.254.0.0/16, fe80::/10, including cloud metadata endpoints), checked against the resolved address at send time. Private network ranges are allowed.
Find the config's id with GET /channels; its kind is smtp_email.
Template content
Same as SES:
| Field | Type | Notes |
|---|---|---|
subject |
string, 1 to 998 | Params inserted as-is; a line break in the rendered subject fails the send with 400. |
bodyHtml |
string | Params are HTML-escaped. |
bodyText |
string | Params inserted as-is. |
Recipient
{
"email": "[email protected]",
"name": "Ada Lovelace",
"cc": [{ "email": "[email protected]" }],
"bcc": [{ "email": "[email protected]" }],
"from": { "name": "Ada at Example" },
"replyTo": [{ "email": "[email protected]" }],
"headers": { "List-Unsubscribe": "<https://example.com/unsubscribe/123>" },
"attachments": [
{ "filename": "logo.png", "content": "iVBORw0KGgo=", "contentType": "image/png", "disposition": "inline", "contentId": "logo" }
]
}
| Field | Type | Notes |
|---|---|---|
email |
string | Required. Checked against the config's suppression list. |
name |
string, 1 to 255 | Optional. No line breaks. |
cc, bcc |
array, max 50 each | [{ "email", "name"? }]. |
from |
object | { "email"?, "name"? }. Overrides the config's from address and name for this send; a field you leave out keeps the config's value. The address must be on the same domain as the config's fromAddress (another domain, including a subdomain, is a 400). It is also used as the envelope sender, so the server must let you send as it. |
replyTo |
array, 1 to 10 | [{ "email", "name"? }]. Sets Reply-To. |
headers |
object, max 15 | Extra headers, { "Name": "value" }. Same rules as SES: printable ASCII, no line breaks, and Ray-managed headers such as From, Reply-To and Subject, plus provider controls such as X-SMTPAPI, X-PM-* or X-Mailgun-*, are rejected. |
attachments |
array, max 20 | filename, content (base64, max 14,000,000 characters), contentType?, disposition (attachment default, or inline), contentId? for <img src="cid:...">. Total at most 34,000,000 base64 characters. |
Your SMTP provider may enforce lower limits on recipients or message size.
Send an email
curl -X POST https://ray-api.gege.mn/send \
-H "Authorization: Bearer $RAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-user_123" \
-d '{
"channelConfigId": "5d2e8f1a-6b7c-4d8e-9f0a-2b3c4d5e6f7a",
"templateId": "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
"params": { "name": "Ada", "loginUrl": "https://app.example.com/login" },
"recipient": { "email": "[email protected]", "name": "Ada Lovelace" },
"trackClicks": true
}'
const res = await fetch("https://ray-api.gege.mn/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RAY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "welcome-user_123",
},
body: JSON.stringify({
channelConfigId: "5d2e8f1a-6b7c-4d8e-9f0a-2b3c4d5e6f7a",
content: {
subject: "Welcome, {{name}}",
bodyHtml: '<p>Hi {{name}}, <a href="{{loginUrl}}">sign in</a> to get started.</p>',
bodyText: "Hi {{name}}, sign in at {{loginUrl}} to get started.",
},
params: { name: "Ada", loginUrl: "https://app.example.com/login" },
recipient: { email: "[email protected]", name: "Ada Lovelace" },
}),
});
if (res.status !== 202) throw new Error(`Ray ${res.status}: ${await res.text()}`);
The delivery row's providerMessageId is the Message-ID of the sent email. delivered means the SMTP server accepted the message.
Bounces
SMTP has no standard delivery callback, so Ray does not learn about bounces or complaints on this channel and never adds suppressions for it. Handle bounces with your provider's own tools. If you send through Amazon SES, the SES channel handles bounces and complaints automatically.
Errors
Errors appear on the delivery row as providerError: { "name", "message" }, with the server's reply in message.
| Result | SMTP errors |
|---|---|
failed_terminal (not retried) |
5xx replies (unknown mailbox, message refused, relay denied), authentication failure (EAUTH), a host that resolves to a blocked address, SenderNotAllowedError (a scheduled send's from.email is no longer on the config's domain after a config edit) |
failed_retryable (retried) |
4xx replies (greylisting, mailbox busy, temporary rate limit), connection, DNS, socket errors and timeouts |
Common fixes:
EAUTHor535 Authentication failed: wrong username or password; for Gmail use an app password; for SendGrid the username isapikey.550or553sender rejected: the from address isn't verified or allowed on this account.- Repeated connection timeouts: wrong port or TLS mode. Use 587 with implicit TLS off, or 465 with it on.
Request errors from POST /send are listed in Errors.