--- title: Notificação de pedido pelo WhatsApp e email: aviso de envio automático a partir dos webhooks da sua loja description: Notificação de pedido pelo WhatsApp e aviso de envio automático com código de rastreio, mais confirmação por email, a partir dos webhooks da Nuvemshop. date: 2026-10-19 author: Zavu Team locale: pt source: https://www.zavu.dev/pt/blog/order-notifications-ecommerce tags: Tutorial, API v2, E-commerce, Email, WhatsApp, Webhooks --- # Notificação de pedido pelo WhatsApp e email: aviso de envio automático a partir dos webhooks da sua loja ## TL;DR - **O email de confirmação sai quando o pedido é pago.** O webhook da sua loja dispara `POST /v2/messages` a partir de um endereço num domínio verificado. - **Nenhuma mensagem sai duas vezes.** Cada envio leva uma `idempotencyKey` montada com o id do pedido e o evento, então um webhook repetido recebe `409 conflict` e nada é enviado. - **O aviso de envio é um template de utilidade do WhatsApp** com o código de rastreio, e a Meta precisa aprová-lo antes de você usar. - **O email é o fallback.** Se o cliente não tem WhatsApp, o envio é recusado ou falha depois como `message.failed`, o seu código manda o aviso por email. - **Os contatos ficam sincronizados.** Cada pedido atualiza o contato do cliente na Zavu. O cliente paga e a primeira coisa que ele quer é uma prova: um email de confirmação com o que comprou. Dois dias depois ele quer saber onde está o pacote, e no Brasil essa pergunta chega pelo WhatsApp. A **notificação de pedido pelo WhatsApp** e o aviso de envio automático deveriam sair sozinhos da sua loja, a partir dos webhooks que Nuvemshop, Shopify ou WooCommerce já enviam. Este tutorial monta isso com a API v2 da Zavu, num serviço pequeno em Node/TypeScript. No final você tem: - Um domínio de email verificado e um endereço como `pedidos@sualoja.com.br` conectado como canal. - Um endpoint que recebe os webhooks de "pedido pago" e "pedido enviado" da loja. - Um email de confirmação que nunca sai duas vezes para o mesmo pedido. - O aviso de envio como template de utilidade do WhatsApp aprovado, com o código de rastreio. - Um fallback por email quando o cliente não tem WhatsApp ou o envio falha. - Os contatos da Zavu sincronizados com os seus pedidos. ## O que você precisa - **Uma conta Zavu e uma API key.** A v2 usa a mesma key da v1, como Bearer token. URL base: `https://api.zavu.dev/v2`. - **Um canal de email.** Precisa de um domínio seu e acesso ao DNS dele. Publicar os registros leva minutos; a propagação pode levar horas, então comece por aqui. - **WhatsApp, se você quer o aviso de envio lá.** É o canal com mais atrito: ele se conecta pelo cadastro da Meta em **Canais** no dashboard, e as mensagens que a sua empresa inicia precisam de um template aprovado pela Meta. Se você não conectar, o mesmo código manda o aviso por email. - **Uma loja que envie webhooks.** Nuvemshop, Shopify e WooCommerce enviam. O código mapeia o pedido da Shopify; o mapeamento é uma função só, então trocar de plataforma é mudar um lugar. Os SDKs (`@zavudev/sdk`, `zavudev`) chamam a v1 hoje. Tudo aqui chama a v2 por HTTP com `fetch`. Se a v2 é nova para você, [Zavu 2.0](/pt/blog/zavu-2-0) explica o que mudou: você envia `from` um endereço, e um erro diz o que conectar. ## 1. Verifique o domínio de email e conecte o endereço Adicione o domínio: ```bash curl -X POST https://api.zavu.dev/v2/email-domains \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "sualoja.com.br"}' ``` A resposta traz os registros DNS a publicar: ```json { "domain": { "id": "emd_abc123", "domain": "sualoja.com.br", "status": "pending", "verifiable": true, "dnsRecords": [ { "type": "CNAME", "name": "...", "value": "...", "purpose": "dkim", "required": true }, { "type": "TXT", "name": "...", "value": "...", "purpose": "spf", "required": false }, { "type": "TXT", "name": "...", "value": "...", "purpose": "dmarc", "required": false } ] } } ``` Publique cada registro exatamente como veio. Os `dkim` são obrigatórios; SPF, DMARC e MAIL FROM são recomendados e valem a pena para a entregabilidade. Adicionar um domínio que o projeto já tem responde `200` em vez de `201` e devolve o mesmo domínio. Se `verifiable` vier `false`, o domínio ficou sem registros DKIM e não vai verificar, publique o que publicar. `setupError` explica: apague o domínio e adicione de novo. Depois peça para a Zavu checar o DNS: ```bash curl -X POST https://api.zavu.dev/v2/email-domains/emd_abc123/verify \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" ``` Ela devolve o domínio com o `status` atual. Repita até ficar `verified`, ou assine o evento `domain.verified` no seu webhook. Um `503 verification_unavailable` quer dizer que a checagem não conseguiu rodar; nada mudou, tente de novo em alguns minutos. Agora conecte o endereço como canal: ```bash curl -X POST https://api.zavu.dev/v2/channels \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type": "email", "address": "pedidos@sualoja.com.br", "fromName": "Sua Loja"}' ``` O `201` lista as entradas desse endereço, com `active`. Se o domínio ainda não foi verificado, a resposta é `409 email_domain_not_verified`, com o `status` do domínio e as chamadas que resolvem em `details.recommendation`. Um domínio que o projeto não tem responde `409 email_domain_missing`. Se `active` vier `false`, ative com `POST /v2/channels/pedidos@sualoja.com.br/email/activate`. ## 2. Receba os webhooks da loja A loja chama você quando algo acontece com um pedido. Cada plataforma faz do seu jeito: - **Shopify** usa as assinaturas `orders/paid` e `orders/fulfilled`, com o tópico no header `X-Shopify-Topic` e a assinatura em `X-Shopify-Hmac-Sha256`: um HMAC-SHA256 em base64 do corpo bruto. - **Nuvemshop** tem eventos como `order/paid` e `order/fulfilled`, mas o webhook traz o id do pedido, não o pedido. A sua função de mapeamento busca o pedido na API da Nuvemshop antes de converter. - **WooCommerce** manda o pedido inteiro em `order.created` e `order.updated`, assinado em `X-WC-Webhook-Signature`. Deixe a plataforma fora do resto do código. Uma função transforma o payload no seu próprio `Pedido`: ```ts // pedido.ts export interface Pedido { id: string numero: string // o que o cliente vê, ex. "#1042" nome: string email?: string telefone?: string // E.164, ex. "+5511987654321" total: string moeda: string itens: { nome: string; quantidade: number }[] envio?: { id: string; transportadora?: string; rastreio?: string; urlRastreio?: string } } // Pedido da Shopify -> Pedido. Para outra plataforma, troque esta função. export function daShopify(o: any): Pedido { const f = o.fulfillments?.[o.fulfillments.length - 1] return { id: String(o.id), numero: o.name, nome: o.customer?.first_name ?? "", email: o.email || undefined, telefone: o.phone || o.shipping_address?.phone || undefined, total: o.total_price, moeda: o.currency, itens: (o.line_items ?? []).map((i: any) => ({ nome: i.name, quantidade: i.quantity })), envio: f ? { id: String(f.id), transportadora: f.tracking_company ?? undefined, rastreio: f.tracking_number ?? undefined, urlRastreio: f.tracking_url ?? undefined } : undefined, } } ``` O telefone que o cliente digita no checkout nem sempre vem com `+55`. Normalize para E.164 antes de usar. A v2 aceita espaços, parênteses e hífen (`+55 (11) 98765-4321`), mas recusa dígitos sem `+` num envio por WhatsApp. O endpoint: ```ts // server.ts import express from "express" import crypto from "node:crypto" import { daShopify } from "./pedido" import { enviarConfirmacao, enviarAvisoDeEnvio, sincronizarContato } from "./notificar" const app = express() function assinaturaShopifyOk(raw: Buffer, header: string | undefined) { if (!header) return false const digest = crypto.createHmac("sha256", process.env.SHOPIFY_WEBHOOK_SECRET!).update(raw).digest("base64") const a = Buffer.from(digest), b = Buffer.from(header) return a.length === b.length && crypto.timingSafeEqual(a, b) } app.post("/webhooks/loja", express.raw({ type: "application/json" }), async (req, res) => { if (!assinaturaShopifyOk(req.body, req.get("X-Shopify-Hmac-Sha256"))) return res.sendStatus(401) const topico = req.get("X-Shopify-Topic") const pedido = daShopify(JSON.parse(req.body.toString("utf8"))) try { await sincronizarContato(pedido) if (topico === "orders/paid") await enviarConfirmacao(pedido) if (topico === "orders/fulfilled") await enviarAvisoDeEnvio(pedido) res.sendStatus(200) } catch (err) { console.error(err) res.sendStatus(500) // a loja tenta de novo; as idempotency keys tornam a nova tentativa segura } }) app.listen(3000) ``` Lojas repetem webhooks que consideram falhos, e às vezes entregam o mesmo duas vezes. Aqui isso não é problema: cada envio abaixo leva uma `idempotencyKey`. Um helper para as chamadas à Zavu: ```ts // zavu.ts export async function zavu(path: string, method = "POST", body?: unknown) { const res = await fetch(`https://api.zavu.dev/v2${path}`, { method, headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}`, "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined, }) const json = res.status === 204 ? null : await res.json() return { status: res.status, json } } ``` Todo erro da v2 é plano, `{code, message, details}`, então você decide pelo `json.code`. ## 3. Envie o email de confirmação do pedido ```ts // notificar.ts import { zavu } from "./zavu" import type { Pedido } from "./pedido" const esc = (s: string) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]!)) export async function enviarConfirmacao(p: Pedido) { if (!p.email) return null const linhas = p.itens.map((i) => `${i.quantidade} x ${i.nome}`) const { status, json } = await zavu("/messages", "POST", { from: process.env.ZAVU_EMAIL_FROM, // "pedidos@sualoja.com.br" to: p.email, subject: `Pedido ${p.numero} confirmado`, text: `Olá ${p.nome}, recebemos o seu pedido ${p.numero}.\n\n${linhas.join("\n")}\n\nTotal: ${p.total} ${p.moeda}`, htmlBody: `

