# Contacts and memory

Source: https://agentboxd.com/docs/contacts

> Who your agent is talking to: a record per outside address, with notes and metadata your agent can read and update.

A contact is an outside address your workspace has exchanged mail with. Agentboxd creates and updates contacts on every inbound and outbound message, so your agent can ask “who is this?” before it reads a word. Your own inbox addresses are never contacts, and neither are automated senders.

Contact:

```json
{
  "id": "c1000000-…",
  "address": "priya.n@gmail.com",
  "name": "Priya N",
  "kind": "person",
  "kind_source": "auto",
  "name_source": "auto",
  "domain": "gmail.com",
  "last_subject": "Order 1042",
  "notes": "Wholesale. Prefers short replies. Out of office until 2 October.",
  "metadata": { "crm_id": "hs_48213", "plan": "wholesale", "shopify_customer": 7730912 },
  "labels": ["customer", "wholesale"],
  "message_count": 3,
  "first_seen_at": "2026-09-23T09:12:00.000Z",
  "last_seen_at": "2026-09-25T11:02:00.000Z",
  "created_at": "2026-09-23T09:12:00.000Z"
}
```

## How contacts are filled in

- Inbound mail: the sender, and the other people in To and Cc (none of them when there are more than 20). Sent mail: every To, Cc and Bcc recipient.
- Automated senders never become contacts: mail with an `Auto-Submitted` header (other than `no`), no-reply style addresses (noreply@, mailer-daemon@, bounces@, notifications@, …) and complaint reports. The mail arrives as usual, with `contact_id: null`. A reply to mail you sent always counts.
- Mailing-list mail (`List-Id`, or `List-Unsubscribe` with `Precedence: bulk`, `list` or `junk`): the person who wrote it becomes a contact, but not the list or its other readers.
- The address is stored in lower case; `domain` is the part after the @.
- The name comes from the sender’s own display name, or the name you give a recipient when you write. Names others put in To or Cc are not used. The name is replaced when a real one arrives while the stored one is empty, the address, its first part or a no-reply name. A name you set with `PATCH` (`name_source: "manual"`) is never replaced; clearing it lets mail fill it again.
- `message_count` counts the messages involving the address: from it, or to, Cc or Bcc it.
- `last_subject` is the subject of the newest message with the contact, without `Re:` or `Fwd:`. A cheap memory of what you last talked about.
- Every message has a `contact_id`: the sender’s contact for inbound mail, the first recipient’s for outbound mail. It is `null` once that contact is deleted.
- `GET /v1/contacts/:id` includes `recent_threads`: up to 10 threads with this contact, across all your inboxes.

## Kinds: people first

Each contact has a `kind`: `person`, `automated` (notifications, verification codes), `sales` (sales and newsletters) or `unknown` (not sorted yet). A new sender starts as `unknown`. Writing to an address, or an answer to mail you sent, makes it a `person`. Otherwise, when [AI processing](https://agentboxd.com/docs/ai-processing) reads its first mail, the category sorts it: sales and newsletters make it `sales`, notifications and verification codes `automated`, everything else `person`. An uncertain category, or a message that looks like phishing, leaves it `unknown`. Once sorted, later mail never changes the kind, so one invoice from a person you know does not turn them into a robot.

Set the kind yourself with `PATCH { kind }` (or `update_contact`): `kind_source` becomes `manual` and later mail never changes it. `GET /v1/contacts?kind=person` lists people only; the dashboard shows people by default.

## Cleaning up and forgetting

`DELETE /v1/contacts/:id` deletes one contact; `POST /v1/contacts/delete` deletes every contact of some kinds and/or a domain, for example `{ "kind": ["automated", "sales"] }`. Try it with `"dry_run": true` first: it only counts. The mail stays either way.

With `forget` (`?forget=true`, or `"forget": true` in the bulk delete), the contact is removed and stops coming back: mail from the address, or to or Cc’ing it, no longer creates it, also with a +tag (forgetting a@x covers a+news@x). Agentboxd keeps only a keyed hash of the address, nothing else. The mail itself stays in your inboxes: forgetting is not an erasure of the messages. Sending to the address yourself brings it back as a new contact. The MCP tool `delete_contact` never forgets.

## By organisation

`GET /v1/contacts/domains` counts your contacts per domain, so you can see which companies you talk to; free-mail providers like Gmail or Outlook are left out. `GET /v1/contacts?domain=acme.com` lists the people at one of them. The dashboard has the same view.

## Notes, labels and metadata

**Notes** are free text, up to 10,000 characters: what your agent should remember, like preferences, open issues or what not to do. **Labels** work like message labels, up to 100 per contact: a change that would leave more answers `400 too_many_labels` and changes nothing. Removing labels always works. **Metadata** holds your own key/value pairs, typically ids from your CRM or shop; see [Metadata](https://agentboxd.com/docs/metadata).

contacts.ts:

```ts
// Who is this? Name, notes, metadata and labels, created from mail.
const c = await mr.contacts.byAddress('priya.n@gmail.com');
console.log(c.notes, c.metadata.crm_id); // "Wholesale…" "hs_48213"

// Link it to your CRM and remember something. Metadata merges; null deletes a key.
await mr.contacts.update(c.id, {
  notes: `${c.notes ?? ''}
Asked about order 1042 on 25 Sep.`,
  metadata: { crm_id: 'hs_48213', plan: 'wholesale' },
  add_labels: ['customer'],
});

// Find contacts by your own id.
const { data } = await mr.contacts.list({ metadata: { crm_id: 'hs_48213' } });

```

contacts.py:

```python
c = mr.contacts.by_address("priya.n@gmail.com")
print(c["notes"], c["metadata"].get("crm_id"))

mr.contacts.update(
    c["id"],
    notes=(c["notes"] or "") + "
Asked about order 1042 on 25 Sep.",
    metadata={"crm_id": "hs_48213", "plan": "wholesale"},
    add_labels=["customer"],
)

```

## Routes

| Method & path | Notes |
| --- | --- |
| `GET /v1/contacts` | `?q=` (address or name), `?label=`, `?kind=` (comma-separated), `?domain=`, `?metadata.<key>=<value>`, `cursor`, `limit`. Newest `last_seen_at` first. |
| `GET /v1/contacts/domains` | Contacts per domain, free mail left out. `?kind=`, `?limit=`. |
| `GET /v1/contacts/:id` | The contact and its `recent_threads`. |
| `GET /v1/contacts/by-address/:address` | 404 if unknown. |
| `PATCH /v1/contacts/:id` | `{ name?, notes?, metadata?, add_labels?, remove_labels?, kind? }`. |
| `DELETE /v1/contacts/:id` | Deletes the contact with its name, notes, labels and metadata → `204`. Messages stay. The address becomes a contact again if you exchange mail with it later, unless you add `?forget=true`. Workspace-wide keys only. |
| `POST /v1/contacts/delete` | `{ kind?, domain?, forget?, dry_run? }`: bulk delete, up to 1,000 per call. Workspace-wide keys only. |

## From an MCP client

`get_contact` takes an id or an address; `update_contact` changes notes, metadata, labels or the kind; `delete_contact` deletes one (only when you ask it to, and without forgetting the address). The dashboard has the same under [Contacts](https://agentboxd.com/app/contacts), with a notes editor, a metadata editor, a people/automated/sales filter, a by-organisation view and a clean-up button.

> **Note:** Notes and metadata are what you or your agent wrote, so they are trusted. With AI processing set to `full`, the contact’s name, notes and metadata are part of what a reply draft sends to DeepSeek. Keep secrets out of them.
