FORMS · Published 2026-10-06 · Updated 2026-10-06 · SecureSMTP
Forms API
Send submissions from your own form to SecureSMTP: notification emails, auto-replies and spam checks.
Use POST /forms/submit when you have your own form and want SecureSMTP to store each submission, check it for spam and email it to you. It works with any site or framework.
How it works
- The visitor’s browser posts the form to your server.
- Your server calls
POST /forms/submitwith the key, the field values and the visitor’s IP address. - SecureSMTP checks the submission, stores it and emails it to you.
- Your server shows the visitor a thank-you message.
Full example
The form posts to your own /contact route:
<form method="post" action="/contact">
<label>Name <input name="name" required></label>
<label>Email <input name="email" type="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<button type="submit">Send</button>
</form>Your server forwards it to SecureSMTP:
// server.mjs — run with: npm install express && node server.mjs
import express from 'express';
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post('/contact', async (req, res) => {
const field = (name) => String(req.body[name] ?? '').slice(0, 10000);
const r = await fetch('https://securessmtp.com/api/v1/forms/submit', {
method: 'POST',
headers: {
'x-securessmtp-api-key': process.env.SECURESSMTP_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
form_id: 1,
form_name: 'Contact',
fields: { name: field('name'), email: field('email'), message: field('message') },
visitor: {
ip: req.ip ?? '',
user_agent: (req.get('user-agent') ?? '').slice(0, 500),
referrer: (req.get('referer') ?? '').slice(0, 500),
},
}),
});
const result = await r.json();
if (!r.ok) {
console.error('SecureSMTP error', r.status, result.error, result.details);
return res.status(500).send('Sorry, something went wrong. Please try again later.');
}
// 200: stored, blocked or held as spam. Show every visitor the same message.
res.send('Thanks. We got your message.');
});
app.listen(3000);If your server runs behind a proxy or load balancer, make sure the IP you send is the visitor’s, not the proxy’s (in Express, set trust proxy). The IP limit and IP blocks only work when you send it.
Request fields
- form_idintegerrequired
- A positive whole number that names the form on your side. Use a different number for each form.
- form_namestringoptional
- Up to 200 characters. Used in the notification subject: “New form_name submission — site domain”. Default
Form. - fieldsobjectoptional
- Field key to value. Every value must be a string of up to 10,000 characters. Keys are shown as labels in the email (
first_namebecomes “First Name”). - notify_emailsstring[]optional
- Who gets the notification. Up to 10 entries; the first 3 valid addresses are used. If none is valid,
notify_emailis used, then the site’s notify email. - notify_emailstringoptional
- A single recipient. Older form of notify_emails.
- from_namestringoptional
- Sender name of the notification, up to 120 characters. Default: the site’s from name, then the site name.
- visitorobjectoptional
ip(up to 64),user_agent(up to 500),referrer(up to 500). Sendip: the IP limit and blocks use it. Longer values are rejected, so cut them first.- captcha_tokenstringoptional
- The captcha token from the visitor’s browser, up to 6,000 characters. See Spam protection.
- captcha_providerstringoptional
- Which widget you showed. A hint only: the site’s saved captcha setting decides how the token is checked.
- confirmobjectoptional
- Auto-reply to the visitor:
enabled(boolean),subject(up to 200),body(HTML, up to 20,000),from_name. See below. - site_urlstringoptional
- The page’s site URL. Must be a full URL such as https://example.com when sent.
- templatestringoptional
- A label of up to 50 characters, stored with the submission.
Name your email field email. SecureSMTP looks for it to set the notification’s Reply-To and to send the auto-reply. If there is no such key, the first value that looks like an email address is used.
Captcha
Show the captcha widget set for your site, read its token from the posted form and pass it on. With the Turnstile example, the token arrives in the cf-turnstile-response field:
body: JSON.stringify({
form_id: 1,
form_name: 'Contact',
fields: { name: field('name'), email: field('email'), message: field('message') },
visitor: { ip: req.ip ?? '' },
captcha_token: String(req.body['cf-turnstile-response'] ?? ''),
}),- If the captcha provider rejects the token, the answer is
{ "ok": true, "blocked": true }and the submission is not stored. - A submission sent without a token is not checked by the captcha. Always pass the token.
- If the provider cannot be reached, the submission goes through.
Which widget to show and which field holds the token: Spam protection.
Auto-reply
Add confirm to send the visitor an email. It goes out only if enabled is true, the submission contains an email address, the submission is not classified as spam and the notification was not skipped for the monthly limit. It is never sent to a suppressed address or to someone who unsubscribed from your site. Replies go to the first notification recipient.
curl -X POST https://securessmtp.com/api/v1/forms/submit \
-H "x-securessmtp-api-key: $SECURESSMTP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"form_id": 1,
"form_name": "Contact",
"fields": {
"name": "Ana Silva",
"email": "[email protected]",
"message": "Do you deliver on Saturdays?"
},
"notify_emails": [
"[email protected]"
],
"visitor": {
"ip": "203.0.113.7"
},
"confirm": {
"enabled": true,
"subject": "Thanks for contacting {site_name}",
"body": "<p>Hi {name},</p><p>We got your message and will reply soon.</p>",
"from_name": "Example Support"
}
}'Placeholders work in the subject and the body:
| Placeholder | Replaced with |
|---|---|
{name} | The first non-empty value of a field called name, full_name, first_name, your-name or similar; otherwise the first field whose key contains “name”. |
{email} | The submitter’s email address (see below). |
{form_name} | The form name. |
{site_name} | The site’s display name. |
{site_domain} | The site’s domain, for example example.com. |
{field:KEY} | The value of the field with key KEY, for example {field:phone}. |
The auto-reply is sent after the response, so the response does not say whether it went out. If the subject is empty it is “Thanks — site name”.
Responses
Every outcome that the visitor should see as a success comes back as HTTP 200:
| Result | Body | What happened |
|---|---|---|
| Sent | {"ok":true,"id":"…","email_sent":true} | Stored and emailed. |
| Email not sent | {"ok":true,"id":"…","email_sent":false} | Stored. The email failed, or your plan’s monthly form-submission limit is used up. |
| Spam | {"ok":true,"id":"…"} | Stored as spam. No email. There is no email_sent field. |
| Blocked | {"ok":true,"blocked":true} | Not stored, no email. The captcha token was rejected, the IP is blocked, or the IP went over the limit. |
id is the submission’s ID (a UUID). Show the visitor the same message for all four, so bots learn nothing.
Errors use an error field:
| HTTP | error | Meaning |
|---|---|---|
400 | invalid_json | The body is not valid JSON. |
400 | invalid_payload | A field failed validation. details says which. |
400 | no_notify_email_configured | No valid recipient in the request and the site has no notify email. |
401 | missing_api_key, invalid_api_key | No key, or the key is wrong. |
403 | site_disabled | The site is disabled. See Blocks. |
{
"error": "invalid_payload",
"details": {
"formErrors": [],
"fieldErrors": {
"form_id": [
"Required"
]
}
}
}Limits
- Per visitor IP: when an IP already has more than 3 submissions in the last 60 seconds, the next one is dropped and the IP is blocked for 1 hour. You can lift that block yourself — see Blocks.
- Per month: your plan’s form-submission count, for all your sites together. Spam and submissions over the limit do not count. See Plans and limits.
- Field values: 10,000 characters each.
Every field and response is also listed in the API reference.