Skip to main content

Email Notifications

SalesArck sends transactional emails via the Resend REST API. Implementation is in apps/api/src/lib/email.ts.

Architecture

  • Provider: Resend (REST API, no SDK dependency — uses plain fetch)
  • Pattern: Fire-and-forget — failures are logged but never throw or block the caller
  • Configuration: Optional — if RESEND_API_KEY is not set, all sends are silently skipped

Configuration

VariableRequiredDescription
RESEND_API_KEYFor email sendsResend API key
RESEND_FROM_EMAILNoSender address (default: SalesArck <noreply@salesarck.io>) — must be a verified Resend domain

Email Templates

All templates use a shared baseLayout() function that produces responsive HTML email with:

  • SalesArck branded header (indigo #4f46e5)
  • Content area (white background)
  • Footer with unsubscribe context
  • Mobile-safe table layout

Points Earned

Trigger: After creditWallet() successfully credits points to a consumer's wallet.

Sent by: sendPointsEarnedEmail() — called fire-and-forget from the ingestion pipeline.

Content:

  • Points earned (e.g., "+15 pts")
  • Merchant name
  • Purchase amount (formatted with Intl.NumberFormat)
  • Two-column: points earned vs new balance
  • CTA: "View my wallet" link to https://salesarc-web.vercel.app/wallet

Subject: +{points} pts at {tenantName}

Redemption Code

Trigger: After consumer confirms a redemption via POST /consumer/wallets/:walletId/redeem/confirm.

Sent by: sendRedemptionCodeEmail() — called from the consumer redemption route.

Content:

  • 6-character redemption code (large monospace display)
  • 15-minute expiry window
  • Two-column: points redeemed vs discount value
  • Security notice ("If you didn't request this...")

Subject: Your redemption code: {code}

Wallet Frozen

Trigger: After admin freezes a consumer's wallet via POST /admin/wallets/:walletId/freeze.

Sent by: sendWalletFrozenEmail() — called from the admin wallet freeze route.

Content:

  • Notification that wallet has been temporarily frozen
  • Optional reason
  • Contact support message

Subject: Your SalesArck wallet has been frozen

Integration Points

Ingestion Pipeline

In services/ingestion.ts, after a successful wallet credit:

creditWallet() → success

Async IIFE (non-blocking):
1. Fetch user email from users table
2. Fetch tenant name from tenants table
3. Fetch updated wallet balance
4. sendPointsEarnedEmail({ to, points, tenantName, amountMinorUnits, currency, newBalance })

Email lookup failures are caught and logged — they never affect the transaction outcome.

Consumer Redemption

In routes/consumer.ts, after redemption confirmation:

Wallet debit → Generate 6-char code → sendRedemptionCodeEmail()

Admin Actions

In routes/admin.ts, wallet freeze triggers:

Set wallet status → Audit log → sendWalletFrozenEmail()

Operational Behavior

  • If RESEND_API_KEY is not set: all email functions return immediately with a debug log
  • If Resend returns a non-2xx status: error is logged with recipient and status code, but execution continues
  • Successful sends are logged at info level with recipient and subject
  • No retry logic for failed sends (fire-and-forget pattern)
  • PII redaction in logs prevents email addresses from appearing in standard log output (handled by @salesarc/observability)
Written byDhruv Doshi