Ray
PricingDocsSign inStart free
Browse the docs

Getting started

Ray documentationAuthenticationUsing Ray with AI coding agentsMCP serverTypeScript SDK

Guides

Sending notificationsIdempotencyTemplatesScheduling and test sendsStatus and feedsClick trackingDelivery webhooks

Channels

Email with Amazon SESEmail with SMTPPush notifications with Firebase Cloud MessagingSlack notificationsDiscord notificationsTelegram notificationsSMS with TwilioSMS with sendsms.mnHTTPS webhook channel

Reference

Rate limits and quotasErrors

Getting started

Ray documentationAuthenticationUsing Ray with AI coding agentsMCP serverTypeScript SDK

Guides

Sending notificationsIdempotencyTemplatesScheduling and test sendsStatus and feedsClick trackingDelivery webhooks

Channels

Email with Amazon SESEmail with SMTPPush notifications with Firebase Cloud MessagingSlack notificationsDiscord notificationsTelegram notificationsSMS with TwilioSMS with sendsms.mnHTTPS webhook channel

Reference

Rate limits and quotasErrors

Channels

Email with Amazon SES

Send email through your own Amazon SES account with Ray. AWS setup, channel config fields, recipient, per-send from and reply-to, custom headers, attachments, bounce handling and errors.

View as Markdownllms.txtAPI reference

The ses_email channel sends email through your own Amazon SES account using the SES v2 API. Ray renders the email_html template, applies suppressions, paces sends to your SES rate, and adds addresses to a suppression list when SES reports hard bounces or complaints. API calls go to https://ray-api.gege.mn with Authorization: Bearer $RAY_API_KEY.

Channel type ses_email
Template kind email_html (shared with SMTP)
Recipient { "email", "name"?, "cc"?, "bcc"?, "from"?, "replyTo"?, "headers"?, "attachments"? }
Pacing ratePerMinute, default 840, max 1600
Suppression Hard bounces and complaints, via SNS
Click tracking Yes (Starter plan and above)

What you need from AWS

  1. A verified sending identity. In the AWS console open Amazon SES, pick the region you will send from, go to Identities, click Create identity, and verify a domain (recommended, via the DNS records SES shows you) or a single email address. Your from address must belong to this identity.
  2. Production access. New SES accounts are in the sandbox: they can only send to verified addresses and have a low sending rate. In Account dashboard, request production access before sending to real users. Note your maximum send rate there.
  3. An IAM user with permission to send. In IAM, create a user and attach a policy that allows sending, for example:
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": ["ses:SendEmail", "ses:SendRawEmail"], "Resource": "*" }
  ]
}
  1. An access key for that user. In the user's Security credentials tab, create an access key and copy the access key ID and secret access key.
  2. Optional, for bounce and complaint handling: a configuration set and an SNS topic. See Bounces and complaints.

Channel config

Create the config in the dashboard: Channels, New channel, Amazon SES. Credentials are stored encrypted.

Field Required Notes
Name yes Internal label, for example transactional.
AWS region (region) yes The region of your SES identity, for example us-east-1.
Access key ID (accessKeyId) yes At least 16 characters.
Secret access key (secretAccessKey) yes At least 20 characters. Leave blank when editing to keep the stored secret.
From address (fromAddress) yes A valid email address on a verified identity. The default; a send can override it with from.
From name (fromName) no Display name, sent as From: "Name" <address>. The default; a send can override it.
SES configuration set (configurationSet) no Needed to receive bounce and complaint events.
SNS topic ARN (snsTopicArn) no The only SNS topic allowed to deliver bounce and complaint events. Leave blank to pin the first topic that confirms a subscription.
Rate per minute (ratePerMinute) no Default 840. Max 1600. Set to your SES maximum send rate times 60 (a sandbox account allows about 60).

Find the config's id with GET /channels; its kind is ses_email.

Template content

email_html content, used both in templates and as inline content on POST /send:

Field Type Notes
subject string, 1 to 998 Params are inserted as-is; a line break in the rendered subject fails the send with 400.
bodyHtml string Params are HTML-escaped.
bodyText string Plain-text alternative. Params are inserted as-is.

Ray sends the HTML and text bodies together as alternatives. See Templates for syntax.

Recipient

