INTEGRATIONS · Published 2026-10-06 · Updated 2026-10-06 · SecureSMTP
Go
A send function with net/http.
A send function with net/http and encoding/json from the standard library — there is no SecureSMTP module to install. Or send over SMTP with net/smtp.
Setup
Every request needs your site’s API key. Get it when you add the site under Sites (/app/forms/sites) in the dashboard — see Sites and API keys. Keep it in an environment variable on the server:
export SECURESSMTP_API_KEY="qcs_live_..."A send function
// securessmtp/securessmtp.go
package securessmtp
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const apiURL = "https://securessmtp.com/api/v1/mail/send"
type From struct {
Name string `json:"name,omitempty"`
}
type Attachment struct {
Filename string `json:"filename"`
Content string `json:"content"` // base64
ContentType string `json:"content_type,omitempty"`
}
type Message struct {
To []string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html,omitempty"`
Text string `json:"text,omitempty"`
Cc []string `json:"cc,omitempty"`
Bcc []string `json:"bcc,omitempty"`
ReplyTo string `json:"reply_to,omitempty"`
From *From `json:"from,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
}
type Result struct {
OK bool `json:"ok"`
Sent bool `json:"sent"`
Held bool `json:"held"`
Mode string `json:"mode"`
MessageID string `json:"message_id"`
Reason string `json:"reason"`
Error string `json:"error"`
}
// Error is returned when the API answers with "ok": false.
type Error struct {
Status int // HTTP status
Reason string // e.g. rate_limited, over_quota, invalid_payload
Detail string // the "error" field, set for send_failed
}
func (e *Error) Error() string {
return fmt.Sprintf("securessmtp: %s (HTTP %d) %s", e.Reason, e.Status, e.Detail)
}
var client = &http.Client{Timeout: 30 * time.Second}
func Send(ctx context.Context, msg Message) (*Result, error) {
body, err := json.Marshal(msg)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("x-securessmtp-api-key", os.Getenv("SECURESSMTP_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return nil, err // network error or timeout
}
defer res.Body.Close()
var result Result
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("securessmtp: HTTP %d, body is not JSON: %w", res.StatusCode, err)
}
if !result.OK {
reason := result.Reason
if reason == "" {
reason = result.Error
}
return nil, &Error{Status: res.StatusCode, Reason: reason, Detail: result.Error}
}
return &result, nil
}Use it from your app (change the import path to your module):
package main
import (
"context"
"errors"
"log"
"example.com/yourapp/securessmtp"
)
func main() {
res, err := securessmtp.Send(context.Background(), securessmtp.Message{
To: []string{"[email protected]"},
Subject: "Your order has shipped",
HTML: "<p>Your order is on its way.</p>",
Text: "Your order is on its way.",
From: &securessmtp.From{Name: "Acme Store"},
ReplyTo: "[email protected]",
})
var apiErr *securessmtp.Error
switch {
case errors.As(err, &apiErr) && apiErr.Reason == "rate_limited":
log.Println("rate limited: wait a minute, then retry")
case errors.As(err, &apiErr):
log.Printf("not sent: %v", apiErr)
case err != nil:
log.Printf("request failed (the email may or may not have been sent): %v", err)
default:
log.Println("sent:", res.MessageID)
}
}From is an object; only its name is used. Every field is listed in the API reference.
Attachments
Send the file content as base64 (add context, encoding/base64 and os to your imports). Up to 20 files and 15 MB in total per email.
pdf, err := os.ReadFile("invoice-1042.pdf")
if err != nil {
log.Fatal(err)
}
_, err = securessmtp.Send(context.Background(), securessmtp.Message{
To: []string{"[email protected]"},
Subject: "Invoice 1042",
Text: "Your invoice is attached.",
Attachments: []securessmtp.Attachment{{
Filename: "invoice-1042.pdf",
Content: base64.StdEncoding.EncodeToString(pdf),
ContentType: "application/pdf",
}},
})Handle errors
Send returns *securessmtp.Error when the API answers "ok": false, and a plain error for network problems and timeouts. Use errors.As to tell them apart, as in the example above.
| Response | Meaning | What to do |
|---|---|---|
200 ok: true | Accepted for sending. | Nothing. Keep message_id if you want to find the email later. |
200 rate_limited | More than 120 sends from this site in 60 seconds. | Wait a minute, then try again. |
200 over_quota | The account’s monthly email count is used up (relay mode only). | Wait for the new month, upgrade, or send from a verified domain. |
200 send_failed | The email was not sent. The error field says why. | Read error. Do not retry in a loop. |
400 invalid_payload, no_body, … | The request is wrong. details lists the fields for invalid_payload. | Fix the request. Retrying the same request fails the same way. |
400 content_flagged, ai_flagged | The content was judged to be spam. | Change the content. See Deliverability. |
401 missing_api_key, invalid_api_key | No key was sent, or the key is wrong or was rotated. | Check the environment variable on the server. |
403 site_disabled | The site is disabled. | See Blocks. |
- Check
okin the body, not only the HTTP status. Rate limit, monthly limit and send failures come back with HTTP 200. - There is no idempotency key. If a request times out, the email may already have been sent, and sending it again can deliver it twice.
- Every reason is listed in the error catalogue.
Or send over SMTP with net/smtp
| Setting | Value |
|---|---|
| Host | smtp.securessmtp.com |
| Port | 587 with STARTTLS, or 465 with SSL/TLS |
| Username | securessmtp |
| Password | Your site’s API key (qcs_live_...) |
package main
import (
"errors"
"log"
"net/smtp"
"net/textproto"
"os"
"strings"
"time"
)
func main() {
host := "smtp.securessmtp.com"
auth := smtp.PlainAuth("", "securessmtp", os.Getenv("SECURESSMTP_API_KEY"), host)
msg := strings.Join([]string{
"From: Acme Store <[email protected]>",
"To: [email protected]",
"Subject: Your order has shipped",
"Date: " + time.Now().Format(time.RFC1123Z),
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=UTF-8",
"",
"Your order is on its way.",
}, "\r\n")
// SendMail upgrades the connection with STARTTLS before it logs in.
err := smtp.SendMail(host+":587", auth, "[email protected]",
[]string{"[email protected]"}, []byte(msg))
var tpErr *textproto.Error
if errors.As(err, &tpErr) {
log.Fatalf("rejected: %d %s", tpErr.Code, tpErr.Msg) // e.g. 450, 535, 550
} else if err != nil {
log.Fatal(err)
}
}smtp.SendMail starts TLS with STARTTLS on port 587 before it sends the password, and PlainAuth refuses to log in without TLS. A refused message comes back as a *textproto.Error with the reply code.
[email protected] with your From name, and the From address you set becomes the Reply-To. Verify your domain and the email goes out from your own address. See Domains and DNS.| SMTP reply | Meaning |
|---|---|
250 | Accepted. |
535 | Login failed: the password is not a valid API key. |
450 4.7.0 | Rate limit: more than 120 sends from this site in 60 seconds. Retry later. |
550 5.7.0 | Monthly email limit reached. |
550 5.7.1 | Rejected as spam, the site is disabled, or every recipient has unsubscribed. |
550 5.1.1 | Every recipient is on the suppression list. |
552 5.3.4 | Attachments are over 15 MB in total. |
4xx 4.3.0 | Temporary failure. Retry later. |
Next
- Every field and response: POST /mail/send.
- SMTP details: SMTP.
- Get delivery and bounce events: Webhooks.