MailSenpai / SMTP Senpai / API

Developer documentation

SMTP Senpai API and SMTP

Send your software's transactional emails with a REST call that answers in JSON, or over SMTP with a username and password. With the same key you read the sending log and manage the addresses you no longer email. Plus, an address to plug into your website form.

Base URLhttps://app.mailsenpai.com/relay/v1
AuthenticationBearer msp_…
FormatJSON · UTF-8
SMTPrelay.mailsenpai.com:2525 STARTTLS

Trademarks belong to their respective owners.

Page contents
  1. Overview
  2. Authentication
  3. First send in one minute
  4. Node, Python, PHP examples
  5. Endpoint reference
    1. POST /invio
    2. GET /stato
    3. GET /statistiche
    4. GET /eventi
    5. GET /soppressi
    6. POST /sopprimi
    7. POST /riammetti
  6. Errors
  7. Limits
  8. Website forms
  9. SMTP
  10. Best practices
  11. FAQ

Three ways to send

They all lead to the same servers, with the same DKIM signature on your domain, the same log and the same suppression list. Pick the one that fits what you already have.

API REST

One HTTPS call per email, JSON response with the outcome. Ideal for custom software, serverless functions and automations.

SMTP

Server, port, username and password: works with any mail program or library. Supports attachments and multiple recipients.

Website forms

An address for your form's action attribute: every submission reaches you by email. No server-side code.

Before sending you need a verified domain: the customer area shows the DNS records to publish (they sign your emails with DKIM on your domain) and we check them. The sender of every email must be on that domain or one of its subdomains.

Authentication

Every API call carries your key in the header Authorization: Bearer msp_…. The key starts with msp_ and is 52 characters long: you find it in the customer area, SMTP Senpai page, «Key for your applications» box.

HTTP
Authorization: Bearer msp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • If you regenerate it in the customer area, the old one stops working immediately.
  • Keep it on the server, in an environment variable: the API does not accept browser calls (it sends no CORS headers), so the key never ends up in a web page. For website forms use theform endpoint, which needs no key.
  • If your platform cannot set custom headers, you can pass the key in the chiave field of the body (JSON or form). For GET requests it is also accepted as a URL parameter, but this is discouraged: URLs end up in server and proxy logs.
  • SMTP credentials (username and password) are separate from the API key and are changed separately.

Missing or wrong key:

HTTP 401
{
  "ok": false,
  "errore": "chiave non valida"
}

Your first send in one minute

  1. Verify the domain you will send from (customer area, Domains page). If you have just started the trial, it is the domain you entered when signing up.
  2. Copy the key and put it in an environment variable: export SMTPSENPAI_KEY=msp_…
  3. Run this request, with a recipient of yours and a sender on the verified domain:
curl
curl https://app.mailsenpai.com/relay/v1/invio \
  -H "Authorization: Bearer $SMTPSENPAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "from": "orders@yourdomain.com",
    "subject": "Test from SMTP Senpai",
    "text": "It works!"
  }'

If everything is in place you get this response, and within seconds the email is in the inbox:

HTTP 200
{
  "ok": true,
  "inviato": true,
  "id_messaggio": "9f2c4e1a7b3d5f6e8a9b0c1d@yourdomain.com",
  "residuo": 8759,
  "oltre_il_piano": false
}

Keep id_messaggio: it is also the email's Message-ID and helps you find it in the log.

Ready-made examples: Node, Python, PHP

For each language there is a REST API version and an SMTP version. The examples read key, username and password from environment variables: SMTPSENPAI_KEY, SMTPSENPAI_USER, SMTPSENPAI_PASSWORD.

