> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Crie um serviço de e-mail pré-pago com cobrança baseada em créditos

> Crie o MailKit, uma plataforma de e-mail transacional com créditos de e-mail pré-pagos. Um plano de assinatura mensal, pacotes de recarga avulsos, dedução instantânea de créditos a cada envio e alertas proativos de saldo baixo, usando Resend e Dodo Payments.

<Tip>
  <strong>Deixe o Sentra escrever seu código de integração para você.</strong><br />
  Use nosso assistente de IA no VS Code, Cursor ou Windsurf para gerar código de SDK/API, manipuladores de webhooks e muito mais, apenas descrevendo o que você deseja.

  <a href="https://dodopayments.com/sentra" target="_blank" rel="noopener noreferrer">
    Experimente o Sentra: integração com tecnologia de IA →
  </a>
</Tip>

Neste tutorial, você criará o **MailKit**, uma plataforma de e-mail transacional na qual os clientes pagam antecipadamente por um conjunto de créditos de e-mail. O plano concede uma quantidade mensal de e-mails; quando os clientes ficam com poucos créditos, podem comprar um pacote de recarga em vez de esperar pelo próximo ciclo. Cada envio deduz automaticamente um crédito.

<Note>
  Este tutorial usa o [Resend](https://resend.com) como provedor de e-mail. O plano gratuito (3.000 e-mails/mês) é suficiente para criar e testar todo o fluxo sem uma conta paga. O padrão funciona com qualquer provedor; substitua `resend.emails.send` por SendGrid, Postmark, SES ou seu próprio relay SMTP.
</Note>

Ao final deste tutorial, você saberá como:

* Criar um entitlement de crédito personalizado (e-mails) no dashboard
* Associar créditos a um plano de assinatura e a um produto de recarga avulso
* Enviar e-mails reais via Resend e debitar um crédito por envio usando uma entrada no ledger
* Consultar um saldo de créditos atualizado no frontend
* Verificar corretamente os webhooks do Dodo e lidar com `credit.balance_low` para avisar os clientes antes que o saldo chegue a zero

## O que vamos criar

Este é o modelo de preços do MailKit:

| Produto           | Preço         | E-mails             |
| ----------------- | ------------- | ------------------- |
| Plano MailKit     | US\$ 19/mês   | 5.000 e-mails/ciclo |
| Pacote de recarga | US\$ 9 avulso | +5.000 e-mails      |

A unidade é **um e-mail = um crédito**. Os clientes não precisam pensar em tokens, lotes ou unidades ponderadas. Eles simplesmente veem "você tem 4.231 e-mails restantes este mês".

<Info>
  Antes de começar, certifique-se de ter:

  * Uma conta do Dodo Payments (o modo de teste é suficiente)
  * Uma conta gratuita do [Resend](https://resend.com) e uma API key
  * Node.js 18+ e familiaridade básica com TypeScript
</Info>

## Etapa 1: crie seu entitlement de crédito de e-mail

O entitlement de crédito define a unidade que sua plataforma vende: neste caso, um envio de e-mail.

<Frame caption="The Credits tab under Products lists all your credit entitlements.">
  <img src="https://mintcdn.com/dodopayments/Uc5BUwzydK5AJ2P-/images/CBB/Desktop%20-%20Cookbook%20-%20Credits.png?fit=max&auto=format&n=Uc5BUwzydK5AJ2P-&q=85&s=7cb0896037c7e8578bf85f2009c26837" alt="Página de listagem de créditos" style={{ maxHeight: '500px', width: 'auto' }} width="3250" height="1702" data-path="images/CBB/Desktop - Cookbook - Credits.png" />
</Frame>

<Steps>
  <Step title="Open the Credits section">
    1. Acesse o dashboard do Dodo Payments
    2. Clique em **Products** na barra lateral esquerda
    3. Selecione a aba **Credits**
    4. Clique em **Create Credit**
  </Step>

  <Step title="Configure the credit unit">
    Preencha os detalhes do crédito:

    **Credit Name**: `Email Credits`

    **Credit Type**: selecione **Custom Unit**

    **Unit Name**: `email`

    **Precision**: `0` (um e-mail é sempre uma unidade inteira; não é possível enviar meio e-mail)

    **Credit Expiry**: `30 days` (a quantidade de cada ciclo é redefinida)

    <Warning>
      A precisão não pode ser alterada após a criação. Para unidades discretas, como e-mails, mensagens ou sessões, `0` está correto.
    </Warning>
  </Step>

  <Step title="Leave the other defaults as-is">
    Não habilitaremos rollover nem overage neste cookbook; o objetivo é obter o fluxo de CBB mais simples possível. Você poderá revisar essas opções posteriormente no anexo do crédito.
  </Step>

  <Step title="Save and copy the credit ID">
    Clique em **Create Credit**. Abra o crédito e copie o ID. Você precisará dele para consultar o saldo no backend. Ele terá uma aparência semelhante a `cent_xxxxxxxxxxxx`.

    <Check>
      Seu entitlement `Email Credits` está pronto. Em seguida, criaremos os produtos que concedem créditos aos clientes.
    </Check>
  </Step>
</Steps>

## Etapa 2: crie o plano e o pacote de recarga

Você criará dois produtos: um plano recorrente de **Subscription** e uma recarga de **Single Payment**. O plano concede 5.000 e-mails a cada ciclo; a recarga adiciona outros 5.000 sob demanda. Ambos associam o mesmo entitlement `Email Credits`.

<Tip>
  Este cookbook deduz créditos com entradas diretas no ledger, em vez de meters baseados em uso. As entradas no ledger são imediatas (o saldo é atualizado em milissegundos), não exigem configuração adicional e são a escolha certa quando uma ação do usuário equivale exatamente a um crédito. Se você preferir a dedução automática a partir de eventos de uso ingeridos (útil para unidades ponderadas, como "tokens" ou "MB processados"), consulte [Credit-Based Billing → Usage Billing with Credits](/features/credit-based-billing) para ver o padrão baseado em meters.
</Tip>

### Plano MailKit (US\$ 19/mês, 5.000 e-mails)

<Steps>
  <Step title="Create the subscription">
    1. Acesse **Products → Create Product**
    2. Preencha os detalhes do produto:

    **Product Name**: `MailKit Plan`

    **Description**: `5,000 transactional emails per month.`

    3. Selecione **Subscription** como tipo de produto
    4. Defina o preço recorrente:

    **Recurring Price**: `19.00`

    **Billing Cycle**: `Monthly`

    **Currency**: `USD`
  </Step>

  <Step title="Attach the email credit entitlement">
    Role até **Entitlements → Credits → Attach** e configure:

    **Credit Entitlement**: `Email Credits`

    **Credits issued per billing cycle**: `5000`

    **Low Balance Threshold**: `20` (percentual; dispara `credit.balance_low` quando o saldo fica abaixo de 20% da quantidade do ciclo, ou seja, 1.000 e-mails)

    **Import Default Credit Settings**: habilitado (usa a expiração de 30 dias da Etapa 1)

    Clique em **Add to Product** e depois em **Save** para salvar o produto. Copie o ID do produto (`pdt_xxxxxxxxxxxx`).

    <Check>
      Plano: US\$ 19/mês → 5.000 e-mails renovados a cada ciclo.
    </Check>
  </Step>
</Steps>

### Pacote de recarga (US\$ 9 avulso, 5.000 e-mails)

<Steps>
  <Step title="Create a one-time product">
    1. Acesse **Products → Create Product**
    2. Preencha os detalhes do produto:

    **Product Name**: `Email Top-Up Pack`

    **Description**: `Add 5,000 emails to your MailKit balance instantly.`

    3. Selecione **Single Payment** como tipo de produto
    4. Defina o preço:

    **Price**: `9.00`

    **Currency**: `USD`
  </Step>

  <Step title="Attach the credit grant">
    Em **Entitlements → Credits → Attach**:

    * Credit Entitlement: `Email Credits`
    * Credits issued: `5000`

    <Info>
      Produtos avulsos concedem créditos com sua própria expiração (30 dias após a compra, conforme a Etapa 1). As recargas são acumuladas sobre os créditos da assinatura; elas não os substituem.
    </Info>

    Salve e copie o ID do produto.

    <Check>
      Pacote de recarga: US\$ 9 → +5.000 e-mails, disponíveis imediatamente.
    </Check>
  </Step>
</Steps>

## Etapa 3: configure o backend

Agora crie o servidor Express que gerenciará o checkout, os envios, as consultas de saldo e os webhooks.

<Steps>
  <Step title="Initialize the project">
    ```bash theme={null}
    mkdir mailkit && cd mailkit
    npm init -y
    npm install dodopayments resend express dotenv
    npm install -D tsx @types/node @types/express
    ```

    Adicione um script de desenvolvimento ao `package.json`:

    ```json theme={null}
    {
      "scripts": {
        "dev": "tsx watch server.ts"
      }
    }
    ```

    <Tip>
      [`tsx`](https://tsx.is) executa TypeScript diretamente, sem uma etapa de build ou `tsconfig.json`, o que é perfeito para um tutorial. Em produção, adicione um `tsconfig.json` e um script `build`.
    </Tip>
  </Step>

  <Step title="Configure environment variables">
    Crie `.env`:

    ```bash .env theme={null}
    # Dodo Payments
    DODO_PAYMENTS_API_KEY=your_dodo_test_api_key
    DODO_WEBHOOK_KEY=your_dodo_webhook_signing_key
    CREDIT_ENTITLEMENT_ID=cent_xxxxxxxxxxxx
    PLAN_PRODUCT_ID=pdt_xxxxxxxxxxxx
    TOPUP_PRODUCT_ID=pdt_xxxxxxxxxxxx

    # Resend
    RESEND_API_KEY=re_xxxxxxxxxxxx

    # App
    BASE_URL=http://localhost:3000
    PORT=3000
    ```

    Você preencherá o `DODO_WEBHOOK_KEY` na Etapa 4, depois de criar o endpoint. A API key do Resend vem de [resend.com/api-keys](https://resend.com/api-keys).

    <Warning>
      Adicione `.env` ao `.gitignore` imediatamente. Nunca faça commit de API keys.
    </Warning>
  </Step>

  <Step title="Build the server">
    Crie `server.ts` na raiz do projeto:

    <CodeGroup>
      ```typescript server.ts expandable theme={null}
      import 'dotenv/config';
      import express, { Request, Response } from 'express';
      import DodoPayments from 'dodopayments';
      import { Resend } from 'resend';

      const app = express();

      const dodo = new DodoPayments({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
        webhookKey: process.env.DODO_WEBHOOK_KEY!,
        environment: 'test_mode',
      });

      const resend = new Resend(process.env.RESEND_API_KEY!);

      const CREDIT_ENTITLEMENT_ID = process.env.CREDIT_ENTITLEMENT_ID!;
      const BASE_URL = process.env.BASE_URL!;

      // ---------------------------------------------------------------
      // Webhook endpoint MUST receive the raw body for signature
      // verification. Register it BEFORE express.json().
      // ---------------------------------------------------------------
      app.post(
        '/webhooks/dodo',
        express.raw({ type: 'application/json' }),
        async (req: Request, res: Response) => {
          const headers = {
            'webhook-id': req.headers['webhook-id'] as string,
            'webhook-signature': req.headers['webhook-signature'] as string,
            'webhook-timestamp': req.headers['webhook-timestamp'] as string,
          };

          let event: any;
          try {
            event = await dodo.webhooks.unwrap(req.body.toString('utf8'), { headers });
          } catch (err) {
            console.error('Webhook signature verification failed:', err);
            return res.status(401).json({ error: 'invalid signature' });
          }

          switch (event.type) {
            case 'credit.balance_low': {
              const { customer_id, credit_entitlement_name, available_balance, threshold_percent } =
                event.data;
              console.log(
                `[low-balance] ${customer_id} has ${available_balance} ${credit_entitlement_name} ` +
                  `left (under ${threshold_percent}%)`
              );
              await notifyCustomerLowBalance(customer_id, Number(available_balance));
              break;
            }
            case 'credit.added':
              console.log('[credit.added]', event.data);
              break;
            case 'credit.rolled_over':
              console.log('[rolled_over]', event.data);
              break;
          }

          res.json({ received: true });
        }
      );

      // JSON parsing for everything else.
      app.use(express.json());

      // ---------------------------------------------------------------
      // POST /checkout/subscribe → start the MailKit subscription.
      // ---------------------------------------------------------------
      app.post('/checkout/subscribe', async (req, res) => {
        const { email, name } = req.body as { email: string; name: string };

        const session = await dodo.checkoutSessions.create({
          product_cart: [{ product_id: process.env.PLAN_PRODUCT_ID!, quantity: 1 }],
          customer: { email, name },
          return_url: `${BASE_URL}/?subscribed=1`,
        });

        res.json({ checkout_url: session.checkout_url });
      });

      // ---------------------------------------------------------------
      // POST /checkout/topup → buy a 5,000-email top-up for an existing
      // customer. In a real app, customer_id is resolved from the
      // authenticated session, never trusted from request input.
      // ---------------------------------------------------------------
      app.post('/checkout/topup', async (req, res) => {
        const { customer_id } = req.body as { customer_id: string };

        const session = await dodo.checkoutSessions.create({
          product_cart: [{ product_id: process.env.TOPUP_PRODUCT_ID!, quantity: 1 }],
          customer: { customer_id },
          return_url: `${BASE_URL}/?topped_up=1`,
        });

        res.json({ checkout_url: session.checkout_url });
      });

      // ---------------------------------------------------------------
      // GET /credits/:customerId → live balance for the dashboard widget.
      // ---------------------------------------------------------------
      app.get('/credits/:customerId', async (req, res) => {
        const balance = await dodo.creditEntitlements.balances.retrieve(req.params.customerId, {
          credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
        });

        res.json({ balance: balance.balance });
      });

      // ---------------------------------------------------------------
      // POST /send → send an email via Resend, then write a ledger entry
      // to debit 1 credit from the customer's balance. The deduction is
      // instant; the next /credits call reflects it.
      // ---------------------------------------------------------------
      app.post('/send', async (req, res) => {
        const { customer_id, to, subject, html } = req.body as {
          customer_id: string;
          to: string;
          subject: string;
          html: string;
        };

        // 1. Pre-flight balance check: refuse to send if the balance is at zero.
        const balance = await dodo.creditEntitlements.balances.retrieve(customer_id, {
          credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
        });

        if (Number(balance.balance) <= 0) {
          return res.status(402).json({
            error: 'No email credits remaining. Buy a top-up pack or upgrade your plan.',
          });
        }

        // 2. Send via Resend.
        const { data, error } = await resend.emails.send({
          from: 'MailKit <onboarding@resend.dev>', // swap for your verified domain
          to: [to],
          subject,
          html,
        });

        if (error) {
          return res.status(500).json({ error: error.message });
        }

        // 3. Debit 1 credit. Resend's message id is the idempotency key, so if
        //    the client retries this request, Dodo deduplicates and the
        //    customer is only debited once for that send.
        await dodo.creditEntitlements.balances.createLedgerEntry(customer_id, {
          credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
          amount: '1',
          entry_type: 'debit',
          reason: `email send ${data!.id}`,
          idempotency_key: data!.id,
        });

        res.json({ id: data!.id });
      });

      async function notifyCustomerLowBalance(customerId: string, available: number) {
        // In production: send an email to the account owner, push a banner,
        // open an in-app modal, etc. For the demo we just log.
        console.log(`[NOTIFY] ${customerId}: ${available} emails left. Consider topping up.`);
      }

      app.use(express.static('public'));

      const port = Number(process.env.PORT) || 3000;
      app.listen(port, () => {
        console.log(`MailKit running on http://localhost:${port}`);
      });
      ```
    </CodeGroup>

    <Warning>
      **O corpo do webhook deve ser bruto.** `express.json()` analisa e serializa novamente o corpo, o que quebra a verificação da assinatura. Defina `/webhooks/dodo` com `express.raw()` *antes* da linha `app.use(express.json())`.
    </Warning>

    <Check>
      Backend pronto: assinatura, recarga, saldo, envio e manipulador de webhook configurados.
    </Check>
  </Step>

  <Step title="Add a demo UI">
    Crie `public/index.html`:

    <CodeGroup>
      ```html public/index.html expandable theme={null}
      <!doctype html>
      <html>
        <head>
          <title>MailKit Demo</title>
          <style>
            body {
              font-family: system-ui, -apple-system, sans-serif;
              max-width: 720px;
              margin: 40px auto;
              padding: 0 20px;
              color: #1a1a2e;
            }
            h1 { font-size: 28px; margin-bottom: 4px; }
            h2 { font-size: 16px; margin-top: 32px; padding-bottom: 6px; border-bottom: 1px solid #eee; }
            label { display: block; font-size: 13px; font-weight: 600; margin: 12px 0 4px; }
            input, select, textarea {
              width: 100%;
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 6px;
              font-family: inherit;
              font-size: 14px;
              box-sizing: border-box;
            }
            button {
              background: #1a1a2e;
              color: white;
              padding: 10px 18px;
              border: none;
              border-radius: 6px;
              cursor: pointer;
              font-size: 14px;
              margin-top: 12px;
            }
            button:hover { background: #2d2d4a; }
            .out {
              background: #f6f6fa;
              padding: 12px;
              border-radius: 6px;
              margin-top: 12px;
              font-size: 13px;
              font-family: ui-monospace, monospace;
              white-space: pre-wrap;
              word-break: break-all;
            }
            .balance { font-size: 36px; font-weight: 700; color: #4f46e5; }
            .balance-sub { color: #888; font-size: 13px; margin-top: 4px; }
          </style>
        </head>
        <body>
          <h1>MailKit</h1>
          <p>Prepaid transactional email, billed per send.</p>

          <h2>1. Subscribe to MailKit ($19/mo, 5,000 emails)</h2>
          <label>Email</label>
          <input id="subEmail" type="email" placeholder="you@example.com" />
          <label>Name</label>
          <input id="subName" type="text" placeholder="Your name" />
          <button onclick="subscribe()">Get checkout link</button>
          <div id="subOut" class="out" hidden></div>

          <h2>2. Check your balance</h2>
          <label>Customer ID</label>
          <input id="balCust" type="text" placeholder="cus_xxxxxxxxxxxx" />
          <button onclick="checkBalance()">Refresh</button>
          <div id="balOut" class="out" hidden></div>

          <h2>3. Send a transactional email</h2>
          <label>Customer ID</label>
          <input id="sendCust" type="text" placeholder="cus_xxxxxxxxxxxx" />
          <label>To (Resend's sandbox accepts delivered@resend.dev)</label>
          <input id="sendTo" type="email" value="delivered@resend.dev" />
          <label>Subject</label>
          <input id="sendSubj" type="text" value="Hello from MailKit" />
          <label>HTML body</label>
          <textarea id="sendBody" rows="3">&lt;strong&gt;It works!&lt;/strong&gt;</textarea>
          <button onclick="sendEmail()">Send</button>
          <div id="sendOut" class="out" hidden></div>

          <h2>4. Run low? Buy a top-up pack</h2>
          <label>Customer ID</label>
          <input id="topCust" type="text" placeholder="cus_xxxxxxxxxxxx" />
          <button onclick="topup()">Buy 5,000 emails ($9)</button>
          <div id="topOut" class="out" hidden></div>

          <script>
            const show = (id, content) => {
              const el = document.getElementById(id);
              el.hidden = false;
              el.innerHTML = content;
            };

            async function subscribe() {
              const r = await fetch('/checkout/subscribe', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                  email: document.getElementById('subEmail').value,
                  name: document.getElementById('subName').value,
                }),
              });
              const data = await r.json();
              show('subOut', r.ok
                ? `<a href="${data.checkout_url}" target="_blank">Open checkout →</a>`
                : `Error: ${data.error}`);
            }

            async function checkBalance() {
              const id = document.getElementById('balCust').value;
              const r = await fetch(`/credits/${id}`);
              const data = await r.json();
              show('balOut', r.ok
                ? `<div class="balance">${Number(data.balance).toLocaleString()}</div>
                   <div class="balance-sub">emails available</div>`
                : `Error: ${data.error}`);
            }

            async function sendEmail() {
              const r = await fetch('/send', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                  customer_id: document.getElementById('sendCust').value,
                  to: document.getElementById('sendTo').value,
                  subject: document.getElementById('sendSubj').value,
                  html: document.getElementById('sendBody').value,
                }),
              });
              const data = await r.json();
              show('sendOut', r.ok ? `Sent. Message id: ${data.id}` : `Error: ${data.error}`);
            }

            async function topup() {
              const r = await fetch('/checkout/topup', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ customer_id: document.getElementById('topCust').value }),
              });
              const data = await r.json();
              show('topOut', r.ok
                ? `<a href="${data.checkout_url}" target="_blank">Open top-up checkout →</a>`
                : `Error: ${data.error}`);
            }
          </script>
        </body>
      </html>
      ```
    </CodeGroup>
  </Step>
</Steps>

## Etapa 4: conecte o endpoint de webhook

O evento `credit.balance_low` permite avisar os clientes *antes* que eles fiquem sem créditos. Sem ele, a primeira vez que perceberão o problema será quando um e-mail não conseguir ser enviado.

<Steps>
  <Step title="Expose your local server">
    Webhooks precisam de uma URL pública. Use o [ngrok](https://ngrok.com) (ou qualquer tunnel) durante o desenvolvimento:

    ```bash theme={null}
    ngrok http 3000
    ```

    Copie a URL de encaminhamento HTTPS (por exemplo, `https://1234abcd.ngrok-free.app`).
  </Step>

  <Step title="Register the endpoint in Dodo">
    1. Acesse **Developers → Webhooks → Add Endpoint**
    2. **URL**: `https://1234abcd.ngrok-free.app/webhooks/dodo`
    3. **Events**: assine `credit.added`, `credit.balance_low` e `credit.rolled_over`
    4. Salve e copie a **signing key** para o seu `.env` como `DODO_WEBHOOK_KEY`
    5. Reinicie o servidor
  </Step>
