In this tutorial, you’ll build MailKit, a transactional email platform where customers pay upfront for a pool of email credits. The plan grants a monthly email allowance; when customers run low, they can buy a top-up pack instead of waiting for the next cycle. Every send deducts one credit automatically.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.
resend.emails.send for SendGrid, Postmark, SES, or your own SMTP relay.- Create a custom credit entitlement (emails) in your dashboard
- Attach credits to a subscription plan and a one-time top-up product
- Send real emails via Resend and debit one credit per send via a ledger entry
- Query a live credit balance from your frontend
- Verify Dodo webhooks correctly and handle
credit.balance_lowto nudge customers before they hit zero
What We’re Building
Here’s the pricing model for MailKit:| Product | Price | Emails |
|---|---|---|
| MailKit Plan | $19/month | 5,000 emails/cycle |
| Top-Up Pack | $9 one-time | +5,000 emails |
- A Dodo Payments account (test mode is fine)
- A free Resend account and API key
- Node.js 18+ and basic TypeScript familiarity
Step 1: Create Your Email Credit Entitlement
The credit entitlement defines the unit your platform sells: in this case, one email send.
Open the Credits section
- Log into your Dodo Payments dashboard
- Click Products in the left sidebar
- Select the Credits tab
- Click Create Credit
Configure the credit unit
Email CreditsCredit Type: Select Custom UnitUnit Name: emailPrecision: 0 (an email is always a whole unit; you can’t send half an email)Credit Expiry: 30 days (each cycle’s allowance resets)Leave the other defaults as-is
Step 2: Create the Plan and Top-Up Pack
You’ll create two products: a recurring Subscription plan and a Single Payment top-up. The plan grants 5,000 emails each cycle; the top-up adds another 5,000 on demand. Both attach the sameEmail Credits entitlement.
MailKit Plan ($19/month, 5,000 emails)
Create the subscription
- Go to Products → Create Product
- Fill in the product details:
MailKit PlanDescription: 5,000 transactional emails per month.- Select Subscription as the product type
- Set the recurring price:
19.00Billing Cycle: MonthlyCurrency: USDAttach the email credit entitlement
Email CreditsCredits issued per billing cycle: 5000Low Balance Threshold: 20 (percent; fires credit.balance_low when the balance drops below 20% of the cycle allowance, i.e. 1,000 emails)Import Default Credit Settings: enabled (uses the 30-day expiry from Step 1)Click Add to Product, then Save the product. Copy the product ID (pdt_xxxxxxxxxxxx).Top-Up Pack ($9 one-time, 5,000 emails)
Create a one-time product
- Go to Products → Create Product
- Fill in the product details:
Email Top-Up PackDescription: Add 5,000 emails to your MailKit balance instantly.- Select Single Payment as the product type
- Set the pricing:
9.00Currency: USDAttach the credit grant
- Credit Entitlement:
Email Credits - Credits issued:
5000
Step 3: Set Up the Backend
Now build the Express server that handles checkout, sending, balance queries, and webhooks.Configure environment variables
.env:DODO_WEBHOOK_KEY in Step 4 after creating the endpoint. The Resend API key comes from resend.com/api-keys.Build the server
server.ts in the project root:Step 4: Wire Up the Webhook Endpoint
Thecredit.balance_low event is what lets you nudge customers before they run out. Without it, the first time they notice the problem is when an email fails to send.
Expose your local server
https://1234abcd.ngrok-free.app).Step 5: Test the Full Flow
Subscribe a test customer
- In section 1, enter a test email and name, click Get checkout link
- Open the link, complete checkout with a test card
- After payment, find the
customer_idin your dashboard under Customers
Send a real email
- Paste the
customer_idinto section 3 - Leave
toset todelivered@resend.dev(Resend’s sandbox inbox that accepts everything) - Click Send
Trigger the low-balance webhook
- Go to Customers → [Customer] → Credits → Email Credits
- Click Adjust Balance and debit
4000 - Send one more email through the demo
Buy a top-up pack
- Paste the
customer_idinto section 4 - Click Buy 5,000 emails, complete the test checkout
- Refresh the balance, and it jumps by 5,000
credit.added event fires with grant_source: one_time. The top-up stacks on top of the subscription credits; both pools are consumed FIFO (oldest non-expired grant first).Troubleshooting
Webhook signature verification fails (401)
Webhook signature verification fails (401)
express.json() parses and re-serializes the payload, which breaks the HMAC. Make sure /webhooks/dodo is registered with express.raw({ type: 'application/json' }) above the app.use(express.json()) line, and that DODO_WEBHOOK_KEY matches the signing key shown on the endpoint detail page.Balance is 0, customer not found, or credits don't deduct
Balance is 0, customer not found, or credits don't deduct
- The customer completed checkout (credits are issued on successful payment, not on session creation)
CREDIT_ENTITLEMENT_IDin your.envmatches the credit attached to the product (mismatched IDs silently write to the wrong credit)- The
customer_idyou’re passing came from Dodo (thecustomerstable in the dashboard), not your own database
Resend rejects the recipient
Resend rejects the recipient
onboarding@resend.dev only delivers to the email on your Resend account or to delivered@resend.dev. To send to anyone else, verify a domain and use a from address on it.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
Top-up pack
Instant ledger debits
createLedgerEntry call after each send. No meter, no aggregation lag, idempotent on retry via Resend’s message id.