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

***

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

## Before You Start

- SpamFoo Client is installed and running. See [Install SpamFoo Client](https://help.spamfoo.com/guides/install).
- `curl http://localhost:16253/health/ready` on the server running SpamFoo Client returns `200`.
- 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 `-stream` endpoints, 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](https://help.spamfoo.com/api#authentication) 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](https://help.spamfoo.com/guides/configure), and send the secret on every request.

## Classify a Message

`POST /classify-stream`

Send 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](https://help.spamfoo.com/integrations/local-api#user-ids). |
| `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](https://help.spamfoo.com/api/classify#request).

### curl

```
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: sender@example.com" \
    -H "X-User-ID: user@example.com" \
    -H "X-Recipient-Email: user@example.com" \
    --data-binary @message.eml
```

### C#

```
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", "sender@example.com");
    request.Headers.Add("X-User-ID", "user@example.com");
    request.Headers.Add("X-Recipient-Email", "user@example.com");

    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);
```

### Python

```
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": "sender@example.com",
        "X-User-ID": "user@example.com",
        "X-Recipient-Email": "user@example.com",
    }
    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](https://help.spamfoo.com/api/classify) for the full list.

## Act on the Result

- **Spam:** when `isSpam` is `true`, move the message to the junk folder, or handle it the way your server handles spam. You may want to treat `phishing` more strictly than other spam.
- **Not spam:** when `isSpam` is `false`, deliver the message. If your mail client shows classification tabs or folders, file it by `classification`.
- **No classification:** when `classification` is 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 `isSpam` and `classification` with the message if you can. You send them back as the original verdict when a user corrects the message. See [Send Corrections Back](https://help.spamfoo.com/integrations/send-corrections).

### curl

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: user@example.com" \
    --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"
```

### C#

```
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"
    };
}
```

### Python

```
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](https://help.spamfoo.com/integrations/classification-only#license-behavior). |
| `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 `user@example.com`.
- 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](https://help.spamfoo.com/guides/personalization).

## Check Health

`GET /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
```

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

## Next Steps

- [Send Corrections Back](https://help.spamfoo.com/integrations/send-corrections) when users mark spam or move messages.
- [Use the Command Line](https://help.spamfoo.com/integrations/command-line) to test with saved messages.
- [Classify a Message](https://help.spamfoo.com/api/classify) for the full request and response reference.
