Definitive offline reference for operators running Maison Relay — the messaging backbone for the Maison Doclar ecosystem. Covers campaigns, audience filtering, WhatsApp templates, FFmpeg asset rendering, inbound keywords, reports, forms, host dashboards, and environment configuration.

Overview

Maison Relay is the messaging backbone for the Maison Doclar ecosystem. It ingests guests, builds audiences, runs WhatsApp/SMS/email campaigns, pre-renders personalised assets to Backblaze B2, dispatches via Twilio and Resend, handles inbound RSVP keywords, and generates host reports.

Relay connects to Gateway (sync/writeback), md-assets (access cards + personalised video completion), Tally (forms), and Video Utility (compliant masters registered into Assets). Operator UI: APP_BASE_URL.

Guests Audience Filter Campaign Template Resolver Channel Dispatch WhatsApp Email SMS Delivery Writeback

Campaign execution runs on BullMQ workers backed by Redis. The prepare phase resolves the audience and pre-renders personalised assets to B2. The send phase dispatches per-guest messages with batch delays, send windows, and channel fallback. The complete phase finalises the campaign, generates reports, and sends operator alerts.

The home dashboard shows a cross-event summary via GET /api/events/dashboard-overview: active event count, campaigns currently sending, messages sent today (UTC day), and overall delivery rate.

The Active Events table lists each non-archived event with campaign count, send/delivery totals, RSVP rate, and status. Click a row (or Open) to set that event as active and jump to Campaigns.

Archive calls POST /api/events/:id/archive (sets archivedAt; guest data is preserved). Archived events appear in the collapsible section; Unarchive clears archivedAt. The sidebar event list excludes archived events unless GET /api/events?includeArchived=true.

The Guests nav section lists all guests for the active event. Import via CSV (POST /api/events/:eventId/guests/import) or manual create. AI data cleaning flags invalid contacts before campaigns.

Guest detail panel: RSVP status, household, suppression state, 24h conversation window (lastInboundAt), dietary notes, check-in/attendance. Guest Portal button issues a 72h magic link (POST /api/guest-portal/link).

Check-in stats banner: GET /api/events/:eventId/checkin-stats (refreshes every 30s).

Per-event cross-campaign analytics: GET /api/events/:eventId/analytics (requires operator auth).

Returns aggregate sends, delivery and read rates, RSVP confirmed/declined/pending counts, channel breakdown, and per-campaign summaries. The Analytics nav section renders summary cards, channel and campaign tables, and an inline RSVP bar chart.

Operator actions are recorded in admin_audit_log. Query via GET /api/system/audit-log with optional entityType, adminUsername, startDate, endDate, limit (default 50), and offset.

Each entry includes action (e.g. event.archived, template.update), entityType, entityId, beforeState, and afterState JSON snapshots. The System UI shows a compact diff of up to three changed fields.

A campaign targets an audience segment (or inline filter), binds a message template, optionally attaches personalised assets, and sends via a primary channel with optional fallback.

Lifecycle statuses

StatusMeaning
draftBeing configured. Not queued for send.
scheduledApproved configuration; will start at scheduledAt or on manual launch.
sendingPrepare and/or send workers are active.
pausedSend queue paused; no new dispatches until resumed.
completedAll sends finished (sent, failed, suppressed, or skipped).
cancelledTerminated by operator; no further sends.
draft scheduled sending paused completed cancelled schedule launch pause resume complete cancel (from most states)

Valid transitions

  • draftscheduled, cancelled
  • scheduledsending, cancelled, draft (revert)
  • sendingpaused, completed, cancelled
  • pausedsending, cancelled
  • completed and cancelled are terminal

Scheduling

Set scheduledAt (UTC) and transition to scheduled. The campaign scheduler polls and moves eligible campaigns to sending when the time arrives and send-window rules allow.

