Pular para o conteúdo
couryo

Guia do desenvolvedorEnvio

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.

Ver em Markdown

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.

Resposta 202
{ "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, cc e bcc. Para mais, use o 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.
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:

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, 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
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:

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.

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.

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.

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