> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Email

Jelliu agents send and answer email through a mailbox your workspace connects: **Gmail, Outlook or Zoho Mail**. Mail leaves from that mailbox, is signed by that provider with your domain, and appears in its Sent folder. Every send goes through the same do-not-contact, opt-out and daily-cap checks as the rest of the platform.

Email is always sent from your own mailbox. Jelliu does not send your customer email from a platform address. Until a mailbox is connected and selected, `POST /api/email/send` fails with `409`.

## How it works

```mermaid
flowchart LR
    A[Connect Gmail, Outlook or Zoho Mail<br />Integrations] --> B[Select it as the sender<br />PUT /api/email/settings/sending]
    B --> C[POST /api/email/send]
    C --> D{Checks}
    D -->|contact, compliance,<br />opt-out, daily cap| E[Sent from your mailbox]
    E --> F[Contact replies to your mailbox]
    F --> G{Reply gate}
    G -->|we opened the thread, or<br />active email campaign| H[Agent answers in the same thread]
    G -->|uninvited, automated<br />or opted out| I[No answer]
```

1. **Connect a mailbox** as an integration. See [Integrations](/platform/integrations).
2. **Select it as the sender.** Jelliu checks with the provider that the connection works before saving it.
3. **Send.** Proactive email goes out through the API, campaigns or an agent.
4. **Receive replies.** Turn on reply ingestion for the mailbox so the agent can answer. A reply is answered only if the conversation is one your workspace started.

## Endpoints

| Method  | Path                                           | API key scope |
| ------- | ---------------------------------------------- | ------------- |
| `POST`  | `/api/email/send`                              | `write`       |
| `GET`   | `/api/email/settings`                          | `read`        |
| `PUT`   | `/api/email/settings/sending`                  | `full`        |
| `PATCH` | `/api/email/settings`                          | `full`        |
| `GET`   | `/api/composio/triggers/email-replies/support` | `full`        |
| `POST`  | `/api/composio/triggers/email-replies`         | `full`        |

## Sending identity

### Read the settings

