Overview
urBackend Mail Platform extends transactional sending into a full delivery workflow with:- async queue-backed single sending (
/api/mail/send) - direct provider batch sending (
/api/mail/send-batch) - BYOK (Bring Your Own Key) with encrypted project-level Resend keys
- delivery tracking via persistent
MailLog - audience/contact management (BYOK-gated)
- marketing broadcasts (BYOK + Pro gated)
- webhook-driven status updates with Svix verification
How Mail Platform fits urBackend
- Public API (
/api/mail/*): app/runtime sending, logs, live status, webhook receiver. - Dashboard API (
/api/projects/:projectId/mail/*): operator/admin workflows (logs, live checks, audiences, contacts, broadcasts). - Dashboard UI (
/project/:projectId/mail): unified Mail Platform control plane.
Architecture
Feature gating matrix
Getting started
Prerequisites
- Resend account
- Valid Resend key with format:
re_[A-Za-z0-9_]+ - urBackend project with a Secret Key (
sk_live_...) for public API calls
Configure BYOK in Dashboard
- Open Project Settings for your project.
- Set
resendApiKeywith your Resend key (re_...). - Optionally set
resendFromEmail. - Save. urBackend stores this key encrypted at rest.
Required server environment
Register webhook in Resend
In Resend dashboard, configure webhook URL:POST https://<your-public-api-domain>/api/mail/webhook
Enable relevant events at minimum:
email.sentemail.deliveredemail.bouncedemail.complained
Sending emails
POST /api/mail/send (single)
Request schema
Quota and limits
- per-request monthly quota slot is reserved in Redis
- on terminal failure, slot is refunded
- over-limit returns HTTP
429
Example
Success envelope
POST /api/mail/send-batch (max 100)
Request schema
Array of 1..100 items:to, subject, html?, text?.
Quota behavior
- reserves one quota slot per batch item before provider call
- if provider call fails, each reserved slot is refunded
- response includes per-item provider result objects
Partial success
data returns per-recipient/provider results. Treat each item independently in your caller logic.
Example
Mail logs
GET /api/mail/logs (public) and GET /api/projects/:projectId/mail/logs (dashboard)
MailLog fields
resendEmailIdtosubjectstatususingByoktemplateUsedsentAt
queued | sent | delivered | bounced | complained | failed
Sorting/pagination behavior:
- current implementation returns latest 50, sorted by
sentAt DESC
Live status
- Public:
GET /api/mail/logs/:resendId - Dashboard:
GET /api/projects/:projectId/mail/logs/:resendId/live
404if log entry does not belong to the current project (cross-project isolation)
Audiences & Contacts (BYOK-gated)
These endpoints proxy Resend API and require a valid BYOK key on the project. Error status/message semantics follow upstream Resend responses where applicable.Audiences endpoints
GET /api/mail/audiencesPOST /api/mail/audiencesbody:{ "name": "VIP Customers" }DELETE /api/mail/audiences/:audienceId
Contacts endpoints
GET /api/mail/audiences/:audienceId/contactsPOST /api/mail/audiences/:audienceId/contactsPATCH /api/mail/audiences/:audienceId/contacts/:contactIdDELETE /api/mail/audiences/:audienceId/contacts/:contactId
email, firstName, lastName, unsubscribed
Marketing broadcasts (BYOK + Pro)
Endpoints:POST /api/mail/broadcastsPOST /api/mail/broadcasts/:id/sendGET /api/mail/broadcastsGET /api/mail/broadcasts/:idDELETE /api/mail/broadcasts/:id
Two-step flow
- Create broadcast (draft/scheduled payload)
- Send broadcast by id
from resolution order
fromfrom request bodyproject.resendFromEmailEMAIL_FROMenv fallback
Quota checks
Broadcast create/send/list/detail/delete paths run behind mail usage gating middleware and plan checks.Webhook integration
Endpoint:POST /api/mail/webhook
Why raw-body parsing is required
Svix signatures are computed over raw payload bytes.express.raw({ type: 'application/json' }) must run before JSON parser on this path.
Secret configuration
SetRESEND_WEBHOOK_SECRET in server env. Webhook signature verification and event processing only occur when this secret is configured correctly.
- Current behavior when missing/misconfigured: the handler returns HTTP
200with{"success":true,"message":"Webhook ignored: secret not configured."}and skips verification/processing.
Event mapping to MailLog.status
email.sent represents provider acceptance (intermediate state), not final inbox delivery.
Retry behavior
Resend controls webhook retries for non-2xx/timeout outcomes. Keep endpoint idempotent and safe for repeated delivery attempts.Dashboard UI guide
Route:/project/:projectId/mail
Delivery Logs tab
- status badge (
queued/sent/delivered/bounced/complained/failed) - subject, recipient(s), provider id, sent time
- live status modal fetches provider status by resend id
Audiences & Contacts tab
- create/delete audiences
- add/remove contacts
- locked if BYOK key is not configured
Marketing Broadcasts tab
- compose audience + subject + html
- send campaign via broadcast API
- locked unless BYOK is configured and account is Pro
Add product screenshots/annotated walkthrough images in this section if your docs deployment supports hosted image assets.
Security notes
- BYOK format validation:
^re_[A-Za-z0-9_]+$ - key encryption at rest uses shared
encrypt/decrypthelpers (AES) - rotate/clear BYOK through project update workflows (
PATCH /api/projects/:projectIdwithresendApiKey: null, then set a newre_...key) - live status endpoints validate project ownership before provider lookups
- webhook authenticity is checked with Svix verification
Error reference
Most endpoints follow urBackend envelope shape:
Changelog & migration notes
Compared to legacy single-send usage:/api/mail/sendis now asynchronous queue-backed/api/mail/send-batchadds bulk dispatch (up to 100 items/request)MailLogis first-class for auditability and dashboard visibility- new BYOK-gated resources: audiences, contacts
- new BYOK+Pro resource: broadcasts
- webhook path requires raw-body middleware placement before JSON parser
- new env dependencies for production-grade mail processing (
RESEND_WEBHOOK_SECRET, sender defaults)
