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

***

# Push an Address Map

An address map tells SpamFoo Client which mailboxes your mail server has and which addresses deliver to each one: users, their other addresses, alias domains, and catch-all addresses. SpamFoo Client then files mail sent to an alias under the user who owns it, so each user's corrections and rules apply to all of their addresses. You can also give it a CardDAV address book for each domain to use for phishing protection.

`PUT /integration/address-map`

`GET /integration/address-map`

## When to Use It

- Your users receive mail at more than one address, through aliases, alias domains, or `+tag` addresses, and you want one set of corrections and rules per person.
- A domain has a catch-all address that delivers to one mailbox.
- You want SpamFoo Client to protect the people in a domain's address book against impersonation.

Push the whole address map each time it changes, or on a schedule. Each push replaces the one before it.

## How Addresses Are Matched to Users

On every request that sends the proxy secret, SpamFoo Client looks up the `X-User-ID` header in the address map and replaces it with the owning user's `id`. It tries these in order and uses the first match:

1. The exact address, as a user's `id` or one of their `addresses`.
2. The same address with any `+tag` removed.
3. The same local part at the primary domain, when the address is at one of that domain's `aliases`.
4. The domain's `catchAll` user. This step is used only when the last push had `"complete": true`, and never for a list address.

An address with no match is left as it is. Only the `X-User-ID` header is matched. A `userId` in a JSON body or query string is not.

## Request

Send the proxy secret in `X-SpamFoo-Proxy-Auth`, even from loopback. Send the body as JSON with `Content-Type: application/json`. The body can be up to 32 MiB.

```
{
    "source": "mymailserver",
    "complete": true,
    "domains": [
        { "name": "example.com", "aliases": ["example.net"], "catchAll": "info@example.com" }
    ],
    "users": [
        { "id": "pat@example.com", "addresses": ["patricia@example.com"], "displayName": "Pat Lee" },
        { "id": "info@example.com" }
    ],
    "lists": [
        { "address": "sales@example.com", "targets": ["pat@example.com"] }
    ],
    "sources": [
        {
            "kind": "carddav",
            "domain": "example.com",
            "url": "https://mail.example.com/carddav/gal/",
            "username": "gal-reader",
            "password": "..."
        }
    ]
}
```

