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

***

# Classify Without Spam Checks

Send a raw message to get only its Email Classification: `primary`, `transactions`, `updates`, or `promotions`. No spam, phishing, or allow and block checks run.

`POST /tabs/classify-stream`

## When to Use It

- Another system already filters spam, and you only want SpamFoo to sort the mail that is left.
- The message is already known not to be spam, and you want to file it.
- For a spam verdict, or a spam verdict and Email Classification together, use [Classify a Message](https://help.spamfoo.com/api/classify) instead.

Requests run on SpamFoo Client's background queue, separate from the queue that `/classify-stream` uses. The license must include Email Classification. When SpamFoo Client has not yet confirmed the license, the request still runs. When its saved license information has expired, the request still runs in embedded mode, and returns `403` in standalone mode.

SpamFoo Client records each result for the admin console. The same message for the same `X-User-ID` is recorded only once in a five-minute window, but it is still classified every time.

## Request

The body is the raw message (raw .eml content), sent unchanged. See [Send the Message in the Request Body](https://help.spamfoo.com/api#request-style).

| Header | Required | Type | Description |
| --- | --- | --- | --- |
| `Content-Type` | Recommended | string | `message/rfc822` |
| `X-SpamFoo-Proxy-Auth` | In standalone mode, or when not loopback | string | The proxy secret. See [Authenticate Requests](https://help.spamfoo.com/api#authentication). |
| `X-User-ID` | No | string | The recipient mailbox, usually a full email address. When sent, the user's own classification corrections can set the result. |
| `X-Sender-IP` | No | IPv4 or IPv6 | IP address of the server that delivered the message. Only recorded with the result for the admin console. If omitted, SpamFoo Client reads it from the message headers. |
| `X-Sender-Email` | No | email address | Envelope sender. Only recorded with the result for the admin console. If omitted, SpamFoo Client reads it from the message headers. |
| `X-SpamFoo-Log-ID` | No | string, up to 64 characters | Your own ID for the message, written to the classification log. Letters, digits, and `. _ : -` only; other values are ignored. |
| `X-SpamFoo-Priority` | No | `bulk` | Marks work that isn't live delivery, such as sorting a mailbox's old mail. See [Mark Bulk Work](https://help.spamfoo.com/api/classify#priority). |
| `X-SpamFoo-Deadline-Ms` | No | integer, 1 to 600000 | How many milliseconds you'll wait for the result. When the background queue can't meet it, the request gets `503` with `"code": "deadline_exceeded"` instead of waiting. See [Set a Deadline](https://help.spamfoo.com/api/classify#deadline). |

This endpoint ignores `X-SpamFoo-Classify` and `X-Recipient-Email`.

## Response

A successful request returns `200` with a JSON object:

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

| Field | Type | Description |
| --- | --- | --- |
| `classification` | string | `primary`, `transactions`, `updates`, or `promotions`. SpamFoo Client picks `primary` when the model is not confident, or when `primary` scored close to the top classification. |
| `confidence` | number | The highest probability the model gave any classification, from 0 to 1. When SpamFoo Client falls back to `primary`, this can be another classification's probability. `1` when the user's corrections decided. |
| `classificationProbabilities` | object | Probability for each of the four classifications, keyed by name. |
| `personalizationSource` | string or null | `tabs_knn` when the user's past classification corrections set the result. `null` when the global model decided. |
| `elapsedMs` | integer | Time SpamFoo Client spent classifying, in milliseconds. Time spent waiting in the queue is not included. |

## Errors

| Status | Body | Cause |
| --- | --- | --- |
| `400` | `{"error": "No email content provided"}` | The body was empty. |
| `400` | `{"error": "X-SpamFoo-Deadline-Ms must be ..."}` | `X-SpamFoo-Deadline-Ms` isn't a whole number from 1 to 600000, or was sent more than once. |
| `401` | `{"error": "unauthorized"}` | The proxy secret was missing or wrong, and the request needed it. |
| `403` | `{"error": "feature_not_licensed", "features": ["classification"]}` | The license does not include Email Classification, or, in standalone mode, the license has expired. |
| `413` | `{"error": "request_body_too_large", "maxBytes": ...}` | The message is over the body size limit. |
| `429` | `{"error": "...", "code": "auth_blocked"}` | Too many wrong secrets came from this address. See [Repeated Wrong Secrets](https://help.spamfoo.com/api#blocked). |
| `503` | `{"error": "server_busy", "code": "server_busy", ...}` or `{"error": "Classification queue timeout", "code": "queue_timeout", ...}` | SpamFoo Client is busy. See [Handle Busy Responses](https://help.spamfoo.com/api#busy). |
| `503` | `{"error": "Classification deadline exceeded", "code": "deadline_exceeded", ...}` | Only with `X-SpamFoo-Deadline-Ms`. The deadline couldn't be met. See [Set a Deadline](https://help.spamfoo.com/api/classify#deadline). |

## Example

### 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 System.Text.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<JsonElement>();
var classification = result.GetProperty("classification").GetString();
var confidence = result.GetProperty("confidence").GetDouble();
Console.WriteLine($"classification={classification} confidence={confidence:F2}");
```

### Python

```
import os

import requests

headers = {
    "Content-Type": "message/rfc822",
    "X-SpamFoo-Proxy-Auth": os.environ.get("SPAMFOO_PROXY_SECRET", ""),
    "X-User-ID": "user@example.com",
}

with open("message.eml", "rb") as eml:
    response = requests.post(
        "http://localhost:16253/tabs/classify-stream",
        data=eml,
        headers=headers,
        timeout=60,
    )

response.raise_for_status()
result = response.json()
print(f"classification={result['classification']} confidence={result['confidence']:.2f}")
```
