API REFERENCE · Published 2026-10-06 · Updated 2026-10-06 · SecureSMTP

Send an email

POST /api/v1/mail/send

POSThttps://securessmtp.com/api/v1/mail/send

Sends one email from the site the key belongs to. The email is checked for spam, suppressed addresses and unsubscribes, sent, and written to the email log.

Always read ok in the body. Rate limit, monthly limit and send failures come back with HTTP 200 and "ok": false.

Headers

x-securessmtp-api-keystringrequired
The site’s API key (qcs_live_…). x-securesmtp-api-key and x-qcs-api-key are accepted too. See Sites and API keys.
Content-Typestringrequired
application/json

Body

Unknown fields are ignored.

tostring | string[]required
One email address, or an array of 1–50 addresses.
subjectstringoptional
Up to 500 characters. If empty, the template’s subject is used (when you send a template), otherwise Message from <site name>.
htmlstringoptional
HTML body, up to 500,000 characters. Send html, text or both (or a template). If one is missing it is made from the other, so every email has both parts.
textstringoptional
Plain-text body, up to 500,000 characters.
ccstring | string[]optional
One address or an array of 1–50 addresses.
bccstring | string[]optional
One address or an array of 1–50 addresses.
reply_tostring | string[]optional
One address or up to 50 (each up to 320 characters). If you leave it out in relay mode, Reply-To is set to original_from.
fromobjectoptional
{ "name": "…", "address": "…" }. A plain string is rejected with invalid_payload. Only from.name (up to 200 characters) is used, as the sender name; if it is empty we use the site’s default From name, then the site name. from.address (up to 320 characters) is accepted but ignored: the From address is [email protected] in relay mode, or the verified domain’s From address in domain mode.
original_fromstringoptional
Up to 320 characters. The address the email was originally from, for example the visitor who filled in a form. In relay mode it becomes the Reply-To when you do not send reply_to.
headersobjectoptional
Extra headers as { "Name": "value" }. Names up to 120 characters, values up to 2,000. We always set Feedback-ID. When the email has exactly one recipient in total (to + cc + bcc) and you did not send your own List-Unsubscribe, we add one-click List-Unsubscribe and List-Unsubscribe-Post. See Unsubscribe links.
sender_mode'auto' | 'relay' | 'domain'optional
Default auto. auto and domain send from your own domain when the site has a verified sending domain with a From address; otherwise they send in relay mode without an error. relay always uses relay mode. The mode actually used comes back in mode. See Domains and DNS.
source_pluginstringoptional
Up to 120 characters. A label shown in the email log, for example orders-service. Some values mark the email as a contact-form notification, which gets an extra spam check (see Notes).
templatestringoptional
Up to 120 characters. The ID or slug of a template you own, or of a starter template. The template’s HTML and text replace html and text. Its subject is used only when subject is empty. See Templates and variables.
dataobjectoptional
Values for the template’s {{variables}}: keys up to 64 characters, values string, number or boolean. Values are HTML-escaped in the HTML part. A variable with no value becomes empty.
attachmentsobject[]optional
Up to 20 files, 15 MB in total after decoding. Each item:
  • filename (string, required, 1–255 characters)
  • content (string, required) — the file as base64
  • content_type (string, optional, up to 200 characters), e.g. application/pdf
  • content_id (string, optional, up to 200 characters) — makes the file an inline image you reference in the HTML as cid:<content_id>

Request

curl -X POST https://securessmtp.com/api/v1/mail/send \
  -H "x-securessmtp-api-key: $SECURESSMTP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "[email protected]",
  "subject": "Your order #1042 has shipped",
  "html": "<p>Hi Sam,</p><p>Your order #1042 is on its way.</p>",
  "text": "Hi Sam,\n\nYour order #1042 is on its way.",
  "from": {
    "name": "Acme Store"
  },
  "reply_to": "[email protected]",
  "source_plugin": "orders-service"
}'

With a template

curl -X POST https://securessmtp.com/api/v1/mail/send \
  -H "x-securessmtp-api-key: $SECURESSMTP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "[email protected]",
  "template": "order-shipped",
  "data": {
    "first_name": "Sam",
    "order_number": 1042
  }
}'

With an attachment

