Send your application's own email - a helpdesk reply, a portal sign-in code - for a workspace that subscribes to you, without an email provider account of your own.

### Who it is from

Every email leaves from Connect's sending address, `support@cosmoops.com`, under a display name: the `fromName` you send, or the workspace's name when you send none. You choose the name and the `replyTo`, never the address - so a customer's answer reaches your inbound address, and nobody can send as Connect's billing or sign-in mail.

### Threads

Mail clients thread on `In-Reply-To` and `References` and nothing else. Every send answers the `messageId` the email actually left under - Connect's mail provider replaces any Message-ID you propose, so store the one in the response, not your own. Reply with `headers.inReplyTo` set to the message you are answering and `headers.references` to the thread so far.

### Addresses that cannot receive

Connect keeps one suppression list for the whole platform: an address that hard-bounced or marked mail as spam is never sent to again until an operator clears it. Your mail shares that list and that reputation. A suppressed recipient is skipped, the others are sent, and the skipped ones come back in `suppressed` - show the address as undeliverable rather than retrying, because a retry is skipped the same way.

### Budgets

Per key, and per key and recipient - a ticket thread is a few replies an hour. Send an `Idempotency-Key` with every call: a retried send without one is a second copy in somebody's inbox.


## Endpoints

### POST `/api/v1/application/email/send`

Send an email on a workspace's behalf

**Scope** `email:send` &nbsp;·&nbsp; **Rate limit** 120 requests / min per key, 30 requests / hour per key+recipient

Sends your application's own email - a support reply, a portal sign-in code - for a workspace that subscribes to your application. Server-to-server only: a request carrying a member's session token is refused.

#### Implementation

**1. Prerequisite** - an API key scoped to `email:send`.

**2. Send** - `html` and `text` are both required (256 KB each); `to` and `cc` take up to 10 addresses each:
```bash
curl -X POST https://connect.cosmoops.com/api/v1/application/email/send \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ticket-1042-reply-3" \
  -d '{"workspaceId":"WORKSPACE_ID","to":["priya@example.com"],"subject":"Re: Refund for order 1042","html":"<p>Done - it is on its way.</p>","text":"Done - it is on its way.","replyTo":"t-1042@in.example.com","fromName":"Acme Support","headers":{"inReplyTo":"<CAF0x9@mail.gmail.com>","references":"<CAF0x9@mail.gmail.com>"}}'
```

**3. The sender.** Every email leaves from Connect's address (`support@cosmoops.com`) with `fromName` as the display name - the workspace's name when you send none. Put your own inbound address in `replyTo` to receive the answer.

**4. Threading.** Store the response's `messageId`: it is the Message-ID the email actually left under (Connect's mail provider replaces any `headers.messageId` you send), and a customer's reply names it in `In-Reply-To`. Send the next reply with `headers.inReplyTo` and `headers.references` set from the thread. Header values may not contain line breaks.

**5. Suppression.** An address that hard-bounced or marked Connect's mail as spam is skipped and listed in `suppressed`; the rest are sent. When every recipient is suppressed nothing is sent and the call still answers `201` - show the customer's address as undeliverable rather than retrying.

#### Errors
`validation_error` (400 - bad body, too many recipients, an oversize body, or a line break in a header value), `forbidden` (403 - key missing `email:send`, a session token was forwarded, or your application is not installed in that workspace), `idempotency_key_reused`/`request_in_progress` (409), `rate_limited` (429 - per key, and per key and recipient), `delivery_failed` (502 - retry).

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `workspaceId` | `string` | yes |  |
| `to` | `array` | yes |  |
| `cc` | `array` | no |  |
| `subject` | `string` | yes |  |
| `html` | `string` | yes |  |
| `text` | `string` | yes |  |
| `replyTo` | `email` | no |  |
| `fromName` | `string` | no |  |
| `headers` | `object` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `201` | Sent (or every recipient suppressed - see `suppressed`) |
| `400` | validation_error - bad body, recipient cap, oversize body, or a line break in a header |
| `403` | forbidden - key missing email:send, a forwarded session, or not installed in that workspace |
| `409` | idempotency_key_reused / request_in_progress |
| `429` | rate_limited |
| `502` | delivery_failed - the mail provider refused the message; retry |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `messageId` | `string` | yes |  |
| `suppressed` | `array` | yes |  |