</Steps>

## Etapa 5: teste o fluxo completo

<Steps>
  <Step title="Start the server">
    ```bash theme={null}
    npm run dev
    ```

    Você deverá ver `MailKit running on http://localhost:3000`. Abra-o no navegador.
  </Step>

  <Step title="Subscribe a test customer">
    1. Na seção 1, insira um e-mail e um nome de teste e clique em **Get checkout link**
    2. Abra o link e conclua o checkout com um [cartão de teste](/miscellaneous/testing-process)
    3. Após o pagamento, encontre o `customer_id` no dashboard, em **Customers**

    <Check>
      Agora o cliente deverá ter **5.000 e-mails** no saldo. Verifique em **Customers → \[Customer] → Credits**.
    </Check>
  </Step>

  <Step title="Send a real email">
    1. Cole o `customer_id` na seção 3
    2. Mantenha `to` definido como `delivered@resend.dev` (a caixa de entrada sandbox do Resend que aceita tudo)
    3. Clique em **Send**

    Você receberá de volta um ID de mensagem do Resend. Atualize o saldo na seção 2; a contagem cairá imediatamente para 4.999. Cada débito no ledger é refletido no saldo atualizado assim que é gravado.
  </Step>

  <Step title="Trigger the low-balance webhook">
    O limite é 20% (1.000 dos 5.000 e-mails permitidos). Para acioná-lo sem enviar 4.000 e-mails reais, **debite manualmente o saldo** no dashboard:

    1. Acesse **Customers → \[Customer] → Credits → Email Credits**
    2. Clique em **Adjust Balance** e debite `4000`
    3. Envie mais um e-mail pela demonstração

    Seu servidor deverá registrar, em poucos segundos:

    ```
    [low-balance] cus_xxx has 999 Email Credits left (under 20%)
    [NOTIFY] cus_xxx: 999 emails left. Consider topping up.
    ```

    <Check>
      Seu servidor recebeu e verificou o webhook. Em produção, é aqui que você enviaria um e-mail ao cliente ou exibiria um banner no app.
    </Check>
  </Step>

  <Step title="Buy a top-up pack">
    1. Cole o `customer_id` na seção 4
    2. Clique em **Buy 5,000 emails** e conclua o checkout de teste
    3. Atualize o saldo; ele aumentará em 5.000

    <Check>
      Um evento `credit.added` é disparado com `grant_source: one_time`. A recarga é acumulada sobre os créditos da assinatura; os dois conjuntos são consumidos em ordem FIFO (a concessão não expirada mais antiga primeiro).
    </Check>
  </Step>

  <Step title="Test the hard stop">
    Debite manualmente o saldo até zero e tente enviar mais um e-mail. Você receberá:

    ```json theme={null}
    { "error": "No email credits remaining. Buy a top-up pack or upgrade your plan." }
    ```

    Esse 402 é a aplicação da regra no nível da sua aplicação. A API de saldo do Dodo é a fonte de verdade; nunca armazene esse valor em cache no cliente.
  </Step>
