Last Updated: 2026-06-28 Status: Active Companion docs: architecture.md · api-spec.md
1. Credit Rate
| Operation | Cost |
|---|---|
| Campaign send | Math.ceil(recipients / 5) credits |
| Test send | 1 credit |
| Subscribe/confirm | Free (system tenant bypass) |
1 credit = 5 emails. A 100-recipient campaign costs 20 credits. A 101-recipient campaign costs 21 credits (ceiling).
2. Charge Point — Upfront Before Enqueue
Credits are charged before any queue messages are written. If the charge fails, no emails are sent. If the charge succeeds but the Worker crashes before enqueue completes, the partial enqueue may send fewer emails than charged — this is intentional (prevents double-charging on retry).
fireCampaign()
├─ Resolve recipients (filter + suppress)
├─ charge(Math.ceil(count / 5)) via Core DB ← credits debited here
│ ├─ OK → continue
│ └─ 402 → return error, nothing queued
└─ Enqueue jobs (one per recipient)3. Ledger Entry
Charges are written to the core database (CORE_DATABASE_URL):
| Field | Value |
|---|---|
tenantId |
From request context |
amount |
Math.ceil(recipients / 5) |
reason |
"email.send" |
referenceId |
campaignId |
idempotencyKey |
campaign:{campaignId}:fire:{originalStatus} |
description |
"Newsletter campaign {campaignId} — {count} recipients" |
Idempotency Key Design
The key includes originalStatus (the status before CAS to sending). This means:
- Retrying a failed fire for the same campaign (before it moved to
sending) → same key → no double charge - User manually resets a campaign to
draftand re-fires → different key (draftvssending) → new charge
This is intentional: a re-fire is a new send decision by the user.
4. Refund Policy
No automatic refund on delivery failure.
Once credits are charged and jobs are enqueued, comms-service has been called (or will be called) for each recipient. A failed delivery (provider_4xx, network error, etc.) is a delivery problem, not a billing error. Credits are not refunded.
| Scenario | Refund? |
|---|---|
| Charge fails (402) — nothing sent | N/A (never charged) |
| Campaign cancelled before send fires | No (cancelled before fire = no charge yet) |
| Subscriber bounces post-send | No |
| Send job fails after retry exhaustion | No |
| Test send recipient doesn't receive email | No |
5. System Tenant Bypass
Tenants matching isSystemTenant(tenantId) skip all credit checks. Confirmation emails and internal sends are always free.
6. Test Send
POST /templates/:id/test-send charges 1 credit regardless of audience size. Test sends are single-recipient (to the requesting user's email) and not tracked in nl_campaign_sends.
7. Error Codes
| Status | Meaning |
|---|---|
402 Payment Required |
Insufficient credits; response includes neededCredits |
500 { code: "billing_misconfigured" } |
CORE_DATABASE_URL missing or unreachable |
8. Pricing Anchor
- ₹0.83 / credit (see memory: Credit pricing anchor)
- AI usage rates calibrated at ₹0.20 per operation (~10× markup)
- Email billing: 5 emails per credit → ₹0.166 per email at rack rate
9. Related Tables
| Table | Used for |
|---|---|
tenant_credits (Core DB) |
Balance check |
credit_transactions (Core DB) |
Charge + idempotency record |