Pular para o conteúdo
couryo

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.

Ver em Markdown

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#

Terminal
npm install @couryo/node
.env.local
COURYO_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:

lib/couryo.ts
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)#

app/contato/actions.ts
"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;
  }
}
app/contato/page.tsx
"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.

app/api/pedidos/[id]/confirmacao/route.ts
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.

app/api/boas-vindas/route.tsx
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:

app/api/webhooks/couryo/route.ts
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ó:

TypeScript
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()