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:

Shell
export SECURESSMTP_API_KEY="qcs_live_..."
Never put the key in browser JavaScript, a mobile app or a public repository. Anyone who has it can send email as your site.

A send function

securessmtp.go
// 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):

main.go
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.

Go
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.

ResponseMeaningWhat to do
200 ok: trueAccepted for sending.Nothing. Keep message_id if you want to find the email later.
200 rate_limitedMore than 120 sends from this site in 60 seconds.Wait a minute, then try again.
200 over_quotaThe 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_failedThe 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_flaggedThe content was judged to be spam.Change the content. See Deliverability.
401 missing_api_key, invalid_api_keyNo key was sent, or the key is wrong or was rotated.Check the environment variable on the server.
403 site_disabledThe site is disabled.See Blocks.
  • Check ok in 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

SettingValue
Hostsmtp.securessmtp.com
Port587 with STARTTLS, or 465 with SSL/TLS
Usernamesecuressmtp
PasswordYour site’s API key (qcs_live_...)
Go
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.

In relay mode (no verified domain) the From address is replaced: the email goes out from [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 replyMeaning
250Accepted.
535Login failed: the password is not a valid API key.
450 4.7.0Rate limit: more than 120 sends from this site in 60 seconds. Retry later.
550 5.7.0Monthly email limit reached.
550 5.7.1Rejected as spam, the site is disabled, or every recipient has unsubscribed.
550 5.1.1Every recipient is on the suppression list.
552 5.3.4Attachments are over 15 MB in total.
4xx 4.3.0Temporary failure. Retry later.

Next