Pause, resume, cancel

  • Pause — transition sendingpaused. In-flight jobs finish; new sends wait.
  • Resume — transition pausedsending.
  • Cancel — allowed from draft, scheduled, sending, or paused. Pending sends are stopped.

Transitioning to sending runs validation: audience non-empty, template present, WhatsApp templates approved (if applicable), and test-send gate satisfied (testSendStatus must not be none).

If a campaign appears stuck in sending, use POST /api/system/queues/campaign/:campaignId/recover (see Troubleshooting).

Campaign detail — Monitor tab

Open a campaign and select the Monitor tab. While status is sending, live stats poll every five seconds via GET /api/campaigns/:id/live-stats.

  • Stat cards — Total, Delivered, Failed, Send rate (messages sent in the last 60 seconds), and Delivery % (colour-coded: green ≥90%, gold ≥70%, red <70%).
  • Progress bar — Delivered count vs total sends.
  • Delivery rate sparkline — Last ten polling samples (sending campaigns only).
  • Activity log — Latest 30 send events with guest name, channel, status, and timestamp. Skipped and suppressed rows show the worker lastError reason (e.g. Skipped: delivered within 24h, Deferred: outside send window).

For draft or scheduled campaigns, stats are static (no polling or sparkline).

Failed Sends tab

When any sends have status failed, a Failed Sends tab appears with a triage table: guest, channel, error (truncated with full text on hover), attempt count, next retry time, and Retry Now per row (POST /api/campaigns/:id/sends/:sendId/retry). Retry All Failed requeues each failed send sequentially.

Pre-flight checklist

For draft and scheduled campaigns, a checklist card loads from GET /api/campaigns/:id/preflight: audience size, template and WhatsApp approval, send window, asset config, and validator errors/warnings. Green ✓, amber ⚠, red ✗. Footer text indicates ready to send, warnings to review, or blocking errors.

Resume confirmation banners

While sending, blue and amber banners summarise completed vs queued sends and warn when prepareAttempts > 1 (campaign was restarted).

Duplicate campaign

POST /api/campaigns/:id/duplicate creates a draft copy named {original} (copy) with the same audience, template, channels, send window, and asset config (not send history or schedule).

Graceful asset degradation

Before dispatch, Relay performs a 3-second HEAD check on image/video URLs. If the asset is unreachable, the send continues without media, logs dispatch.asset_unreachable, and records assetDegraded: true on the send's providerResponse.

During pre-warm, per-guest render failures do not block the campaign. If more than 90% of guests fail asset generation, the campaign is paused and a system alert is written.

Access card campaigns

When messageType is access_card and the primary channel is WhatsApp, Relay attaches the guest's card image as MediaUrl. It uses send.imageUrl when set; otherwise GET {ASSETS_BASE_URL}/api/assets/cards/{nucleusEventId}/{nucleusGuestId} with ASSETS_API_KEY bearer auth. Generate cards in Assets before launching.

Guest portal links

From the guest detail panel, Guest Portal creates a 72-hour magic link (POST /api/guest-portal/link) for self-service RSVP and dietary updates.

WhatsApp messages outside the 24-hour session window require a Meta-approved template registered via the Twilio Content API. Relay stores the Content SID (HX…) on each message_templates record.

Admin UI workflow (create → submit → approve)

When editing a WhatsApp template in Relay, the WhatsApp Template Manager panel replaces manual SID entry with three accordion steps:

  1. Create in Twilio (shown when no Content SID yet) — friendly name, body (supports {{1}} variables), variable map, optional media URL, and category. Create in Twilio Content API calls POST /api/twilio/content-templates/create and stores the returned HX… SID (pending_creation). Save the template in Relay first so a templateId exists.
  2. Submit for Meta Approval (shown when SID exists but not approved) — Meta template name (lowercase + underscores) and category. Submit for Approval calls POST /api/twilio/content-templates/:sid/submit-approval (submitted).
  3. Approval Status — displays SID, status badge, rejection reason if any, and Check Status (GET /api/twilio/content-templates/:sid/approval-status?templateId=). When approved, WhatsApp campaigns using this template may enter sending.