```bash
curl -sS "https://api.jelliu.co/api/email/settings" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

```json
{
  "data": {
    "fromEmail": "ventas@clinicanorte.example.com",
    "senderName": "Clínica Norte",
    "inboundEmail": null,
    "replyToEmail": "contacto@clinicanorte.example.com",
    "sendingProvider": "gmail",
    "sendingConnectionId": "5e0c7a1b-2d3f-4e5a-9b8c-7d6e5f4a3b21",
    "sendingIssue": null,
    "canSend": true,
    "replyToSupported": false,
    "availableMailboxes": [
      {
        "provider": "gmail",
        "connectionId": "5e0c7a1b-2d3f-4e5a-9b8c-7d6e5f4a3b21",
        "address": "ventas@clinicanorte.example.com",
        "status": "active",
        "usable": true,
        "issue": null
      }
    ]
  }
}
```

| Field                 | Description                                                                                                                                                           |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `canSend`             | `true` only when a connected mailbox is selected and resolves. **Check this before sending.**                                                                         |
| `fromEmail`           | The address mail will actually leave from, resolved the same way a send resolves it.                                                                                  |
| `senderName`          | Display name used with the address.                                                                                                                                   |
| `sendingProvider`     | `gmail`, `outlook`, `zoho_mail`, or `null` when no mailbox is selected.                                                                                               |
| `sendingConnectionId` | The integration connection used to send.                                                                                                                              |
| `sendingIssue`        | Why the selected mailbox cannot be used right now, or `null`.                                                                                                         |
| `replyToEmail`        | Your workspace contact address.                                                                                                                                       |
| `inboundEmail`        | The workspace's inbound address for replies, when that feature is enabled; otherwise `null`.                                                                          |
| `replyToSupported`    | `false` whenever a mailbox is selected: the providers' send tools do not accept a Reply-To header, so replies arrive in the mailbox itself.                           |
| `availableMailboxes`  | Connected mailboxes you can select. `usable` is `true` only for an active connection with a known address and no recorded error (and, for Zoho Mail, its account ID). |

### Select the sending mailbox

**`provider`** `string` — required

`gmail`, `outlook` or `zoho_mail`. `null` is rejected: there is no platform sender to go back to.

---

**`connectionId`** `string (uuid)`

Which connection to use when several of the same provider are connected. Defaults to the most recently connected one.

---

**`cURL`**

```bash title="cURL"
curl -sS -X PUT "https://api.jelliu.co/api/email/settings/sending" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "gmail", "connectionId": "5e0c7a1b-2d3f-4e5a-9b8c-7d6e5f4a3b21" }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/email/settings/sending', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    provider: 'gmail',
    connectionId: '5e0c7a1b-2d3f-4e5a-9b8c-7d6e5f4a3b21',
  }),
});
const { data } = await res.json();
console.log(data.fromEmail, data.canSend);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.put(
    "https://api.jelliu.co/api/email/settings/sending",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    json={"provider": "gmail", "connectionId": "5e0c7a1b-2d3f-4e5a-9b8c-7d6e5f4a3b21"},
    timeout=60,
)
res.raise_for_status()
print(res.json()["data"]["fromEmail"])
```

Before saving, Jelliu makes a read-only call to the provider to confirm the connection still works and to learn the address it controls. The response is the full settings object with the address that will really be used. A connection that is missing, not active, has a recorded error, has no known address, or (for Zoho Mail) has no account ID is refused with `409` and a message naming what to fix, for example:

```json
{
  "error": {
    "code": "EMAIL_ERROR",
    "message": "No hay una conexion de outlook en este workspace. Conectala primero en Integraciones."
  }
}
```

Connecting a mailbox in Integrations gives agents tools to use it, but does not by itself change where email is sent from. Selecting the sender with this endpoint (or in the dashboard under **Settings → Account → Email**) is a separate step.

### Set the contact address

`PATCH /api/email/settings` with `{ "replyToEmail": "contacto@clinicanorte.example.com" }` (a valid email, up to 254 characters) sets the workspace contact address and returns the settings object.

## Sending email

`POST /api/email/send` sends one email to one contact.

**`contactId`** `string (uuid)` — required

The contact to write to. It must belong to your workspace and have an email address.

---

**`subject`** `string` — required

1 to 998 characters. Line breaks are replaced with spaces.

---

**`message`** `string` — required

Plain-text body, 1 to 50,000 characters. Leading and trailing whitespace is trimmed.

---

**`inReplyTo`** `string`

Continue an existing thread instead of starting a new one: the provider's thread ID (Gmail) or message ID (Outlook, Zoho Mail), up to 998 characters.

---

**`cURL`**

```bash title="cURL"
curl -sS -X POST "https://api.jelliu.co/api/email/send" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "subject": "Tu cotización de Clínica Norte",
    "message": "Hola Ana,\n\nTe comparto la cotización que pediste. Si tienes preguntas, responde a este correo.\n\nClínica Norte"
  }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/email/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    contactId: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
    subject: 'Tu cotización de Clínica Norte',
    message:
      'Hola Ana,\n\nTe comparto la cotización que pediste. Si tienes preguntas, responde a este correo.\n\nClínica Norte',
  }),
});

const body = await res.json();
if (!res.ok) {
  throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
}
console.log('Sent to', body.data.to);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.post(
    "https://api.jelliu.co/api/email/send",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    json={
        "contactId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "subject": "Tu cotización de Clínica Norte",
        "message": "Hola Ana,\n\nTe comparto la cotización que pediste. "
                   "Si tienes preguntas, responde a este correo.\n\nClínica Norte",
    },
    timeout=60,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}")
print("Sent to", body["data"]["to"])
```

**`200 OK`**

```json title="200 OK"
{
  "data": {
    "sent": true,
    "to": "ana@example.com"
  }
}
```

The message is also recorded in the contact's email conversation, so when the contact replies the agent knows what was sent. Read it through [Conversations](/resources/conversations).

### Checks before every send

In order, stopping at the first failure:

1. The contact exists in your workspace and has an email address.
2. A connected mailbox is selected and usable.
3. **Compliance.** The contact is checked against the suppression list, do-not-call status, blocked prefixes and the daily and total attempt limits of your [compliance configuration](/platform/compliance).
4. **Opt-out.** Contacts with status `dnc`, `do_not_call` or an opt-out date are refused.
5. **Daily cap.** One unit of the workspace's daily email cap is consumed. It is refunded if the send fails.

**Allowed contact hours do not apply to email.** They come from telemarketing calling-time rules, which say nothing about when an email is delivered. WhatsApp and calls still respect them.

### Threads

Pass `inReplyTo` to answer inside a conversation the contact is already reading. The mailbox's reply action is then used (Gmail replies to the thread; Outlook and Zoho Mail reply to the message), so the email lands in the same thread instead of arriving as a new message.

* When replying in a thread, the provider's thread determines the subject; your `subject` is still required and validated.
* When `inReplyTo` is omitted, a new email is sent.
* `inReplyTo` changes only how the email is delivered. Every check above still runs.

Agent replies to inbound mail always use the thread or message ID of the email they answer.

### Attachments and formatting

The API sends plain-text bodies only. There is no attachment field on `POST /api/email/send`.

### Retries and duplicates

A failed send is not retried when the result is uncertain, because retrying could deliver the same email twice. If you receive a `5xx` or a network error, check the contact's conversation before sending again.

## Receiving replies

Replies arrive in the connected mailbox. For an agent to answer them, turn on reply ingestion for that mailbox.

### Check support

```bash
curl -sS "https://api.jelliu.co/api/composio/triggers/email-replies/support" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