Olá ${esc(p.nome)}, recebemos o seu pedido ${esc(p.numero)}.

Total: ${esc(p.total)} ${esc(p.moeda)}

`, replyTo: "atendimento@sualoja.com.br", idempotencyKey: `${p.id}:confirmado`, metadata: { pedidoId: p.id, evento: "confirmado" }, }) if (status === 202) return json.message.id if (status === 409 && json.code === "conflict") return json.details.message.id // já foi enviado throw new Error(`${json.code}: ${json.message}`) } ``` `subject` é obrigatório num email; `text` é o corpo em texto puro e `htmlBody` o HTML. O `202` diz como o envio foi roteado: ```json { "message": { "id": "jd7x...", "to": "ana@exemplo.com.br", "from": "pedidos@sualoja.com.br", "channel": "email", "status": "queued" }, "routing": { "channel": "email", "from": "pedidos@sualoja.com.br", "reason": "recipient_type", "estimatedCost": null } } ``` `reason: "recipient_type"`: o `to` é um email, então vai por email. `estimatedCost` vem `null` porque email não tem preço por mensagem no momento do envio. Na segunda vez que a mesma key chega (um webhook repetido), nada é enviado. A resposta é `409 conflict` e a mensagem original vem em `details.message`. A Zavu guarda a key para o projeto inteiro, então monte-a com o id do pedido e o evento, nunca com um horário. ## 4. Notificação de pedido pelo WhatsApp: o aviso de envio, com fallback por email Uma mensagem que você inicia fora da janela de 24 horas de atendimento precisa ser um template. Crie um de utilidade para o aviso de envio: ```bash curl -X POST https://api.zavu.dev/v2/templates \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "pedido_enviado", "language": "pt_BR", "whatsappCategory": "UTILITY", "body": "Olá {{1}}, o seu pedido {{2}} já está a caminho pela {{3}}. Código de rastreio: {{4}}. Avisamos se algo mudar." }' ``` O WhatsApp não aceita uma variável no começo nem no fim do corpo, e a Zavu recusa o template com `400` se você tentar. Um nome que o projeto já usa responde `409 conflict`. Envie para a Meta, indicando o número de WhatsApp cuja conta comercial recebe o template: ```bash curl -X POST https://api.zavu.dev/v2/templates/tmpl_abc123/submit \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "+5511912345678", "category": "UTILITY"}' ``` O `202` passa o template para `pending`. Se `from` existe mas não tem WhatsApp, a resposta é `400 no_whatsapp`, com o que esse endereço tem em `details.types`. A decisão da Meta chega como `template.status_changed`, ou leia com `GET /v2/templates/{templateId}`. Envie só quando estiver `approved`. O envio. `channel: "whatsapp"` vai explícito de propósito: sem `channel`, um template segue a ordem automática, e num número que também tem SMS ele poderia sair como SMS. `fallbackEnabled: false` desliga o fallback de WhatsApp para SMS da própria Zavu, para que o seu código decida o que acontece quando o WhatsApp falha. ```ts // notificar.ts (continuação) const FALLBACK_POR_EMAIL = new Set(["no_channel", "channel_inactive", "not_found"]) export async function enviarAvisoDeEnvio(p: Pedido) { const e = p.envio if (!e) return null const key = `${p.id}:enviado:${e.id}` if (p.telefone && e.rastreio && process.env.ZAVU_WHATSAPP_FROM) { const { status, json } = await zavu("/messages", "POST", { from: process.env.ZAVU_WHATSAPP_FROM, // "+5511912345678" to: p.telefone, channel: "whatsapp", messageType: "template", content: { templateId: process.env.ZAVU_TEMPLATE_ENVIADO_ID, templateVariables: { "1": p.nome || "cliente", "2": p.numero, "3": e.transportadora ?? "transportadora", "4": e.rastreio }, }, fallbackEnabled: false, idempotencyKey: `${key}:whatsapp`, metadata: { pedidoId: p.id, evento: "enviado", envioId: e.id }, }) if (status === 202) return json.message.id if (status === 409 && json.code === "conflict") return json.details.message.id if (!FALLBACK_POR_EMAIL.has(json.code)) throw new Error(`${json.code}: ${json.message}`) } return enviarAvisoPorEmail(p, `${key}:email`) } export async function enviarAvisoPorEmail(p: Pedido, idempotencyKey: string) { const e = p.envio! if (!p.email) return null const { status, json } = await zavu("/messages", "POST", { from: process.env.ZAVU_EMAIL_FROM, to: p.email, subject: `O seu pedido ${p.numero} foi enviado`, text: `Olá ${p.nome}, o seu pedido ${p.numero} já está a caminho${e.transportadora ? ` pela ${e.transportadora}` : ""}.${e.rastreio ? ` Código de rastreio: ${e.rastreio}.` : ""}${e.urlRastreio ? `\nAcompanhe aqui: ${e.urlRastreio}` : ""}`, idempotencyKey, metadata: { pedidoId: p.id, evento: "enviado", envioId: e.id }, }) if (status === 202) return json.message.id if (status === 409 && json.code === "conflict") return json.details.message.id throw new Error(`${json.code}: ${json.message}`) } ``` A tentativa pelo WhatsApp e o fallback por email usam keys diferentes. Precisam usar: uma key que já enviou o WhatsApp que falhou responderia o email com `409 conflict` e não mandaria nada. O que dispara o fallback antes de qualquer envio: | Código | O que significa | |---|---| | `409 no_channel` | O endereço `from` não tem WhatsApp. `details.connect` diz como conectar. | | `403 channel_inactive` | O WhatsApp está conectado mas desligado. | | `404 not_found` | Nenhum canal do projeto tem esse endereço `from`. | O outro caso é um WhatsApp aceito com `202` que falha depois: o número não tem WhatsApp, ou a Meta recusa. Isso chega como evento `message.failed` no webhook do projeto. Assine uma vez: ```bash curl -X PUT https://api.zavu.dev/v2/webhook \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://notificacoes.sualoja.com.br/webhooks/zavu", "events": ["message.failed", "message.delivered", "template.status_changed", "domain.verified"]}' ``` O `201` devolve `webhook.secret` uma única vez; guarde. Cada entrega vem assinada em `X-Zavu-Signature` como `t=...,v2=...`, onde `v2` é o HMAC-SHA256 em hex de `"{t}.{body}"`. O handler valida a assinatura e depois lê a mensagem para achar o pedido pela `metadata` que você colocou: ```ts // server.ts (continuação) import { zavu } from "./zavu" import { enviarAvisoPorEmail } from "./notificar" import { carregarPedido } from "./db" // a sua própria base de pedidos function assinaturaZavuOk(raw: Buffer, header: string | undefined) { const partes = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=") as [string, string])) if (!partes.t || !partes.v2) return false const esperado = crypto.createHmac("sha256", process.env.ZAVU_WEBHOOK_SECRET!).update(`${partes.t}.${raw.toString("utf8")}`).digest("hex") const a = Buffer.from(esperado), b = Buffer.from(partes.v2) return a.length === b.length && crypto.timingSafeEqual(a, b) } app.post("/webhooks/zavu", express.raw({ type: "application/json" }), async (req, res) => { if (!assinaturaZavuOk(req.body, req.get("X-Zavu-Signature"))) return res.sendStatus(401) const evt = JSON.parse(req.body.toString("utf8")) res.sendStatus(200) if (evt.event !== "message.failed" || evt.type !== "whatsapp") return const { json } = await zavu(`/messages/${evt.data.messageId}`, "GET") const m = json.message if (m.metadata?.evento !== "enviado") return const pedido = await carregarPedido(m.metadata.pedidoId) await enviarAvisoPorEmail(pedido, `${m.metadata.pedidoId}:enviado:${m.metadata.envioId}:email`) }) ``` `GET /v2/messages/{messageId}` também traz o `errorCode` nas palavras da Zavu (por exemplo `WHATSAPP_REJECTED`, com o código do provedor em `errorDetails.providerCode`) e `attemptedChannels`, onde apareceria um fallback de WhatsApp para SMS se você o tivesse deixado ligado. A Zavu repete uma entrega que não recebe `2xx`, até 5 tentativas; a key do fallback torna inofensivo um evento repetido. ## 5. Mantenha os contatos sincronizados Cada pedido também é a chance de manter o contato do cliente atualizado, para que a caixa de Conversas mostre o nome e você possa segmentar depois. ```ts // notificar.ts (continuação) export async function sincronizarContato(p: Pedido) { const channels = [ ...(p.email ? [{ channel: "email", identifier: p.email, isPrimary: true }] : []), ...(p.telefone ? [{ channel: "whatsapp", identifier: p.telefone, isPrimary: true }] : []), ] if (channels.length === 0) return null const metadata = { origem: "shopify", ultimoPedidoId: p.id, ultimoPedidoNumero: p.numero } const { status, json } = await zavu("/contacts", "POST", { displayName: p.nome || undefined, channels, metadata }) if (status === 201) return json.id if (status === 400 && json.code === "duplicate_identifier") { const contactId = json.details.conflicts[0].contactId await zavu(`/contacts/${contactId}`, "PATCH", { metadata }) return contactId } throw new Error(`${json.code}: ${json.message}`) } ``` Três coisas para saber. Os valores de `metadata` são strings. Um identificador de telefone em `whatsapp` ou `sms` precisa estar em E.164, e a v2 recusa outra coisa com `400 invalid_request`, sugerindo a forma com `+` quando os dígitos parecem um número. E `PATCH` substitui o objeto `metadata` inteiro, então mande todas as chaves que quer manter. A v2 ainda não adiciona um canal novo a um contato existente; `POST /v1/contacts/{contactId}/channels` adiciona, com a mesma API key. Sobre a LGPD: guarde em `metadata` só o que a notificação precisa, como o id do último pedido. Se um cliente pedir a exclusão dos dados, `DELETE /v2/contacts/{contactId}` apaga o contato e os seus canais. ## Testando, e o que pode dar errado Teste com uma key live e o seu próprio email e telefone. Uma key de teste sempre envia pelo sandbox do WhatsApp, então não serve para testar o caminho do email. Aponte os webhooks da loja para um túnel até a sua máquina e faça um pedido de teste. | Você vê | Significa | Faça isto | |---|---|---| | `409 email_domain_not_verified` | O DNS do domínio ainda não foi verificado | Publique os registros, `POST /v2/email-domains/{id}/verify`, tente de novo | | `400 invalid_request` com `path: ["subject"]` | Email sem assunto | Mande sempre `subject` | | `400 invalid_request` no `to` | Telefone sem `+`, lido como chat id | Guarde os números em E.164 | | `409 no_channel` | Nada conectado alcança esse `to` | Leia `details.connect` | | `409 conflict` | A key já foi usada | Normal em novas tentativas; o original está em `details.message` | | `402 insufficient_balance` | O saldo não cobre o email | Adicione saldo pelo dashboard | | `429 daily_limit_exceeded` | O teto diário de um canal | Espere até 00:00 UTC, ou aumente o teto | | `message.failed` com `EMAIL_RECIPIENT_SUPPRESSED` | Esse endereço já voltou ou reclamou antes | Peça outro email ao cliente | A Zavu confere o destinatário de cada email antes de despachar. Um endereço malformado, um domínio sem registros de email ou um endereço na sua lista de supressão fazem a mensagem falhar com `EMAIL_INVALID_RECIPIENT`, `EMAIL_DOMAIN_NOT_FOUND` ou `EMAIL_RECIPIENT_SUPPRESSED`, em vez de mandar um bounce certo. ## Indo para produção **Cobrança do email.** Nos planos atuais o email é cobrado do seu saldo pré-pago em blocos de 1.000 emails transacionais, a US$ 0,40 por bloco; com saldo zero os envios param com `402 insufficient_balance`. O plano Free ainda limita o email a 100 por dia e 3.000 por mês. Times em planos anteriores mantêm as condições de email originais. Todo plano tem um teto móvel pela idade da conta, de 200 por hora e 1.000 a cada 24 horas na primeira semana até 5.000 por hora e 50.000 a cada 24 horas depois do dia 30; passado esse teto, a mensagem falha com `EMAIL_RATE_LIMIT_HOURLY` ou `EMAIL_RATE_LIMIT_DAILY`. O email de saída também passa pela revisão de risco da Zavu, e uma mensagem retida fica em `queued` até uma pessoa aprovar. **Limites e cobrança do WhatsApp.** No Free, o WhatsApp divide com Telegram, Instagram e Messenger uma franquia mensal de 2.000 mensagens, contando as duas direções, e um teto diário de 250 com a conta verificada (100 antes). Os planos pagos não têm nenhum dos dois. A Meta cobra os templates de utilidade na sua conta do WhatsApp Business pela tarifa do país do cliente; a Zavu não soma margem. Os detalhes estão em [o WhatsApp começa a cobrar as suas respostas](/pt/blog/whatsapp-per-message-pricing-explained), e o que fazer antes da mensagem 1.001 em [mensagens de serviço e forma de pagamento](/pt/blog/whatsapp-service-messages-payment-method). **Aprovação de templates.** A aprovação é da Meta, não da Zavu, e pode levar de minutos a mais tempo. Deixe o caminho por email funcionando desde o primeiro dia, assim um template ainda `pending` ou `rejected` não custa nada além do canal. **Links.** Links em email e WhatsApp não passam por verificação de URL; a Zavu confere URLs só em SMS, SMS unidirecional e voz, e bloqueia encurtadores lá. Se depois você mandar o aviso por SMS, verifique antes o domínio de rastreio com `POST /v1/urls` e use a URL completa. **Opt-in.** A Meta exige o opt-in do cliente antes de você escrever pelo WhatsApp. Peça no checkout e deixe o fallback por email para quem não deu. A referência completa está na [visão geral da API v2](https://docs.zavu.dev/api-v2/overview), no [guia do webhook](https://docs.zavu.dev/api-v2/webhook) e na [lista de erros](https://docs.zavu.dev/api-v2/errors). Se o seu projeto ainda roda na v1, [migrar da v1](https://docs.zavu.dev/api-v2/migrate-from-v1) traduz cada chamada; as duas versões funcionam com a mesma key.