---
title: Send with a shared domain connection
description: Connect SMTP once, choose same-domain senders per campaign step, and receive replies in one inbox.
---

> Examples use the deployed API contract: snake_case fields and native Rust enum values. Check the [live OpenAPI](https://api.norbelys.com/openapi.json) when integrating another installation.

Norbelys sends through your SMTP provider. A mailbox resource stores one encrypted connection; a sequence step can choose another From identity in that mailbox's exact domain. You do not need a separate Norbelys mailbox or SMTP login for each sender. The provider must authorize those identities.

## Connect once

Use `https://api.norbelys.com/v1` with `Authorization: Bearer $NORBELYS_API_KEY`. The key belongs to the intended workspace. Keep it and your SMTP password in environment variables or a secret manager, never browser code or Git. All examples below are JSON bodies for the named HTTP request; substitute your own values.

`POST /mailboxes`:

```json
{
  "email": "smtp@example.com",
  "name": "Example",
  "provider": "Custom",
  "smtp": {
    "host": "smtp.example.com",
    "port": 587,
    "security": "Starttls",
    "username": "smtp@example.com",
    "password": "YOUR_DOMAIN_SMTP_KEY"
  },
  "imap": { "host": "smtp.example.com", "port": 993, "security": "Tls" },
  "daily_limit": 50
}
```

Save the returned mailbox ID. Call `PATCH /mailboxes/{id}` with `{"status":"Verifying"}`, then poll `GET /mailboxes/{id}` until `status` is `Active`. Verification authenticates SMTP; it does not send a message or prove inbox placement. Inspect `status_detail` if it fails. Passwords are never returned.

The SMTP username may differ from the message's From address. IMAP uses the SMTP username and password. Enable IMAP only on the central mailbox so its replies are ingested once. Addresses with separate, existing inboxes keep their own connections.

## Register a domain and enable tracking

`POST /domains` with `{"hostname":"example.com"}` registers a domain with tracking disabled. Publish its returned TXT record to verify ownership (`Verified`). This does not create mailboxes or configure sending DNS.

For branded tracking, register a subdomain with the capability enabled:

`POST /domains`:

```json
{ "hostname": "track.example.com", "tracking_enabled": true }
```

`PATCH /domains/{id}` with `{"tracking_enabled":true}` also enables tracking on an existing subdomain. Apex domains support ownership verification but cannot carry the required CNAME.

Publish both DNS records returned in `records`: the ownership TXT and the CNAME. Use the returned values, not a token copied from an example. On Cloudflare, set the CNAME to **DNS only**, with no flattening or proxy that hides it. Keep the TXT in place after activation. Do not replace the apex website or mail MX records.

Call `PATCH /domains/{id}` with `{"status":"PendingVerification"}`, then poll its GET endpoint. The lifecycle is `PendingVerification`, `PendingCertificate`, then `Active`. Activation requires a valid HTTPS certificate and the tracker's proof, not just a DNS match. Resolver caches may delay onboarding. On a timeout, inspect DNS and `status_detail` before retrying; do not delete and recreate the domain.

Disable with `{"tracking_enabled":false}`. Ownership verification remains, but the hostname is withdrawn from the tracker after manifest refresh: previously sent custom links stop working, and new messages fall back to the platform tracking host. Keep tracking enabled while recipients still need those links. Existing domain IDs remain valid. The campaign field `tracking_domain_id` selects a domain specifically for its tracking capability.

## Create a simple campaign

`POST /campaigns`:

```json
{
  "name": "Requested product walkthrough",
  "mailbox_ids": ["YOUR_MAILBOX_ID"],
  "timezone": "America/Bogota",
  "sequences": [
    {
      "name": "Introduction",
      "delay_seconds": 0,
      "tracking_domain_id": "YOUR_TRACKING_DOMAIN_ID",
      "track_opens": true,
      "track_clicks": true,
      "from": { "email": "santi@example.com", "name": "Santi" },
      "variants": [
        {
          "name": "A",
          "subject": "Your requested walkthrough",
          "html": "<p>Hello,</p><p>Here is the <a href=\"https://example.com\">walkthrough you requested</a>.</p>",
          "text": "Here is the walkthrough you requested: https://example.com"
        }
      ]
    }
  ]
}
```

Save the campaign ID and the first `sequences[].id`, used as `step_id` when sending.

Omit `from` to use the mailbox identity. An identity in another domain, including a subdomain, is rejected. The SMTP provider can still reject a same-domain identity it has not authorized. `PATCH /campaigns/{id}` with the complete `sequences` graph and a changed `from` publishes a revision; `"from": null` returns to the mailbox identity. Existing enrollments retain their captured revisions. Accepted messages freeze the From identity and return it in `from`.

All identities on the same mailbox share its `daily_limit`, enabled state and SMTP password. Creating more identities does not multiply that mailbox's sending allowance. SMTP rate limits may be stricter.

Activate with `PATCH /campaigns/{id}` with `{"status":"Active"}`, then send `POST /messages` with an `Idempotency-Key` header unique to that intended message:

```json
{
  "step_id": "YOUR_SEQUENCE_EMAIL_ID",
  "recipient": {
    "email": "consenting-recipient@example.net",
    "name": "Recipient"
  }
}
```

Save the returned message ID and poll `GET /messages/{id}`. Reuse the same idempotency key and body if an acceptance request times out. `Sent` means SMTP accepted the message; inspect the recipient inbox and server queue for actual delivery. Do not automatically resend `Uncertain` messages. Pause a completed test with `PATCH /campaigns/{id}` with `{"status":"Paused"}`.

## Replies and catch-all

Configure catch-all routing at your SMTP provider so previously unregistered addresses reach the central mailbox. Preserve exact routes for real mailboxes; catch-all rules can otherwise capture their mail too. Norbelys reads the central mailbox by IMAP and associates replies through `In-Reply-To` and `References`. Catch-all addresses do not have independent passwords or inboxes.

## Recipient opt-out

Every campaign email includes a visible Unsubscribe link and `List-Unsubscribe` / `List-Unsubscribe-Post` headers. GET displays a confirmation without changing subscriptions. POST with the form field `List-Unsubscribe=One-Click` suppresses the recipient across that workspace; repeating it is safe. No login or API key is needed: the encrypted link authorizes only that recipient's opt-out. Treat that link as a capability and do not publish it in logs or docs.

See [Deliverability and sending policy](/deliverability) before expanding volume, and the [live OpenAPI contract](https://api.norbelys.com/openapi.json) for fields and responses.