API-only alternative: link an existing SID via POST /api/twilio/content-templates/link with { templateId, twilioContentSid } (must start with HX).

List all Content templates: GET /api/twilio/content-templates.

Approval statuses

StatusMeaning
pending_creationSID assigned; not yet submitted to Meta.
submittedWaiting on Meta / Twilio approval.
approvedTemplate cleared for outbound WhatsApp campaigns.
rejectedMeta rejected — fix copy/category and resubmit.
(empty)No Content SID linked — session-window freeform only.
Blocking rule: Campaigns with WhatsApp as primary channel cannot enter sending unless the bound template has twilioApprovalStatus === 'approved' and a valid twilioContentSid. Unapproved templates are blocked at validation time.

Utility quality checker

Before submitting to Meta, run POST /api/templates/:id/check-utility. The checker scores template copy 0–100 against utility classification rules (promotional language, personalisation variables, CTA type). Score ≥ 70 is required to submit via POST /api/templates/:id/submit-whatsapp.

Import from Twilio console

Templates created directly in Twilio can be synced with GET /api/templates/twilio-sync and linked via POST /api/templates/twilio-import with { contentSid, variableMapping }.

Rejection reasons

CodeFix
PROMOTIONAL_CONTENTRemove offers; reframe as service message (attendance, access, schedule).
MISSING_VARIABLEAdd {{1}} guest personalisation.
CALL_TO_ACTION_MISMATCHUse utility CTA: Confirm attendance, Download access card.
INVALID_FORMATVariables must start at {{1}} and increment.

Content Template Status Callback

In Twilio console → WhatsApp Senders → your sender → set Content Template Status Callback URL to:

https://relay.maisondoclar.com/api/webhooks/twilio/content-approval

Relay updates template approval status and sends operator alerts on approve/reject.

Preflight

POST /api/campaigns/:id/preflight resolves the audience with duplicate-number handling (first / all / flag), household dedup (primary_only / all_members), optional Termii DND check for +234 numbers, cost estimate, and Twilio balance snapshot.

Resend targeting

  • failed — original-round failed sends
  • undelivered_24h — still sent after 24 hours
  • Optional channel override for alternate-channel resend (SMS fallback, etc.)

Archive

POST /api/campaigns/:id/archive on completed/cancelled campaigns aggregates metrics into archiveMetricsSnapshot, deletes individual campaign_sends rows, and sets archivedAt. Use when operational detail is no longer needed but summary metrics must be preserved.

Daily maintenance (01:00 UTC) prunes old records. Defaults:

DataDefault retention
Delivered campaign sends90 days
Failed/suppressed/skipped sends30 days
Read guest message history7 days
Info system events14 days
Gateway event log30 days

Configure via GET/PATCH /api/system/retention-config. Campaign archive is manual and preserves metrics independently of automatic retention.

Message templates define copy per channel and message type. When editing an existing template, the Performance section loads GET /api/templates/:id/analytics: campaign count, total sends, delivery and failure rates, top failure reasons (up to three), and last-used date.

HTML email templates

Email templates support Plain text or HTML mode. HTML body is stored in bodyHtml with isHtmlTemplate; plain bodyTemplate is sent as the text fallback via Resend. Use Preview / Mobile preview (sandboxed iframe) before saving.

SMS templates

SMS editor shows live character and segment counts (GSM-7: 160/153; Unicode: 70/67). Colour indicates cost risk: green (1 segment), amber (2), red (3+). Include opt-out copy such as Reply STOP to unsubscribe. smsCharCount and smsSegmentCount are saved on the template record.

Email campaigns use Resend. Templates support plain text always; optional HTML mode stores bodyHtml with isHtmlTemplate and sends HTML + text fallback.

Editor: toggle Plain/HTML, preview in sandboxed iframe, mobile width preview. Dispatch uses Resend with both html and text parts when configured.

