> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Worked email lifecycle samples

> Worked request and response samples for the email senders lifecycle: register a sender, verify the domain's DNS records, run a test-send, manage suppressions, and validate recipients.

## Worked email lifecycle samples

Copy a request as written, substitute your own ids, and compare the response envelope. Errors follow Devotel Orbit's `{ error, meta }` envelope and carry a `request_id` in `meta` you quote when reporting. The sender chain: **register a sender → publish the domain's DNS records → run a test-send → keep suppressions clean**.

### 1. Register a sender

<Note>
  `POST /api/v1/email/senders`
</Note>

**Request**

```json theme={null}
{
  "domain": "mail.acme.com",
  "default_from_email": "sales@acme.com",
  "default_from_name": "Acme Sales",
  "default_reply_to": "support@acme.com",
  "isDefault": true
}
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "esend_9f3ab2d61c4a",
      "domain": "mail.acme.com",
      "default_from_email": "sales@acme.com",
      "default_from_name": "Acme Sales",
      "default_reply_to": "support@acme.com",
      "isDefault": true,
      "streamId": null
    },
    "meta": {
      "request_id": "req_send_reg",
      "timestamp": "2026-08-01T12:00:00.000Z"
    }
  }
  ```
</ResponseExample>

The `domain` is registered with the sending provider, which issues real SPF/DKIM records — publish them with your DNS provider, then verify with the DNS-status read below before the sender can send. Omit `default_from_email` to send as `noreply@<domain>`.

### 2. Update the sender's defaults

<Note>
  `PUT /api/v1/email/senders/{senderId}`
</Note>

**Request**

```json theme={null}
{
  "default_from_email": "offers@acme.com",
  "default_from_name": "Acme Offers",
  "default_reply_to": "support@acme.com"
}
```

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "esend_9f3ab2d61c4a",
      "domain": "mail.acme.com",
      "default_from_email": "offers@acme.com",
      "default_from_name": "Acme Offers",
      "default_reply_to": "support@acme.com",
      "isDefault": true,
      "streamId": null
    },
    "meta": {
      "request_id": "req_send_upd",
      "timestamp": "2026-08-01T12:05:00.000Z"
    }
  }
  ```
</ResponseExample>

`PUT` replaces the From/Reply-To defaults — pass every field you want to keep. Promoting back to default uses the dedicated endpoint below (flipping `isDefault: false` here is a no-op).

### 3. Promote a sender to the org default

<Note>
  `POST /api/v1/email/senders/{senderId}/set-default`
</Note>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "esend_9f3ab2d61c4a",
      "domain": "mail.acme.com",
      "default_from_email": "offers@acme.com",
      "default_from_name": "Acme Offers",
      "default_reply_to": "support@acme.com",
      "isDefault": true,
      "streamId": null
    },
    "meta": {
      "request_id": "req_send_default",
      "timestamp": "2026-08-01T12:06:00.000Z"
    }
  }
  ```
</ResponseExample>

The compose dropdown fills with this sender first; the previous default is demoted. Deleting a sender (`DELETE /api/v1/email/senders/{senderId}`) removes it from the dropdown and accepts no body.

### 4. Check the domain's DNS records

<Note>
  `GET /api/v1/email/domains/{domainId}/dns-status`
</Note>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "domain": "acme.com",
      "punycode": "acme.com",
      "overallStatus": "valid",
      "checkedAt": "2026-08-01T12:15:00.000Z",
      "fromCache": false,
      "isDefault": false,
      "records": [
        {
          "kind": "spf",
          "status": "valid",
          "recordExpected": "v=spf1 include:_spf.send.devotel.io ~all",
          "recordActual": "v=spf1 include:_spf.send.devotel.io ~all",
          "issues": []
        },
        {
          "kind": "dkim",
          "selector": "resend",
          "status": "valid",
          "recordExpected": "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA",
          "recordActual": "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA",
          "issues": []
        },
        {
          "kind": "dmarc",
          "status": "valid",
          "recordExpected": "v=DMARC1",
          "recordActual": "v=DMARC1; p=none",
          "issues": []
        },
        {
          "kind": "mx",
          "status": "valid",
          "recordExpected": "9 inbound.devotel.io",
          "recordActual": "9 inbound.devotel.io",
          "issues": []
        }
      ]
    },
    "meta": {
      "request_id": "req_dns_status",
      "timestamp": "2026-08-01T12:15:00.000Z"
    }
  }
  ```
</ResponseExample>

Each record renders a traffic light (`valid` / `warning` / `invalid` / `unknown`); `overallStatus` is the worst of them. Compare `recordExpected` against `recordActual` to see what your DNS provider is missing. Pass `?refresh=true` to re-verify at the provider, not just at DNS. `GET /dns-history?days=30` returns the same id with an `entries` array of dated record snapshots so you can trace propagation.

### 5. Run a test-send and poll the result

<Note>
  `POST /api/v1/email/domains/{domainId}/test-send`
</Note>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "testId": "emailTestSend_53a1c0f9d2",
      "to": "emailTestSend_53a1c0f9d2@orbittest.devotel.io",
      "from": "noreply@orbit.devotel.io",
      "providerMessageId": "r_eml_4c2d1b8f9a",
      "status": "pending"
    },
    "meta": {
      "request_id": "req_test_send",
      "timestamp": "2026-08-01T12:17:00.000Z"
    }
  }
  ```
