Basic Email Sending
EmailEngine sends email through a registered account's own SMTP server, or through the Gmail and MS Graph APIs for accounts registered that way. This guide covers the fundamentals of sending with the submit API.
Why It Matters
When your application sends on behalf of a customer, talking SMTP directly means handling each provider's authentication, rate limits, retry rules, and error codes. EmailEngine puts one REST endpoint in front of the customer's mailbox instead, with the same JSON response shape whichever provider is behind it, and a queue that retries failed submissions.
Step-by-Step Guide
1. Register the Account
Before sending, register an email account in EmailEngine using the account registration API.
Endpoint: POST /v1/account
curl -XPOST "https://emailengine.example.com/v1/account" \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"account": "example",
"name": "Andris Reinman",
"email": "andris@example.com",
"imap": {
"auth": { "user": "andris", "pass": "secretpass" },
"host": "mail.example.com",
"port": 993,
"secure": true
},
"smtp": {
"auth": { "user": "andris", "pass": "secretpass" },
"host": "mail.example.com",
"port": 465,
"secure": true
}
}'
Expected response:
{
"account": "example",
"state": "new"
}
Important: If you use an SMTP port other than 465, set "secure": false.
2. Wait for Connection
Before sending, ensure the account is connected. Poll the account status:
curl "https://emailengine.example.com/v1/account/example" \
-H "Authorization: Bearer <your-token>"
Wait until state becomes "connected". A submission is handled by the worker that holds the account, and an account is assigned to a worker shortly after registration; until then the API answers 503 with No active handler for requested account. Try again later. Replies and forwards also need the mailbox to be reachable, because the referenced message is read from it.
3. Submit a Simple Email
Endpoint: POST /v1/account/:id/submit
curl -XPOST "https://emailengine.example.com/v1/account/example/submit" \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"to": [
{
"name": "Recipient Name",
"address": "recipient@example.com"
}
],
"subject": "Test message",
"text": "Hello from myself!",
"html": "<p>Hello from myself!</p>"
}'
Expected response (queued, not yet delivered):
{
"response": "Queued for delivery",
"messageId": "<99f7f0ec-90a1-caaf-698b-18e096c7679e@example.com>",
"sendAt": "2025-05-14T10:22:31.312Z",
"queueId": "4646ac53857fd2b2"
}
The message is now queued for delivery. EmailEngine will handle the actual SMTP transmission.
Message Components
Recipients
You can specify multiple recipients in to, cc, and bcc fields:
{
"to": [
{ "name": "Alice", "address": "alice@example.com" },
{ "name": "Bob", "address": "bob@example.com" }
],
"cc": [{ "address": "manager@example.com" }],
"bcc": [{ "address": "archive@example.com" }]
}
The name field is optional but recommended for a better recipient experience.
Content
Plain Text and HTML
Always provide both plain text and HTML versions for best compatibility:
{
"subject": "Welcome to our service",
"text": "Welcome! Visit https://example.com to get started.",
"html": "<p>Welcome!</p><p>Visit <a href='https://example.com'>our site</a> to get started.</p>"
}
HTML Only
An html body with no text is sent as HTML only. EmailEngine does not derive a plaintext alternative from it, so supply text yourself for clients and filters that prefer one:
{
"subject": "HTML Newsletter",
"html": "<h1>Hello!</h1><p>This is an HTML email.</p>"
}
Attachments
Add attachments using the attachments array:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Document attached",
"text": "Please find the document attached.",
"attachments": [
{
"filename": "document.pdf",
"content": "JVBERi0xLjQKJSBtaW5pbWFsIGV4YW1wbGUK",
"contentType": "application/pdf"
}
]
}
Attachment options:
filename- Attachment filename (optional, recommended)content- Base64 encoded content (required, unlessreferenceis set, in which case it must be left out)contentType- MIME type (optional, derived fromfilenamewhen omitted)contentDisposition-attachment(the default) orinlinecid- Content ID for inline images (optional)encoding- Encoding ofcontent. Onlybase64is accepted, and it is the defaultreference- The ID of an attachment on a stored message, taken from that message's attachment list, to attach it without downloading and re-uploading it (optional)
Inline Images
Reference inline images in HTML using Content ID:
{
"html": "<p>Logo: <img src='cid:logo' /></p>",
"attachments": [
{
"filename": "logo.gif",
"content": "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=",
"contentType": "image/gif",
"cid": "logo"
}
]
}
Custom Headers
Add custom email headers:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Test",
"text": "Test message",
"headers": {
"X-Custom-Header": "value",
"X-Campaign-ID": "campaign-123",
"List-Unsubscribe": "<mailto:unsubscribe@example.com>"
}
}
Common custom headers:
X-Custom-*- Your custom headersList-Unsubscribe- Unsubscribe link for bulk emailX-Priority- Set message priority (1-5)
Reply-To has a field of its own, replyTo, described below. Use that rather than a header.
Sender Information
Override default sender information:
{
"from": {
"name": "Support Team",
"address": "support@example.com"
},
"replyTo": {
"name": "No Reply",
"address": "noreply@example.com"
},
"to": [{ "address": "recipient@example.com" }],
"subject": "Support Ticket Response",
"text": "Your ticket has been updated."
}
If from is omitted, EmailEngine uses the account's configured email and name. replyTo accepts a single address object or a list of them.
Advanced Options
Scheduled Sending
Schedule an email for future delivery:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Scheduled message",
"text": "This will be sent at the specified time",
"sendAt": "2025-12-25T09:00:00.000Z"
}
The message stays in the queue until the specified time, and the response's sendAt echoes it. The Date header of the message is set to that time as well. A sendAt in the past sends immediately.
Skip Sent Folder
Prevent saving a copy to the Sent Mail folder:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "No copy saved",
"text": "This won't appear in Sent Mail",
"copy": false
}
Useful for bulk sending to avoid cluttering the Sent folder. Leave copy unset to follow the account's default. The flag only applies to SMTP deliveries: Gmail and MS Graph file sent messages themselves, so it is a no-op for accounts that send through those APIs. sentMailPath names a different folder for the copy.
Custom Message ID
Specify your own Message-ID for threading:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Custom ID",
"text": "Message with custom ID",
"messageId": "<custom-id-12345@example.com>"
}
Important for maintaining email threads (see Threading).
Delivery Status Notifications
Request delivery status notifications (DSN):
{
"to": [{ "address": "recipient@example.com" }],
"subject": "With DSN",
"text": "Request delivery notification",
"dsn": {
"return": "headers",
"notify": ["success", "failure", "delay"]
}
}
return is required and is either headers or full; notify takes any of never, success, failure, and delay; id sets the envelope identifier (ENVID) and recipient the address the notification goes to (ORCPT). DSN support varies by email provider.
Email Tracking
Enable open and click tracking for your emails:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Tracked Email",
"html": "<p>Check out <a href='https://example.com'>our website</a>!</p>",
"trackOpens": true,
"trackClicks": true
}
When enabled, EmailEngine will:
- Insert a tracking pixel to detect email opens (
trackOpens) - Rewrite links to track clicks (
trackClicks) - Send
trackOpenandtrackClickwebhook events when detected
When a message leaves both fields out, the instance settings apply (trackOpens and trackClicks, falling back to trackSentMessages). The pixel and the rewritten links point at the instance's serviceUrl, so tracking is only added when that setting, or baseUrl below, is set. Separate trackOpens and trackClicks fields were added in v2.46.1.
Optional: Override the base URL for tracking links:
{
"trackOpens": true,
"trackClicks": true,
"baseUrl": "https://yourdomain.com"
}
The trackOpen and trackClick pages describe the events and their payloads.
Preview Mode (Dry Run)
Generate email preview without actually sending:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Test Email",
"html": "<p>Preview this email</p>",
"dryRun": true
}
Response:
{
"response": "Dry run",
"messageId": "<generated-message-id@example.com>",
"preview": "TUlNRS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IGhlbGxvIHdvcmxkDQoNCkhlbGxvIQ0K"
}
The preview field contains the complete RFC822 formatted email (base64 encoded). Decode it to see what would be sent, minus the tracking pixel and rewritten links, which are not added on a dry run. Nothing is queued. A dry run of a mail merge returns no preview.
Network Configuration
Proxy Routing
Route SMTP connection through a proxy server:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Via Proxy",
"text": "Sent through proxy",
"proxy": "http://proxy.company.com:8080"
}
Accepted URL schemes are http, https, socks, socks4, socks4a, and socks5. Without it, the instance-wide proxy setting applies, if one is configured.
Local Address Binding
Bind to a specific local IP address:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "From Specific IP",
"text": "Sent from specific interface",
"localAddress": "192.168.1.100"
}
Useful for multi-interface systems or IP-based routing.
proxy decides where a connection carrying the account's credentials goes, so a token with a restricted permission set cannot set it; the request is refused with 403.
Delivery Control
Custom Retry Attempts
Override the default retry count for this message:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "High Priority",
"text": "Will retry up to 15 times",
"deliveryAttempts": 15
}
The default is 10, and the instance-wide default is settable under Configuration > Email Processing. A higher value keeps a message retrying for longer rather than making it more likely to arrive, so raise it only where a late delivery still has value.
SMTP Gateway
Route through a specific SMTP gateway:
{
"to": [{ "address": "recipient@example.com" }],
"subject": "Via Gateway",
"text": "Sent through custom gateway",
"gateway": "gateway-id-123"
}
Gateways are SMTP accounts (like SendGrid, Mailgun, or Amazon SES) that EmailEngine can use to send emails on behalf of any account. Register gateways via the Gateway API.
SMTP Envelope
Specify SMTP envelope separately from message headers:
{
"to": [{ "address": "recipient@example.com" }],
"from": { "address": "noreply@example.com" },
"subject": "Envelope Example",
"text": "Header From and SMTP MAIL FROM can differ",
"envelope": {
"from": "bounce@example.com",
"to": ["actualrecipient@example.com"]
}
}
Useful for bounce handling and advanced email routing.
Idempotency
Prevent duplicate message submission with idempotency keys:
Request:
curl -XPOST "https://emailengine.example.com/v1/account/example/submit" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-key-12345" \
-d '{
"to": [{ "address": "recipient@example.com" }],
"subject": "Important",
"text": "This will only send once even if request is retried"
}'
If the same request is sent multiple times with the same Idempotency-Key header, EmailEngine will:
- Process it only once
- Return the same response for duplicate requests
- Prevent accidental double-sends
The response carries an idempotency object saying which happened: {"key": "unique-key-12345", "status": "MISS"} for the request that was processed, "status": "HIT" for a replay. The key is scoped to the account and can be any string of up to 1024 characters; use UUIDs or request-specific identifiers. Supported since v2.52.0.
Sending Stored Drafts
Send a draft that already exists in the mailbox by referencing its message ID:
curl -XPOST "https://emailengine.example.com/v1/account/example/message/AAAAAQAACnA/submit" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{}'
The referenced message must be a draft: stored in the Drafts folder or flagged as \Draft for IMAP accounts, carrying the DRAFT label on Gmail, or a draft message on MS Graph. Drafts can come from the user's mail client, or be created through the message upload API by uploading to the Drafts folder with the \Draft flag.
The submission is queued like any other email, so scheduled sending, retry handling, gateways, idempotency keys, and the delivery webhooks described below all behave the same as with /submit. The response contains the familiar queueId and messageId values.
What happens to the draft after sending depends on the account type:
- Gmail and MS Graph accounts use the provider's native draft-send call. The provider files the message into the Sent Mail folder and removes the draft.
- IMAP accounts download the draft and deliver it over SMTP. A copy is stored in the Sent Mail folder (unless disabled with
copy: falseor the mail server stores sent messages itself, as Gmail and Outlook do for SMTP submissions), and the draft is then deleted. If no sent copy exists anywhere, the draft is moved to Trash instead so its content is not lost.
The request body is optional and accepts the delivery options described above - envelope, copy, sentMailPath, sendAt, deliveryAttempts, gateway, dsn, proxy, and localAddress. Content fields are not accepted; the draft is sent as stored. A draft with no recipients is refused with 400 (DraftHasNoRecipients). See the draft submission API reference for the full schema. Draft submission was added in v2.76.0.
Webhook Notifications
EmailEngine sends webhook notifications for delivery status updates. Configure the webhook URL under Configuration > Webhooks and include the events below in the enabled event list (see the webhooks overview).
messageSent
Delivered to the outbound MTA (SMTP server accepted the message):
{
"account": "example",
"date": "2025-05-14T10:32:39.499Z",
"event": "messageSent",
"data": {
"messageId": "<a00576bd-f757-10c7-26b8-885d7bbd9e83@example.com>",
"response": "250 2.0.0 Ok: queued as 5755482356",
"envelope": {
"from": "andris@example.com",
"to": ["recipient@example.com"]
}
}
}
messageDeliveryError
Emitted after every failed delivery attempt. EmailEngine retries automatically until delivery succeeds or the maximum number of attempts is reached:
{
"serviceUrl": "https://emailengine.example.com",
"account": "example",
"date": "2025-05-14T15:07:35.832Z",
"event": "messageDeliveryError",
"data": {
"queueId": "1833c8a88a86109a1bf",
"envelope": {
"from": "andris@example.com",
"to": ["recipient@example.com"]
},
"messageId": "<29e26263-7125-ff56-4f80-83a5cf737d5e@example.com>",
"error": "400 Message Not Accepted",
"errorCode": "EPROTOCOL",
"smtpResponseCode": 400,
"job": {
"attemptsMade": 1,
"attempts": 10,
"nextAttempt": "2025-05-14T15:07:45.465Z"
}
}
}
messageFailed
Raised once EmailEngine gives up retrying (max attempts reached):
{
"account": "example",
"date": "2025-05-14T11:58:50.181Z",
"event": "messageFailed",
"data": {
"messageId": "<97ac5d9a-93c7-104b-8d26-6b25f8d644ec@example.com>",
"queueId": "610c2c93e608bd37",
"error": "Error: Invalid login: 535 5.7.8 Error: authentication failed: "
}
}
Bounce Handling
A messageSent event means the account's email server (e.g., Gmail, Outlook) accepted the email for delivery. However, when that server's MTA attempts to deliver the email to the recipient's mail server (MX), the recipient MX may reject it (e.g., mailbox full, user doesn't exist, domain not found).
When this happens, the sender's MTA generates a bounce response email and sends it back to the sender's inbox. This is an informational email that explains why delivery failed. EmailEngine monitors the inbox, detects these bounce emails, parses them to extract the bounced recipient and error details, and triggers a messageBounce webhook event if it can identify which original message bounced.
See:
- Bounce Detection - How EmailEngine detects and processes bounces
- messageBounce Webhook - Webhook payload and handling
Testing Sent Emails
Using Ethereal Email
For testing, use Ethereal Email to create temporary test accounts:
# Send to your Ethereal test address
curl -XPOST "https://emailengine.example.com/v1/account/example/submit" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "address": "test@ethereal.email" }],
"subject": "Test Email",
"text": "Testing EmailEngine sending"
}'
Check Sent Folder
Verify the email was saved to the Sent folder:
curl "https://emailengine.example.com/v1/account/example/messages?path=Sent" \
-H "Authorization: Bearer <token>"
Monitor Queue
Check the outbox queue status. The outbox is a single global queue (it is not scoped per account):
curl "https://emailengine.example.com/v1/outbox" \
-H "Authorization: Bearer <token>"
Common Pitfalls
Authentication Issues
Problem: Gmail, Outlook, and Yahoo may refuse SMTP logins that look like bots.
Solution:
- Gmail: Use OAuth2 or app-specific passwords (account passwords no longer work)
- Outlook: Use OAuth2 (password authentication completely disabled)
- Yahoo: Use OAuth2 or app-specific passwords
See Gmail Setup and Outlook Setup.
Timeout Errors
Problem: Heroku dynos cut idle sockets.
Solution:
- Move off Heroku or increase dyno size
- Use a different deployment platform
Account Not Connected
Problem: Submission rejected with 503 and No active handler for requested account. Try again later.
Solution: The account has not been assigned to a worker yet, which happens shortly after registration and after a restart. Poll /v1/account/:id until state becomes "connected", then retry.
Rate Limiting
Problem: Too many emails sent too quickly.
Solution:
- Implement throttling in your application
- Use mail merge for bulk sending
- Spread sends over time
- Check provider rate limits
Large Attachments
Problem: Email rejected due to size limits.
Solution:
- Reduce attachment size
- Use external file hosting with links
- Compress attachments
- Check provider size limits (typically 25-50MB)
Spam Filters
Problem: Emails flagged as spam.
Solution:
- Provide both HTML and plain text versions
- Avoid spam trigger words
- Include unsubscribe links for bulk email
- Authenticate domain with SPF/DKIM
- Warm up new accounts gradually
Performance Considerations
Optimize for Bulk Sending
For sending multiple emails:
- Use Mail Merge instead of individual calls
- Enable
copy: falseto skip Sent folder storage - Implement rate limiting
- Monitor the outbox queue
Connection Pooling
EmailEngine maintains SMTP connection pools automatically. For high-volume sending:
- Keep accounts connected
- Avoid frequent reconnections
- Monitor connection status
Webhook Processing
Handle webhooks asynchronously:
- Return 200 OK quickly from webhook endpoint
- Process delivery status in background jobs
- Implement webhook retry logic
See Also
- Replies and forwards - Answering a stored message rather than composing one
- Templates - Reusing a subject and body across sends
- Outbox queue - Watching, retrying, and cancelling a queued message
- Sending API - The endpoint reference for every field on this page
- messageSent - The event that confirms the handoff to the server