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:- 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.
The Credits tab under Products lists all your credit entitlements.
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
Save and copy the credit ID
cent_xxxxxxxxxxxx.Email Credits entitlement is ready. Next: the products that grant credits to customers.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.Initialize the project
package.json: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:Add a demo UI
public/index.html: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).Register the endpoint in Dodo
- Go to Developers → Webhooks → Add Endpoint
- URL:
https://1234abcd.ngrok-free.app/webhooks/dodo - Events: subscribe to
credit.added,credit.balance_low, andcredit.rolled_over - Save, then copy the signing key into your
.envasDODO_WEBHOOK_KEY - Restart your server
Step 5: Test the Full Flow
Start the server
MailKit running on http://localhost:3000. Open it in your browser.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).Test the hard stop
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.