Guia do desenvolvedorIntegrações
Enviar e-mail no Next.js
Enviar e-mail transacional pelo Couryo no Next.js (App Router), com Server Action, Route Handler, React Email e webhook assinado.
Este guia usa o App Router do Next.js e o SDK Node. Tudo roda no servidor: a chave de API nunca vai para o navegador.
1. Instale e configure a chave#
npm install @couryo/nodeCOURYO_API_KEY=ck_test_...
COURYO_WEBHOOK_SECRET=whsec_...Comece com uma chave ck_test_: nada é entregue e cada e-mail aparece no painel (modo de teste). Troque por ck_live_ quando o domínio estiver verificado.
Crie o cliente uma vez, só no servidor. A criação preguiçosa evita erro no next build quando a variável ainda não existe no ambiente de build:
import "server-only";
import { Couryo } from "@couryo/node";
let client: Couryo | undefined;
export function couryo(): Couryo {
client ??= new Couryo(); // lê COURYO_API_KEY
return client;
}O SDK usa módulos do Node (
node:crypto). Deixe as rotas que enviam e-mail no runtime padrão do Node.js, não no Edge.
2. Server Action (formulário de contato)#
"use server";
import { CouryoError } from "@couryo/node";
import { couryo } from "@/lib/couryo";
export type Estado = { ok: boolean; erro?: string };
export async function enviarContato(_anterior: Estado, form: FormData): Promise<Estado> {
const email = String(form.get("email") ?? "").trim();
const mensagem = String(form.get("mensagem") ?? "").trim();
if (!email || !mensagem) return { ok: false, erro: "Preencha o e-mail e a mensagem." };
try {
await couryo().emails.send({
from: "Site Exemplo <contato@exemplo.com.br>",
to: "equipe@exemplo.com.br",
reply_to: email,
subject: `Contato pelo site: ${email}`,
text: mensagem,
});
return { ok: true };
} catch (e) {
// A mensagem do Couryo já vem em português e diz o que corrigir.
if (e instanceof CouryoError) return { ok: false, erro: e.message };
throw e;
}
}"use client";
import { useActionState } from "react";
import { enviarContato, type Estado } from "./actions";
const inicial: Estado = { ok: false };
export default function Contato() {
const [estado, acao, enviando] = useActionState(enviarContato, inicial);
return (
<form action={acao}>
<input name="email" type="email" required placeholder="Seu e-mail" />
<textarea name="mensagem" required placeholder="Mensagem" />
<button disabled={enviando}>{enviando ? "Enviando..." : "Enviar"}</button>
{estado.ok && <p>Recebemos a sua mensagem.</p>}
{estado.erro && <p role="alert">{estado.erro}</p>}
</form>
);
}3. Route Handler (e-mail disparado pelo seu backend)#
Quando o mesmo evento pode chegar duas vezes (um webhook de pagamento repetido, por exemplo), passe uma chave de idempotência que identifica o evento. A segunda chamada recebe a mesma resposta e nada sai duplicado.
import { couryo } from "@/lib/couryo";
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const pedido = await buscarPedido(id); // a sua função
const email = await couryo().emails.send(
{
from: "Loja Exemplo <pedidos@exemplo.com.br>",
to: pedido.cliente.email,
subject: `Pedido ${pedido.numero} confirmado`,
html: `<p>Oi, ${pedido.cliente.nome}! Recebemos o seu pedido ${pedido.numero}.</p>`,
text: `Oi, ${pedido.cliente.nome}! Recebemos o seu pedido ${pedido.numero}.`,
tags: { tipo: "pedido" },
metadata: { pedido_id: pedido.id },
},
{ idempotencyKey: `pedido-${pedido.id}-confirmacao` },
);
return Response.json({ email_id: email.id, status: email.status });
}A resposta 202 só chega quando o e-mail entrou na fila. Se algo impede o envio (domínio não verificado, limite do dia, destinatário suprimido), o SDK lança CouryoError com o código estável.
4. Templates com React Email#
React Email funciona como está: renderize o componente para HTML e texto e mande os dois.
import { render } from "@react-email/render";
import BoasVindas from "@/emails/boas-vindas"; // o seu componente React Email
import { couryo } from "@/lib/couryo";
export async function POST(req: Request) {
const { email, nome } = await req.json();
const html = await render(<BoasVindas nome={nome} />);
const text = await render(<BoasVindas nome={nome} />, { plainText: true });
await couryo().emails.send({ from: "Exemplo <oi@exemplo.com.br>", to: email, subject: `Boas-vindas, ${nome}!`, html, text });
return new Response(null, { status: 204 });
}Prefere editar no painel? Use um template salvo: template: "boas-vindas" e variables: { nome } no lugar de html e text.
5. Receber webhooks#
Crie o webhook no painel (ou pela API) apontando para https://seu-site.com/api/webhooks/couryo e guarde o segredo whsec_. Leia o corpo como texto, sem converter para JSON antes de verificar:
import { verifyWebhook, WebhookVerificationError } from "@couryo/node";
export async function POST(req: Request) {
const body = await req.text();
try {
const evento = verifyWebhook({ secret: process.env.COURYO_WEBHOOK_SECRET!, headers: req.headers, body });
if (evento.type === "bounced") {
// evento.data.recipient, evento.data.smtp_code, evento.data.explanation (em português)
}
return new Response(null, { status: 204 });
} catch (e) {
if (e instanceof WebhookVerificationError) return new Response("assinatura inválida", { status: 400 });
throw e;
}
}Qualquer resposta 2xx confirma a entrega; sem ela, o Couryo tenta de novo por cerca de 3 dias. Detalhes em Webhooks.
Sem SDK#
Em qualquer runtime com fetch (inclusive o Edge), a API é uma chamada só:
const res = await fetch("https://api.couryo.com/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.COURYO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ from: "Exemplo <oi@exemplo.com.br>", to: "ana@exemplo.com.br", subject: "Olá", text: "Enviado pelo Couryo." }),
});
if (!res.ok) throw new Error((await res.json()).error.message);Problemas comuns#
| Sintoma | O que fazer |
|---|---|
domain_not_verified |
o domínio do from ainda não está verificado neste projeto: veja Domínios e DNS |
sandbox_recipient |
a conta está no nível 0 e só envia para os e-mails da própria conta até o primeiro domínio verificado |
Couryo: missing API key no build |
crie o cliente dentro da função (como em lib/couryo.ts) e defina COURYO_API_KEY no ambiente de produção |
| Webhook sempre com assinatura inválida | o corpo foi lido com req.json() antes; use req.text() |