Source: https://help.spamfoo.com/integrations/classification-only
Last updated: 2026-10-27

***

# Use Classification Only

This page shows how to get Email Classifications (Primary, Transactions, Updates, and Promotions) from SpamFoo Client when another product already decides what is spam, and how a license without spam filtering changes the API's behavior.

## Choose an Approach

| Approach | Use It When |
| --- | --- |
| `POST /tabs/classify-stream` | Your other product has already decided a message is not spam, and you want only its Email Classification. This endpoint never runs spam checks, whatever your license includes. |
| `POST /classify-stream` | You want one integration that works with any license. With a license that has no spam filtering, SpamFoo Client skips the spam checks and returns only the Email Classification, in the same response format as always. |

Both endpoints take the raw message as the body, the same way as described in [Start With the Local API](https://help.spamfoo.com/integrations/local-api).

## Classify Without Spam Checks

`POST /tabs/classify-stream`

Send only messages your other product passed as not spam. The endpoint assumes the message is not spam and records it that way.

| Header | Value |
| --- | --- |
| `X-User-ID` | The recipient mailbox, as a full email address. Turns on personalization for that user. |
| `X-Sender-IP` | IP address of the server that delivered the message. Shown in the admin console. |
| `X-Sender-Email` | Envelope sender (SMTP `MAIL FROM`). Shown in the admin console. |
| `X-SpamFoo-Log-ID` | Your own ID for this message, written to SpamFoo Client's classification log. |
| `X-SpamFoo-Proxy-Auth` | The proxy secret. Required in standalone mode, and for any request that does not come from loopback. |

### curl

```
curl -X POST http://localhost:16253/tabs/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
```

### C#

```
using System.Net.Http.Headers;
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new Uri("http://localhost:16253") };

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/classify-stream") { Content = content };
request.Headers.Add("X-SpamFoo-Proxy-Auth", Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET"));
request.Headers.Add("X-User-ID", "user@example.com");

using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TabsResult>();
Console.WriteLine($"{result!.Classification} ({result.Confidence:P0})");

record TabsResult(
    string Classification,
    float Confidence,
    Dictionary<string, float> ClassificationProbabilities,
    string? PersonalizationSource,
    long ElapsedMs);
```

### Python

```
import os
import requests

with open("message.eml", "rb") as eml:
    response = requests.post(
        "http://localhost:16253/tabs/classify-stream",
        data=eml,
        headers={
            "Content-Type": "message/rfc822",
            "X-SpamFoo-Proxy-Auth": os.environ.get("SPAMFOO_PROXY_SECRET", ""),
            "X-User-ID": "user@example.com",
        },
        timeout=60,
    )
response.raise_for_status()
result = response.json()
print(result["classification"], result["confidence"])
```

The response:

```
{
    "classification": "promotions",
    "confidence": 0.91,
    "classificationProbabilities": {
        "primary": 0.04,
        "transactions": 0.02,
        "updates": 0.03,
        "promotions": 0.91
    },
    "personalizationSource": null,
    "elapsedMs": 41
}
```

| Field | Meaning |
| --- | --- |
| `classification` | `primary`, `transactions`, `updates`, or `promotions`. File the message by this value. |
| `confidence` | The highest probability among the four classifications, from 0 to 1. When the model is unsure, or Primary is close behind, SpamFoo Client returns `primary`, so this value can belong to a different classification than the one returned. |
| `classificationProbabilities` | The model's probability for each classification. |
| `personalizationSource` | `tabs_knn` when the user's past corrections decided the classification, or `null` when the global model decided. |
| `elapsedMs` | Time SpamFoo Client spent classifying the message, in milliseconds. |

Things to know about this endpoint:

- Allow and block rules from the admin console are not checked, because no spam decision is made.
- Messages appear in the admin console as not spam, but they are left out of the spam and not spam counts.
- It uses a separate queue from `/classify-stream`. When it cannot classify the message in time, it returns `503`; leave the message in the inbox.

See [Classify Without Spam Checks](https://help.spamfoo.com/api/classify-tabs) for the full reference.

## Know How Licenses Affect Requests

A SpamFoo license includes one or both of these features, shown in the admin console as **Spam Filtering** and **Classification**. SpamFoo Client learns which features you have when it checks your license with SpamFoo, and it adjusts each request to match.

| License Includes | `/classify-stream` | `/tabs/classify-stream` |
| --- | --- | --- |
| Spam Filtering and Classification | Spam verdict and Email Classification. | Email Classification. |
| Classification only | Email Classification only. `isSpam` is `false` and `spamProbability` is `0`, except when an admin console allow or block rule decides the message. | Email Classification. |
| Spam Filtering only | Spam verdict only. `classification` is empty for mail that is not spam. | `403` with `"error": "feature_not_licensed"`. |

- **Spam-only requests need Spam Filtering.** Sending `X-SpamFoo-Classify: false` to `/classify-stream` with a Classification only license returns `403`.
- **Before the license check:** until SpamFoo Client has current license information, for example just after it starts and before it reaches SpamFoo, it does not block local work. `/classify-stream` runs both spam checks and classification, and `/tabs/classify-stream` is available.
- **After the license expires:** in embedded mode, SpamFoo Client keeps running both spam checks and classification. In standalone mode, once the license has expired and its grace period has ended, or SpamFoo Client hasn't reached SpamFoo for too long, both endpoints return `403` with `"error": "feature_not_licensed"` until a valid license is active. Treat it as "no verdict" and deliver the message normally.
- **Same response format:** with a Classification only license, `/classify-stream` still returns every field it normally does, so code written for a full license keeps working. Check `classification` and ignore the spam fields.

To see which features your license includes, open the license details in the [admin console](https://help.spamfoo.com/guides/admin-console).

## Send Classification Corrections

When a user moves a message to a different classification, send the correction to `POST /tabs/feedback-stream`. This works with any license and is how SpamFoo Client learns each user's preferences. See [Report a Classification Fix](https://help.spamfoo.com/integrations/send-corrections#classification-feedback).

Spam and not spam corrections from your other product do not need to go to SpamFoo Client unless SpamFoo Client also makes spam decisions for you.