</Steps>

## Solução de problemas

<AccordionGroup>
  <Accordion title="Webhook signature verification fails (401)">
    A assinatura é calculada sobre o corpo HTTP bruto. `express.json()` analisa e serializa novamente o payload, quebrando o HMAC. Certifique-se de que `/webhooks/dodo` esteja registrado com `express.raw({ type: 'application/json' })` *acima* da linha `app.use(express.json())` e que `DODO_WEBHOOK_KEY` corresponda à signing key exibida na página de detalhes do endpoint.
  </Accordion>

  <Accordion title="Balance is 0, customer not found, or credits don't deduct">
    Verifique estas três coisas, nesta ordem:

    1. O cliente **concluiu o checkout** (os créditos são concedidos após o pagamento bem-sucedido, não na criação da sessão)
    2. `CREDIT_ENTITLEMENT_ID` no seu `.env` corresponde ao crédito associado ao produto (IDs incompatíveis gravam silenciosamente no crédito errado)
    3. O `customer_id` que você está enviando veio do Dodo (a tabela `customers` no dashboard), não do seu próprio banco de dados
  </Accordion>

  <Accordion title="Resend rejects the recipient">
    O remetente sandbox `onboarding@resend.dev` só entrega para o e-mail da sua conta do Resend ou para `delivered@resend.dev`. Para enviar a qualquer outra pessoa, [verifique um domínio](https://resend.com/docs/dashboard/domains/introduction) e use um endereço `from` nele.
  </Accordion>
</AccordionGroup>

## O que você criou

<CardGroup cols={2}>
  <Card title="One reusable credit unit" icon="envelope">
    `Email Credits`, definido uma única vez e associado ao plano de assinatura e ao pacote de recarga.
  </Card>

  <Card title="Subscription with prepaid allowance" icon="layer-group">
    US\$ 19/mês concede 5.000 e-mails por ciclo. Os clientes sabem pelo que estão pagando, e você conhece seu custo máximo.
  </Card>

  <Card title="Top-up pack" icon="circle-plus">
    Um produto avulso que concede 5.000 e-mails. Ele é acumulado sobre os créditos da assinatura sem exigir alteração no plano.
  </Card>

  <Card title="Instant ledger debits" icon="bolt">
    Uma única chamada `createLedgerEntry` após cada envio. Sem meter, sem atraso de agregação e idempotente em novas tentativas por meio do ID da mensagem do Resend.
  </Card>
</CardGroup>

<Card title="Credit-Based Billing Reference" icon="book" href="/features/credit-based-billing">
  Leia a documentação completa de CBB para conhecer rollover, modos de overage, gerenciamento do ledger e toda a superfície da API.
</Card>

Precisa de ajuda?

* [Comunidade no Discord](https://discord.gg/bYqAp4ayYh)
* [support@dodopayments.com](mailto:support@dodopayments.com)
