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-keyandx-qcs-api-keyare 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,textor both (or atemplate). 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 withinvalid_payload. Onlyfrom.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 setFeedback-ID. When the email has exactly one recipient in total (to + cc + bcc) and you did not send your ownList-Unsubscribe, we add one-clickList-UnsubscribeandList-Unsubscribe-Post. See Unsubscribe links. - sender_mode'auto' | 'relay' | 'domain'optional
- Default
auto.autoanddomainsend 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.relayalways uses relay mode. The mode actually used comes back inmode. 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
htmlandtext. Its subject is used only whensubjectis 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 base64content_type(string, optional, up to 200 characters), e.g.application/pdfcontent_id(string, optional, up to 200 characters) — makes the file an inline image you reference in the HTML ascid:<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
truewhen 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
truewhen a contact-form notification was held as spam and not sent. Comes withreason: "held_as_spam",ok: trueandsent: 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.
| HTTP | Value | Meaning | What to do |
|---|---|---|---|
| 401 | missing_api_key | No key header was sent. | Send the key in the x-securessmtp-api-key header. |
| 401 | invalid_api_key | No 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. |
| 403 | site_disabled | The site is disabled. | Check GET /blocks/status. See Blocks. |
| 400 | invalid_json | The body is not valid JSON. | Send a JSON body with Content-Type: application/json. |
| 400 | invalid_payload | A 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. |
| 400 | template_not_found | No template with this ID or slug belongs to you, and no starter template matches. | Check the ID or slug in the dashboard under Templates. |
| 400 | no_body | Neither html nor text was sent (or the template has neither). | Send html, text or a template that has content. |
| 400 | invalid_attachment | An attachment’s content is not valid base64. filename says which. | Base64-encode the file content. |
| 400 | attachments_too_large | Attachments add up to more than 15 MB after decoding. | Send fewer or smaller files, or send a link instead. |
| 200 | rate_limited | The site has 120 or more emails in the email log from the last 60 seconds. | Wait a minute and retry. Spread large batches out. |
| 400 | content_flagged | The subject and body scored as spam. Logged as flagged. | Change the content. See Deliverability. |
| 400 | ai_flagged | Our spam classifier judged the email as spam. Logged as flagged. | Change the content. See Deliverability. |
| 200 | over_quota | The 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. |
| 200 | send_failed | The 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— everytoaddress is on the suppression list. See Bounces and suppressions.all_recipients_unsubscribed— everytoaddress 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
tobefore sending; the rest still get the email.ccandbccare not checked. - Held contact-form spam. When
source_pluginis a known form plugin (for examplecontact-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.