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

***

# Report Spam or Not Spam

Tell SpamFoo Client that a user marked a message as spam, or rescued it from junk as not spam. SpamFoo Client learns from the correction for that user and may report it to SpamFoo to improve the global models.

`POST /feedback-stream`

## When to Use It

- A user moves a message into the junk folder, or clicks a "Mark as spam" control.
- A user moves a message out of the junk folder, or clicks a "Not spam" control.
- To report that a message belongs in a different Email Classification, use [Report a Classification Fix](https://help.spamfoo.com/api/feedback-tabs) instead.

What SpamFoo Client does with a correction:

- **Learns for the user:** it stores an embedding of the message (a list of numbers that represents its content, not the text) in the user's correction memory. Later messages from the same sender, or very similar messages, are judged with that correction in mind. See [How Personalization Works](https://help.spamfoo.com/guides/personalization).
- **Keeps a local copy:** it saves a compressed copy of the corrected message on the SpamFoo Client server, so the correction can be rebuilt when the embedding model changes.
- **Reports model mistakes:** when the correction disagrees with what the global models decided, SpamFoo Client queues an anonymized correction for SpamFoo. The user ID is hashed first. It skips the report when the user's own past corrections decided the message, or when the global models already agreed. See [How Corrections Are Used](https://help.spamfoo.com/guides/corrections).
- **Feeds reputation:** when SpamFoo Client reports to SpamFoo, the correction also counts toward the reputation of the IP address in `X-Sender-IP` and, when the message passed DMARC or aligned SPF, the sender's domain.

Send the message the user corrected, as it is stored in their mailbox. This endpoint has no license check.

## 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` | Yes | string | The proxy secret, required even from loopback. See [Authenticate Requests](https://help.spamfoo.com/api#authentication). |
| `X-SpamFoo-Priority` | No | `bulk` | Send `bulk` for corrections that aren't from a user acting right now, such as a batch import. They run on the background queue with their own limit, so they can't slow down live mail. Without it, the correction is handled right away. |
| `X-User-ID` | Yes | string | The user who made the correction. Use the same value you send as `X-User-ID` when classifying. |
| `X-Is-Spam` | Yes | `true` or `false` | `true` when the user marked the message as spam. `false` when the user marked it as not spam. |
| `X-Original-Is-Spam` | No | `true` or `false` | The `isSpam` value SpamFoo returned when the message arrived. If you omit it, SpamFoo Client scores the message again at correction time and uses that verdict instead, which can differ from the original one. |
| `X-Sender-IP` | No | IPv4 or IPv6 | IP address of the server that delivered the message. Used for authentication checks and IP reputation. |
| `X-Sender-Email` | No | email address | Envelope sender (SMTP `MAIL FROM`). Used for authentication checks. |
| `X-Send-Email-Text` | No | `true` or `false` | Send `true` only when the user agreed to share message content. The correction report to SpamFoo then includes the message text, with email addresses, phone numbers, card numbers, and similar details replaced by placeholders, plus phishing details built from the message's text, headers, and links. Default `false`. |
| `X-Subject-Strip-Prefixes` | No | string, repeatable | A literal tag your mail server adds to subjects, such as `[SPAM]`. SpamFoo Client removes it before it analyzes or reports the subject. Send the header once per tag. A single JSON array of strings, such as `["[SPAM]", "[EXTERNAL]"]`, is also accepted. |
| `X-SpamFoo-Dont-Report` | No | `true` or `false` | Send `true` when the admin has turned off reporting to SpamFoo. The correction still trains the user's local memory, but nothing about it is sent to SpamFoo. Default `false`. Any other value is rejected with `400 Bad Request`. |
| `X-SpamFoo-Event-Id` | No | string, up to 128 characters | Your own ID for this correction, so a retry isn't applied twice. See [Send a Correction More Than Once](https://help.spamfoo.com/api/feedback#duplicates). |

## Response

A successful request returns `200`:

```
{
    "status": "personalization_only",
    "userId": "user@example.com"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | Always `personalization_only` on success. It does not tell you whether a report was queued for SpamFoo. |
| `userId` | string | The `X-User-ID` you sent. |

### Send a Correction More Than Once

If your integration can send the same correction twice, such as a retry after a lost answer, add `X-SpamFoo-Event-Id` with an ID that stays the same across retries.

- The ID is 1 to 128 printable ASCII characters with no spaces. An ID that doesn't fit is ignored, and the correction is applied normally.
- A repeat of an ID that was already applied for the same user on this endpoint returns `200` with `{"status": "duplicate", "duplicate": true, "userId": "user@example.com"}` and changes nothing.
- An ID counts as used only after its correction succeeds, so you can retry a failed attempt with the same ID.
- SpamFoo Client remembers IDs in memory for 24 hours. A restart forgets them.
- Without the header, every correction is applied, because a user can correct the same message more than once.

## Errors

| Status | Body | Cause |
| --- | --- | --- |
| `400` | `{"error": "X-User-ID header is required"}` | `X-User-ID` was missing or blank. |
| `400` | `{"error": "X-Is-Spam header is required (true/false)"}` | `X-Is-Spam` was missing or not `true` or `false`. |
| `400` | `{"error": "No email content provided"}` | The body was empty. |
| `400` | `{"error": "Email could not be parsed"}` | The body is not a readable message. |
| `401` | Empty | The proxy secret was missing or wrong. |
| `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", ...}` | Only with `X-SpamFoo-Priority: bulk`. SpamFoo Client is busy. Wait the seconds in `Retry-After` and send it again. |

## Example

This example reports that a user rescued a message from junk, and that SpamFoo had originally marked it as spam.

### curl

```
curl -X POST http://localhost:16253/feedback-stream \
    -H "Content-Type: message/rfc822" \
    -H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
    -H "X-User-ID: user@example.com" \
    -H "X-Is-Spam: false" \
    -H "X-Original-Is-Spam: true" \
    -H "X-Sender-IP: 203.0.113.50" \
    -H "X-Sender-Email: sender@example.org" \
    --data-binary @message.eml
```

### C#

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

var secret = Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET")
    ?? throw new InvalidOperationException("Set SPAMFOO_PROXY_SECRET.");

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, "/feedback-stream") { Content = content };
request.Headers.Add("X-SpamFoo-Proxy-Auth", secret);
request.Headers.Add("X-User-ID", "user@example.com");
request.Headers.Add("X-Is-Spam", "false");
request.Headers.Add("X-Original-Is-Spam", "true");
request.Headers.Add("X-Sender-IP", "203.0.113.50");
request.Headers.Add("X-Sender-Email", "sender@example.org");

using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Python

```
import os

import requests

headers = {
    "Content-Type": "message/rfc822",
    "X-SpamFoo-Proxy-Auth": os.environ["SPAMFOO_PROXY_SECRET"],
    "X-User-ID": "user@example.com",
    "X-Is-Spam": "false",
    "X-Original-Is-Spam": "true",
    "X-Sender-IP": "203.0.113.50",
    "X-Sender-Email": "sender@example.org",
}

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

print(response.status_code, response.text)
```