```json
{
  "data": [
    { "toolkit": "gmail", "connected": true, "triggerSlug": "GMAIL_NEW_GMAIL_MESSAGE", "support": "supported", "reason": null },
    { "toolkit": "zoho_mail", "connected": true, "triggerSlug": null, "support": "unsupported", "reason": "Este buzón no publica eventos de correo nuevo" }
  ]
}
```

| Mailbox   | Can receive replies                                             |
| --------- | --------------------------------------------------------------- |
| Gmail     | Yes                                                             |
| Outlook   | Yes. The full message body is fetched before the agent answers. |
| Zoho Mail | No. The provider publishes no new-mail events.                  |

### Turn on replies

**`toolkit`** `string` — required

The connected mailbox: `gmail` or `outlook`.

---

**`agentId`** `string (uuid)` — required

The agent that answers.

---

**`createContact`** `boolean` — default: false

Create a contact for a sender your workspace has never seen and let the agent answer people who write to the mailbox on their own. Off by default.

---

**`cURL`**

```bash title="cURL"
curl -sS -X POST "https://api.jelliu.co/api/composio/triggers/email-replies" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "toolkit": "gmail", "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4" }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/composio/triggers/email-replies', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ toolkit: 'gmail', agentId: '7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4' }),
});
console.log(res.status, (await res.json()).data);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.post(
    "https://api.jelliu.co/api/composio/triggers/email-replies",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    json={"toolkit": "gmail", "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4"},
    timeout=60,
)
print(res.status_code, res.json()["data"])
```

**`201 Created`**

```json title="201 Created"
{
  "data": {
    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "toolkit": "gmail",
    "triggerSlug": "GMAIL_NEW_GMAIL_MESSAGE",
    "status": "active",
    "action": "email",
    "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4"
  }
}
```

The result is an ordinary trigger. Pause, resume or delete it through the trigger endpoints described in [Integrations](/platform/integrations).

Activating an email campaign sends email but does not, by itself, make the agent answer replies. Reply ingestion is a switch for the whole mailbox, not for a campaign.

### The reply gate

A mailbox receives newsletters, vendor mail and cold lists, and knowing a sender's address is not a reason to answer them. For every incoming email, Jelliu decides whether the agent may reply:

| Rule                                                                                                                         | Answered |
| ---------------------------------------------------------------------------------------------------------------------------- | -------- |
| The first message of the oldest email conversation with that address was sent by your side (an agent, a person, or the API). | Yes      |
| The contact belongs to a campaign that is `active` and whose channel is `email`.                                             | Yes      |
| Reply ingestion was turned on with `createContact: true`.                                                                    | Yes      |
| Anything else.                                                                                                               | No       |

Then, even when the gate allows it, the agent does **not** answer if:

* **the email is automated**: an `Auto-Submitted` header other than `no`; `Precedence: bulk`, `list` or `junk`; auto-reply headers; mailing-list headers such as `List-Id` or `List-Unsubscribe`; a sender such as `mailer-daemon`, `postmaster` or `no-reply`; or an out-of-office or delivery-failure subject. The contact is left untouched and their next real email is answered;
* **the email is an opt-out** (see below);
* **the contact has already opted out**;
* **the daily email cap is reached**, or the monthly AI message allowance is used up.

Unanswered emails are not an error. When the provider delivers the same email as two events, it is claimed by its message ID and processed once.

## Opt-out

An incoming email is an opt-out when:

* its **first non-empty line** is one of the opt-out keywords used on every text channel: `stop`, `stopall`, `stop all`, `cancel`, `unsubscribe`, `quit`, `end`, `baja`, `cancelar`, `darme de baja`, `dar de baja`, `no molestar`; or
* its first 500 characters contain a phrase such as `unsubscribe`, `darme de baja`, `dar de baja`, `no me envíen más`, `remove me from` or `stop emailing`.

Jelliu then marks every contact in your workspace with that email address (compared case-insensitively) as `dnc`, records the date and the phrase, and **sends nothing back**. Later sends to those contacts fail with `403`: `Contact has opted out — cannot send email`.

