Start With the Local API
This walkthrough shows how to connect a mail server or application to SpamFoo Client's local API: send a message, read the result, act on it, and check that the service is healthy. For every field and header, see the API Reference.
Before You Start
- SpamFoo Client is installed and running. See Install SpamFoo Client.
curl http://localhost:16253/health/readyon the server running SpamFoo Client returns200.- Your code can read each incoming message as raw bytes, exactly as it was received.
Know the Basics
- Base URL:
http://localhost:16253 - Request body: the raw message, also called raw .eml content. This is the full RFC 5322 message, headers and body, as it arrived over SMTP. Send it with
Content-Type: message/rfc822. - Delivery details: information that is not inside the message, such as the connecting IP address, goes in
X-request headers. - Responses: JSON with camelCase field names.
- Endpoints: use the
-streamendpoints, which take the message in the request body. SpamFoo Client does not read message files from disk on your behalf.
Authenticate Your Requests
SpamFoo Client checks who is calling with a shared secret called the proxy secret, sent in the X-SpamFoo-Proxy-Auth header. Send this secret on every request.
The samples on this page read the secret from the SPAMFOO_PROXY_SECRET environment variable on the calling machine. See Authenticate Requests for which requests need it, where to find it, and what happens when it changes.
SpamFoo Client listens on localhost only by default. To accept requests from another machine, set SPAMFOO_BIND_ADDRESS as described in Configure SpamFoo Client, and send the secret on every request.
Classify a Message
/classify-streamSend the raw message as the body. All headers are optional, but each one you leave out gives SpamFoo Client less to work with.
| Header | Value |
|---|---|
X-Sender-IP |
IP address of the server that connected to your mail server to deliver the message. Used for authentication checks and IP reputation. |
X-Sender-Email |
Envelope sender, the address from the SMTP MAIL FROM command. Used for authentication checks and to match allow and block rules. |
X-User-ID |
The recipient mailbox, as a full email address. Turns on personalization for that user. See Identify Users. |
X-Recipient-Email |
The recipient address. Used to match domain-level allow and block rules. |
X-SpamFoo-Log-ID |
Your own ID for this message, such as a queue ID. It appears in SpamFoo Client's classification log so you can match log lines to your own. Up to 64 letters, digits, and . _ : -; other values are ignored. |
X-SpamFoo-Classify |
Send false to get only a spam verdict with no Email Classification. Omit it, or send true, to get both. |
X-SpamFoo-Proxy-Auth |
The proxy secret. Required in standalone mode, and for any request that does not come from loopback. |
Some optional headers are for mail servers that already check SPF, DKIM, and DMARC, or that classify old mail in bulk. See Classify a Message.
curl -X POST http://localhost:16253/classify-stream \
-H "Content-Type: message/rfc822" \
-H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
-H "X-Sender-IP: 203.0.113.50" \
-H "X-Sender-Email: [email protected]" \
-H "X-User-ID: [email protected]" \
-H "X-Recipient-Email: [email protected]" \
--data-binary @message.eml
using System.Net.Http.Headers;
using System.Net.Http.Json;
using var http = new HttpClient
{
BaseAddress = new Uri("http://localhost:16253"),
Timeout = TimeSpan.FromSeconds(60)
};
var result = await ClassifyAsync(http, "message.eml");
Console.WriteLine(result is null
? "No verdict. Deliver normally."
: $"isSpam={result.IsSpam} classification={result.Classification}");
static async Task<ClassifyResult?> ClassifyAsync(HttpClient http, string emlPath)
{
await using var eml = File.OpenRead(emlPath);
var content = new StreamContent(eml);
content.Headers.ContentType = new MediaTypeHeaderValue("message/rfc822");
using var request = new HttpRequestMessage(HttpMethod.Post, "/classify-stream") { Content = content };
request.Headers.Add("X-SpamFoo-Proxy-Auth", Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET"));
request.Headers.Add("X-Sender-IP", "203.0.113.50");
request.Headers.Add("X-Sender-Email", "[email protected]");
request.Headers.Add("X-User-ID", "[email protected]");
request.Headers.Add("X-Recipient-Email", "[email protected]");
try
{
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadFromJsonAsync<ClassifyResult>();
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
return null;
}
}
record ClassifyResult(
bool IsSpam,
string Classification,
double SpamProbability,
double? DisplayConfidence,
bool ShouldEscalate,
string? BypassReason);
import os
import requests
SPAMFOO_URL = "http://localhost:16253"
def classify(eml_path):
headers = {
"Content-Type": "message/rfc822",
"X-SpamFoo-Proxy-Auth": os.environ.get("SPAMFOO_PROXY_SECRET", ""),
"X-Sender-IP": "203.0.113.50",
"X-Sender-Email": "[email protected]",
"X-User-ID": "[email protected]",
"X-Recipient-Email": "[email protected]",
}
try:
with open(eml_path, "rb") as eml:
response = requests.post(f"{SPAMFOO_URL}/classify-stream",
data=eml, headers=headers, timeout=60)
except requests.RequestException:
return None
if not response.ok:
return None
return response.json()
result = classify("message.eml")
print("No verdict. Deliver normally." if result is None
else f"isSpam={result['isSpam']} classification={result['classification']}")
Each sample returns no result when SpamFoo Client is busy, stopped, or returns an error, so the caller can deliver the message normally.
Read the Result
A successful request returns 200 with a JSON body. This example is shortened to the fields most integrations use:
{
"isSpam": false,
"classification": "updates",
"spamProbability": 0.04,
"displayConfidence": 0.06,
"shouldEscalate": false,
"bypassReason": null
}
| Field | Meaning |
|---|---|
isSpam |
The spam verdict. Base your delivery decision on this field. |
classification |
For mail that is not spam: primary, transactions, updates, or promotions.
For spam: spam, or phishing when SpamFoo Client detected phishing.
Empty when no Email Classification was made, for example when an allow or block rule decided the message or the request sent X-SpamFoo-Classify: false.
Compare values without regard to case.
|
spamProbability |
The raw spam score from 0 to 1. The spam threshold is not fixed at 0.5, so do not compare this value to 0.5 to decide spam. Use isSpam. |
displayConfidence |
The spam score rescaled so the spam threshold sits at 0.5: spam is 0.5 or higher, and other mail is 0.5 or lower. Use it when you show a score to people. It is null when a rule or personalization decided the message. |
shouldEscalate |
true when the models were unsure or disagreed with each other. You can use it to flag a message for review. |
bypassReason |
What decided the message when it was not the global models, such as system_override (an allow or block rule from the admin console) or knn_memory (the user's own past corrections). null when the global models decided. |
The response has more fields, including authentication scores and phishing details. See Classify a Message for the full list.
Act on the Result
- Spam: when
isSpamistrue, move the message to the junk folder, or handle it the way your server handles spam. You may want to treatphishingmore strictly than other spam. - Not spam: when
isSpamisfalse, deliver the message. If your mail client shows classification tabs or folders, file it byclassification. - No classification: when
classificationis empty and the message is not spam, deliver it to the inbox. - No result: when the request fails, deliver the message as if SpamFoo Client were not installed.
- Remember the verdict: store
isSpamandclassificationwith the message if you can. You send them back as the original verdict when a user corrects the message. See Send Corrections Back.
This shell example uses jq to read the result and pick a folder.
result=$(curl -s -f -X POST http://localhost:16253/classify-stream \
-H "Content-Type: message/rfc822" \
-H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
-H "X-User-ID: [email protected]" \
--data-binary @message.eml) || result=""
if [ -z "$result" ]; then
folder="Inbox"
elif [ "$(echo "$result" | jq -r '.isSpam')" = "true" ]; then
folder="Junk"
else
case "$(echo "$result" | jq -r '.classification | ascii_downcase')" in
transactions) folder="Transactions" ;;
updates) folder="Updates" ;;
promotions) folder="Promotions" ;;
*) folder="Inbox" ;;
esac
fi
echo "$folder"
static string ChooseFolder(ClassifyResult? result)
{
if (result is null)
return "Inbox";
var classification = result.Classification?.ToLowerInvariant() ?? "";
if (result.IsSpam)
return classification == "phishing" ? "Quarantine" : "Junk";
return classification switch
{
"transactions" => "Transactions",
"updates" => "Updates",
"promotions" => "Promotions",
_ => "Inbox"
};
}
def choose_folder(result):
if result is None:
return "Inbox"
classification = (result.get("classification") or "").lower()
if result["isSpam"]:
return "Quarantine" if classification == "phishing" else "Junk"
return {
"transactions": "Transactions",
"updates": "Updates",
"promotions": "Promotions",
}.get(classification, "Inbox")
Handle Errors
| Status | Meaning | What to Do |
|---|---|---|
400 |
The body was empty, or X-SpamFoo-Classify was not true or false. |
Fix the request. Deliver the message normally. |
401 |
The proxy secret was missing or wrong, and the request needed it. | Send the correct X-SpamFoo-Proxy-Auth header. |
403 |
The license does not include the feature this request needs, or, in standalone mode, the license has expired. The body is {"error": "feature_not_licensed", ...}. |
Deliver the message without classification. See Know How Licenses Affect Requests. |
413 |
The message is larger than the configured size limit. | Deliver the message without classification. |
429 |
Standalone mode only. Too many requests from this address sent a wrong secret, so it is blocked for a while. The Retry-After header says how many seconds are left. |
Fix the secret. Deliver the message without classification. |
503 |
SpamFoo Client is busy. The code field says why: queue_timeout when the request waited too long in the classification queue, or server_busy when too many requests were already in progress. The response includes a Retry-After header. |
Deliver the message without classification. Retry only if your mail flow can wait. |
Identify Users for Personalization
Personalization means SpamFoo Client adjusts results for each mailbox based on that user's own corrections. It only works when SpamFoo Client knows which user a message belongs to.
- Send the recipient's full email address in
X-User-ID, such as[email protected]. - Use the same value every time for the same mailbox, in both classification requests and corrections. A different spelling counts as a different user.
- For a message with several local recipients, classify it once per recipient, each with its own
X-User-ID, if each recipient gets their own copy. - Without
X-User-ID, SpamFoo Client applies no personalization to that message. - The user ID also labels the message in the admin console, so administrators can find a user's mail.
To learn how corrections change future results, see How Personalization Works.
Check Health
/health/ready
Use this endpoint to decide whether to send mail to SpamFoo Client. It needs no secret. It returns 200 when SpamFoo Client can classify mail, and 503 with a list of reasons when it can't, such as while its models download or while it shuts down.
curl http://localhost:16253/health/ready
/health
Use this endpoint to watch the queues. It needs no secret and always returns 200 while the service is up. Check the status field: healthy, or shutting_down while the service is stopping.
curl http://localhost:16253/health
{
"status": "healthy",
"shuttingDown": false,
"activeRequests": 1,
"modelReloading": false,
"spoolQueue": {
"active": 1,
"waiting": 0,
"maxConcurrency": 2,
"totalProcessed": 5120,
"totalTimedOut": 0
}
}
This example is shortened. A rising waiting count or totalTimedOut means messages are waiting for classification. See Check Health for every field.
Next Steps
- Send Corrections Back when users mark spam or move messages.
- Use the Command Line to test with saved messages.
- Classify a Message for the full request and response reference.