Delivery status may update via POST /api/webhooks/resend when RESEND_WEBHOOK_SECRET is set.

SMS templates show a live character and segment counter (GSM-7: 160/153; Unicode: 70/67). Colour: 1 segment green, 2 amber, 3+ red.

On save, smsCharCount and smsSegmentCount persist on the template. Include opt-out language (e.g. Reply STOP to unsubscribe) for compliance.

Dispatch via Termii using TERMII_API_KEY and TERMII_SENDER_ID. Nigerian DND registry is checked when preflight DND option is enabled.

In the campaign create/edit form, use AI Send Time next to the send window fields. This calls GET /api/ai/send-time?eventId= and suggests a start–end window in WAT (West Africa Time) based on recent delivery histograms for that event, optionally refined by AI when the SEND_TIME_OPTIMISATION flag is enabled.

Click Apply to populate the send window start and end inputs. Send-window rules still apply at dispatch time; jobs outside the window are deferred with lastError: Deferred: outside send window.

Audience filters

Campaigns target guests via a saved segment or an inline filterDefinition JSON object. Filters can include RSVP status, source, tags, channel eligibility (valid phone/email), table assignment, and custom fields. The audience resolver runs at prepare time and materialises campaign_sends rows.

Suppression lists

Suppressed guests are excluded from sends. Reasons stored on suppression_lists:

ReasonTypical cause
opted_out_allGuest opted out of all channels (preference centre or admin).
opted_out_channelSTOP keyword or channel-specific opt-out (e.g. WhatsApp).
admin_suppressedOperator manually suppressed the guest.
duplicateDuplicate contact detected during import.
invalid_contactPhone/email failed validation or provider unreachable codes.

Household grouping

Guests sharing a householdId are treated as one unit for WhatsApp and SMS deduplication. Only the primary household contact receives the message; other members are skipped with household_dedup.

Cross-campaign 24-hour deduplication

Before dispatch, Relay checks whether the guest already has a delivered send on the same channel within the last 24 hours (any campaign). If so, the current send is marked skipped automatically. This prevents message fatigue across concurrent or back-to-back campaigns.

To investigate: query campaign_sends for the guest, channel, and recent sentAt timestamps.

Personalised images and videos are rendered server-side with FFmpeg drawtext (same approach as md-assets). Cloudinary pre-warm has been removed — assets are written to Backblaze B2 before the campaign enters sending.

Overlay presets

Presets define text overlay behaviour:

  • ZoneshorizontalZone (left/center/right), verticalZone (upper/middle/lower), pixel offsets
  • ColourcolorHex (default gold B79F85)
  • Auto-size rules — array of { max_chars, font_size } matched to guest display name length
  • Video timing — optional videoOverlayStartS, videoOverlayDurationS

Font file: /app/fonts/InstrumentSans-SemiBold.ttf (bundled in Docker image).

Manage presets via GET/POST /api/assets/overlays.

B2 storage path

relay/{eventId}/campaigns/{campaignId}/{guestId}.{ext}

Public URL format: https://f005.backblazeb2.com/file/md-media-assets/{key}

Pre-render before sending

During campaign prepare, the asset worker downloads source media from B2, applies drawtext per guest, uploads the rendered file, and attaches URLs to send rows. Failures surface in prepare logs before any Twilio/Resend dispatch.

Assets service integration

When the Assets service completes a personalised video render job, it automatically notifies Relay at POST /api/campaigns/asset-job-complete. Relay updates guest_assets and campaign_sends with guest video URLs. The operator does not need to manually transfer URLs between apps. Requires ASSETS_API_KEY (or default Bearer internal) on the Assets side.

Per-guest preview

GET /api/campaigns/:id/preview/:guestId

Returns the resolved message body and asset URLs for one guest — use to verify placeholders and overlay output before launch. Campaign-level summary: GET /api/campaigns/:id/preview.