</ResponseExample>

Poll `GET /api/v1/email/domains/{domainId}/test-send/{testId}/result` with the returned `testId`:

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "testId": "emailTestSend_53a1c0f9d2",
      "domain": "acme.com",
      "status": "received",
      "spamScore": 0.4,
      "estimated": false,
      "authResults": {
        "spf": "pass",
        "dkim": "pass",
        "dmarc": "pass",
        "raw": ""
      },
      "verifierSummary": null,
      "placement": null,
      "headersRaw": null,
      "createdAt": "2026-08-01T12:17:00.000Z",
      "receivedAt": "2026-08-01T12:19:11.000Z"
    },
    "meta": {
      "request_id": "req_test_result",
      "timestamp": "2026-08-01T12:20:00.000Z"
    }
  }
  ```
</ResponseExample>

Until the proof lands the result endpoint returns 404 (retry — it usually settles within a minute). `estimated: true` means the `spamScore` is a provisional estimate from the domain's DNS grid, not the measured receiver verdict. `GET /inbox-placement?days=30` aggregates delivered proofs into an inbox/spam/promotions breakdown with an `inboxRate` percentage.

### 6. Validate recipient addresses

<Note>
  `POST /api/v1/email/validate`
</Note>

**Request**

```json theme={null}
{
  "email": "recipient@example.com"
}
```

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "email": "recipient@example.com",
      "valid": true,
      "suggestion": null,
      "reason": "deliverable"
    },
    "meta": {
      "request_id": "req_val",
      "timestamp": "2026-08-01T12:12:00.000Z"
    }
  }
  ```
</ResponseExample>

Check addresses before a campaign or import. `suggestion` carries the corrected domain on mistyped common domains (e.g. `gamil.com` → `gmail.com`). For batch checks, `POST /api/v1/email/validate/bulk`:

**Request**

```json theme={null}
{
  "emails": [
    "alice@example.com",
    "bad.domain",
    "bob@example.com"
  ]
}
```

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "total": 3,
      "valid": 2,
      "invalid": 1,
      "results": {
        "alice@example.com": {
          "valid": true,
          "reason": "deliverable"
        },
        "bad.domain": {
          "valid": false,
          "reason": "syntax_failure"
        }
      },
      "invalidAddresses": [
        "bad.domain"
      ]
    },
    "meta": {
      "request_id": "req_val_bulk",
      "timestamp": "2026-08-01T12:13:00.000Z"
    }
  }
  ```
</ResponseExample>

Each address reports `deliverable`, `mailbox_full / undeliverable`, `mailbox_disabled`, `spamtrap`, `potential`, `invalid_mx`, `no_mx`, or `syntax_failure`. The bulk form returns a `results` map plus the `invalidAddresses` list so you don't have to cross-check every entry.

### 7. Manage the suppression list

<Note>
  `POST /api/v1/email/suppressions`
</Note>

**Request**

```json theme={null}
{
  "email": "customer@example.com",
  "reason": "manual"
}
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "esuppression_2a9c5f1d",
      "email": "customer@example.com",
      "reason": "manual",
      "bounceType": null,
      "addedAt": "2026-07-20T09:14:00.000Z",
      "expiresAt": null
    },
    "meta": {
      "request_id": "req_supp_add",
      "timestamp": "2026-08-01T12:07:00.000Z"
    }
  }
  ```
</ResponseExample>

Suppressed addresses never receive sends — the send path rejects them before they reach the provider. Manual reason accepts `manual`, `unsubscribe`, `complaint`, or `hard_bounce`; bounce/automatic entries (collector handles them) carry the machine reasons. Repeat an add and it returns 200 with the existing entry rather than 201.

Bulk-import (`POST /api/v1/email/suppressions/bulk-import`) accepts either CSV text (one address per line, optional `email` header row tolerated) or an `emails` array:

**Request**

```json theme={null}
{
  "csv": "email\nbounced@example.com\ncomplained@example.com",
  "reason": "hard_bounce"
}
```

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "imported": 2,
      "skipped": 0,
      "invalid": 0
    },
    "meta": {
      "request_id": "req_supp_bulk",
      "timestamp": "2026-08-01T12:08:00.000Z"
    }
  }
  ```