Automated email is checked before opt-out, so an unsubscribe footer quoted inside a bounce never suppresses a contact.

## Bounces

Delivery failures and out-of-office replies are recognized as automated mail and are not answered. They do not change the contact's status: Jelliu does not currently suppress an address because a message to it bounced.

## Email campaigns

A campaign with `channel` set to `email` writes first to every contact, using the campaign's `emailSubject` (up to 300 characters) and `emailBody` (up to 20,000 characters). Campaign sends are deliberately paced, about one every 700 milliseconds per worker, and stop at 80% of the daily cap so replies always have room. See [Campaigns](/resources/campaigns).

## Errors

Send and settings errors use code `EMAIL_ERROR` with the HTTP status that describes the cause. Messages are shown verbatim; some are in Spanish.

| Status | Code                  | Message or cause                                                                                                                                                                                                                                                                                         |
| ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_FAILED`   | The body failed validation. See `details`.                                                                                                                                                                                                                                                               |
| `400`  | `EMAIL_ERROR`         | `Contact has no email address`                                                                                                                                                                                                                                                                           |
| `403`  | `EMAIL_ERROR`         | `Contact has opted out — cannot send email`                                                                                                                                                                                                                                                              |
| `409`  | `EMAIL_ERROR`         | No mailbox selected: `Este workspace todavía no tiene un buzón propio para enviar correo. Conecta Gmail, Outlook o Zoho Mail en Integraciones y elígelo en Ajustes → Cuenta → Email → «Sale desde». El correo sale siempre desde tu buzón, nunca desde una dirección de la plataforma.`                  |
| `409`  | `EMAIL_ERROR`         | The selected mailbox stopped working, for example `El buzón de envío conectado (gmail) no está disponible: la conexión ya no existe. Vuelve a conectar la aplicación en Integraciones, o cambia el remitente a la dirección de la plataforma en Ajustes → Email.` Reconnect the mailbox in Integrations. |
| `409`  | `EMAIL_ERROR`         | `PUT /api/email/settings/sending` with `provider: null`, or with a connection that is not usable.                                                                                                                                                                                                        |
| `422`  | `VALIDATION_FAILED`   | Turning on replies for a mailbox that cannot receive them, such as Zoho Mail.                                                                                                                                                                                                                            |
| `423`  | `EMAIL_ERROR`         | Blocked by compliance: `Phone number is on the opt-out suppression list`, `Contact is on the Do Not Call list`, a blocked prefix, or an attempt limit.                                                                                                                                                   |
| `429`  | `EMAIL_ERROR`         | `Daily email send cap reached for this tenant (1000/day). Try again tomorrow or contact support to raise the limit.`                                                                                                                                                                                     |
| `429`  | `RATE_LIMIT_EXCEEDED` | Per-minute rate limit. Honor `Retry-After`.                                                                                                                                                                                                                                                              |
| `403`  | `FORBIDDEN`           | The key's scope does not cover the route.                                                                                                                                                                                                                                                                |

## Limits

| Limit                                       | Value                                                                                                                                                 |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Requests to `/api/email/*`                  | 20 per minute per workspace, and fails closed. See [Rate limits](/rate-limits).                                                                       |
| `POST /api/email/send` and settings changes | Also count against the shared 10 per minute budget for configuration mutations.                                                                       |
| Daily email sends                           | 1,000 per workspace per UTC day by default; 25,000 on plans with unlimited monthly AI messages. Includes API sends, campaign sends and agent replies. |
| Reply reserve                               | Campaign sends stop at 80% of the daily cap, keeping at least 25 messages for replies.                                                                |
| Subject                                     | 998 characters (300 in campaigns).                                                                                                                    |
| Body                                        | 50,000 characters (20,000 in campaigns).                                                                                                              |
| AI replies                                  | Count against the plan's monthly AI message allowance shared by all text channels. See [Billing and usage](/platform/billing-and-usage).              |

Your mailbox provider's own sending limits also apply to every email sent from it.

### Scopes at a glance

| Operation                                                               | Minimum key scope |
| ----------------------------------------------------------------------- | ----------------- |
| Read email settings                                                     | `read`            |
| Send email                                                              | `write`           |
| Select the sending mailbox, change the contact address, turn on replies | `full`            |

## Related

#### [Integrations](/platform/integrations)

Connect Gmail, Outlook or Zoho Mail.

#### [Conversations](/resources/conversations)

Read email threads and messages.

#### [Compliance](/platform/compliance)

Suppression lists, opt-out and attempt limits.

#### [WhatsApp](/channels/whatsapp)

Templates, the 24-hour window and inbound routing.