Skip to main content
To have your coding agent write the integration, install the Dodo Agent Plugin. It adds the Dodo Payments skills and MCP servers to Claude Code, Codex CLI, Cursor, VS Code / GitHub Copilot, Kiro, and OpenCode.
You’ll build MailKit, a transactional email service where customers prepay for email credits. A monthly plan grants 5,000 emails per billing cycle. A customer who runs low buys a top-up pack instead of waiting for the next cycle. Each send debits one credit.
This tutorial uses Resend as the email provider. Its free tier (3,000 emails per month) covers building and testing the whole flow. The billing pattern works with any provider: replace resend.emails.send with a call to SendGrid, Postmark, Amazon SES, or your own SMTP relay.
When you finish, you’ll know how to:
  • Create a custom credit entitlement for emails in the dashboard.
  • Attach credits to a subscription plan and a one-time top-up product.
  • Send email through Resend and debit one credit per send with a ledger entry.
  • Read a customer’s live credit balance from your frontend.
  • Verify Dodo Payments webhooks and handle credit.balance_low to warn customers before their balance reaches zero.

What We’re Building

MailKit sells two products: The unit is one email = one credit. Customers don’t need to reason about tokens, batches, or weighted units. They see “4,231 emails left this month.” Before you start, you need:
  • A Dodo Payments account. Build everything in test mode.
  • A free Resend account and API key.
  • Node.js 22 or later, and working knowledge of TypeScript.

Step 1: Create Your Email Credit Entitlement

The credit entitlement defines the unit MailKit sells: one email send.
Credits tab under Products, listing the business's credit entitlements

The Credits tab under Products lists all your credit entitlements.

1

Open the Credits Section

  1. Log in to the Dodo Payments dashboard.
  2. Click Products in the sidebar.
  3. Select the Credits tab.
  4. Click Create Credit.
2

Configure the Credit Unit

Enter these values:Credit Name: Email CreditsCredit Type: Custom UnitUnit Name: emailDefine Precision: 0. An email is a whole unit, so the balance never needs decimals.Credit Expiry: 30 days. Unused credits expire 30 days after they’re issued.
Precision can’t be changed after you create the credit. For discrete units such as emails, messages, or sessions, use 0.
3

Leave the Other Defaults

This tutorial leaves rollover and overage off to keep the credit flow minimal. You can turn them on later, either on the credit or on each product’s credit attachment.
4

Save and Copy the Credit ID

Click Create Credit. Open the credit and copy its ID, which starts with cde_. The backend uses it for balance reads and ledger entries.
The Email Credits entitlement is ready. Next, create the products that grant it to customers.

Step 2: Create the Plan and Top-Up Pack

Create two products that attach the same Email Credits entitlement: a Subscription plan that grants 5,000 emails each billing cycle, and a One Time top-up that adds 5,000 more on demand.
This tutorial debits credits with ledger entries instead of usage meters. A ledger debit is applied when the API call returns, needs no meter setup, and fits cases where one user action costs exactly one credit. To deduct credits automatically from ingested usage events, which suits weighted units such as tokens or megabytes processed, see Usage Billing with Credits in the Credit-Based Billing guide.

MailKit Plan ($19/month, 5,000 Emails)

1

Create the Subscription

  1. Go to Products and click Add Product.
  2. Enter the product details:
Product Name: MailKit PlanDescription: 5,000 transactional emails per month.
  1. Under Pricing Type, select Subscription.
  2. Set the recurring price:
Price: 19.00Repeat payment every: 1 monthCurrency: USD
2

Attach the Email Credit Entitlement

In the Entitlements section, click Attach next to Credits and configure:Select credits: Email CreditsCredits issued per billing cycle: 5000Low Balance Threshold (%): 20. Dodo Payments sends credit.balance_low when the balance falls below 20% of the credits issued per cycle, which is 1,000 emails.Import Default Credit Settings: on, so the product uses the 30-day expiry from Step 1.Add the credit to the product, then save the product. Copy the product ID, which starts with pdt_.
Plan: $19/month, with 5,000 emails issued each billing cycle.

Top-Up Pack ($9 One-Time, 5,000 Emails)

1

Create a One-Time Product

  1. Go to Products and click Add Product.
  2. Enter the product details:
Product Name: Email Top-Up PackDescription: Add 5,000 emails to your MailKit balance.
  1. Under Pricing Type, select One Time.
  2. Set the price:
Price: 9.00Currency: USD
2

Attach the Credit Grant

In the Entitlements section, click Attach next to Credits and configure:
  • Select credits: Email Credits
  • No of credits issued: 5000
A one-time product grants credits with their own expiry: 30 days from purchase, from the default you set in Step 1. Top-up credits add to the subscription credits. They don’t replace them.
Save the product and copy its ID.
Top-Up Pack: $9 for 5,000 emails, added to the balance after the payment succeeds.

Step 3: Set Up the Backend

Build the Express server that creates checkouts, sends email, reads balances, and receives webhooks.
1

Initialize the Project

Add a dev script to package.json:
tsx runs TypeScript directly, without a build step or a tsconfig.json. For production, add a tsconfig.json and a build script.
2

Configure Environment Variables