Inbound WhatsApp messages arrive at POST /api/webhooks/twilio (signature-validated, rate-limited to 60 requests/minute per IP). Relay matches From to guests.phoneE164, processes keywords, logs every message, and optionally writebacks opt-outs to the Gateway.

Keyword handling

KeywordAction
STOPOpt out of WhatsApp; add opted_out_channel suppression; Gateway delivery writeback (OPT_OUT); confirmation message sent.
STARTRemove WhatsApp from optedOutChannels; resubscribe confirmation sent.
HELPSend help text (STOP/START instructions).
YESConfirm RSVP (blocked after rsvpCloseDate); household pending members updated to yes; custom confirm message from event settings.
NODecline RSVP (same close-date rules); household pending members updated to no; change-from-yes acknowledgement.
DIETARY: …Store text after DIETARY: in guests.dietaryNotes; confirmation echoes the note.

Configure RSVP close date and custom messages via sidebar ⚙ Event settingsPATCH /api/events/:id/rsvp-settings. Daily send budget and max sends per guest are enforced in the send worker.

Inbound log (admin UI)

The Inbound nav section lists messages via GET /api/events/:eventId/inbound-messages with channel/keyword filters, colour-coded actionTaken badges, and pagination.

Conversation window

Each inbound message sets lastInboundAt and conversationWindowOpen (24-hour session). Guest detail shows window status. STOP closes the window.

Non-keyword messages

Messages that do not match a keyword are stored in inbound_messages with keywordDetected: null and passed to AI intent classification (classifyInboundMessage) for operator review and future automation.

Operators generate a link with POST /api/guest-portal/link (guestId, eventId). The URL is {HOST_DASHBOARD_BASE_URL}/guest/{token} (72-hour JWT).

Guests can confirm or decline RSVP (unless rsvpCloseDate has passed), save dietary notes, and view their access card image when available.

Public API: POST /api/guest-portal/rsvp and POST /api/guest-portal/dietary with the token in the body.

Gateway webhooks checkin.created set guests.attended = true. attendance.reconciled updates attendance on guest records and writes attendanceOutcome on matching send records.

The Guests view shows check-in stats from GET /api/events/:eventId/checkin-stats: totals, no-show, walk-in, rate, and recent check-ins (refreshes every 30 seconds).

When a campaign completes, Relay can generate PDF reports with metrics charts and an AI narrative summary.

Report types

TypeAudienceContent
internalOperatorsFull delivery metrics, failures, channel breakdown, suppression stats.
hostEvent hostsCurated, host-safe view — high-level outcomes without internal diagnostics.

PDF storage

PDFs upload to B2 at reports/{campaignId}-{timestamp}.pdf. Generate via POST /api/reports/generate (BullMQ report-generate queue).

Magic links

Share reports with hosts without dashboard login:

  • POST /api/reports/:id/magic-link — create link (30-day expiry)
  • POST /api/reports/:id/resend-link — new link with 72-hour expiry; revokes prior token
  • DELETE /api/reports/:id/magic-link — revoke

The Reports UI shows link status (active with expiry, expired, or never sent). Resend Link opens a modal to copy the URL or open WhatsApp with a pre-filled message to the host.

Scheduled delivery

In Event settings, enable Auto-generate host report: set hours after event end (default 24) and delivery time in WAT (default 09:00). Saved via PATCH /api/events/:id/rsvp-settings (reportScheduleTime, reportScheduleHoursAfterEvent).

A daily job at 08:00 WAT checks completed campaigns; if the host report has no active magic link, generation is queued automatically.

Tally webhook

Tally form submissions POST to Relay's Tally webhook endpoint. Requests are verified with TALLY_SIGNING_SECRET (HMAC). Mapped fields update guest records per the form configuration.

Form tokens

Prefilled Tally links use signed tokens with configurable tokenExpiryHours (default 168). Tokens can embed guest name, phone, and guest ID as visible or hidden fields.

Field mapping