{
  "email": "[email protected]",
  "name": "Ada Lovelace",
  "cc": [{ "email": "[email protected]", "name": "Team" }],
  "bcc": [{ "email": "[email protected]" }],
  "from": { "email": "[email protected]", "name": "Example Billing" },
  "replyTo": [{ "email": "[email protected]", "name": "Support" }],
  "headers": { "X-Entity-Ref-ID": "invoice-1042" },
  "attachments": [
    {
      "filename": "invoice-1042.pdf",
      "content": "JVBERi0xLjQK",
      "contentType": "application/pdf"
    }
  ]
}
Field Type Notes
email string Required. The primary recipient, and the address checked against suppressions.
name string, 1 to 255 Optional display name. No line breaks.
cc array, max 50 [{ "email", "name"? }].
bcc array, max 50 [{ "email", "name"? }].
from object { "email"?, "name"? }. Overrides the config's from address and name for this send. The address must be on the config's from-address domain. See below.
replyTo array, 1 to 10 [{ "email", "name"? }]. Where replies go.
headers object, max 15 Extra message headers, { "Name": "value" }. See below.
attachments array, max 20 See below.

SES allows at most 50 recipients per message across To, Cc and Bcc combined. Only the primary email is checked against the suppression list.

From, reply-to and headers

from changes the sender for one send. Each field falls back to the channel config on its own: { "name": "Ada at Example" } keeps the config's address and only changes the display name, and { "email": "[email protected]" } keeps the config's fromName.

The address must be on the same domain as the config's fromAddress: with [email protected] configured, [email protected] works, but [email protected] or an address on another domain is rejected with 400 before anything is sent. This keeps the choice of sending domain with the workspace admins who edit channel configs, so an API key can't send as another domain your AWS account happens to have verified. To send from another domain, create a channel config for it. SES also checks the address: it must belong to a verified identity in the config's region, or SES rejects the message with MessageRejected and the delivery fails without a retry.

replyTo sets the Reply-To header, so replies go to those addresses instead of the from address.

headers adds custom headers, for example List-Unsubscribe, In-Reply-To and References for threading, or your own X- tags:

Rule Limit
Name Printable ASCII, no colon or spaces, 1 to 126 characters. Names are case-insensitive, so the same name can't appear twice.
Value Printable ASCII, no line breaks, 1 to 995 characters. Name and value together at most 996.
Count At most 15 headers (the SES limit).
Not allowed From, Sender, Reply-To, To, Cc, Bcc, Subject, Date, Message-ID, Return-Path, MIME-Version, Content-Type, Content-Disposition, Content-Transfer-Encoding. Ray sets these itself; use the recipient's own fields instead.
Provider controls Any X-SES-* header, SendGrid's X-SMTPAPI, and the equivalents for relays an SMTP config may use: X-MC-* (Mandrill), X-MSYS-* (SparkPost), X-PM-* (Postmark), X-Mailgun-*, X-MJ-* and X-Mailjet-* (Mailjet), X-Mailin-* (Brevo). These change provider routing, such as the configuration set that reports bounces to Ray, so they stay on the channel config.

A request that breaks one of these rules fails with 400 before anything is sent.

Attachments

Field Type Notes
filename string, 1 to 255 No line breaks.
content base64 string File bytes, standard base64-encoded (the examples use a tiny placeholder). Max 14,000,000 characters (about 10 MiB of file).
contentType string Optional MIME type, for example application/pdf.
disposition "attachment" or "inline" Default "attachment".
contentId string For inline images: reference it from bodyHtml as <img src="cid:logo">.

All attachments on one recipient may total at most 34,000,000 base64 characters (about 25 MiB), and the whole POST /send body at most 45 MiB. Ray stores attachment bytes encrypted until the delivery finishes, then deletes them; GET /sends/{sendId} shows attachment metadata only.

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: invoice-1042" \
  -d '{
    "channelConfigId": "9f1c2a7e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
    "content": {
      "subject": "Invoice {{invoiceNumber}}",
      "bodyHtml": "<p>Hi {{name}},</p><p>Your invoice for <b>{{amount}}</b> is attached.</p>",
      "bodyText": "Hi {{name}}, your invoice for {{amount}} is attached."
    },
    "params": { "name": "Ada", "invoiceNumber": "1042", "amount": "$59.00" },
    "recipient": {
      "email": "[email protected]",
      "name": "Ada Lovelace",
      "attachments": [
        { "filename": "invoice-1042.pdf", "content": "JVBERi0xLjQK", "contentType": "application/pdf" }
      ]
    },
    "externalUserId": "user_123",
    "showInFeed": true
  }'