Node.js · fetch
// Node.js 18+: fetch is built in
async function sendEmail() {
  const response = await fetch('https://app.mailsenpai.com/relay/v1/invio', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SMTPSENPAI_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: 'customer@example.com',
      from: 'orders@yourdomain.com',
      from_name: 'Your shop',
      subject: 'Order 10293 confirmed',
      html: '<p>Thanks, your order is confirmed.</p>',
      reply_to: 'support@yourdomain.com',
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw new Error(`${response.status}: ${result.errore}`);
  }
  console.log(result.inviato ? `Sent: ${result.id_messaggio}` : `Not sent: ${result.motivo}`);
}

sendEmail().catch((e) => { console.error(e.message); process.exitCode = 1; });

REST API, no dependencies.

Node.js · Nodemailer
// npm install nodemailer
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  host: 'relay.mailsenpai.com',
  port: 2525,
  secure: false,      // start in plain text...
  requireTLS: true,   // ...and upgrade to STARTTLS right away
  auth: {
    user: process.env.SMTPSENPAI_USER,
    pass: process.env.SMTPSENPAI_PASSWORD,
  },
});

async function sendEmail() {
  const info = await transporter.sendMail({
    from: '"Your shop" <orders@yourdomain.com>',
    to: 'customer@example.com',
    subject: 'Your invoice',
    text: 'Please find your invoice attached.',
    html: '<p>Please find your invoice attached.</p>',
    attachments: [{ filename: 'invoice.pdf', path: './invoice.pdf' }],
  });
  console.log('Accepted:', info.messageId);
}

sendEmail().catch((e) => { console.error(e.message); process.exitCode = 1; });

Over SMTP, with an attachment.

Python · requests
# pip install requests
import os
import requests

response = requests.post(
    "https://app.mailsenpai.com/relay/v1/invio",
    headers={"Authorization": f"Bearer {os.environ['SMTPSENPAI_KEY']}"},
    json={
        "to": "customer@example.com",
        "from": "orders@yourdomain.com",
        "from_name": "Your shop",
        "subject": "Order 10293 confirmed",
        "html": "<p>Thanks, your order is confirmed.</p>",
        "text": "Thanks, your order is confirmed.",
    },
    timeout=30,
)
result = response.json()
if not result["ok"]:
    raise RuntimeError(f"{response.status_code}: {result['errore']}")
print(result["id_messaggio"] if result["inviato"] else result["motivo"])

REST API.

Python · smtplib
import os
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "Your shop <orders@yourdomain.com>"
msg["To"] = "customer@example.com"
msg["Subject"] = "Your invoice"
msg.set_content("Please find your invoice attached.")
msg.add_alternative("<p>Please find your invoice attached.</p>", subtype="html")
with open("invoice.pdf", "rb") as attachment:
    msg.add_attachment(attachment.read(), maintype="application",
                       subtype="pdf", filename="invoice.pdf")

with smtplib.SMTP("relay.mailsenpai.com", 2525, timeout=30) as server:
    server.starttls()
    server.login(os.environ["SMTPSENPAI_USER"], os.environ["SMTPSENPAI_PASSWORD"])
    server.send_message(msg)

Over SMTP, standard library only, with an attachment.

PHP · cURL
<?php
$ch = curl_init('https://app.mailsenpai.com/relay/v1/invio');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('SMTPSENPAI_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => 'customer@example.com',
        'from' => 'orders@yourdomain.com',
        'from_name' => 'Your shop',
        'subject' => 'Order 10293 confirmed',
        'html' => '<p>Thanks, your order is confirmed.</p>',
    ], JSON_UNESCAPED_UNICODE),
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode((string) $body, true);
if (empty($result['ok'])) {
    throw new RuntimeException($status . ': ' . ($result['errore'] ?? 'invalid response'));
}
echo $result['inviato'] ? $result['id_messaggio'] : $result['motivo'];

REST API, no dependencies.

PHP · PHPMailer
<?php
// composer require phpmailer/phpmailer
use PHPMailer\PHPMailer\PHPMailer;

