# Compliance evidence

Technical guide for DPOs, auditors, and engineers who need to know exactly what Werkmail records and how to get it out. Product thesis: [Regulated transactional](/regulated). Operational controls: [Compliance](/compliance).

## What the consent ledger stores per contact

Each ledger event is **immutable and append-only**. Nothing is overwritten — withdrawals and updates are new events, so the history stays readable in order.

| Field | Meaning |
| --- | --- |
| `email` / `contact_id` | The contact the event belongs to |
| `channel` | `termin`, `recall`, `newsletter`, `transactional`, `other` |
| `legal_basis` | Simplified GDPR Art. 6 labels: `consent`, `contract`, `legal_obligation`, `vital`, `public_interest`, `legitimate_interest` |
| `action` | `granted`, `withdrawn`, `updated`, `exported`, `erased`, `doi_pending`, `doi_confirmed` |
| `source` | Where the grant came from (form, import, API, preference centre) |
| `actor_email` | Who performed the change, when there is an operator |
| `details` | Free-form evidence, including the wording the person saw, IP, and user-agent |
| `created_at` | Timestamp |

Retention follows the workspace's DSGVO settings; redaction and erase are first-class actions (below). Werkmail does not silently delete ledger history for active contacts.

## Pulling an audit export

The workspace audit log is live:

```
GET /api/practices/{id}/audit
```

A packaged, DPO-ready audit export is planned — from/to window plus CSV or JSON:

```
GET /api/practices/{id}/audit-export?from=2026-08-01&to=2026-08-31&format=csv|json
```

```bash
# DPO Audit Export (CSV or JSON)
curl -sS -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/practices/$PRACTICE_ID/audit-export?from=2026-08-01&to=2026-08-31&format=csv"
```

## Data Processing Agreement (AVV / DPA)

Werkmail provides a self-service, electronically signable Data Processing Agreement (Art. 28 GDPR) tailored to European compliance:

```bash
# Fetch template preview
curl -sS -H "Authorization: Bearer $TOKEN" \
  "$BASE/api/practices/$PRACTICE_ID/avv?region=eu-central-1"

# Electronically execute agreement
curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"signer_name": "Dr. Med. Müller", "signer_title": "Managing Director", "region": "eu-central-1"}' \
  "$BASE/api/practices/$PRACTICE_ID/avv/sign"
```

## Consent history and DSAR export

Per-contact consent history is live today:

```
GET /api/practices/{id}/contacts/{contactID}/consent
```

A friendlier alias and a DSAR package endpoint are planned:

- `GET /api/practices/{id}/contacts/{contactID}/consent-history` — planned alias of `/consent`
- `POST /api/practices/{id}/dsar-export` — planned; accepts a contact identifier, runs asynchronously, and fires the workspace webhook when the package is ready

Contact export and erase are live today:

- `GET /api/practices/{id}/contacts/{contactID}/export`
- `DELETE /api/practices/{id}/contacts/{contactID}`

## How `outgoing`/`broadcast` isolation is enforced

Isolation is not a UI rule — it is wired into the sending infrastructure:

- Each workspace is its **own sending tenant** on the outbound provider, with its own configuration set and reputation profile. A neighbour's complaints cannot affect your tenant.
- `outgoing` (transactional) and `broadcast` (marketing) messages never share the same reputation path inside a tenant.
- The content gate — the same one used in compose, batch, and API — rejects marketing-shaped content on transactional routes.
- Werkmail maintains its **own suppression store and rate limiter**; provider-account suppression lists and account-wide send-rate caps are not the only gates.

## Reputation pause: exact thresholds

Werkmail computes rates from its own event log over a rolling window and pauses a tenant when any of these hold:

| Trigger | Condition |
| --- | --- |
| Bounce rate | ≥ 5% of sends in the window, with at least 20 sends |
| Complaint rate | ≥ 0.1% of sends in the window, with at least 20 sends |
| Absolute complaint spike | ≥ 3 complaints with at least 5 sends in the window |

A per-practice bounce-threshold override can tighten or relax the 5% default. While paused, sends are rejected with a `paused` result, and an owner must resume the tenant after fixing the list.

### Getting notified before a pause

- Webhooks — [message events](/webhooks): `message.hard_bounced`, `message.soft_bounced`, `message.spam_complaint`, `message.failed`
- Slack / Teams alerts: `POST /api/practices/{id}/alerts`
- The deliverability cockpit shows live bounce and complaint rates before thresholds are hit.

Topic guide: [Webhooks API](/api/webhooks). Reputation guardrails: [Compliance](/compliance).
