# emails.send

> Part of the Cowliss docs. Markdown for every page: append `index.md` to its URL. Site map: /llms.txt

The mail your own code decides to send, with no automation and no rule behind it. This page is the send call. The templates it can name, the `emails/` directory and its props, are [Email templates](/emails/).

```ts
const { deliveryId } = await cow.emails.send({
  to: "ada@acme-demo.io",
  from: "Acme <hello@acme.com>",
  template: { key: "receipt", props: { orderId: "A-2291" } },
});
```

Or write the message inline:

```ts
await cow.emails.send({
  to: "ada@acme-demo.io",
  from: "Acme <hello@acme.com>",
  subject: "Your receipt",
  html: "<p>Thanks for your order.</p>",
});
```

Or send to a profile, which is how a message its recipient can turn off goes out, such as a weekly report:

```ts
await cow.emails.send({
  to: { userId: "shop_1" },
  from: "Acme <hello@acme.com>",
  template: { key: "weekly-report", props: { visits: 120 } },
});
```

| Field | Required | Notes |
| --- | --- | --- |
| `to` | yes | One address, or a list of up to 50: `"ada@acme.com"` or `"Ada <ada@acme.com>"`. Or one identifier naming a profile, `{ userId: "shop_1" }`, of any kind: `userId`, `anonymousId`, `clerkId` or `email`. |
| `cc` | no | One address, or a list. Refused when `to` is a profile. |
| `bcc` | no | Same as `cc`. |
| `replyTo` | no | One address, or a list. |
| `from` | yes | An address on a domain your organization verified. There is no default. |
| `template` | either | `{ key, props }`: a template you pushed, and the props it declares. It writes the whole message, subject included. |
| `subject` | either | Required when you write the body yourself; refused beside a template. |
| `html` / `text` | either | The inline body, one or both. Refused beside a template. |
| `headers` | no | Passed on as you wrote them. |
| `sourceId` | on the wire | The client's, unless the call names one. |
| `idempotencyKey` | no | Your own name for this send, such as an order id. Sent as the `Idempotency-Key` header, never in the body. |

The promise resolves once the message has been handed over, and `deliveryId` is the row in [Deliveries](/emails/delivery/) that records what happened to it.

A retry after a lost reply never sends twice: every attempt of one call carries the same key, and a repeated key answers with the first send's `deliveryId`. Without your own `idempotencyKey` each call gets a fresh one, so running your code again (a queue redelivering a job, say) sends again. Pass a key that names the message, and a second run sends nothing:

```ts
await cow.emails.send({
  to: { email: order.email },
  from: "Acme <hello@acme.com>",
  template: { key: "receipt", props: { orderId: order.id } },
  idempotencyKey: `receipt-${order.id}`,
});
```

A list in `to` is one message with one `deliveryId`, and every address on it sees the others in its `To:` line. To send each person their own message, call `emails.send` once per address.

## What is checked

Your credit, then your suppression list, then the from-address. An address that has bounced or complained is dropped from the message; if that leaves nobody, the call still answers with an id and the delivery records that it was suppressed.

## Addresses and profiles

A message to an address goes to that address and asks nobody's choice, so only mail nobody can turn off may go this way: an inline body, or a template sent as bare `transactional`, such as a receipt or a password reset. A template whose purpose its recipient can turn off, any `marketing.<name>` or `transactional.<name>`, is refused as a 422 `validation_failed`, and the message says to address a profile instead.

A message to a profile is how those go out. Name one identifier of the person. Cowliss looks it up in the app the source belongs to, so the same value in another of your apps is another person, and sends to the profile's `email` trait, else its newest `email` identifier. Then:

