Source: https://help.spamfoo.com/api
Last updated: 2026-10-27

***

# 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](https://help.spamfoo.com/api#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_SECRET` environment variable, that value is the secret.
- Otherwise the secret is read from `etc/sf.proxysecret` in 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.1` or `::1` on 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](https://help.spamfoo.com/guides/configure#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 `400` with `{"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 returns `413`. [Get Integration Info](https://help.spamfoo.com/api/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](https://help.spamfoo.com/api/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](https://help.spamfoo.com/api/classify#licenses). |
| `/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](https://help.spamfoo.com/api#authentication). |
| `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](https://help.spamfoo.com/api#blocked). |
| `500` | An unexpected error inside SpamFoo Client. Check the SpamFoo Client logs. |
| `503` | SpamFoo Client is busy. See [Handle Busy Responses](https://help.spamfoo.com/api#busy). |

### 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}` with `Retry-After: 1`. The request was turned away before SpamFoo Client read the message. The limit is set by `SPAMFOO_MAX_CLASSIFY_INFLIGHT`. Requests marked as bulk have their own, smaller limit, add `"priority": "bulk"`, and get `Retry-After: 2`. See [Mark Bulk Work](https://help.spamfoo.com/api/classify#priority).
- **`queue_timeout`:** the request waited too long for a free classification slot. `{"error": "Classification queue timeout", "code": "queue_timeout", "queueDepth": 12}`. `queueDepth` is how many requests were waiting.
- **`deadline_exceeded`:** only for requests that send `X-SpamFoo-Deadline-Ms`, when the wait for a slot would be longer than the deadline. See [Set a Deadline](https://help.spamfoo.com/api/classify#deadline).
- **`not_ready` or `shutting_down`:** from `/health/ready`, when SpamFoo Client can't classify mail yet or is stopping. See [Check Health](https://help.spamfoo.com/api/health#ready).

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](https://help.spamfoo.com/api/health).

## Endpoints

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/classify-stream` | Get a spam verdict and Email Classification. See [Classify a Message](https://help.spamfoo.com/api/classify). |
| `POST` | `/tabs/classify-stream` | Get an Email Classification only. See [Classify Without Spam Checks](https://help.spamfoo.com/api/classify-tabs). |
| `POST` | `/feedback-stream` | Report that a user marked a message as spam or not spam. See [Report Spam or Not Spam](https://help.spamfoo.com/api/feedback). |
| `POST` | `/tabs/feedback-stream` | Report that a user moved a message to a different Email Classification. See [Report a Classification Fix](https://help.spamfoo.com/api/feedback-tabs). |
| `DELETE` | `/personalization/data` `/personalization/tabs/data` | Delete a user's stored corrections. See [Manage Personal Data](https://help.spamfoo.com/api/personal-data). |
| `GET` | `/personalization/knn/stats` `/personalization/tabs/knn/stats` | Count a user's stored corrections. See [Manage Personal Data](https://help.spamfoo.com/api/personal-data). |
| `GET` | `/health` `/health/live` `/health/ready` | Check service status, readiness, and queue load. See [Check Health](https://help.spamfoo.com/api/health). |
| `GET` | `/integration/info` | Read the version, mode, license, and limits of SpamFoo Client. See [Get Integration Info](https://help.spamfoo.com/api/integration-info). |
| `GET` `PUT` | `/integration/address-map` | Send or read the map of mailboxes and the addresses that deliver to them. See [Push an Address Map](https://help.spamfoo.com/api/address-map). |
| `DELETE` | `/integration/users/{userId}` | Delete everything stored for a user when their mailbox is removed. See [Delete a User](https://help.spamfoo.com/api/personal-data#delete-user). |
| `POST` | `/generate-login-link` | Make a one-time dashboard sign-in link. See [Create a Sign-In Link](https://help.spamfoo.com/api/login-link). |
| `GET` `PUT` | `/admin/api/settings` | Read or change SpamFoo Client's settings. See [Read and Change Settings](https://help.spamfoo.com/api/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.