</ResponseExample>

`imported` counts newly-added entries; `skipped` counts duplicates already on the list; `invalid` counts malformed addresses dropped. `GET /api/v1/email/suppressions` pages the list (`?limit=&offset=&search=&reason=`):

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "entries": [
        {
          "id": "esuppression_2a9c5f1d",
          "email": "customer@example.com",
          "reason": "manual",
          "bounceType": null,
          "addedAt": "2026-07-20T09:14:00.000Z",
          "expiresAt": null
        },
        {
          "id": "esuppression_7b21e03c",
          "email": "bounced@example.com",
          "reason": "hard_bounce",
          "bounceType": "hard",
          "addedAt": "2026-07-18T22:41:00.000Z",
          "expiresAt": "2026-08-17T22:41:00.000Z"
        }
      ],
      "total": 2
    },
    "meta": {
      "request_id": "req_supp_list",
      "timestamp": "2026-08-01T12:09:00.000Z"
    }
  }
  ```
</ResponseExample>

Remove an entry with `DELETE /api/v1/email/suppressions/{id}` (returns `{ "ok": true }`). `GET /api/v1/email/suppressions/reputation?days=30` reports a suppression-rate breakdown by reason over trailing send volume — the number a deliverability review quotes.

### Warmup, dedicated IP, and inbound routing (advanced)

These complete the sender's onboard checklist; most tenants never need them.

`POST /api/v1/email/inbound-routes` registers an inbound subdomain→destination mapping (webhook):

**Request**

```json theme={null}
{
  "pattern": "support/tickets-<tenant>",
  "destination": {
    "type": "webhook",
    "url": "https://your-app.example/inbound/email",
    "authHeader": "Bearer your-token"
  }
}
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "einrt_3b7f2a9d1c",
      "pattern": "support/tickets-<tenant>",
      "destination": {
        "type": "webhook",
        "url": "https://your-app.example/inbound/email"
      }
    },
    "meta": {
      "request_id": "req_inrt_cre",
      "timestamp": "2026-08-01T12:30:00.000Z"
    }
  }
  ```
</ResponseExample>

`GET /api/v1/email/warmup-plan` returns the day-by-day ramp for your `?targetDailyVolume=`:

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "targetDailyVolume": 50000,
      "startingVolume": 750,
      "growthFactor": 2,
      "lookbackDays": 30,
      "days": [
        {
          "day": 1,
          "dailyVolume": 750
        },
        {
          "day": 3,
          "dailyVolume": 1500
        },
        {
          "day": 9,
          "dailyVolume": 3750
        },
        {
          "day": 25,
          "dailyVolume": 18750
        }
      ]
    },
    "meta": {
      "request_id": "req_warmup_plan",
      "timestamp": "2026-08-01T12:26:00.000Z"
    }
  }
  ```
</ResponseExample>

`GET /api/v1/email/warmup-status` adds what you have actually sent (`sent.totalSent`, the live-day `current.maxAllowed`, and reputation). `GET /api/v1/email/warmup-enforcement` answers "which batches get sent when the pattern doesn't match a plan day" for a `requested` batch.

The dedicated-IP flow is three calls: `GET /api/v1/email/dedicated-sending/eligibility` (trailing-30-day volume ≥ 50 000/day), `POST /api/v1/email/dedicated-sending/request` with a free-text justification, and `GET /api/v1/email/dedicated-sending` to read eligibility plus request status (`pending`/`approved`/`fulfilled`/`denied`).