import { readFile } from "node:fs/promises";

const pdf = await readFile("./invoice-1042.pdf");

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": "invoice-1042",
  },
  body: JSON.stringify({
    channelConfigId: "9f1c2a7e-3b4d-4e5f-8a6b-1c2d3e4f5a6b",
    templateId: "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a", // an email_html template
    params: { name: "Ada", invoiceNumber: "1042", amount: "$59.00" },
    recipient: {
      email: "[email protected]",
      name: "Ada Lovelace",
      attachments: [
        { filename: "invoice-1042.pdf", content: pdf.toString("base64"), contentType: "application/pdf" },
      ],
    },
  }),
});
if (res.status !== 202) throw new Error(`Ray ${res.status}: ${await res.text()}`);
const { sendId } = (await res.json()) as { sendId: string };

The delivery row's providerMessageId is the SES message id.

Bounces and complaints

Ray can add addresses to the channel config's suppression list when SES reports a permanent (hard) bounce or a complaint. Future sends to a suppressed address are recorded as suppressed and not sent. Transient (soft) bounces do not suppress. You can review and remove suppressions in the dashboard under Suppressions.

Setup:

  1. In Amazon SNS (same region), create a standard topic, for example ses-events.
  2. Subscribe Ray to the topic: protocol HTTPS, endpoint https://ray-api.gege.mn/webhooks/ses_email/<channelConfigId> (your config's id). Leave raw message delivery disabled. Ray verifies the SNS signature and confirms the subscription automatically.
  3. In Amazon SES, create a configuration set, add an event destination of type Amazon SNS for Bounces and Complaints, and choose the topic.
  4. In the Ray channel config, set SES configuration set to that configuration set's name. Ray sends every message with it.

Topic pinning: SNS signatures only prove that some AWS topic signed a message, so Ray accepts events only from one topic per channel config. If snsTopicArn is blank, the topic that completes the first subscription confirmation is pinned. Messages from any other topic are rejected, and notifications for a config that has no pinned topic are refused. To move to a different topic, paste its ARN into the config.

Errors

Provider errors appear on the delivery row as providerError: { "name", "message" }.

Result SES errors
failed_terminal (not retried) SenderNotAllowedError (a scheduled send's from.email is no longer on the config's domain after a config edit), MessageRejected (for example an unverified address while in the sandbox), MailFromDomainNotVerifiedException, AccountSendingPausedException, ConfigurationSetDoesNotExistException, ConfigurationSetSendingPausedException, AccountSuspendedException, InvalidParameterValue, InvalidEmailAddress, NotAuthorized, AccessDeniedException, and any other HTTP 4xx
failed_retryable (retried) TooManyRequestsException, ThrottlingException, Throttling, RequestThrottled, HTTP 429, HTTP 5xx, network errors and timeouts

Common fixes:

  • MessageRejected: Email address is not verified: the account is in the SES sandbox, or the from address isn't on a verified identity in this region.
  • AccessDeniedException or NotAuthorized: the IAM user lacks ses:SendEmail, or the key belongs to a different account.
  • ConfigurationSetDoesNotExistException: the configuration set name is wrong or in another region.
  • Frequent throttling retries: lower ratePerMinute to your SES maximum send rate times 60.

Request errors from POST /send (for example recipient: Invalid email address) are listed in Errors.

PreviousDelivery webhooksNextEmail with SMTP

On this page

  • What you need from AWS
  • Channel config
  • Template content
  • Recipient
  • From, reply-to and headers
  • Attachments
  • Send an email
  • Bounces and complaints
  • Errors
Ray

Notification delivery for teams that already have providers.

Product

PricingDocsAPI referenceMCP serverTypeScript SDKAgent skillsllms.txt

Legal

TermsPrivacyRefunds

Contact

[email protected]

© 2026 L’Atelier Gege LLC