Send Corrections Back
This page shows when and how your integration should report user corrections to SpamFoo Client, and what SpamFoo Client does with them. Corrections are how SpamFoo Client learns each user's preferences.
Know When to Send a Correction
Send a correction when a person changes SpamFoo Client's result for a message:
- Marks a message as spam, or moves it to the junk folder, after it was delivered as not spam.
- Marks a message as not spam, or moves it out of the junk folder.
- Moves a message to a different classification, such as from Promotions to Primary.
Do not send corrections for moves made by your own server rules or filters. Those are not user preferences, and they would teach SpamFoo Client the wrong thing. Send one correction per user action.
Before You Start
- Proxy secret: correction requests always need the
X-SpamFoo-Proxy-Authheader, even fromlocalhost. See Authenticate Your Requests. - The message: send the raw message as the request body, the same way you send it to
/classify-stream. The user's stored copy is fine. - The same user ID: send the same
X-User-IDvalue you used when the message was classified. Otherwise the correction applies to a different user. - The original verdict (optional): if you stored
isSpamandclassificationfrom the classification result, send them back as the original verdict. - Bulk corrections (optional): when you send many old corrections at once, such as after an import, add
X-SpamFoo-Priority: bulk. They then wait in the background queue and can't slow down live mail. See Report Spam or Not Spam. - Retries (optional): if your integration can send the same correction twice, add
X-SpamFoo-Event-Idwith an ID that stays the same across retries. SpamFoo Client then applies it only once. See Send a Correction More Than Once.
Report Spam or Not Spam
/feedback-stream| Header | Required | Value |
|---|---|---|
X-SpamFoo-Proxy-Auth |
Yes | The proxy secret. |
X-User-ID |
Yes | The mailbox that made the correction, as a full email address. |
X-Is-Spam |
Yes | The correct answer: true if the user marked the message as spam, false if the user marked it as not spam. |
X-Original-Is-Spam |
No | The isSpam value SpamFoo Client returned when the message arrived. If you leave it out, SpamFoo Client classifies the message again when the correction arrives and treats that as the original verdict. |
X-Sender-IP |
No | IP address of the server that delivered the message to your mail server. |
X-Sender-Email |
No | Envelope sender (SMTP MAIL FROM). |
X-Send-Email-Text |
No | true only if the user agreed to share message text. See Share Message Text Only With Consent. |
X-Subject-Strip-Prefixes |
No | A tag your mail server adds to subjects, such as [SPAM]. SpamFoo Client removes it from the subject before it learns from the message. Send the header once per tag. |
curl -X POST http://localhost:16253/feedback-stream \
-H "Content-Type: message/rfc822" \
-H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
-H "X-User-ID: [email protected]" \
-H "X-Is-Spam: true" \
-H "X-Original-Is-Spam: false" \
-H "X-Sender-IP: 203.0.113.50" \
-H "X-Sender-Email: [email protected]" \
--data-binary @message.eml
using System.Net.Http.Headers;
using var http = new HttpClient { BaseAddress = new Uri("http://localhost:16253") };
var secret = Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET")
?? throw new InvalidOperationException("Set SPAMFOO_PROXY_SECRET.");
await using var eml = File.OpenRead("message.eml");
var content = new StreamContent(eml);
content.Headers.ContentType = new MediaTypeHeaderValue("message/rfc822");
using var request = new HttpRequestMessage(HttpMethod.Post, "/feedback-stream") { Content = content };
request.Headers.Add("X-SpamFoo-Proxy-Auth", secret);
request.Headers.Add("X-User-ID", "[email protected]");
request.Headers.Add("X-Is-Spam", "true");
request.Headers.Add("X-Original-Is-Spam", "false");
request.Headers.Add("X-Sender-IP", "203.0.113.50");
request.Headers.Add("X-Sender-Email", "[email protected]");
using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
import os
import requests
headers = {
"Content-Type": "message/rfc822",
"X-SpamFoo-Proxy-Auth": os.environ["SPAMFOO_PROXY_SECRET"],
"X-User-ID": "[email protected]",
"X-Is-Spam": "true",
"X-Original-Is-Spam": "false",
"X-Sender-IP": "203.0.113.50",
"X-Sender-Email": "[email protected]",
}
with open("message.eml", "rb") as eml:
response = requests.post("http://localhost:16253/feedback-stream",
data=eml, headers=headers, timeout=60)
print(response.status_code, response.text)
A successful correction returns 200:
{
"status": "personalization_only",
"userId": "[email protected]"
}
See Report Spam or Not Spam for the full reference.
Report a Classification Fix
/tabs/feedback-stream
Use this endpoint when a user moves a message that is not spam to a different Email Classification. The message must have a From address.
| Header | Required | Value |
|---|---|---|
X-SpamFoo-Proxy-Auth |
Yes | The proxy secret. |
X-User-ID |
Yes | The mailbox that made the correction, as a full email address. |
X-Classification |
Yes | The correct classification: primary, transactions, updates, or promotions. Not case-sensitive. |
X-Original-Classification |
No | The classification SpamFoo Client returned when the message arrived. If you leave it out, SpamFoo Client uses its own prediction for the message at correction time. |
X-Send-Email-Text |
No | true only if the user agreed to share message text. |
X-Subject-Strip-Prefixes |
No | A subject tag your mail server adds. Send the header once per tag. |
curl -X POST http://localhost:16253/tabs/feedback-stream \
-H "Content-Type: message/rfc822" \
-H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
-H "X-User-ID: [email protected]" \
-H "X-Classification: primary" \
-H "X-Original-Classification: promotions" \
--data-binary @message.eml
using System.Net.Http.Headers;
using var http = new HttpClient { BaseAddress = new Uri("http://localhost:16253") };
var secret = Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET")
?? throw new InvalidOperationException("Set SPAMFOO_PROXY_SECRET.");
await using var eml = File.OpenRead("message.eml");
var content = new StreamContent(eml);
content.Headers.ContentType = new MediaTypeHeaderValue("message/rfc822");
using var request = new HttpRequestMessage(HttpMethod.Post, "/tabs/feedback-stream") { Content = content };
request.Headers.Add("X-SpamFoo-Proxy-Auth", secret);
request.Headers.Add("X-User-ID", "[email protected]");
request.Headers.Add("X-Classification", "primary");
request.Headers.Add("X-Original-Classification", "promotions");
using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
import os
import requests
headers = {
"Content-Type": "message/rfc822",
"X-SpamFoo-Proxy-Auth": os.environ["SPAMFOO_PROXY_SECRET"],
"X-User-ID": "[email protected]",
"X-Classification": "primary",
"X-Original-Classification": "promotions",
}
with open("message.eml", "rb") as eml:
response = requests.post("http://localhost:16253/tabs/feedback-stream",
data=eml, headers=headers, timeout=60)
print(response.status_code, response.text)
A successful correction returns 200:
{
"status": "success",
"userId": "[email protected]",
"classification": "primary"
}
See Report a Classification Fix for the full reference.
Share Message Text Only With Consent
By default, a correction that SpamFoo Client sends on to SpamFoo carries only numbers derived from the message, not its text. The X-Send-Email-Text header changes that for one correction.
- Send
X-Send-Email-Text: trueonly when the user has agreed to share message content for training. - With it, the correction also carries the message text with sensitive patterns removed, plus the headers and link addresses used for phishing training.
- Without it, or with any other value, no message text is shared.
Know What Happens After a Correction
- The user's memory updates right away. SpamFoo Client stores the corrected message in that user's memory of past corrections. Later messages that closely match it, especially from the same sender, can follow the user's correction instead of the global models.
- Earlier results are cleared. Cached results are dropped, so the next classification reflects the correction.
- Wrong memories are fixed. If the user's own memory caused the mistake, SpamFoo Client removes or weakens the memory entries that caused it.
- Global model errors are reported. SpamFoo Client sends the correction to SpamFoo to help improve the global models only when the global models made the mistake. It skips this when the user's own personalization made the decision, or when the correction matches what the models already said. The user ID is hashed before it leaves the server.
- Sender reputation is updated. A spam or not spam correction also counts toward the sending IP address and, when the message passed DMARC or aligned SPF, the sending domain.
For a plain-language version of this process, see How Corrections Are Used.
Understand How Corrections Personalize Results
Personalization comes entirely from corrections. Your integration does not need to create rules for each user.
- Per user: each
X-User-IDhas its own memory. One user's corrections do not change another user's personalization. - Visible in results: when a user's memory decides a message,
/classify-streamreturnsbypassReasonset toknn_memoryorknn_memory_cross_sender(the match came from a different sender). When memory decides the Email Classification, the value istabs_knn(inbypassReasonfrom/classify-stream, or inpersonalizationSourcefrom/tabs/classify-stream). - Admin-wide rules are separate: allow and block rules set in the admin console apply to many users at once. See Manage Spam Rules and Senders.
To learn more, see How Personalization Works.
Remove a User's Personal Data
To delete everything SpamFoo Client has learned for one user, for example when a mailbox is deleted, call both endpoints. The first clears spam personalization and the second clears classification personalization.
curl -X DELETE http://localhost:16253/personalization/data \
-H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
-H "Content-Type: application/json" \
-d '{"userId": "[email protected]"}'
curl -X DELETE http://localhost:16253/personalization/tabs/data \
-H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
-H "Content-Type: application/json" \
-d '{"userId": "[email protected]"}'
See Manage Personal Data for details.
Handle Errors
| Status | Meaning |
|---|---|
400 |
A required header is missing or has a bad value, the body is empty, the message could not be parsed, or (for classification fixes) the message has no From address. The error field says which. |
401 |
The proxy secret is missing or wrong. |
413 |
The message is larger than the configured size limit. |
429 |
Standalone mode only. Too many requests from this address sent a wrong secret, so it is blocked for a while. |
503 |
Only for corrections sent with X-SpamFoo-Priority: bulk: SpamFoo Client is busy. Wait the seconds in the Retry-After header and send it again. |
A failed correction does not affect mail delivery. Log it, and retry later if the failure was temporary.