require 'vendor/autoload.php';

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host       = 'relay.mailsenpai.com';
$mail->Port       = 2525;
$mail->SMTPAuth   = true;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Username   = getenv('SMTPSENPAI_USER');
$mail->Password   = getenv('SMTPSENPAI_PASSWORD');
$mail->CharSet    = 'UTF-8';

$mail->setFrom('orders@yourdomain.com', 'Your shop');
$mail->addAddress('customer@example.com');
$mail->Subject = 'Your invoice';
$mail->isHTML(true);
$mail->Body    = '<p>Please find your invoice attached.</p>';
$mail->AltBody = 'Please find your invoice attached.';
$mail->addAttachment('invoice.pdf');
$mail->send();

Over SMTP, with an attachment.

In the customer area, «Ready-made examples» box, you also find the WordPress setup (WP Mail SMTP or FluentSMTP) already filled in with your details.

Endpoint reference

Base URL https://app.mailsenpai.com/relay/v1. Every response is JSON with an ok field. Responses use Italian field names and Italian error messages; /invio also accepts English field names.

POST/relay/v1/invio

Sends an email to one recipient. It does not support attachments, CC and BCC: for those use SMTP.

Fields

FieldRequiredDescription
toyesRecipient: one address per call.
fromyesSender, on a verified domain (or one of its subdomains).
subjectyesSubject. Non-ASCII characters are encoded automatically.
html / textat least oneHTML and/or plain-text body. If you send only HTML, we derive the text version.
from_namenoSender display name.
reply_tonoAddress for replies (Reply-To). Ignored if not valid.
headersnoJSON object of custom headers. Only names starting with X- (e.g. X-Order-Id); others are dropped without error.

The API field names are Italian; each one also has an English alias, used in the examples on this page:

ItalianEnglish
ato
dafrom
nome_mittentefrom_name
oggettosubject
testotext
rispondi_areply_to
intestazioniheaders

The body can be JSON (recommended) or a standard application/x-www-form-urlencoded form.

Example

curl
curl https://app.mailsenpai.com/relay/v1/invio \
  -H "Authorization: Bearer $SMTPSENPAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "from": "orders@yourdomain.com",
    "subject": "Test from SMTP Senpai",
    "text": "It works!"
  }'

Responses

CodeWhen
200 "inviato": trueAccepted: it goes out right away. The residuo field estimates the remaining volume; oltre_il_piano is true if it went out thanks to sending continuity.
200 "inviato": falseThe recipient is on the suppression list: nothing is sent and no volume is used.
400Invalid recipient or sender, missing subject, no body.
403Sender on an unverified domain, or account suspended.
429Monthly volume used up and sending continuity off (or spending cap reached).
502The sending server did not accept the message: the text reports its reply.
HTTP 200, suppressed recipient
{
  "ok": true,
  "inviato": false,
  "motivo": "indirizzo nella lista di soppressione"
}

GET/relay/v1/stato

Account status, SMTP details (without the password), monthly volume and number of suppressed addresses. stato is active or pending (being activated). per_ora is your plan's hourly pace.

curl
curl https://app.mailsenpai.com/relay/v1/stato \
  -H "Authorization: Bearer $SMTPSENPAI_KEY"
HTTP 200
{
  "ok": true,
  "stato": "active",
  "smtp": {
    "server": "relay.mailsenpai.com",
    "porta": 2525,
    "porta_alternativa": 2525,
    "utente": "r1234@yourdomain.com",
    "sicurezza": "STARTTLS"
  },
  "volume": {
    "mese": "2026-10",
    "incluso": 10000,
    "usato": 1240,
    "residuo": 8760,
    "per_ora": 400
  },
  "tracciamento": false,
  "soppressi": 17
}

GET/relay/v1/statistiche

Totals for the last days and day-by-day trend. Parameter giorni: 1 to 90, default 30. The per_giorno detail covers at most the last 30 days and, with no activity, comes as an empty array []. Opens and clicks appear only if you activated tracking.