| Field | Type | Description |
| --- | --- | --- |
| `source` | string | A label for what sent the address map, up to 256 characters. It's returned in the summary. |
| `complete` | boolean | `true` when the push lists every address on the mail server. Catch-all matching is used only then. Defaults to `false`. |
| `domains` | array | Each domain has a `name`, optional `aliases` (other domain names that deliver to it), and an optional `catchAll`: the `id` or an address of a user in the same push. |
| `users` | array | Each user has an `id`, the address you send as `X-User-ID`, and optional `addresses`: the user's other addresses. Users can also have `displayName`, `kind`, `active`, `role` (`user`, `domainAdmin`, or `systemAdmin`), and `adminDomains`. SpamFoo Client stores and counts these, but they don't change results. |
| `lists` | array | Mailing lists, each with an `address` and its `targets`. A list address can't also be a user's address. Lists are stored and counted, but they don't change results. |
| `sources` | array | CardDAV address books, one per domain. See [Address Book Sources](https://help.spamfoo.com/api/address-map#sources). |

### Rules for Entries

- Addresses and domains are compared without regard to letter case. International domain names can be sent in either form.
- An address can belong to only one user, and an alias domain to only one domain. An alias can't also be listed as a domain.
- An entry that breaks a rule is skipped and listed in `rejected` in the response. The rest of the push is still saved. A bad optional field on a user falls back to its default, and the user is kept.
- The whole push is refused with `400` if the JSON doesn't match the format, such as `"active": "yes"` instead of `true`, or if it has more than 50,000 domains, 200,000 users, 50,000 lists, 1,000 sources, or 500,000 entries in total.
- Each domain or user can have up to 1,000 aliases, addresses, or admin domains, and each list up to 10,000 targets. Entries past the limit are skipped and reported.

### Address Book Sources

A source gives SpamFoo Client a CardDAV address book for one domain. SpamFoo Client reads it about once an hour and adds its contacts to the domain's protected identities, so mail that pretends to come from those people is caught. See [Protect Against Phishing](https://help.spamfoo.com/guides/phishing-protection).

| Field | Description |
| --- | --- |
| `kind` | Always `carddav`. |
| `domain` | The domain the address book belongs to. |
| `url` | The address book's address, up to 2048 characters. It must use `https://`, or `http://` to the same machine. |
| `username`, `password` | The account SpamFoo Client signs in with. The password is required the first time you send a domain. After that you can leave it out to keep the saved one. Passwords are stored encrypted. |
| `tlsPinSha256` | Optional, for a server with a self-signed certificate. The SHA-256 hash of the server's public key (its SubjectPublicKeyInfo), as 64 hex digits or base64. When it's set, SpamFoo Client accepts the server only if the key matches, and skips the usual certificate checks. Needs an `https://` address. |

When a later push leaves out a domain's source, SpamFoo Client stops reading that address book.

## Response

`PUT` saves the address map and returns a summary. `GET` returns the same summary for the address map now in use, with `version` `0` before the first push.

```
{
    "version": 3,
    "updatedAt": "2026-10-13T14:00:00+00:00",
    "source": "mymailserver",
    "complete": true,
    "counts": {
        "domains": 1,
        "users": 2,
        "activeUsers": 2,
        "addresses": 1,
        "lists": 1,
        "sources": 1,
        "rejected": 1
    },
    "rejected": [
        { "value": "bob@other.example", "reason": "..." }
    ]
}
```

- `rejected` lists up to 1,000 skipped entries, each with the value and the reason. `counts.rejected` is the total.
- Rejected sources never echo the address book's URL or password.
- The address map is saved as `address-map.json` in the data directory, so it survives a restart.

## Errors

| Status | Body | Cause |
| --- | --- | --- |
| `400` | `{"error": "..."}` | The body is missing, isn't valid JSON for this endpoint, or is over a count limit. Nothing was changed. |
| `401` | Empty | The proxy secret was missing or wrong. |
| `413` | `{"error": "request_body_too_large", "maxBytes": 33554432}` | The body is over 32 MiB. |
| `415` | `{"error": "Content-Type must be application/json"}` | The request wasn't sent as JSON. |
| `429` | `{"error": "...", "code": "auth_blocked"}` | Too many wrong secrets came from this address. See [Repeated Wrong Secrets](https://help.spamfoo.com/api#blocked). |
| `500` | `{"error": "the address map could not be saved"}` | Saving failed. The previous address map stays in use. |

## Example

### curl

```
curl -s -X PUT http://localhost:16253/integration/address-map \
    -H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
    -H "Content-Type: application/json" \
    --data-binary @address-map.json
```

### C#

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

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") };
http.DefaultRequestHeaders.Add("X-SpamFoo-Proxy-Auth", secret);

var addressMap = new
{
    source = "mymailserver",
    complete = true,
    domains = new[] { new { name = "example.com", aliases = new[] { "example.net" } } },
    users = new[] { new { id = "pat@example.com", addresses = new[] { "patricia@example.com" } } }
};

using var response = await http.PutAsJsonAsync("/integration/address-map", addressMap);
response.EnsureSuccessStatusCode();
var summary = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"rejected={summary.GetProperty("counts").GetProperty("rejected")}");
```

### Python

```
import os
import requests

address_map = {
    "source": "mymailserver",
    "complete": True,
    "domains": [{"name": "example.com", "aliases": ["example.net"]}],
    "users": [{"id": "pat@example.com", "addresses": ["patricia@example.com"]}],
}

response = requests.put(
    "http://localhost:16253/integration/address-map",
    headers={"X-SpamFoo-Proxy-Auth": os.environ["SPAMFOO_PROXY_SECRET"]},
    json=address_map,
    timeout=60,
)
response.raise_for_status()
print("rejected:", response.json()["counts"]["rejected"])
```
