# Madnion Email API integration and campaign guide

Contract review: 22 October 2026. Audience: application developers and operators.

Optional provider: [InboxIt integration pilot](./inboxit-integration.md).
This is separate from existing native email delivery; customer sending remains
disabled until the documented rollout checks pass.

**Current live API:** [native campaigns](https://www.madnion.com/docs/email/campaigns)
and [single-recipient tests/reliability](https://www.madnion.com/docs/email/reliability).
Use `email-campaigns` for real customer campaign and test sends. The
`postal-live-pilot` operations below describe the earlier capture/setup pilot;
its `send` action is not the supported customer live-send route.

For app SSO and external customer workspaces, use the
[self-serve onboarding contract](./email-customer-onboarding.md). It covers
owner/admin SSO and trusted backend delegation, isolated DNS setup and native
webhook configuration. This does not enable customer sending.

Madnion is the email integration and delivery layer. Your app owns the customer
experience: campaign builder, contact lists, inbox screens and reporting views.
Madnion exposes authenticated operations, message/reply data, delivery state,
governance, logs and events. An operational inspection screen is not a required
customer inbox product. Resend remains a separate integration; nothing in this
guide reroutes existing transactional delivery.

**Release boundary:** `postal-live-pilot` below retains its existing opt-in pilot
controls; it has not been globally enabled or substituted for existing delivery.
The separate **`email-campaigns` API** now provides durable native campaigns for
explicitly certified workspace/app/sender combinations. See the
[native campaign contract](./native-email-campaigns.md) for the new endpoint,
activation, inbox/stats, webhooks, limits and current rollout scope. This supersedes
the campaign-roadmap portions of this older guide, not its legacy endpoint contract.
Other senders remain disabled until certified. No automatic fake warm-up or
high-volume deliverability claim is made.

## 1. Capability and responsibility matrix

| Capability | Available now | Production gap |
|---|---|---|
| Existing Madnion credentials | App service key, dashboard admin and app SSO owner/admin; customer-workspace delegation | Dedicated email-only scopes |
| Domain provisioning | Generate DNS records; verify ownership/SPF/DKIM/DMARC | Self-service registrar connection and safe DNS apply API |
| Outbound queue | Native PostgreSQL queue, fenced workers, shared sender pacing | Fair scheduling, provider/IP controls, high-volume certification |
| Campaign sequences | Live enrolment, pause, verified reply stops; separate single-recipient test | Schedules, sender pools and conditional sequences |
| Real inbound | Matched DKIM replies; matched DSNs; reply and delivery events | General alias authorization and external complaint feeds |
| Inbox data | Paginated scoped replies and events | Full thread API, reply sending, HTML/attachments and longer retention |
| Webhooks | Signed reply, bounce, unsubscribe, delivery and hold events | Replay, offsite recovery and external FBL certification |
| Warm-up | Gradual campaign volume ramp and sender/domain reputation guards | Adaptive ramping and measured seed placement; no artificial warm-up network |
| Statistics | Scoped campaign job/event counts | Provider breakdowns, positive-reply metrics and exports |

Native email isolation is **Madnion workspace + app + customer workspace** when
`app_workspace_id` is supplied. The API derives the customer partition from
verified SSO membership or trusted backend delegation; callers cannot select an
arbitrary partition. Follow the customer onboarding contract on every request.
The following pilot endpoint descriptions do not supersede the native contract.

## 2. Authentication and endpoint

All implemented actions use:

```text
POST https://api.madnion.com/functions/v1/postal-live-pilot
Content-Type: application/json
```

Server-side app calls use `x-service-account-key` with an existing active Madnion
app key carrying `integrations`, plus `app_id` in JSON. The server derives the
Madnion workspace from that key. A supplied mismatched app/workspace is rejected.
Keep keys out of browsers, analytics, URLs and logs.

Administrative setup uses `Authorization: Bearer <Madnion dashboard session
access token>` with `workspace_id` (Madnion UUID) and `app_id`. The current
implementation validates a Supabase-backed dashboard session and requires an
active central user and workspace owner/admin membership. Do not substitute an
arbitrary app SSO JWT. App service keys cannot self-enable, change DNS setup or
configure webhooks. Preserve the platform `apikey` header where required by your
gateway/client configuration; it does not replace caller authentication.

```bash
curl --fail-with-body https://api.madnion.com/functions/v1/postal-live-pilot \
  -H 'Content-Type: application/json' \
  -H "x-service-account-key: $MADNION_SERVICE_ACCOUNT_KEY" \
  -d '{"action":"status","app_id":"YOUR_APP_ID"}'
```

Status fields include `opted_in`, `webhook_configured`, `transport`,
`customer_sending_enabled`, `activation`, and `inbound_enabled`. The generic
status reports general inbound as disabled; this does not contradict the
separate controlled reply test. `configure` returning `opted_in: true` does not
authorize customer sends or weaken transport policy.

| Action | Additional fields | Caller | Result |
|---|---|---|---|
| `status` | none | App/admin | Readiness flags |
| `configure` | `enabled` boolean | Admin | Opt-in only |
| `domain.create` | `domain` | Admin | Pending domain and DNS records |
| `domain.verify` | `domain` | Admin | Verification report |
| `domain.list` | none | App/admin | `data` array |
| `webhook.configure` | `url` | Admin | One-time signing secret; rotates on every save |
| `suppress` | `recipient` | Admin | Add suppression; no removal action |
| `send` | `message` | Opted-in app/admin | HTTP 202 job; idempotency header required |
| `list` | none | App/admin | Latest 50 own-app jobs; no cursor |
| `get` | `message_id` | App/admin | Own-app job; otherwise 404 |
| `campaign.create` | `campaign` | App/admin | Sandbox run; idempotency header required |
| `campaign.get` | `campaign_id` | App/admin | Sandbox run, steps and events |
| `campaign.reply` | `campaign_id`, `reply` | App/admin | Synthetic reply only |
| `campaign.pilot.view` | none | App/admin | Controlled-test inbox and stats |

Unknown actions return `INVALID_ACTION`; do not invent `inbox.list`,
`warmup.start` or `dns.apply` calls against this endpoint.

### Provider preference API

The separate `POST /functions/v1/email-provider-settings` endpoint accepts a
dashboard session Bearer token, `workspace_id`, optional `app_id`, and `action`
(`get` or `set`). A workspace member may read; only owner/admin may set.
For `set`, supply `provider: "madnion"` or `"resend"`; an app-level preference
also accepts `"inherit"`. Omit `app_id` to target the workspace default.
Response fields are `selected`, `effective_provider`, `workspace_default`,
`can_manage`, `activation: "prepared_only"`, `existing_delivery: "unchanged"`
and `customer_sending_enabled: false`. This is configuration preparation, not
live routing or an activation bypass. It does not accept service-key-only auth.

## 3. Domain setup and DNS via API

Use a dedicated sending subdomain, for example `outreach.your-domain.com`, to
separate this onboarding from existing mail routing. This is not a guarantee of
reputation isolation. Domain registration and DNS hosting are separate services.

### Generate and verify records

First an admin opts the app in with `configure`, then creates the exact domain:

```json
{"action":"configure","workspace_id":"MADNION_WORKSPACE_UUID","app_id":"YOUR_APP_ID","enabled":true}
```

```json
{"action":"domain.create","workspace_id":"MADNION_WORKSPACE_UUID","app_id":"YOUR_APP_ID","domain":"outreach.your-domain.com"}
```

The response is `{domain, status, dns}`. Use **the actual** `dns.records` values:
each record contains `type`, `name`, `value` and sometimes merge/preserve flags.
Never copy another tenant's ownership token, DKIM key or example SPF value.

| Returned record | Purpose | Handling |
|---|---|---|
| Ownership TXT | Prove control of exact sending domain | Keep the challenge present |
| Selector `_domainkey` TXT | Public DKIM verification key | Publish full returned value; private key stays with Madnion |
| SPF TXT | Authorize sending infrastructure | Merge existing policy; never add a second SPF record |
| `_dmarc` TXT | Authentication policy | Preserve existing stricter policy; do not weaken it |

The current verifier requires explicit DMARC on the exact sending domain;
parent-policy inheritance is not accepted by this pilot. Outbound provisioning
returns `mx_changes_required: false` and `inbound_enabled: false`. It does **not**
configure a reply inbox. Root MX records must remain unchanged.

After records propagate:

```json
{"action":"domain.verify","workspace_id":"MADNION_WORKSPACE_UUID","app_id":"YOUR_APP_ID","domain":"outreach.your-domain.com"}
```

`status: verified` requires `dns.ownership`, `dns.spf`, `dns.dkim`, `dns.dmarc`
all true. Pending is not ready. Admission requires a check within 24 hours and
the live adapter checks DNS again at dispatch. DNS failure must halt sending.
Domain provisioning does not create a mailbox or buy a domain.

### Registrar automation boundary

Namecheap supports programmatic DNS using `getHosts` and `setHosts`. Madnion's
current Namecheap automation is an **operator script restricted to approved test
domains**, not a tenant-facing API. Do not give apps the platform registrar key.

The future adapter must: verify domain authorization and authoritative DNS;
read and securely back up the full zone; calculate a change plan; reject unknown
record types or conflicts; preserve unrelated records and root MX; recheck for
concurrent changes; apply only the approved plan; read back and verify. Serialize
writes per zone and reconcile uncertain outcomes before retrying. A pre-write
comparison alone cannot prevent a concurrent external registrar edit.

Namecheap `setHosts` replaces the host-record set: omitted records are deleted.
Never send only the new email records. See the [official DNS write contract](https://www.namecheap.com/support/api/methods/domains-dns/set-hosts/).

Self-service must support externally hosted DNS with a manual-record path too.
Domain purchases require explicit approval of domain, price and renewal terms;
they are not part of email activation.

### Reply routing

General inbound needs a dedicated reply subdomain, its MX target and corresponding
mail-server A/AAAA records, a working inbound SMTP listener, certificates and
per-tenant routing. PTR is configured with the IP provider, not by adding a zone
TXT record. Obtain exact MX/host values from an approved provisioning response;
no general inbound provisioning response exists yet. Do not repoint a customer's
existing Google/Microsoft mailbox MX to the pilot.

## 4. Queue and outbound example

After opt-in, this example is synthetic: **no SMTP**.

```bash
curl --fail-with-body https://api.madnion.com/functions/v1/postal-live-pilot \
  -H 'Content-Type: application/json' \
  -H "x-service-account-key: $MADNION_SERVICE_ACCOUNT_KEY" \
  -H 'Idempotency-Key: demo:message:001' \
  -d '{"action":"send","app_id":"YOUR_APP_ID","message":{"mode":"sandbox","from":"sender@example.com","to":["lead@example.org"],"subject":"Queue test","text":"Synthetic only; no external delivery."}}'
```

HTTP 202 contains `id`, `mode`, `status`, `created_at`,
`inbox_delivery_confirmed: false`, and `report`. Read it with:

```json
{"action":"get","app_id":"YOUR_APP_ID","message_id":"mail_FROM_SEND"}
```

States: `queued` → `sending` → `sandbox_processed`, `smtp_accepted`, `blocked`,
`transport_blocked`, `failed`, or `needs_review`. Sandbox completion has
`report.smtp_attempted: false`. SMTP acceptance is **not inbox placement**.

Idempotency keys are scoped to workspace and app. Same key plus same normalized
message returns the existing job; different content returns HTTP 409. Keep the
key after timeouts and do not create a fresh key to retry an uncertain send.
An interrupted SMTP attempt becomes `needs_review`, not an automatic resend.
Both queue admission and dispatch check the live gate, domain and suppression.

Live mode is allowlisted and operator-gated. It requires verified STARTTLS under
the generic API policy. Earlier individually approved Namecheap plaintext-first-
hop tests used a separate controlled path. They do not enable or authorize that
transport for customer campaigns. The current API does not accept an override.

## 5. Campaign example and stop on reply

The current campaign API is a simulator, not a bulk-send endpoint. Use synthetic
addresses only (`example.com`, `example.org`, or a single-label `.example` domain).
Supply `Idempotency-Key: demo:campaign:001`:

```json
{
  "action":"campaign.create",
  "app_id":"YOUR_APP_ID",
  "campaign":{
    "mode":"sandbox",
    "sender":"colin@example.com",
    "recipient":"lead@example.org",
    "steps":[
      {"after_seconds":0,"subject":"A relevant idea","text":"Synthetic initial message."},
      {"after_seconds":300,"subject":"Following up","text":"Only if no reply has been recorded."}
    ]
  }
}
```

HTTP 200 returns `campaign_id`, tenant identifiers, sender, recipient, `status`,
`steps`, `events`, `smtp_attempted: false`, `customer_sending_enabled: false`.
Delays are increasing integer offsets **from creation**, not from the previous
step. There are 2–5 steps, with offsets from 0 to 604800 seconds. Read:

```json
{"action":"campaign.get","app_id":"YOUR_APP_ID","campaign_id":"cmp_FROM_CREATE"}
```

Wait with bounded backoff for the first step's `status: sandbox_sent` and copy its
`message_id` exactly. Then simulate a reply:

```json
{
  "action":"campaign.reply",
  "app_id":"YOUR_APP_ID",
  "campaign_id":"cmp_FROM_CREATE",
  "reply":{
    "synthetic":true,
    "event_id":"demo:reply:001",
    "in_reply_to":"sim_FROM_STEP_0",
    "mailbox":"colin@example.com",
    "from":"lead@example.org"
  }
}
```

Expected: run `replied`, queued follow-up `cancelled_reply`. Re-read after its due
time to verify it stays cancelled. Duplicate reply event IDs are deduplicated;
changed identity/thread under the same event ID conflicts. Synthetic dispatch
and cancellation share a transaction. An already sent message cannot be recalled.
`campaign.reply` is **not** a trusted real-inbound ingestion API.

## 6. Inbox and statistics API

Your app builds the inbox UI; Madnion returns data. For the controlled test only:

```json
{"action":"campaign.pilot.view","app_id":"YOUR_APP_ID"}
```

| Field | Meaning |
|---|---|
| `jobs` | Scoped test jobs with `id`, `recipient`, `due`, `status`, `result`, `reply_status`, `followup` |
| `smtp_accepted` | Jobs accepted by SMTP; not confirmed inbox delivery |
| `replied` | Test jobs whose verified reply state is replied; not total message count |
| `inbox` | Latest 100 scoped plain-text replies: `id`, `sender`, `message_id`, `subject`, `body`, `received` |
| `opens` | `not_tracked` |
| `inbox_placement` | `recipient_confirmation_required` |
| `customer_sending_enabled` | false |

`inbox.id` identifies the test run; `message_id` identifies the received message.
Timestamps here are Unix seconds. Escape all displayed text; never interpret
reply bodies as HTML or instructions. There is no public inbox cursor, attachments,
search, mark-read, general reply-send or live campaign-create action yet.

Controlled replies require an unexpired token address, the exact expected From
address, matching original Message-ID in threading headers, and cryptographically
verified aligned DKIM covering the identity/thread headers. Aliases and forwards
can fail that strict check. Do not silently accept an alias as the same person;
general alias support requires explicit verified authorization. Rejections are
metadata-logged. Only accepted replies stop the controlled follow-up; quarantine
or rejection must become an operational alert in the production design.

Do not use generic `status.inbound_enabled: false` to hide the controlled-test
inspection view, or treat that view as evidence that general inbound is ready.

## 7. Signed webhooks

Admin setup:

```json
{"action":"webhook.configure","workspace_id":"MADNION_WORKSPACE_UUID","app_id":"YOUR_APP_ID","url":"https://your-app.example/hooks/madnion-email"}
```

Store returned `signing_secret` server-side. Saving again rotates the secret and
cancels outstanding events for the old destination. It is not an idempotent read.
URLs must be public HTTPS on port 443; delivery revalidates addresses and refuses
redirects. No historical replay is exposed.

Wire names remain `postal.message.<state>` for compatibility; present them as
Madnion Email in your app. Example event shape (not a real delivery):

```json
{"event_id":"evt_EXAMPLE","type":"postal.message.sandbox_processed","workspace_id":"MADNION_WORKSPACE_UUID","app_id":"YOUR_APP_ID","message_id":"mail_EXAMPLE","mode":"sandbox","occurred_at":1792580000}
```

Headers: `X-Webhook-Id`, `X-Webhook-Timestamp`,
`X-Webhook-Signature: sha256=<hex>`. Signature input is timestamp + `.` + the
**exact raw body bytes**. Verify HMAC-SHA256 in constant time, require a short
freshness window, match header ID to event ID and validate both tenant IDs.
The [Node verifier example](./examples/email/verify-webhook.mjs) uses five minutes.

After verification, persist the event to a durable inbox with a unique
`(workspace_id, app_id, event_id)` constraint before acknowledging 2xx. A valid
duplicate can receive 2xx without repeated side effects. Process asynchronously.
Do not acknowledge before durable acceptance, and do not use sandbox events to
send real mail. Treat state events as potentially delayed/out of order; reconcile
via `get` rather than downgrading a terminal job on an old queued event.

Delivery attempts: at most eight, exponentially delayed and capped at one hour
between attempts. Terminal failure stays available to operators; no public
replay endpoint is implemented. Event bodies omit recipient, subject and body.
These are **queue/attempt events**, not bounce, complaint, open, reply or verified
inbox-delivery notifications. Those event contracts are still required.

## 8. Limits, errors and retention

| Limit | Current pilot |
|---|---|
| Request body | 64 KiB |
| Outbound recipients | Exactly one per job |
| Format | Plain text only; no HTML, attachments, tracking or arbitrary headers |
| Subject / outbound text | 200 / 20,000 characters |
| Queue admission | 20 jobs/app/minute; 200/workspace/rolling 24 hours |
| Domains | 10 per app/workspace |
| Synthetic campaigns | 100 runs/app and 500/workspace/rolling 24 hours |
| Campaign step text | 4,000 characters |
| Job list / controlled inbox | Latest 50 / 100; no pagination contract |

Queue caps count accepted jobs, including sandbox and later failures, not only
successful deliveries. Idempotent repeats do not create additional jobs. These
are safety limits, **not a throughput benchmark**. Billing units/prices are not
specified by this pilot; metered API calls are not automatically a settled
per-email bill. Confirm billing before commercial rollout.

| Status / example code | Handling |
|---|---|
| 400 `INVALID_ACTION`, `INVALID_MESSAGE_FIELDS`, `IDEMPOTENCY_KEY_REQUIRED` | Correct input; do not retry unchanged |
| 401 `SIGN_IN_REQUIRED` | Renew the appropriate session |
| 403 `APP_CREDENTIAL_MISMATCH`, `INTEGRATIONS_SCOPE_REQUIRED` | Correct app/key scope; never try another tenant |
| 403 `WORKSPACE_ADMIN_REQUIRED` | Use authorized admin setup, not a service key |
| 403 `APP_NOT_OPTED_IN`, `OPERATOR_ACTIVATION_REQUIRED`, `CAMPAIGN_LIVE_DISABLED` | Keep sending held |
| 403 `RECIPIENT_NOT_APPROVED`, `RECIPIENT_SUPPRESSED`, `TLS_REQUIRED` | Do not bypass policy |
| 404 `MESSAGE_NOT_FOUND`, `CAMPAIGN_NOT_FOUND`, `DOMAIN_NOT_FOUND` | Check own identifiers; may also conceal foreign resources |
| 409 `IDEMPOTENCY_CONFLICT`, `REPLY_THREAD_MISMATCH`, `REPLY_IDENTITY_MISMATCH` | Reconcile; no automatic resend |
| 412 `DOMAIN_VERIFICATION_REQUIRED` | Verify DNS again |
| 413 `BODY_TOO_LARGE` | Reduce payload |
| 429 `APP_MINUTE_CAP`, `WORKSPACE_DAILY_CAP`, `SANDBOX_DAILY_CAP`, `DOMAIN_LIMIT` | Back off; no capacity bypass |
| 503 `POSTAL_PILOT_UNAVAILABLE` | Outcome may be unknown; reuse original key/content only |

This endpoint does not promise a `Retry-After` header or request ID on every
response. Keep your own correlation/idempotency IDs and any returned gateway
request IDs. Record HTTP status, action, elapsed time and sanitized error code.

Terminal outbound payloads are removed once older than seven days from creation;
active queued/sending payloads are preserved. Terminal campaign content is cleared
for non-active runs older than seven days from creation. Controlled inbox bodies
expire seven days after receipt. Terminal outbox events expire after 90 days.
Job metadata, idempotency tombstones and audits currently have no finalized
retention cutoff. Apps needing longer content retention must store authorized
copies under their own policy; do not assume indefinite inbox storage.

## 9. What an app can integrate today

1. Read status and domains with its existing server-side key.
2. Have an authorized admin configure opt-in, domain and webhook through HTTP.
3. Publish the returned DNS records through the domain's authorized operator.
4. Exercise sandbox send, poll completion, verify signed events and deduplication.
5. Exercise synthetic send → reply → cancelled follow-up after due time.
6. For separately approved tests, inspect real reply data through the pilot view.
7. Keep customer activation and automatic external follow-ups disabled.

Selecting Madnion in provider settings currently prepares configuration; it does
not reroute existing Resend/system email. Keep transactional and marketing queues
and credentials separated during rollout and rollback.

## 10. Required production API contract — not implemented

These are design requirements, not endpoint names to call. Version the new public
contract without replacing the working pilot or existing delivery.

| Resource | Required operations and fields |
|---|---|
| Customer scope | Verified app/customer-workspace binding on every resource and event; cannot be a trusted arbitrary request string |
| Domain | Create/list/get; DNS plan/apply/verify with plan hash and provider credential reference; revocation and key rotation |
| Sender | Provision/verify/suspend; sender address, reply route, domain, timezone, reputation state and effective caps |
| Campaign | Draft/update/validate/activate/pause/archive; versioned steps, approved content, sender pool, local schedule and stop rules |
| Enrolment | Idempotent batch enrol/list/remove; contact consent/source, suppression check, personalization, next action and stop reason |
| Inbox | Cursor-paginated threads/messages, sanitized body fetch, reply, read state, attachment policy; exact tenant authorization |
| Capacity/warm-up | Per sender/domain/IP/provider effective capacity, usage, reset time, next eligible send, reason and ramp profile |
| Events | Reply, bounce, complaint, unsubscribe, message state, campaign stop and sender health; stable IDs and authorized replay |
| Reports | Counters by customer → campaign → sender → step; denominators, time range, cursor/export and freshness |

Every asynchronous creation should return a stable resource/job ID. Batch results
must report per-item acceptance/errors. Require idempotency and request hashing
for enrolments, sends and reply writes; never automatically retry an unknown SMTP
outcome. A shared physical sender's safety allowance must not multiply because
several campaigns/apps use it.

## 11. Warm-up and scale design — not implemented

Warm-up means gradual, reputation-aware sending to legitimate recipients. It is
not fake opens/replies or guaranteed inbox placement. No automatic warm-up service
is present today. Ramp profiles need operator-approved starting caps, increment
rules, minimum observation windows and upper bounds. Pause increases on missing
telemetry; reduce/stop on complaints, bounces, deferrals, authentication drift or
reply-ingestion failure. Apply a hierarchy of sender, domain, outbound IP,
destination-provider and tenant budgets. Extra domains must not bypass IP limits.

Bulk Gmail guidance includes SPF/DKIM/DMARC, TLS, alignment, valid DNS and
one-click unsubscribe for marketing/subscription messages. Confirm current
[sender requirements](https://support.google.com/mail/answer/81126?hl=en) before
launch. Our plain-text pilot lacks the production unsubscribe-header path.
Supplier acceptable-use and applicable recipient-consent rules remain release
checks, not something domain verification certifies.

Current architecture: Edge authorization → private sequential HTTP ingress →
SQLite WAL → single locked worker → Postal adapter; a separate controlled reply
store supports the test. Reads use SQLite transactions too. Durable does not mean
horizontally scalable. Do not add worker replicas or simply raise quotas.

Required scale architecture:

1. Stateless ingress, bounded payloads and admission backpressure; no SMTP work
   on the request path. Keep the existing app/workspace authentication boundary.
2. Shared transactional database/queue with atomic claims, leases/fencing and a
   durable event outbox. Separate scheduling, delivery, inbound and webhook workers.
3. Fair tenant scheduling with per-sender serialization and destination rate
   limits. Isolate transactional delivery resources from marketing workloads.
4. Recheck suppression, reply stop, campaign version, authorization, DNS and
   budgets immediately before dispatch. An already accepted email cannot be recalled;
   report races explicitly rather than promising exactly-once SMTP.
5. Durable inbound processing independent of campaign throughput. Authenticate
   before trusting identity; route unverified replies for review, and halt unsafe
   follow-ups if inbound processing is unhealthy.
6. Aggregate immutable event IDs once. Keep accepted/delivered/replied/bounced/
   complained/unsubscribed distinct; opens are optional, unreliable signals.
7. Backups, restore drills, encrypted content storage, deletion/export policies,
   tenant-scoped retention and scoped diagnostic access before adding volume.

Measure queue age and depth, claim delay, ingress p50/p95/p99, SMTP latency,
worker saturation, disk/DB pressure, inbound lag, webhook lag, retry/dead-letter
counts, suppression latency, duplicate rate and per-tenant fairness. Publish
capacity only after sustained tests on the actual VPS alongside voice/video/
crawl workloads. Choose throughput targets from expected active senders and
hourly arrival rates; do not advertise unlimited capacity or infer it from caps.

## 12. Release sequence and acceptance checklist

**Phase A — API onboarding:** customer scope, delegated admin grants, DNS plan/
apply/reverify, sender and reply-route provisioning, read-only readiness report.

**Phase B — campaign safety:** real enrolments, inbound thread correlation,
stop-on-reply, one-click unsubscribe, verified bounces/complaints, suppression
before dispatch, uncertain-outcome reconciliation and pause/resume semantics.

**Phase C — scale:** distributed workers, scheduling fairness, measured load,
backups, monitoring and staged per-tenant activation. Only then offer warm-up
automation and larger volumes, with production observations controlling ramps.

Required tests before customer sending:

- Wrong workspace/app/customer scope fails on every read/write, event and DNS plan.
- Service keys cannot acquire admin capabilities or activate sending themselves.
- DNS apply preserves existing MX/SPF/DMARC and handles concurrent edits/failures.
- Same idempotency key cannot duplicate sends/enrolments across retries or restarts.
- Authentic reply stops pending follow-ups; duplicate, spoofed, forwarded, alias,
  malformed and out-of-order replies have explicit verified outcomes.
- Unsubscribe, hard bounce and complaint stop eligible future dispatch immediately.
- Signature replay/rotation, unavailable receivers and dead letters are exercised.
- Worker crash before/after SMTP acceptance never becomes an automatic duplicate.
- Load tests include many customers, a noisy tenant, provider deferrals, storage
  pressure and coexistence with existing VPS services. Define numerical pass
  thresholds before running; retain the results.
- Rollback disables new campaign sending without interrupting reply ingestion,
  suppressions, audit history or existing transactional delivery.

No production SLO or unlimited sending claim is certified by this guide.