curl
curl "https://app.mailsenpai.com/relay/v1/statistiche?giorni=7" \
  -H "Authorization: Bearer $SMTPSENPAI_KEY"
HTTP 200
{
  "ok": true,
  "giorni": 7,
  "dati": {
    "sent": 812,
    "delivered": 798,
    "bounce": 9,
    "defer": 14,
    "complaint": 0,
    "tracciamento": "non attivo: ...",
    "tasso_consegna": 98.3,
    "tasso_rimbalzo": 1.11,
    "tasso_segnalazioni": 0.0
  },
  "per_giorno": {
    "2026-09-30": { "delivered": 120, "bounce": 2 },
    "2026-10-01": { "sent": 95, "delivered": 93 }
  }
}

GET/relay/v1/eventi

The latest events, newest first: the detailed log covers 90 days. Parameters: quanti (1 to 500, default 50) and tipo. There is no pagination. Dates are UTC; numbers come as strings.

TypeMeaning
sentAccepted via API or form.
deliveredDelivered to the recipient server.
deferDeferred by the recipient: we retry automatically.
bouncePermanent rejection: the address goes on the suppression list.
complaintSpam report: the address goes on the suppression list.
droppedNot sent because the recipient was already suppressed.
open / clickOpens and clicks, only with tracking active (otherwise asking for them returns 403).
curl
curl "https://app.mailsenpai.com/relay/v1/eventi?tipo=bounce&quanti=20" \
  -H "Authorization: Bearer $SMTPSENPAI_KEY"
HTTP 200
{
  "ok": true,
  "eventi": [
    {
      "event_id": "88213",
      "relay_id": "12",
      "type": "bounce",
      "email": "john@example.com",
      "domain": "example.com",
      "from_domain": "yourdomain.com",
      "code": "550",
      "reason": "5.1.1 user unknown",
      "message_id": "",
      "url": "",
      "happened_at": "2026-10-01 08:42:10"
    }
  ]
}

GET/relay/v1/soppressi

The addresses (and whole domains, written @domain.com) we no longer email. Parameters: quanti (1 to 1000, default 100) and cerca. reason is bounce, complaint or manuale; totale counts all entries, ignoring the filter.

curl
curl "https://app.mailsenpai.com/relay/v1/soppressi?cerca=example.com" \
  -H "Authorization: Bearer $SMTPSENPAI_KEY"
HTTP 200
{
  "ok": true,
  "totale": 2,
  "elenco": [
    { "supp_id": "501", "relay_id": "12", "email": "john@example.com",
      "reason": "bounce", "note": "5.1.1 user unknown", "created_at": "2026-10-01 08:42:10" },
    { "supp_id": "488", "relay_id": "12", "email": "@competitor.com",
      "reason": "manuale", "note": "", "created_at": "2026-09-20 10:00:00" }
  ]
}

POST/relay/v1/sopprimi

Adds an address, or a whole domain (@domain.com or domain.com), to the list. Optional field nota (max 255 characters). Returns {"ok": true}, or {"ok": false} if the entry is invalid or already there.

curl
curl https://app.mailsenpai.com/relay/v1/sopprimi \
  -H "Authorization: Bearer $SMTPSENPAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "john@example.com", "nota": "asked not to be contacted"}'

POST/relay/v1/riammetti

Removes from the list an entry you added yourself. Write it exactly as it appears on the list (domains with the at sign). Addresses that entered because of a bounce or a spam report are re-checked by us: ask for it from the customer area, so no one removes by mistake a block that protects deliverability.

curl
curl https://app.mailsenpai.com/relay/v1/riammetti \
  -H "Authorization: Bearer $SMTPSENPAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "john@example.com"}'

Errors

An error always has "ok": false and an errore field with the explanation in Italian, together with the HTTP status:

