logicspike/docs

Newsletter

Newsletter Service — Billing & Credits

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 draft and re-fires → different key (draft vs sending) → 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

Table Used for
tenant_credits (Core DB) Balance check
credit_transactions (Core DB) Charge + idempotency record
Newsletter