- **Consent is checked.** The purpose the template declares, and its root, are read from the profile. If the person turned the purpose off, or a marketing purpose was never granted, the call still answers with an id and the delivery records that it was skipped for consent. A suppressed address is reported before that.
- **They can leave.** The message carries the `List-Unsubscribe` headers and, unless the template renders its own links, a footer with two links: update email preferences (where leaving everything is) and leave this purpose. See [Unsubscribe links](/emails/#unsubscribe-links).
- **A new address is a new person.** `to: { email }` that no profile in the app holds yet creates that profile first, with the address as its `email` identifier and trait, exactly as `identify` would, then sends to it. Like any new profile it starts with marketing off.
- **A person Cowliss cannot reach fails the call.** Any other identifier no profile in the app holds is a `not_found`, and a profile with neither an `email` trait nor an `email` identifier is a `validation_failed`. Nothing is sent or recorded either way, so identify the person, with their email, and send again.

Mail that follows a rule or a wait belongs in an [automation](/automations/).

## Testing a send

Two ways to exercise the real call from your own code or test suite without mailing anyone.

### `test: true`: would this exact send work?

```ts
const { status, rendered, wouldCreateProfile } = await cow.emails.send({
  to: { userId: "shop_1" },
  from: "Acme <hello@acme.com>",
  template: { key: "weekly-report", props: { visits: 120 } },
  test: true,
});
// status: "would_send", or why it would not: "would_skip_consent", "would_skip_suppressed"
```

A test runs every check the real send runs and refuses exactly as it would: the same errors for a from-address on a domain you have not verified, an unknown template, props the template refuses, a profile that does not exist. Then, instead of sending:

- **`status`** is what the real send would record: `would_send`, or `would_skip_suppressed` / `would_skip_consent` when a check would skip it.
- **`rendered`** is the message it would carry: `subject`, `html` and `text`, unsubscribe footer included.
- **`wouldCreateProfile`** is `true` when `to` names an `email` no profile holds, so the real send would create one. The test creates nobody, so it writes no events and no automation hears of it.
- **It leaves a row** in [Deliveries](/emails/delivery/) with the same status, so you can read it there too.

A test sends nothing, uses no credit, and ignores `idempotencyKey`, so the real send with the same key still goes out. The real send answers a `status` too: `sent`, or the skip a check recorded.

### Test addresses: what happens after?

A send to one of these addresses is a real send: every check, a `sent` delivery, credit used as for any send. What happens next is chosen by the address, and arrives the way it would from a real mailbox.

| Address | Outcome |
| --- | --- |
| `delivered@test.cowliss.com` | The delivery moves to `delivered`. |
| `bounced@test.cowliss.com` | The delivery moves to `bounced`. The address is never added to your suppression list, so the next test bounces again. |
| `complained@test.cowliss.com` | The delivery moves to `complained`. On a profile it is a real spam report: marketing mail turns `marketing` off, mail under a `transactional.<name>` turns that name off, and each switch turned off writes `system.consent_revoked`, which automations and segments can listen for. Bare `transactional` mail turns nothing off. |
| `suppressed@test.cowliss.com` | Skipped as an address on your suppression list is: the send answers `skipped_suppressed`, and a test answers `would_skip_suppressed`. |

Add a label after a `+`, such as `bounced+order-42@test.cowliss.com`, to give each test its own address and so its own profile. A profile whose `email` trait is a test address works the same, so one send can test the consent check, the footer and what your app does after a bounce. Resend's `delivered@resend.dev`, `bounced@resend.dev` and `complained@resend.dev` behave like the first three.

With `test: true` a test address is only a dry run: nothing goes out, so nothing happens after.

## A link to their email preferences

If your app has a "Manage email preferences" button, send the person to Cowliss's hosted page instead of building switches of your own. Ask for the link when they click, since it works for an hour, and redirect them to `url`:

```ts
const { url, expiresAt } = await cow.preferences.link({ to: { userId: "shop_1" } });
```

`to` names one profile exactly as a send to a profile does, with the same `sourceId` default and the same refusals: a `not_found` for an identifier no profile in the source's app holds, a `validation_failed` for anything but one identifier. A profile with no `email` trait still gets a link. What the page shows and writes is in [Email preferences](/emails/#email-preferences).

## Your own preferences page

To draw the switches in your own app, in your own language and look, read them with `cow.preferences.get` and save them with `cow.preferences.set`. Both name the person as `preferences.link` does, from your server, with the same refusals.

```ts
const { purposes } = await cow.preferences.get({ to: { userId: "shop_1" } });
// [
//   { key: "marketing", label: "Marketing", description: null, enabled: true, answered: true },
//   { key: "marketing.newsletter", label: "Newsletter", description: "Once a month", enabled: true, answered: true },
//   { key: "transactional.weeklyReport", label: "Weekly report", description: null, enabled: true, answered: false },
// ]
```

There is one row for `marketing` and one for each purpose your app declares in `cow.json`, `marketing` first and the rest by key. Bare `transactional` is not a row, because nobody can turn off a receipt.

- **`enabled`** is what a send under that purpose would do now: `true` means it reaches them. A name is enabled only while its root is too.
- **`answered`** is whether the person (or your app) has given an answer for that key. An unanswered purpose reads as its root decides: a marketing one is off, a transactional one is on.

Save what they chose with `set`. `consent` names only the switches that changed; every key you leave out keeps its answer.

```ts
const { purposes } = await cow.preferences.set({
  to: { userId: "shop_1" },
  consent: { "marketing.newsletter": false },
});
```

`set` saves exactly as the hosted page does. Turning a `marketing.<name>` on turns `marketing` on too, and turning `marketing` off turns off every `marketing.<name>` your app declares. Each switch that changes writes `system.consent_granted` or `system.consent_revoked` with reason `preferences`, and a switch that already held writes nothing. A key your app does not declare is a 422 `validation_failed`, with nothing saved. It answers with the rows as they now stand, so you can redraw the page from the reply.

`identify({ consent })` and `set` write the same answers, but the timeline says who chose. `identify` records your app's choice, such as a signup form's box, and `set` records the person's own, reason `preferences`, as their click on the hosted page would.

The emails themselves can link to your page instead of Cowliss's: see [Sending in several languages](/emails/#sending-in-several-languages).

## Already sending with Resend?

Point the Resend SDK at Cowliss and keep the code you have. See [Resend SDK](/resend/).