CodeMeaningWhat to do
400Missing or invalid data.Fix the request: do not repeat it unchanged.
401Missing or invalid key.Check the Authorization header; if you regenerated the key, update the variable.
403Sender domain not verified, tracking not active, or account suspended (the message says why: plan expired or unpaid, volume used up, spending cap).Verify the domain or check your plan in the customer area.
404Unknown address (endpoint or form).Check the path.
429Monthly volume used up without sending continuity.Raise the volume or turn on continuity: the response contains links to the right pages.
502The sending server refused or could not be reached.Retry after a few seconds, with increasing waits. Before retrying, check /eventi that the email did not already go out.
HTTP 429
{
  "ok": false,
  "errore": "volume del mese esaurito: aumenta il piano o accendi la continuità di invio dalla tua area",
  "residuo": 0,
  "aumenta": "https://app.mailsenpai.com/customer/il-mio-piano",
  "pannello": "https://app.mailsenpai.com/customer/relay-smtp"
}

Limits

WhatLimit
Recipients per API call1 (over SMTP you can address several in one message)
VolumeYour plan's, per calendar month. Emails that reached an outcome count (delivered or bounced); those held by suppression do not. We email you before it runs out.
Volume used upWith sending continuity on you continue in blocks, up to the spending cap you choose; with continuity off sending stops until next month or until you upgrade.
Hourly paceYour plan's (field per_ora of /stato). Above that pace messages wait in the queue and go out as soon as possible; with a new dedicated IP the pace ramps up during the first weeks.
API requestsNo declared rate limit: the plan volume still applies.
SMTP message size50 MB
Detailed log90 days (/eventi, up to 500 rows per call); totals are kept longer.
StatisticsUp to 90 days; daily trend up to 30.
IdempotencyThere is no idempotency key: every call to /invio is a new send.
Website forms25 forms per account, 3 recipients per form (+1 with _cc), 60 fields of 5,000 characters, 25 submissions per hour from the same network.

Website forms

For websites that cannot send email (static pages, themes without plugins, hosting that blocks mail). Create the form in the customer area, copy its address and put it in the action attribute of your form, with method="POST". No key needed.

HTML
<form action="https://app.mailsenpai.com/relay/f/CODICE" method="POST">
  <label>Name <input type="text" name="name" required></label>
  <label>Email <input type="email" name="email" required></label>
  <label>Message <textarea name="message" required></textarea></label>

  <input type="hidden" name="_subject" value="New enquiry from the website">
  <input type="hidden" name="_next" value="https://www.yoursite.com/thank-you/">
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">

  <button type="submit">Send</button>
</form>

Replace CODICE with your form's code. Every submission reaches you by email, laid out, with the Reply-To of the person who wrote: hit «reply» and you write back.

Special fields

FieldWhat it does
_subjectSubject of the email you receive. Otherwise the one set in the form is used.
_replytoReply address. If missing, we use the first field with «mail» in its name that contains a valid address.
_nextThank-you page (full address, http or https) for classic submissions. Otherwise the form's one, or we show a confirmation page.
_ccA copy to another address. Works only if allowed in the form settings.
_gotchaHoneypot: keep it hidden and empty. If a bot fills it, the submission is silently discarded.
_formatWith the value plain you receive the email as plain text.

Other names starting with _ are ignored. Files attached to the form are not forwarded.

Submitting with JavaScript

If you submit the form with fetch and the header Accept: application/json (or a JSON body), the response is JSON and you can stay on the page. The address accepts requests from any website (CORS): to restrict it to yours, list your domains in the form settings.

JavaScript
const form = document.querySelector('form[action*="/relay/f/"]');

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const response = await fetch(form.action, {
    method: 'POST',
    body: new FormData(form),
    headers: { Accept: 'application/json' },
  });
  const result = await response.json();
  if (result.ok) {
    form.replaceWith('Thanks, we will get back to you soon.');
  } else {
    alert(result.errore);
  }
});
ResponseWhen
200 {"ok": true, ...}Received and forwarded. With a classic submission: 303 redirect to _next or a confirmation page.
403Form paused, service not active, or website not among the allowed domains.
404Unknown form code.
409No verified sending domain yet.
422The form arrived with no fields filled in.
429Too many submissions from the same network in the last hour, or monthly volume used up.