Create .env with a test mode API key from Developer → API Keys and the IDs from Steps 1 and 2:
.env
You fill in DODO_PAYMENTS_WEBHOOK_KEY in Step 4, after you create the webhook endpoint. Create the Resend API key at resend.com/api-keys.
Add .env to .gitignore before your first commit. Never commit API keys.
3

Build the Server

Create server.ts in the project root. The server exposes five routes: subscribe checkout, top-up checkout, balance read, send, and the webhook receiver.
The webhook route must receive the raw request body. express.json() replaces the body with a parsed object, and signature verification needs the exact bytes Dodo Payments signed. Keep the /webhooks/dodo route, with express.raw(), above the app.use(express.json()) line.
The backend is ready: subscribe, top-up, balance, send, and the webhook handler.
4

Add a Demo UI

Create public/index.html. It calls each route from a simple form, so you can test the flow in a browser:

Step 4: Wire Up the Webhook Endpoint

The credit.balance_low event lets you warn customers before they run out. Without it, a customer first notices the problem when an email fails to send.
1

Expose Your Local Server

Webhooks need a public URL. While you develop, use ngrok or another tunnel:
Copy the HTTPS forwarding URL, for example https://1234abcd.ngrok-free.app.
2

Register the Endpoint in Dodo Payments

  1. Go to Developer → Webhooks and click Add endpoint.
  2. Enter the URL https://1234abcd.ngrok-free.app/webhooks/dodo, using your own tunnel host.
  3. Select the events credit.added, credit.balance_low, and credit.rolled_over.
  4. Click Create endpoint.
  5. Copy the signing secret from the endpoint’s Overview tab into .env as DODO_PAYMENTS_WEBHOOK_KEY.
  6. Restart the server.

Step 5: Test the Full Flow

1

Start the Server

The server logs MailKit running on http://localhost:3000. Open that URL in your browser.
2

Subscribe a Test Customer

  1. In section 1, enter a test email address and name, then click Get checkout link.
  2. Open the link and complete checkout with a test card.
  3. In the dashboard, go to Customers and copy the new customer’s ID, which starts with cus_.
The customer has 5,000 emails in their balance. To confirm, open the customer in Customers and select the Credits tab.
3

Send an Email

  1. Paste the customer ID into section 3.
  2. Leave To set to delivered@resend.dev, a Resend test address that accepts every message.
  3. Click Send.
The page shows the Resend message ID. Refresh the balance in section 2: it reads 4,999. A ledger debit is part of the balance as soon as the API call returns.
4

Trigger the Low-Balance Webhook

The threshold is 20%, or 1,000 of the 5,000 emails issued per cycle. To reach it without sending 4,000 emails, debit the balance manually in the dashboard:
  1. Open the customer in Customers, select the Credits tab, and choose Email Credits.
  2. Click Apply Credit/Debit, select Debit, and enter 4000. The balance is now exactly 1,000, which isn’t below the threshold yet.
  3. Send one more email from the demo. The balance drops to 999.
When the webhook arrives, the server logs:
The server received and verified the webhook. In production, this is where you email the customer or show an in-app banner.
5

Buy a Top-Up Pack

  1. Paste the customer ID into section 4.
  2. Click Buy 5,000 emails and complete the test checkout.
  3. Refresh the balance. It increases by 5,000.
Dodo Payments sends a credit.added event with transaction_type: "credit_added". The grant behind it has source_type: one_time, which you can read back with the List Customer Grants API. Top-up credits add to the subscription credits. Debits draw from the grant that expires first, and from the oldest grant when two expire at the same time.
6

Test the Hard Stop

Debit the balance to zero in the dashboard, then try to send one more email. The server responds with 402:
That 402 is your application’s enforcement. Treat the Dodo Payments balance API as the source of truth, and don’t cache the balance on the client.

Troubleshooting

The signature covers the raw HTTP body. express.json() replaces the body with a parsed object, so verification fails. Register /webhooks/dodo with express.raw({ type: 'application/json' }) above the app.use(express.json()) line. Then check that DODO_PAYMENTS_WEBHOOK_KEY matches the signing secret on the endpoint’s Overview tab.
Check these three things, in order:
  1. The customer completed checkout. Credits are issued when the payment succeeds, not when the checkout session is created.
  2. CREDIT_ENTITLEMENT_ID in .env matches the credit attached to the product. The balance and ledger calls use this ID, so a mismatch reads or debits a different credit.
  3. The customer_id you pass is the Dodo Payments customer ID (it starts with cus_), not an ID from your own database.
The test sender onboarding@resend.dev delivers only to the email address on your Resend account, or to delivered@resend.dev. To send to anyone else, verify a domain and use a from address on that domain.

What You Built

One Reusable Credit Unit

Email Credits, defined once and attached to both the subscription plan and the top-up pack.

Subscription with Prepaid Allowance

$19/month grants 5,000 emails per billing cycle. Customers know what they pay for, and you know your maximum cost.

Top-Up Pack

A one-time product that grants 5,000 emails on top of subscription credits, with no plan change.

Direct Ledger Debits

One createLedgerEntry call after each send, with no meter and no aggregation delay. The Resend message ID as the idempotency key blocks a second debit for the same send.

Credit-Based Billing Reference

Rollover, overage modes, ledger management, and the full credit API.
For help, ask in the Discord Community or email support@dodopayments.com.
Last modified on September 26, 2026