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.
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, nunca um sucesso falso.
{ "id": "msg_9w2k7c1x0d4e5f6g", "status": "queued" }Com uma chave ck_test_, o status é captured e nada é entregue (veja Modo de teste).
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,ccebcc. Para mais, use o lote. - O corpo da chamada pode ter até 30 MB, contando os anexos em Base64.
- Mande sempre
textjunto comhtml. Provedores confiam mais em e-mails com as duas partes, e quem lê em texto puro agradece.
{
"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:
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_progresscomRetry-After: 1. - Respostas
5xx,409e429nã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, 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.
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:
{
"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_reachedousending_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.
{
"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#
tagsservem 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.
{ "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.
{ "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 |