Form emails are sent from moduli@yourdomain.com, with the form name as sender, and count towards the monthly volume like all the others. Optionally, whoever fills in the form gets an automatic reply in your words, and every submission is archived in the customer area.

SMTP

For programs that already have an «SMTP server» screen (ERPs, e-commerce, CRMs, WordPress) and when you need attachments or multiple recipients.

SettingValue
Hostrelay.mailsenpai.com
Port2525 (the one shown in your area, «porta» field of /stato)
EncryptionSTARTTLS (mandatory: login is only possible after STARTTLS). Plugins often call it «TLS».
AuthenticationLOGIN / PLAIN with the username and password from your area
Maximum size50 MB per message, attachments included
Other8BITMIME, SMTPUTF8

What happens over SMTP

  • Sender (From) on an unverified domain: the message is rejected with 550 5.7.1.
  • Recipient on the suppression list: the server answers OK, so your program does not error, but the email is not sent, uses no volume and shows in the log as dropped.
  • Wrong credentials: 535 5.7.8.
  • SMTP sends end up in the same log and statistics as the API.

Best practices

  • Publish all the DNS records shown in your area: the DKIM signature on your domain and SPF tell providers the emails are really yours. Add a DMARC record, starting with p=none.
  • Always send from an address on the verified domain and use reply_to if replies must go elsewhere.
  • Always include a text version, or let us derive it from the HTML: image-only or HTML-only emails perform worse.
  • Let suppression do its job: hard bounces and spam reports are removed from sending automatically. Add yourself anyone who asks not to be contacted.
  • Retry only on temporary errors (502), with increasing waits; never on 400, 401 and 403.
  • Use X-… headers to link the email to your system (order number, user id) and store id_messaggio.
  • Check /statistiche now and then: a rising bounce rate is the first sign of a list that needs cleaning.
  • Keep service emails separate from promotional ones: for campaigns there is the MailSenpai platform.

Frequently asked questions

Can I send attachments with the API?

Not with the REST API: the /invio endpoint accepts text and HTML. For attachments use SMTP, up to 50 MB per message (see the Nodemailer, smtplib and PHPMailer examples).

How do I send the same email to several people?

With the API you make one call per recipient: it is also the right way for transactional emails, because everyone gets their own and outcomes are separate. CC and BCC are available only over SMTP.

Are there webhooks for deliveries and bounces?

Not yet. To know the outcome read /eventi (for example every few minutes, filtering by type) or /statistiche for totals.

Is there a sandbox?

There is no separate sandbox: the 14-day free trial gives you a real account to send to your own addresses. The test button in the area sends a verification email with your credentials.

Can I call the API from the browser?

No, by design: the key would let anyone send on your behalf. Call the API from your server or a serverless function. For forms use the form endpoint, which is built for that.

Do emails go out immediately?

Yes: the 200 response arrives once the sending servers have taken the message, and they deliver it right away within your plan's hourly pace. If the recipient server defers, we retry automatically.

What happens when the monthly volume runs out?

We email you before it happens. If you turned on sending continuity you continue in blocks up to the spending cap you chose; otherwise sending stops (the API returns 429) until you raise the volume or the new month starts.

Are emails DKIM-signed?

Yes, on your domain, both via API and SMTP, once you publish the DNS records shown in the customer area.

Can I use SMTP Senpai with Zapier, Make or n8n?

Yes, with the platform's generic HTTP module: POST method, /relay/v1/invio address, Authorization header with your key and a JSON body as in the examples. Where there is an SMTP module, you can use that too.

Try it with your domain

14 days free, then you decide. API key, SMTP credentials and forms are ready as soon as the domain is verified.