API Overview
SpamFoo Client exposes a local HTTP API for classifying messages, sending user corrections, managing a user's stored data, and checking service health. This page covers what every endpoint has in common. Each endpoint has its own page with request and response details.
Base URL and Port
| Property | Value |
|---|---|
| Base URL | http://localhost:16253 |
| Port | 16253. In standalone mode, it can be changed with SPAMFOO_PORT. |
| Protocol | HTTP. SpamFoo Client does not serve HTTPS. |
| Listen address | localhost by default. Set SPAMFOO_BIND_ADDRESS to listen on another address. The Docker image listens on all interfaces (0.0.0.0). |
| Responses | JSON with camelCase field names. Enum values are strings. |
| Contract version | Every response has an X-SpamFoo-Contract header with the API's version, currently 1. See Versioning. |
Authenticate Requests
SpamFoo Client checks callers with a shared secret called the proxy secret. Send it in the X-SpamFoo-Proxy-Auth header.
- If you set the
SPAMFOO_PROXY_SECRETenvironment variable, that value is the secret. - Otherwise the secret is read from
etc/sf.proxysecretin the configuration directory, which is the data directory unless SpamFoo Client was started with--config-dir. Unless SmarterMail runs it, SpamFoo Client creates this file with a random value on first start. In the Docker image the file is/data/etc/sf.proxysecret, and with the Linux package it is/var/lib/spamfoo/etc/sf.proxysecret. - When SmarterMail runs SpamFoo Client, SmarterMail writes the secret and sends it on its own requests.
Without SmarterMail, the same value is the admin console password, so store it like a password.
| Endpoints | Secret Required |
|---|---|
Classification: /classify-stream, /tabs/classify-stream |
In standalone mode, always. In embedded mode, only when the request does not come from loopback (localhost). A wrong or missing secret returns 401 with {"error": "unauthorized"}. |
Corrections, personal data, settings, and integration: /feedback-stream, /tabs/feedback-stream, /personalization/..., /admin/api/settings, /integration/..., /generate-login-link |
Always, even from loopback. A wrong or missing secret returns 401 with an empty body. |
Health: /health, /health/live, /health/ready |
Never. |
- Loopback means the connection comes from
127.0.0.1or::1on the same machine. Every local process counts, including a reverse proxy running on the same host. - Docker: requests to the container's published port arrive from Docker's network, not loopback, so they need the secret.
- Standalone mode trusts no caller by its address. See Mode.
When the Secret Changes
In standalone mode, an administrator can change the secret while SpamFoo Client runs. For 15 minutes after the change, SpamFoo Client still accepts the old secret on classification, correction, and /integration/... requests, so your integration has time to switch. Every other endpoint accepts only the new secret right away. Embedded mode reads the secret only when it starts.
Repeated Wrong Secrets
After too many wrong secrets from one IP address, SpamFoo Client answers requests from that address with 429 for a while, including health checks. Requests that carry the correct secret, or come from a signed-in dashboard, still get through, so a mail server behind the same reverse proxy keeps working. The body is {"error": "...", "code": "auth_blocked"}, and the Retry-After header gives the seconds left.
The code samples in this reference read the secret from the SPAMFOO_PROXY_SECRET environment variable on the calling machine.
Send the Message in the Request Body
Endpoints that work on a message take the raw message as the request body. The raw message, also called raw .eml content, is the full RFC 5322 message, headers and MIME body, exactly as it arrived over SMTP. Details that are not inside the message, such as the connecting IP address, go in X- request headers.
- Send the bytes unchanged. Do not decode, re-encode, or wrap them in JSON.
- Set
Content-Type: message/rfc822. SpamFoo Client does not check this header on these endpoints, but it describes the body correctly. - An empty body returns
400with{"error": "No email content provided"}. - The body size limit is set by
SPAMFOO_MAX_CLASSIFY_BODY_BYTES. The default is 520 MiB, or 64 MiB in standalone mode. A larger body returns413. Get Integration Info reports the limit in effect. - SpamFoo Client does not read message files from disk on your behalf. Always send the message in the body.
Endpoints that take JSON, such as the DELETE endpoints on Manage Personal Data, need Content-Type: application/json. Without it they return 415.
Identify the Mailbox and Message
These request headers describe who the message is for and how it arrived. Each endpoint page lists which ones it reads.
| Header | Meaning |
|---|---|
X-User-ID |
The mailbox the message belongs to, usually the recipient's full email address. SpamFoo Client keeps each user's corrections under this ID, so send the same value for the same mailbox on every request, in both classification and correction calls. Without it, only the global models and system rules apply. |
X-Sender-IP |
IP address of the server that connected to your mail server to deliver the message. Used for SPF checks and IP reputation. |
X-Sender-Email |
The envelope sender, from the SMTP MAIL FROM command. Used for authentication checks and to match allow and block rules. |
X-Recipient-Email |
The envelope recipient, from the SMTP RCPT TO command. Used to match domain-level allow and block rules and to check for impersonation of protected people. |
X-SpamFoo-Log-ID |
Your own ID for the message, such as a queue ID. It appears in SpamFoo Client's classification log so you can match its log lines to yours. Up to 64 characters from A-Z a-z 0-9 . _ : -; any other value is ignored. |
SpamFoo Client reads X- header values as UTF-8, so mailbox names with non-ASCII characters work. Make sure your HTTP client sends them as UTF-8. In .NET, HttpClient rejects non-ASCII header values unless you set SocketsHttpHandler.RequestHeaderEncodingSelector.
How Licenses Affect Requests
A SpamFoo license can include spam detection, Email Classification, or both. SpamFoo Client checks the license before it runs an endpoint.
| Endpoint | License Needs |
|---|---|
/classify-stream |
Spam detection or Email Classification. /classify-stream runs only the parts the license includes. See Classify a Message. |
/tabs/classify-stream |
Email Classification. |
| Corrections, personal data, and health | No license check. |
When the license does not include what an endpoint needs, the response is 403 with a body such as {"error": "feature_not_licensed", "features": ["classification"]}. The features list names what the endpoint accepts.
Errors and Status Codes
Most errors return a JSON body with an error field. Some add more fields, such as code, queueDepth, or features.
{
"error": "Classification queue timeout",
"code": "queue_timeout",
"queueDepth": 12
}
| Status | Meaning |
|---|---|
200 |
Success. |
400 |
A required header, field, or body is missing or invalid, or the message could not be parsed. The error field says which. A missing required query parameter or unreadable JSON body returns 400 with no body. |
401 |
The proxy secret is missing or wrong. See Authenticate Requests. |
403 |
feature_not_licensed: the license does not include what this request needs. |
413 |
request_body_too_large: the body is over the size limit. The response includes maxBytes. |
415 |
A JSON endpoint was called without Content-Type: application/json. |
429 |
auth_blocked: too many wrong secrets came from this address. See Repeated Wrong Secrets. |
500 |
An unexpected error inside SpamFoo Client. Check the SpamFoo Client logs. |
503 |
SpamFoo Client is busy. See Handle Busy Responses. |
Handle Busy Responses
SpamFoo Client limits how much work it accepts at once. Every 503 has a Retry-After header, and the code field says why:
server_busy: too many requests were in progress.{"error": "server_busy", "code": "server_busy", "inFlight": 1024}withRetry-After: 1. The request was turned away before SpamFoo Client read the message. The limit is set bySPAMFOO_MAX_CLASSIFY_INFLIGHT. Requests marked as bulk have their own, smaller limit, add"priority": "bulk", and getRetry-After: 2. See Mark Bulk Work.queue_timeout: the request waited too long for a free classification slot.{"error": "Classification queue timeout", "code": "queue_timeout", "queueDepth": 12}.queueDepthis how many requests were waiting.deadline_exceeded: only for requests that sendX-SpamFoo-Deadline-Ms, when the wait for a slot would be longer than the deadline. See Set a Deadline.not_readyorshutting_down: from/health/ready, when SpamFoo Client can't classify mail yet or is stopping. See Check Health.
During mail delivery, treat a 503 as "no verdict" and deliver the message normally rather than holding it. Retry only when your process can wait. To see how close SpamFoo Client runs to these limits, read the queue counters from Check Health.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/classify-stream |
Get a spam verdict and Email Classification. See Classify a Message. |
POST |
/tabs/classify-stream |
Get an Email Classification only. See Classify Without Spam Checks. |
POST |
/feedback-stream |
Report that a user marked a message as spam or not spam. See Report Spam or Not Spam. |
POST |
/tabs/feedback-stream |
Report that a user moved a message to a different Email Classification. See Report a Classification Fix. |
DELETE |
/personalization/data/personalization/tabs/data |
Delete a user's stored corrections. See Manage Personal Data. |
GET |
/personalization/knn/stats/personalization/tabs/knn/stats |
Count a user's stored corrections. See Manage Personal Data. |
GET |
/health/health/live/health/ready |
Check service status, readiness, and queue load. See Check Health. |
GET |
/integration/info |
Read the version, mode, license, and limits of SpamFoo Client. See Get Integration Info. |
GETPUT |
/integration/address-map |
Send or read the map of mailboxes and the addresses that deliver to them. See Push an Address Map. |
DELETE |
/integration/users/{userId} |
Delete everything stored for a user when their mailbox is removed. See Delete a User. |
POST |
/generate-login-link |
Make a one-time dashboard sign-in link. See Create a Sign-In Link. |
GETPUT |
/admin/api/settings |
Read or change SpamFoo Client's settings. See Read and Change Settings. |
Versioning
The API has one version number, sent on every response in the X-SpamFoo-Contract header, and in the body of /health/ready and /integration/info as contract. The current version is 1.
- Within a version, changes only add things: new optional request headers, new response fields, and new endpoints. Ignore response fields you don't know.
- Removing something, or changing what an existing header or field means, raises the version.