curl -X POST https://securessmtp.com/api/v1/mail/send \
  -H "x-securessmtp-api-key: $SECURESSMTP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "[email protected]",
  "subject": "Invoice 1042",
  "text": "Your invoice is attached.",
  "attachments": [
    {
      "filename": "invoice-1042.pdf",
      "content": "JVBERi0xLjcKJ...",
      "content_type": "application/pdf"
    }
  ]
}'

Response

200
{
  "ok": true,
  "sent": true,
  "mode": "relay",
  "message_id": "<[email protected]>"
}
Response fields
okbooleanrequired
true when the email was accepted.
sentbooleanrequired
Same as ok.
mode'relay' | 'domain'optional
The sender mode used. Only on success.
message_idstringoptional
The Message-ID of the email. Only on success.
heldbooleanoptional
true when a contact-form notification was held as spam and not sent. Comes with reason: "held_as_spam", ok: true and sent: true.
reasonstringoptional
Why the email was not sent. See Errors.
errorstringoptional
With send_failed: the failure text.
detailsobjectoptional
With invalid_payload: which fields failed and why.
filenamestringoptional
With invalid_attachment: the file that could not be decoded.

Errors

Errors have "ok": false, "sent": false and a reason. Checks run in the order below; the first one that fails answers.

HTTPValueMeaningWhat to do
401missing_api_keyNo key header was sent.Send the key in the x-securessmtp-api-key header.
401invalid_api_keyNo site has this key. Keys stop working as soon as they are rotated.Copy the current key from the dashboard, or rotate it to get a new one.
403site_disabledThe site is disabled.Check GET /blocks/status. See Blocks.
400invalid_jsonThe body is not valid JSON.Send a JSON body with Content-Type: application/json.
400invalid_payloadA field is missing, has the wrong type or is too long. details says which.Fix the fields listed in details. A common cause is from sent as a string.
400template_not_foundNo template with this ID or slug belongs to you, and no starter template matches.Check the ID or slug in the dashboard under Templates.
400no_bodyNeither html nor text was sent (or the template has neither).Send html, text or a template that has content.
400invalid_attachmentAn attachment’s content is not valid base64. filename says which.Base64-encode the file content.
400attachments_too_largeAttachments add up to more than 15 MB after decoding.Send fewer or smaller files, or send a link instead.
200rate_limitedThe site has 120 or more emails in the email log from the last 60 seconds.Wait a minute and retry. Spread large batches out.
400content_flaggedThe subject and body scored as spam. Logged as flagged.Change the content. See Deliverability.
400ai_flaggedOur spam classifier judged the email as spam. Logged as flagged.Change the content. See Deliverability.
200over_quotaThe account reached its monthly email limit (relay mode only).Upgrade, wait for the next month, or send from a verified domain. See Plans and limits.
200send_failedThe email could not be sent. error gives the reason (see below).Read error. Retry later for temporary failures.

send_failed includes these error values:

  • all_recipients_suppressed — every to address is on the suppression list. See Bounces and suppressions.
  • all_recipients_unsubscribed — every to address unsubscribed from this site. See Unsubscribe links.
  • Any other text — the mail server did not accept the email.

All error values are listed in the Error catalogue.

Notes

  • Suppressions and unsubscribes. Suppressed addresses, and addresses that unsubscribed from this site, are removed from to before sending; the rest still get the email. cc and bcc are not checked.
  • Held contact-form spam. When source_plugin is a known form plugin (for example contact-form-7, wpforms, gravityforms) or the subject looks like a contact-form notification, the message gets a contact-form spam check. Spam at 90% confidence or more is not sent and is logged as flagged, but the answer is { "ok": true, "sent": true, "held": true, "reason": "held_as_spam" } so the WordPress plugin does not send it another way. See Spam protection.
  • Spam checks. Content that scores 30–59 is sent and marked with a warning in the log; 60 or more is refused. Apart from contact-form notifications, the classifier checks every email for sites with fewer than 100 emails in the log, and about 1 in 20 after that (chosen by subject).
  • Limits. The monthly email count is per account (all sites together), counts relay-mode emails only, and resets on the first day of each month (UTC). Emails sent from a verified domain do not count.
  • No idempotency key. If you retry after a timeout, the email may be sent twice.
  • The first successful send marks the site as verified.