# Enviar e-mail

> POST /v1/emails em detalhe. Campos, destinatários em texto ou lista, idempotência, envio em lote, agendamento, tags, metadados, anexos e checagem antes do envio.

Fonte: https://couryo.com/docs/sending

`POST /v1/emails` envia um e-mail (escopo `send`). A resposta `202 Accepted` traz o `id` e o `status`, e só sai **depois** de o e-mail entrar na fila. Se algo impede o envio, você recebe um [erro](https://couryo.com/docs/errors.md), nunca um sucesso falso.

```json title="Resposta 202"
{ "id": "msg_9w2k7c1x0d4e5f6g", "status": "queued" }
```

Com uma chave `ck_test_`, o status é `captured` e nada é entregue (veja [Modo de teste](https://couryo.com/docs/test-mode.md)).

## Campos

| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `from` | string | sim | remetente num domínio verificado do projeto: `"oi@exemplo.com.br"` ou `"Loja <pedidos@exemplo.com.br>"`. Subdomínio de um domínio verificado também vale |
| `to` | string ou string[] | sim | um endereço (`"ana@exemplo.com.br"`) ou uma lista |
| `cc`, `bcc` | string ou string[] | não | cópia e cópia oculta, também em texto ou lista |
| `reply_to` | string ou string[] | não | para onde vão as respostas |
| `subject` | string | sim | assunto, de 1 a 998 caracteres |
| `html` | string | um dos dois | corpo em HTML (até 5 MB) |
| `text` | string | um dos dois | corpo em texto puro (até 5 MB) |
| `attachments` | objeto[] | não | até 20 anexos (veja abaixo) |
| `headers` | objeto | não | cabeçalhos extras, como `{ "X-Pedido": "1042" }` |
| `tags` | objeto | não | pares chave e valor em texto para filtrar e agrupar |
| `metadata` | objeto | não | dados seus em texto, devolvidos nos webhooks |
| `scheduled_at` | string ISO 8601 | não | enviar mais tarde, até 30 dias à frente |
| `stream` | `transactional` ou `marketing` | não | fluxo; o padrão é `transactional` |
| `template` + `variables` | string + objeto | não | templates salvos: em breve (hoje a API responde `invalid_field`; mande `html` ou `text`) |

- **No máximo 50 destinatários** por e-mail, somando `to`, `cc` e `bcc`. Para mais, use o [lote](#envio-em-lote).
- O corpo da chamada pode ter até 30 MB, contando os anexos em Base64.
- Mande sempre `text` junto com `html`. Provedores confiam mais em e-mails com as duas partes, e quem lê em texto puro agradece.

```json title="Corpo com destinatários em texto e em lista"
{
  "from": "Loja Exemplo <pedidos@exemplo.com.br>",
  "to": "ana@exemplo.com.br",
  "cc": ["financeiro@exemplo.com.br", "logistica@exemplo.com.br"],
  "reply_to": "atendimento@exemplo.com.br",
  "subject": "Pedido 1042 confirmado",
  "html": "<p>Seu pedido 1042 foi confirmado.</p>",
  "text": "Seu pedido 1042 foi confirmado.",
  "tags": { "tipo": "pedido" },
  "metadata": { "pedido_id": "1042" }
}
```

## Idempotência

Rede cai, função reinicia, fila repete. Para não mandar o mesmo e-mail duas vezes, envie o cabeçalho `Idempotency-Key` (até 255 caracteres) com um valor único para a operação de negócio:

```http title="Cabeçalho"
Idempotency-Key: pedido-1042-confirmacao
```

- Por **24 horas**, repetir a chamada com a mesma chave e o **mesmo corpo** devolve a **mesma resposta**, com o cabeçalho `Idempotent-Replayed: true`, sem enviar de novo.
- A mesma chave com um **corpo diferente** devolve `409 idempotency_conflict`.
- Se o primeiro pedido ainda estiver em andamento, a repetição recebe `409 idempotency_in_progress` com `Retry-After: 1`.
- Respostas `5xx`, `409` e `429` não ficam guardadas: dá para repetir com a mesma chave.
- Funciona em todo `POST`, inclusive no lote.

Use algo ligado ao evento, como `pedido-{id}-confirmacao` ou `senha-{usuario}-{timestamp}`. Um UUID novo a cada tentativa não protege contra repetição.

## Destinatários suprimidos

Se **alguns** destinatários estão na [lista de supressão](https://couryo.com/docs/suppressions.md), o e-mail sai para os outros e a linha do tempo ganha um evento `suppressed` para cada endereço pulado. Se **todos** estão suprimidos, a resposta é `422 recipient_suppressed`, com `param` apontando o primeiro (`to[0]`).

## Envio em lote

`POST /v1/emails/batch` aceita até **100 e-mails** por chamada, no campo `emails`. Cada item tem os mesmos campos de `POST /v1/emails` e é validado sozinho: um item com problema não derruba os outros.

```bash title="cURL"
curl https://api.couryo.com/v1/emails/batch \
  -H "Authorization: Bearer $COURYO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lembretes-2026-10-07" \
  -d '{
    "emails": [
      { "from": "oi@exemplo.com.br", "to": "ana@exemplo.com.br", "subject": "Lembrete", "text": "Sua aula começa às 19h." },
      { "from": "oi@exemplo.com.br", "to": ["bruno@exemplo.com.br"], "subject": "Lembrete", "text": "Sua aula começa às 19h." }
    ]
  }'
```

A resposta é `200` com um resultado por item, na mesma ordem. O `param` dos erros traz o índice do item:

```json title="Resposta 200"
{
  "data": [
    { "id": "msg_4h8s1m2p0q7r6t5u", "status": "queued" },
    {
      "error": {
        "type": "invalid_request",
        "code": "recipient_suppressed",
        "message": "O endereço bruno@exemplo.com.br está na lista de supressão.",
        "param": "emails[1].to[0]",
        "doc_url": "https://couryo.com/docs/errors#recipient_suppressed",
        "request_id": "req_2m9x4k7c1v0b8n3q"
      }
    }
  ]
}
```

- Mais de 100 itens: `400 batch_too_large`, e nada é enviado.
- Um erro de limite (`daily_limit_reached`, `monthly_limit_reached`, `spend_limit_reached` ou `sending_paused`) para o resto do lote: o item que bateu no limite e todos os seguintes voltam com o mesmo erro.

## Agendamento

Passe `scheduled_at` em ISO 8601 com fuso (`Z` ou `-03:00`), até 30 dias à frente. O e-mail fica com status `queued` até a hora marcada. Uma data no passado (mais de 1 minuto) ou além de 30 dias devolve `invalid_field`.

```json title="Corpo"
{
  "from": "oi@exemplo.com.br",
  "to": "ana@exemplo.com.br",
  "subject": "Sua aula começa em 1 hora",
  "text": "Até já!",
  "scheduled_at": "2026-10-08T18:00:00-03:00"
}
```

## Tags e metadados

- `tags` servem para filtrar a listagem (`GET /v1/emails?tag=tipo:pedido`) e agrupar no painel: `{ "tipo": "pedido", "campanha": "outubro" }`. As chaves usam letras, números, `_` e `-` (até 64); os valores, até 256 caracteres.
- `metadata` é seu: volta em cada webhook, útil para ligar o e-mail a um registro do seu sistema: `{ "pedido_id": "1042" }`. Chaves até 64 caracteres; valores até 1.024.

Os dois aceitam apenas valores em texto.

## Anexos

Cada anexo tem `filename` (até 255 caracteres), `content` (o arquivo em Base64) e, opcionalmente, `content_type`. Até 20 por e-mail.

```json title="Anexo"
{ "filename": "recibo.pdf", "content": "JVBERi0xLjcK...", "content_type": "application/pdf" }
```

> Boleto e nota fiscal: mande **por link**, não como anexo. Anexo de boleto é um dos padrões mais usados em golpes, e os filtros de spam sabem disso. A checagem antes do envio avisa (`billing_attachment`).

## Cabeçalhos

Os cabeçalhos de `headers` vão como você mandou, menos os que pertencem à plataforma ou quebram a autenticação, que são ignorados: `From`, `To`, `Cc`, `Bcc`, `Subject`, `Message-ID`, `Date`, `Return-Path`, `DKIM-Signature`, `List-Unsubscribe`, `List-Unsubscribe-Post`, `Content-Type`, `Content-Transfer-Encoding` e `MIME-Version`.

## Transacional e marketing

O campo `stream` separa os fluxos. Descadastro de marketing nunca bloqueia e-mail transacional (senha, login, cobrança). Em `marketing`, o Couryo inclui o descadastro em um clique (`List-Unsubscribe` e `List-Unsubscribe-Post`, RFC 8058) que Gmail e Yahoo exigem, e quem clica entra na supressão do fluxo `marketing`. Abertura e clique são medidos só no fluxo `marketing`.

## Checagem antes do envio

`POST /v1/emails/check` (escopo `send`) recebe o mesmo corpo do envio e devolve uma nota de 0 a 100 e a lista de problemas, sem enviar nada. Bom para rodar no CI.

```json title="Resposta 200"
{ "score": 90, "issues": [{ "code": "missing_text_part", "severity": "warning", "message": "O e-mail não tem parte em texto." }] }
```

| `code` | Gravidade | O que aponta |
|---|---|---|
| `domain_not_verified` | error | o domínio do `from` não está verificado: o envio seria recusado |
| `link_shortener` | error | link encurtado; use o link completo do seu domínio |
| `missing_text_part` | warning | há `html` sem `text` |
| `insecure_link` | warning | links sem HTTPS |
| `html_too_large` | warning | HTML acima de 100 KB (o Gmail corta a mensagem) |
| `subject_all_caps` | warning | assunto todo em maiúsculas |
| `billing_attachment` | warning | boleto, fatura ou nota fiscal como anexo |
| `image_without_alt` | info | imagem sem texto alternativo |

## Listar e consultar

| Método e caminho | Escopo | O que faz |
|---|---|---|
| `GET /v1/emails` | `read` | lista do mais novo para o mais antigo; filtros `status`, `recipient` (parte do endereço), `tag` (`chave:valor` ou termo livre) e `period` (`24h`, `7d`, `30d`), com `limit` e `starting_after` |
| `GET /v1/emails/{id}` | `read` | um e-mail com a [linha do tempo](https://couryo.com/docs/events.md) |