Each form_config stores fieldMapping — a JSON object mapping Tally field keys to Relay guest columns (e.g. RSVP status, dietary notes, plus-ones).

Duplicate submission handling

onDuplicate policy per form:

  • overwrite — latest submission wins
  • reject — ignore duplicate
  • flag_for_review — queue for operator review

Host dashboards give event hosts a read-only, branded view of campaign metrics without operator credentials.

Magic link access

Operators generate a JWT magic link per dashboard:

POST /api/dashboards/:id/magic-link

URL format: {HOST_DASHBOARD_BASE_URL}/host/{token}

Token hash is stored in host_dashboard_tokens for revocation auditing.

Module configuration

Each dashboard record defines which modules are visible (e.g. RSVP summary, send stats, recent inbound). Configure modules in the admin UI before issuing links.

Token expiry and revocation

  • Expiry follows tokenExpiryDays on the dashboard (JWT exp claim).
  • Revoke by deleting the token record or rotating dashboard settings; expired links show "Link Unavailable".
  • Re-issue a fresh magic link from the dashboard admin screen.

The admin UI Env Health section (and GET /api/system/env-health) reports configuration readiness without exposing secret values.

StatusMeaning
okVariable is set and does not match placeholder heuristics
placeholderValue looks like a template (contains your-, changeme, example, etc.) — replace before production
missingVariable is unset

Health score 0–100 = percentage of variables in ok state. Checked variables include database, JWT secrets, Redis, Twilio, B2, Resend, Anthropic, superadmin passwords, Assets URL, and Gateway credentials.

System → WhatsApp Sender Quality calls GET /api/twilio/sender-quality (Twilio Conversations API).

Displays quality rating, numeric score, status, and recommendation. Score colours: green ≥80, gold ≥50, red below.

Conversation windows: inbound messages open a 24h session (conversationWindowOpen) for session-message sends. Template messages required outside the window.

Send budget: per-event dailySendBudget and maxSendsPerGuest in Event settings reduce spam risk.

Dedup: 24h cross-campaign delivery dedup and household dedup protect quality rating and user experience.

The System nav section includes:

  • Test contacts — three numbers for mandatory test sends before launch
  • Queue management — depths, drain failed jobs, requeue/recover by campaign ID
  • Integration info — webhook URLs, Gateway status
  • WhatsApp sender check — basic number lookup
  • Sender quality — see WhatsApp Business Quality
  • System events — severity filter on system_events
  • Audit log — see Audit Log

Env Health is a dedicated nav item: GET /api/system/env-health with 0–100 score.

Configuration is validated at startup in src/lib/config.ts. Missing required variables prevent boot.

Required

VariablePurpose
DATABASE_URLPostgreSQL connection string
JWT_SECRETOperator session JWT signing
HOST_DASHBOARD_JWT_SECRETHost portal and report magic-link JWT signing
HOST_DASHBOARD_BASE_URLPublic base URL for /host/{token} links
REDIS_URLBullMQ campaign, asset, report, and status workers
TWILIO_ACCOUNT_SIDTwilio account
TWILIO_AUTH_TOKENTwilio auth + webhook signature validation
TWILIO_WHATSAPP_FROMWhatsApp-enabled sender (e.g. whatsapp:+44…)
B2_KEY_IDBackblaze application key ID
B2_APPLICATION_KEYBackblaze application key secret
B2_BUCKET_NAMETarget bucket (md-media-assets)
B2_ENDPOINTS3-compatible endpoint host
RESEND_API_KEYResend email API
RESEND_FROM_EMAILDefault From address
RESEND_WEBHOOK_SECRETResend webhook signature verification
APP_BASE_URLPublic Relay URL (webhooks, CORS, Twilio callback URL)
ANTHROPIC_API_KEYAI features (inbound intent, narratives, suggestions)

Optional

