# Send an email (/docs/api-reference/emails/send-email)

Queues an email from the API key's environment and returns it with `status: "queued"`; it is handed to the delivery provider moments later. Follow it with `GET /v1/emails/{id}` or the `email.sent` / `email.failed` webhooks. Live (`coss_live_…`) keys must send from a verified domain of the organization. Sandbox (`coss_test_…`) keys accept any `from` and never deliver anything; the response has `sandbox: true`. Recipients on the environment's suppression list (for every email, or for the email's `category`) are skipped and get the status `suppressed`. With a future `scheduled_at` the email is stored with the status `scheduled` and sent at that time; the domain and the suppression list are checked again then. Attachments are base64-encoded; only metadata is kept after sending. Recipients count against the organization's send quota (per second and per day; sandbox environments have their own daily quota). With open or click tracking on (the environment's settings, or `tracking`), the HTML body is delivered with a tracking pixel and/or links rewritten to coss.com redirects; the stored `html` stays as sent.

`POST https://api.coss.com/v1/emails`

Authenticate with your API key as a bearer token (`Authorization: Bearer coss_test_…`). Responses are JSON with snake_case fields; errors are `{ error: { type, code, message, param?, issues?, request_id } }` (https://coss.com/docs/api-reference/errors.md).

## Headers

- `Idempotency-Key` (string, 1-256 characters): Makes retries safe: a repeated request with the same key and body returns the original email (with its current status) instead of sending again, for 24 hours. 1-256 characters.

## Request body

- `from` (string, required): An email address, optionally with a display name: "Acme <hello@acme.com>".
- `to` (string | string[], required, 1-50 items): An email address, optionally with a display name: "Acme <hello@acme.com>".
- `cc` (string | string[], 0-50 items): An email address, optionally with a display name: "Acme <hello@acme.com>".
- `bcc` (string | string[], 0-50 items): An email address, optionally with a display name: "Acme <hello@acme.com>".
- `reply_to` (string | string[], 0-50 items): An email address, optionally with a display name: "Acme <hello@acme.com>".
- `subject` (string, 1-998 characters): Required unless the email is sent from a template (`template_id`).
- `html` (string): HTML body. Provide `html`, `text`, or both.
- `text` (string): Plain-text body. Without it, an `html` email goes out with a text part generated from the HTML.
- `template_id` (string, 1-100 characters): Send a published email template (its id or alias) instead of `subject`, `html` and `text`.
- `template_version` (integer, at least 1): A published version of the template to send. Defaults to the latest one.
- `locale` (string, at most 35 characters): The template locale to send, e.g. "pt-BR". Falls back to the language ("pt"), then to the template's default locale.
- `variables` (map of any): Values for the template's variables, keyed by name.
- `headers` (map of string): Up to 15 custom headers. Names are case-insensitive.
- `tags` (object[], at most 50 items)
  - `name` (string, required)
  - `value` (string, required)
- `category` (string): What kind of email this is, e.g. "marketing". Recipients suppressed for this category (or for every email) are skipped.
- `scheduled_at` (string, date-time): Send the email at this time instead of now (ISO 8601 with an offset, at most 30 days ahead). A time that is not in the future sends immediately. Scheduled emails can be rescheduled or canceled until they are sent.
- `attachments` (object[], at most 20 items): Up to 20 files, at most 25 MB combined.
  - `filename` (string, required, 1-255 characters)
  - `content` (string, required, at least 1 character): The file's bytes, base64-encoded.
  - `content_type` (string): Defaults from the filename's extension.
  - `content_id` (string): Makes the attachment inline: reference it from `html` as `<img src="cid:logo@acme">`.
- `tracking` (object): Overrides the environment's tracking settings (its default, or the rule for this email's category) for this email. Omitted fields keep the configured setting.
  - `opens` (boolean): Add an invisible pixel to the HTML body and record `email.opened` events.
  - `clicks` (boolean): Send HTML links through a coss.com redirect and record `email.clicked` events. Links with a `data-coss-no-track` attribute are left alone.
  - `unsubscribe` (boolean): Add one-click unsubscribe headers (`List-Unsubscribe` and `List-Unsubscribe-Post`, RFC 8058). An unsubscribe suppresses the recipient for this email's category and records `email.unsubscribed`. Needs a `category`, a single recipient, no `List-Unsubscribe` header of your own and at most 13 custom headers.
- `stream` ("transactional" | "marketing"): `transactional` (the default) or `marketing`. Marketing email goes out on its own sending infrastructure and reputation, so its complaints never hold up transactional mail. It needs the environment's postal address (marketing settings), exactly one recipient, and no `List-Unsubscribe` header: coss.com adds one-click unsubscribe headers and a footer with the address and an unsubscribe link (place them yourself with `{{{unsubscribe_url}}}`, `{{{preferences_url}}}` and `{{{mailing_address}}}`). Contacts who unsubscribed are skipped.

## Example request

TypeScript (the `coss` package):

```ts
import { Coss } from "coss";

const coss = new Coss({
  apiKey: process.env.COSS_API_KEY,
  baseUrl: "https://api.coss.com",
});

const { data, error } = await coss.email.send({
  from: "Acme <onboarding@acme.com>",
  to: "user@example.com",
  subject: "Welcome to Acme",
  html: "<p>Thanks for signing up!</p>",
  tags: [
    {
      name: "template",
      value: "welcome",
    },
  ],
  category: "onboarding",
}, {
  idempotencyKey: "welcome-user-123",
});
```

HTTP:

```sh
curl -X POST "https://api.coss.com/v1/emails" \
  -H "Authorization: Bearer $COSS_API_KEY" \
  -H "Idempotency-Key: welcome-user-123" \
  -H "Content-Type: application/json" \
  -d '{
  "from": "Acme <onboarding@acme.com>",
  "to": "user@example.com",
  "subject": "Welcome to Acme",
  "html": "<p>Thanks for signing up!</p>",
  "tags": [
    {
      "name": "template",
      "value": "welcome"
    }
  ],
  "category": "onboarding"
}'
```

Python:

```python
from coss import Coss
from coss import Coss, SendEmailRequestTagsItem

client = Coss()  # api_key= or COSS_API_KEY

client.email.send(
    idempotency_key="welcome-user-123",
    from_="Acme <onboarding@acme.com>",
    to="user@example.com",
    subject="Welcome to Acme",
    html="<p>Thanks for signing up!</p>",
    tags=[
        SendEmailRequestTagsItem(
            name="template",
            value="welcome",
        )
    ],
    category="onboarding",
)
```

Go:

```go
import (
	"context"

	coss "github.com/cosscom/coss-go"
	cossclient "github.com/cosscom/coss-go/client"
)

client := cossclient.NewClient() // option.WithAPIKey(…) or COSS_API_KEY

request := &coss.SendEmailBody{
    IdempotencyKey: coss.String(
        "welcome-user-123",
    ),
    Body: &coss.SendEmailRequest{
        From: "Acme <onboarding@acme.com>",
        To: &coss.SendEmailRequestTo{
            String: "user@example.com",
        },
        Subject: coss.String(
            "Welcome to Acme",
        ),
        HTML: coss.String(
            "<p>Thanks for signing up!</p>",
        ),
        Tags: []*coss.SendEmailRequestTagsItem{
            &coss.SendEmailRequestTagsItem{
                Name: "template",
                Value: "welcome",
            },
        },
        Category: coss.String(
            "onboarding",
        ),
    },
}
client.Email.Send(
    context.TODO(),
    request,
)
```

Ruby:

```ruby
require "coss"

client = Coss::Client.new # api_key: or COSS_API_KEY

client.email.send_(
  idempotency_key: "welcome-user-123",
  from: "Acme <onboarding@acme.com>",
  to: "user@example.com",
  subject: "Welcome to Acme",
  html: "<p>Thanks for signing up!</p>",
  tags: [{
    name: "template",
    value: "welcome"
  }],
  category: "onboarding"
)
```

PHP:

```php
<?php

use Coss\Coss;
use Coss\Email\Requests\SendEmailBody;
use Coss\Types\SendEmailRequest;
use Coss\Types\SendEmailRequestTagsItem;

$client = new Coss(); // apiKey: or COSS_API_KEY

$client->email->send(
    new SendEmailBody([
        'idempotencyKey' => 'welcome-user-123',
        'body' => new SendEmailRequest([
            'from' => 'Acme <onboarding@acme.com>',
            'to' => 'user@example.com',
            'subject' => 'Welcome to Acme',
            'html' => '<p>Thanks for signing up!</p>',
            'tags' => [
                new SendEmailRequestTagsItem([
                    'name' => 'template',
                    'value' => 'welcome',
                ]),
            ],
            'category' => 'onboarding',
        ]),
    ]),
);
```

Java:

```java
import com.coss.Coss;
import com.coss.resources.email.requests.SendEmailBody;
import com.coss.types.SendEmailRequest;
import com.coss.types.SendEmailRequestTagsItem;
import com.coss.types.SendEmailRequestTo;
import java.util.Arrays;
import java.util.Optional;

Coss client = Coss.builder().build(); // .apiKey(…) or COSS_API_KEY

client.email().send(
    SendEmailBody
        .builder()
        .body(
            SendEmailRequest
                .builder()
                .from("Acme <onboarding@acme.com>")
                .to(
                    SendEmailRequestTo.of("user@example.com")
                )
                .subject("Welcome to Acme")
                .html("<p>Thanks for signing up!</p>")
                .tags(
                    Optional.of(
                        Arrays.asList(
                            SendEmailRequestTagsItem
                                .builder()
                                .name("template")
                                .value("welcome")
                                .build()
                        )
                    )
                )
                .category("onboarding")
                .build()
        )
        .idempotencyKey("welcome-user-123")
        .build()
);
```

.NET:

```csharp
using Coss;

var client = new CossClient(); // apiKey: or COSS_API_KEY

await client.Email.SendAsync(
    new SendEmailBody
    {
        IdempotencyKey = "welcome-user-123",
        Body = new SendEmailRequest
        {
            From = "Acme <onboarding@acme.com>",
            To = "user@example.com",
            Subject = "Welcome to Acme",
            Html = "<p>Thanks for signing up!</p>",
            Tags = new List<SendEmailRequestTagsItem>()
            {
                new SendEmailRequestTagsItem { Name = "template", Value = "welcome" },
            },
            Category = "onboarding",
        },
    }
);
```

## Response 202

The email, accepted for delivery (`queued` or `scheduled`). A replay returns the original email with its current status.

- `object` ("email", required)
- `id` (string, required)
- `from` (string, required)
- `to` (string[], required)
- `cc` (string[], required)
- `bcc` (string[], required)
- `reply_to` (string[], required)
- `subject` (string, required)
- `html` (string | null, required)
- `text` (string | null, required)
- `headers` (map of string, required)
- `tags` (object[], required)
  - `name` (string, required)
  - `value` (string, required)
- `category` (string | null, required)
- `stream` ("transactional" | "marketing", required)
- `status` ("queued" | "sent" | "failed" | "scheduled" | "canceled", required): Whether the email was handed to the delivery provider. `scheduled` emails wait for `scheduled_at`; `canceled` ones were canceled before that. Per-recipient delivery is in `recipients`.
- `failure` (object | null, required): Why the email failed; `null` unless `status` is `failed`.
  - `code` (string, required): Why the email failed, e.g. `all_recipients_suppressed`, `domain_not_verified`, `content_rejected`, `attachments_unavailable`, `dispatch_failed` or `provider_error`.
  - `message` (string, required)
- `recipients` (object[], required)
  - `address` (string, required): The bare, lower-cased address.
  - `type` ("to" | "cc" | "bcc", required)
  - `status` ("queued" | "sent" | "delivery_delayed" | "delivered" | "bounced" | "suppressed" | "complained" | "failed" | "canceled", required): Delivery status of this recipient. `suppressed` means the address is on the environment's suppression list (for every email or for this email's category), so it was skipped. `canceled` means the scheduled email was canceled before it was sent.
  - `last_event_at` (string | null, required, date-time)
- `attachments` (object[], required): Attachment metadata. Content is never returned, and is discarded once sent.
  - `id` (string, required)
  - `filename` (string, required)
  - `content_type` (string, required)
  - `content_id` (string | null, required): Set for inline attachments (`cid:` references).
  - `size` (integer, required): Size in bytes.
- `sandbox` (boolean, required): True when sent from a sandbox environment (never delivered).
- `tracking` (object, required): Open and click tracking, as resolved when the email was sent, with engagement so far. Opens can be inflated by mail clients that prefetch images, clicks by link scanners.
  - `opens` (boolean, required): Whether opens are tracked for this email.
  - `clicks` (boolean, required): Whether link clicks are tracked for this email.
  - `unsubscribe` (boolean, required): Whether the email carries one-click unsubscribe headers (RFC 8058).
  - `open_count` (integer, required)
  - `first_opened_at` (string | null, required, date-time)
  - `last_opened_at` (string | null, required, date-time)
  - `click_count` (integer, required)
  - `first_clicked_at` (string | null, required, date-time)
  - `last_clicked_at` (string | null, required, date-time)
- `created_at` (string, required, date-time)
- `sent_at` (string | null, required, date-time)
- `scheduled_at` (string | null, required, date-time): When the email was scheduled to be sent; `null` if it was not.
- `batch_id` (string | null, required): The batch that sent the email; `null` if it was sent on its own.
- `template` (object | null, required): The template the email was rendered from; `null` if it was sent without one.
  - `id` (string, required)
  - `version` (integer | null, required): `null` for a dashboard test send of the draft.
  - `locale` (string, required)
- `broadcast_id` (string | null, required): The broadcast the email is part of; `null` for any other email.
- `content_storage` (object, required): Whether coss.com keeps the email's content after sending it.
  - `mode` ("store" | "discard", required): The environment's content storage when the email was sent. With `discard`, the subject, `html`, `text`, `headers`, template variables and attachment filenames are cleared once the email is sent, fails or is canceled.
  - `discarded_at` (string | null, required, date-time): When the content was cleared; `null` while it is kept. A cleared email has an empty `subject`, `html` and `text` `null` and no `headers`.

Example:

```json
{
  "object": "email",
  "id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz",
  "from": "Acme <onboarding@acme.com>",
  "to": [
    "user@example.com"
  ],
  "cc": [],
  "bcc": [],
  "reply_to": [],
  "subject": "Welcome to Acme",
  "html": "<p>Thanks for signing up!</p>",
  "text": null,
  "headers": {},
  "tags": [
    {
      "name": "template",
      "value": "welcome"
    }
  ],
  "category": "onboarding",
  "stream": "transactional",
  "status": "sent",
  "failure": null,
  "recipients": [
    {
      "address": "user@example.com",
      "type": "to",
      "status": "delivered",
      "last_event_at": "2026-10-10T12:00:02.000Z"
    }
  ],
  "sandbox": false,
  "tracking": {
    "opens": false,
    "clicks": false,
    "unsubscribe": false,
    "open_count": 0,
    "first_opened_at": null,
    "last_opened_at": null,
    "click_count": 0,
    "first_clicked_at": null,
    "last_clicked_at": null
  },
  "attachments": [],
  "created_at": "2026-10-10T12:00:00.000Z",
  "sent_at": "2026-10-10T12:00:01.000Z",
  "scheduled_at": null,
  "batch_id": null,
  "template": null,
  "broadcast_id": null,
  "content_storage": {
    "mode": "store",
    "discarded_at": null
  }
}
```

## Errors

- `400`: The body is not valid JSON (`invalid_json`) or the `Idempotency-Key` header is invalid (`invalid_idempotency_key`). Also when the `Coss-Version` header names an unknown API version (`invalid_api_version`).
- `401`: The API key is missing, invalid, revoked or expired (`authentication_error`).
- `402`: Live sending is paused for an overdue invoice or the spend limit (`billing_hold`), or the send would go past the plan's monthly allowance (`usage_limit_exceeded`) (`billing_error`). Never retried: it needs a plan change or a payment.
- `403`: The API key lacks the permission this endpoint needs (`insufficient_permissions`), its organization is suspended (`organization_suspended`), live sending is suspended (`account_suspended`), or sending is paused for the organization because of its sending reputation (`sending_paused`).
- `409`: Requests with this `Idempotency-Key` are racing for an expired key (`request_in_progress`). Retry after `Retry-After` seconds.
- `413`: The request body is larger than 40 MB (`payload_too_large`).
- `415`: The body is not sent as `application/json` (`unsupported_media_type`).
- `422`: The body failed validation (`validation_error`, see `issues`), the `from` domain is not a verified domain of the organization (`domain_not_verified`, live keys only), the `Idempotency-Key` was already used with a different body (`idempotency_key_reused`), or the template can't be sent: it doesn't exist (`template_not_found`), has no published version (`template_not_published`) or no such version (`template_version_not_found`), the variables don't match it (`invalid_template_variables`, see `issues`), or it renders an invalid subject or empty body (`template_render_failed`).
- `429`: Too many requests with this API key (`rate_limited`), or the email's recipients exceed the send quota (`quota_exceeded`). Retry after `Retry-After` seconds; without `Retry-After` the email has more recipients than the quota allows at once.
- `500`: Unexpected server error (`api_error` / `internal_error`).
- `503`: The API key could not be verified right now; retry (`service_unavailable`).