VariablePurpose
TWILIO_MESSAGING_SERVICE_SIDTwilio Messaging Service for SMS
SMS_FROM_NUMBERSMS sender when not using messaging service
ALLOWED_ORIGINSCORS allowlist (comma-separated; falls back to APP_BASE_URL)
ADMIN_ALERT_EMAILOperator email for campaign-complete alerts
GATEWAY_BASE_URLGateway API for guest sync and writeback
GATEWAY_API_KEYGateway bearer token
GATEWAY_WEBHOOK_SECRETHMAC for inbound Gateway webhooks
AXIOM_API_KEYStructured logging to Axiom
AXIOM_DATASETAxiom dataset name
TALLY_SIGNING_SECRETTally webhook HMAC (required when forms enabled)
PORTHTTP listen port (default 3000)
SUPERADMIN_1_PASSWORDBootstrap operator password (seed script)
SUPERADMIN_2_PASSWORDSecond bootstrap operator password
ASSETS_BASE_URLAssets API base (default: https://assets-api-production-f80c.up.railway.app)
ASSETS_API_KEYBearer token for Assets API (access card lookup, asset-job-complete; may match Gateway key)
IssueFix
Campaign stuck in sending Check Redis is running and REDIS_URL includes valid credentials. Inspect BullMQ worker logs for stalled jobs. Recover via POST /api/system/queues/campaign/:campaignId/recover or System → Queue Management in the admin UI.
WhatsApp sends not delivered Check template approval: GET /api/twilio/content-templates/:sid/approval-status. Status must be approved before campaigns can send. Verify twilioContentSid on the template and Twilio console for error 63007 (invalid/unapproved template).
FFmpeg overlay produces no text Confirm font exists at /app/fonts/InstrumentSans-SemiBold.ttf. Check Dockerfile build logs for font download. Review asset worker logs for FFmpeg drawtext filter errors.
B2 upload fails Verify B2_KEY_ID, B2_APPLICATION_KEY, B2_BUCKET_NAME, and B2_ENDPOINT are set. Confirm the bucket is Public in the Backblaze console so Twilio can fetch media URLs.
Inbound STOP not suppressing guest Guest phoneE164 must exactly match the inbound number in E.164 format (Twilio sends whatsapp:+…, normalised on match). Check phone normalisation on import and that the guest belongs to the correct event.
Cross-campaign dedup blocking sends Expected behaviour: guest received a delivered message on that channel within 24 hours. Inspect campaign_sends for the recent delivery. Wait for the window to elapse or adjust campaign timing/channels if intentional re-contact is required.
Test send gate blocks launch Complete test sends for all three test contacts (testSendStatus must not be none). Configure contacts under System → Test Contacts.
Preflight shows WhatsApp not approved Finish Twilio Content API workflow (create → submit → poll until approved). See WhatsApp Templates.
Asset unreachable but send still goes out Expected graceful degradation: HEAD check fails → message sends without media, assetDegraded: true on send. Fix B2 public URL or regenerate assets.
Campaign paused after asset pre-warm >90% guest asset generation failed. Review system events asset.campaign_paused. Fix master/overlay config in Assets, then resume campaign.
Report magic link expired Use Reports → Resend Link (POST /api/reports/:id/resend-link, 72h). Or create link via Copy Link (30-day default on first create).
Scheduled host report not generated Enable report schedule in Event settings (reportScheduleTime, hours after event). Event date must be past; job runs 08:00 WAT. Host report skipped if active magic link already exists.
401 on admin API Re-login. Verify JWT_SECRET unchanged. Check token blocklist if password was rotated.
Env health score low Open Env Health nav. Replace placeholder values (changeme, your-*, example). All required vars in SYSTEM_PASSPORT must be set in Railway.
Access card image missing on WhatsApp Generate cards in Assets. Ensure ASSETS_BASE_URL and ASSETS_API_KEY set. Relay fetches GET .../api/assets/cards/{nucleusEventId}/{nucleusGuestId}.
AI audience query fails Check ANTHROPIC_API_KEY and AI feature flag in AI Settings. Review rate limits in logs.