Documentation & System Encyclopedia
Comprehensive technical guide, UI element directory, and shared-hosting optimization manual for MarbleSend.
System Architecture & Hosting Foundation
Philosophy: Self-hosted, single-tenant, zero-build, shared hosting friendly
MarbleSend is built from the ground up to solve a notorious pain point in email marketing software: the need for expensive dedicated VPS instances, Redis servers, node build pipelines, or complex Docker orchestration. MarbleSend runs on standard shared hosting (cPanel, Hostinger, DirectAdmin, Plesk, LiteSpeed) using pure PHP 8.2+ with zero server-side compilation.
Zero SQL Injection
Every database query uses strict prepared statements without ORM overhead.
At-Rest Encryption
SMTP credentials, AI API keys, and webhook secrets encrypted in Crypto.php.
No Double-Sends
Worker claims queue batches atomically via MySQL status updates with strict deadline handling.
Instant Deployment
Pre-compiled Tailwind CSS & vendored Alpine.js. Upload zip, run installer, done.
0.1 Zero-Build Hosting & Runtime Philosophy
Most modern web applications demand Node.js build pipelines, Redis daemon servers, Docker orchestration, or root VPS access. MarbleSend rejects this complexity. Designed specifically for accessible shared hosting (cPanel, Hostinger, Plesk, LiteSpeed), it runs on pure PHP 8.2+ with zero server-side compilation.
Zero Server Compilation
No npm, Webpack, Vite, or Composer required on the production server. Tailwind CSS is pre-compiled into static assets, Alpine.js is vendored locally, and PHPMailer 6.9.x is committed directly in vendor-lite.
Low Memory Footprint
Peak memory usage remains under 32MB even when processing email queue batches. Runs smoothly within strict shared hosting memory limits (64MB-128MB php.ini allocations).
No Forbidden Functions
Strictly avoids dangerous functions commonly disabled by shared hosting providers (exec, shell_exec, system, passthru, eval, proc_close). All external operations use native cURL with timeouts.
0.2 Security Perimeter & Directory Isolation
MarbleSend implements a strict two-tier directory layout that separates public web assets from sensitive application code and storage, eliminating directory traversal and file exposure risks.
Isolated Document Root (public/)
Only the public/ directory is exposed to the web server. Point your domain or subdomain DocumentRoot to /public. Even if directory listing is enabled in Apache/Nginx, core source files cannot be browsed.
Protected Application Core (app/)
Controllers, services, libraries, views, and configuration live outside document root in app/. Database credentials in app/config/config.php and encryption keys are unreachable via direct HTTP requests.
Secure Storage & Migrations
The storage/ directory (containing worker logs) and migrations/ (containing raw SQL schema files) reside completely outside public/, preventing unauthorized downloads of system logs or database structures.
0.3 Zero-ORM Database & PDO Prepared Statements
Object-Relational Mappers (ORMs) like Eloquent or Doctrine introduce massive memory overhead, complex hydration layers, and unpredictable N+1 query patterns. MarbleSend utilizes direct native PHP PDO with 100% prepared statements.
100% Prepared Statements
Every single database query binds parameters explicitly (PDO::PARAM_STR, PDO::PARAM_INT). SQL query structures and user data never mix, providing mathematical immunity against SQL injection attacks.
Predictable High-Performance Queries
Direct SQL execution eliminates ORM object hydration overhead, executing list operations and batch queue updates up to 5x faster with minimal CPU consumption on shared database servers.
Idempotent Schema Migrations
Database versions are tracked via numbered SQL files (0001_initial.sql through 0019_add_feedback_attachment.sql) executed by an idempotent runner with IF NOT EXISTS guards and recorded in cm_migrations.
Single-Tenant Data Isolation
Designed as single-tenant standalone software without tenant_id columns. Indexes remain compact, sequential scans are avoided, and there is zero risk of cross-tenant data leaks.
0.4 At-Rest Encryption & Cryptographic Standards
Sensitive secrets are never stored in plain text. MarbleSend encrypts credentials using industry-standard authenticated symmetric cryptography through App\Lib\Crypto.
AES-256-GCM Encryption
Uses Advanced Encryption Standard in Galois/Counter Mode (AES-256-GCM), providing both confidentiality and cryptographic integrity verification to detect any tampering.
Unique 96-bit Random IV
Every encrypted value generates a cryptographically secure 12-byte (96-bit) random Initialization Vector (IV) via random_bytes(), ensuring identical plaintexts produce entirely distinct ciphertexts.
Encrypted Secret Catalog
Automatically protects SMTP relay passwords, BYOK AI API keys (OpenAI, Claude, Gemini, DeepSeek, Kimi, OpenRouter), Webhook signing secrets, and Envato Purchase Code.
Master Key Isolation
The 32-byte ENCRYPTION_KEY is generated during installation and stored exclusively in app/config/config.php (chmod 0600). The database never contains the encryption key.
0.5 Dual-Worker Engine & Concurrency Safety
MarbleSend delegates all time-intensive and scheduled tasks to a unified background worker runner, ensuring the admin dashboard remains fast and responsive at all times.
Single Master Background Runner
worker.php coordinates queue batch sending, recurring schedule execution, RSS feed post polling, autoresponder sequence triggering, and unopened campaign resends.
CLI Cron & Web-Cron Flexibility
Run via native system crontab (* * * * * php /path/to/worker.php) or via secure external HTTP ping to /cron.php?key=YOUR_KEY. Both pathways execute identical underlying service logic.
Atomic Queue Claiming (No Double-Sends)
Worker instances claim queue batches using atomic SQL status updates (UPDATE cm_email_queue SET status = 'processing' WHERE status = 'pending' LIMIT N). Even concurrent runs never send duplicate emails.
Heartbeat & Stale Job Recovery
Tracks execution heartbeat in cm_settings. If a worker process is interrupted mid-send (e.g. host timeout), subsequent worker runs automatically detect and recover stalled queue items.
0.6 Server Requirements & Hosting Compatibility Matrix
MarbleSend is verified compatible with all standard shared hosting environments, control panels, and server configurations meeting baseline PHP 8.2+ requirements.
| Component | Minimum Requirement | Recommended | Operational Notes |
|---|---|---|---|
| PHP Runtime | PHP 8.2.0+ | PHP 8.2 or 8.3 | strict_types=1 declared across all application files. |
| Database Engine | MySQL 8.0+ / MariaDB 10.6+ | MariaDB 10.11+ or MySQL 8.0+ | InnoDB engine required. UTF8MB4 full Unicode charset. |
| Required Extensions | pdo_mysql, openssl, mbstring, json, curl, simplexml, sodium | All standard core extensions enabled | Standard on 99% of shared hosting. Optional: intl for locale formatting. |
| Web Server | Apache, Nginx, LiteSpeed, Caddy | Apache + mod_rewrite or LiteSpeed | Pre-configured .htaccess files included in root and public/. |
| Memory Allocation | 32MB memory_limit | 64MB - 128MB | Queue chunks processed in conservative batches to prevent out-of-memory. |
| Cron Scheduler | CLI crontab OR Web-Cron HTTP ping | CLI cron every 1 minute | Web-Cron fallback endpoint (/cron.php?key=...) for hosts without CLI access. |
Tested Hosting Control Panels
Fully tested and verified on cPanel, Hostinger hPanel, Plesk, DirectAdmin, CyberPanel, AApanel, CloudPanel, and vanilla Ubuntu/Debian LAMP/LEMP stacks.
Dashboard Overview (/dashboard)
Aggregated mission control for audience health, delivery KPIs, worker heartbeat, and schedule automation.
Dashboard Component Anatomy & Support Inspector
Hover or click any element below for backend detailsTotal Sent
Logs aggregation & Monthly counterOpened / Open Rate
1x1 tracking pixel & Apple MPP noteClicked / CTR
Click-through redirect injectorUnsubscribed Audience
Opt-out rate & RFC complianceWorker Heartbeat
Cron status & 1-min intervalVolume Growth Chart
7d, 30d, 90d, YTD scaleRecent Broadcasts Table
Status lifecycle & audit trailDeliverability Health
SMTP profile readiness1.1 Email Performance KPI Cards
| Element | Calculation & Source | Visual Meaning | Support Prevention Rationale |
|---|---|---|---|
| Total Sent Card | COUNT(cm_email_logs) + SUM(cm_campaigns.sent_count) | Displays total lifetime broadcast and schedule emails dispatched. Subtext displays volume sent this calendar month. | Prevents confusion when older log entries are pruned; historical campaign counters ensure lifetime totals never drop. |
| Total Opened / Open Rate | COUNT(DISTINCT contact_id WHERE opened_at IS NOT NULL) / Total Sent * 100 | Measures engagement via transparent 1x1 tracking pixel injected into HTML email footer. | Apple MPP Disclaimer: iOS Mail Privacy Protection pre-fetches images via Apple proxy, which can artificially raise opens. CTR is the true business metric. |
| Total Clicked / CTR | COUNT(DISTINCT contact_id WHERE clicked_at IS NOT NULL) / Total Sent * 100 | Measures unique subscriber link clicks. Rewritten through /track/click.php. | Unsubscribe links and confirmation links are excluded from campaign CTR to preserve statistical integrity. |
| Unsubscribed Card | COUNT(status = 'unsubscribed') / Total Contacts * 100 | Rate of recipient list opting out via the mandatory one-click unsubscribe mechanism. | Healthy threshold is < 0.5%. Unsubscribe rate > 1% indicates audience fatigue, purchased lists, or low relevance. |
Total Sent & Monthly Throughput
Tracks total lifetime emails successfully dispatched across broadcasts, recurring schedules, RSS feeds, and autoresponders. Subtext highlights volume dispatched in the current calendar month. Because MarbleSend aggregates historical campaign counters alongside active email delivery logs, lifetime metrics remain permanent and accurate even when older log rows are pruned for database maintenance.
Unique Opened & Apple MPP Dynamics
Recorded via a transparent 1x1 tracking pixel injected into HTML email footers. Opens are tracked uniquely per contact per campaign (first-touch timestamping). Crucial deliverability note: Apple Mail Privacy Protection (MPP) caches images via proxy servers, which can artificially inflate open counts. While Open Rate remains a valuable engagement trend indicator, Click-Through Rate (CTR) represents verifiable subscriber conversion intent.
Unique Clicked & URL Rewrite Engine
Every standard hyperlink in your email body is rewritten to route through /track/click.php, logging the contact ID, campaign ID, destination URL, and timestamp before redirecting the subscriber. To safeguard subscriber trust and legal compliance, unsubscribe links (/unsubscribe.php) and subscription confirmation links (/confirm.php) are strictly exempt from tracking redirects.
Unsubscribed Contacts & Domain Protection
Measures subscribers who opted out using either the mandatory email body unsubscribe link or the RFC 8058 one-click List-Unsubscribe header. A healthy list maintains an unsubscribe rate below 0.5%. When rates exceed 1.0%, MarbleSend warns operators to review list quality and sending cadence to prevent mailbox providers (Gmail, Yahoo, Outlook) from downgrading domain reputation.
1.2 Operations & Worker Health Heartbeat
MarbleSend operates on a single master background runner: worker.php. The dashboard inspects cm_settings['worker_last_run'] to determine if cron is firing every 60 seconds.
Active State (Cron Healthy)
When worker.php runs within the last 3 minutes, the badge displays Active. Queue batches are processed in real time, scheduled newsletters trigger on time, and autoresponders fire promptly.
Inactive State (Cron Not Detected)
If no heartbeat is recorded for > 3 minutes, the badge turns red. Clicking the card immediately navigates to Settings → System with the exact crontab CLI command and web-cron fallback URL.
Active Contacts vs Total Audience
Displays contacts in active status against the total subscriber base. MarbleSend enforces a strict 4-state lifecycle machine: active, pending (awaiting double opt-in confirmation), unsubscribed, and suppressed (bounced/complaint). Outbound campaign workers strictly filter WHERE status = 'active', ensuring unconfirmed or suppressed emails are never dispatched.
Monthly & Lifetime Campaign Volume
Summarizes broadcasts dispatched in the active calendar month alongside total lifetime campaigns. Provides immediate visibility into monthly email marketing cadence and operational output.
Next Upcoming Schedule Resolution
Queries active recurring schedules (ordered by next_run_at ASC LIMIT 1) and displays the next queued newsletter and countdown. All execution times are computed strictly by SchedulerService::computeNextRun() with full timezone awareness and stored in UTC, eliminating daylight saving time (DST) drift.
Worker Heartbeat & Shared Hosting Cron
The system heartbeat verifies whether worker.php has executed within the last 180 seconds. In shared hosting environments (cPanel, Hostinger), configure crontab to run * * * * * php /path/to/worker.php >/dev/null 2>&1. If CLI cron is unavailable, MarbleSend provides a secure Web-Cron fallback endpoint (/cron.php?key=YOUR_KEY) compatible with free external pingers like cron-job.org.
1.3 Campaign Growth Visualizer
The growth visualizer renders volume distribution across four selectable timeframes: 7 Days, 30 Days, 90 Days, and Year-to-Date (YTD). Each bar represents an aggregated time bucket, displaying sent count and open count on hover.
Allows instant switching between short-term weekly cadence (7 Days), monthly cycles (30 Days), quarterly performance (90 Days), and annual delivery volume (Year-to-Date).
Rendered using pure CSS flexbox heights calculated relative to peak sending volume. Eliminates heavyweight JavaScript charting libraries, loading instantly on mobile devices and low-spec hosting.
Hovering over any bar reveals the exact time interval, total messages dispatched, and unique open volume, allowing rapid comparison of engagement over time.
Live Sending & Heartbeat Activity Feed
The right-column feed surfaces recent campaign dispatches in real time, featuring animated status badges (Completed, Sending pulse, Failed, Draft), delivery ratios, and a persistent system heartbeat status card.
status = 'completed'
status = 'sending'
status = 'failed'
status = 'draft'
Campaign History & Delivery Audit Table
The bottom audit table lists recent broadcast executions with full metadata: campaign name, lifecycle status, origin tag (manual, recurring schedule, RSS feed, or unopened resend), recipient dispatch ratio, and verified open rate.
Shows campaign subject, creation date, and origin tag (manual, recurring, rss, or resend_unopened).
Instant count of delivered vs. failed messages, plus live open rate and click rate percentages.
Direct shortcut to the Campaign Report page for recipient-level event logs and the Unopened Resend modal.
1.6 Quick Action Shortcuts & Workflow Automation
Direct action triggers linking the overview dashboard to high-velocity workflows: automated recurring newsletter schedules, RSS-to-email digest pipelines, subscriber list acquisition, double opt-in verification, and suppression protection.
Automate Flow (Recurring & RSS)
Launches automated email dispatches on fixed cron schedules or whenever a connected WordPress/Ghost/Substack RSS feed publishes new content, entirely hands-free.
Audience & Subscriber Management
Enables rapid subscriber imports via CSV, segmented list grouping, double opt-in compliance enforcement, and automatic suppression of bounced or unsubscribed addresses.
Campaign Manager & Reports (/campaigns)
High-speed broadcast composer, tag-targeted recipient selector, preflight safety engine, and in-depth delivery reports.
Interactive Campaigns UI Inspector
Click any component to inspect schema impact and operational tipsLive Dispatch Monitor
Real-time stats & eventsRecipient Selector
Tag filtering & select-allTriple-View Editor
Code, split & previewPreflight Check Engine
6-point automated inspectionTest Send Diagnostic
Zero-queue live sampleUTM & Link Rewriter
UTM params & 302 click trackBroadcast History
Status & origin filtersPost-Send Deep Report
CTR, MPP & auto-resend2.1 Live Campaign Monitor Banner
Whenever a broadcast is actively dispatching, a sticky monitor banner slides into view at the top of /campaigns. It polls /api/campaigns/:id/progress every 1.5 seconds without full page refreshes.
Summer Launch Newsletter
Sending74% complete
7,420 / 10,000 emails
Queued
2,528
Processing
40
Success
7,420
Failed
12
Recent Events Real-time status
1.5-Second Polling Engine
Queries /api/campaigns/:id/progress asynchronously every 1.5 seconds, displaying live throughput without browser page reloads.
Recent Delivery Events Stream
Live event logger streaming the last 5 recipient status changes with real-time delivery confirmations and SMTP error diagnostics.
If background worker.php is delayed or cron is inactive, an alert badge appears with one-click troubleshooting instructions.
2.2 Recipient Selector & Tag Filters (Left Column)
The left column allows laser-focused subscriber targeting. Admins can filter by custom contact tags, search by partial email/name, and select individual contacts or hit Select All Matching.
State Machine Filtering Guarantee:
Regardless of selection or search, the query engine strictly enforces status = 'active'. Unsubscribed, pending, or suppressed contacts are automatically excluded from the queue.
Multi-Tag Intersections
Select specific subscriber segments by tags. The recipient selector dynamically computes intersection counts in real time.
Debounced Live Search (300ms)
Type-ahead search filtering contacts by name or email with a 300ms debounce to prevent database spikes on high-volume lists.
Master Selection Controls
Select individual contacts or use the Select All toggle to target all contacts matching the active filter with a single click.
Groups
Contacts are organised into groups (a contact can be in several; every contact is in at least one, General by default). In Select Recipients choose “By group” to send to every active contact in the picked groups — the full group, not just the page you are viewing — or “Pick manually” to choose individual contacts (optionally filtered by one group).
Groups can also be chosen as the audience of a schedule, as the trigger of an autoresponder rule, as the destination of a signup page, and to narrow a resend to non-openers. Deleting a group never deletes its contacts; autoresponder rules that depend on it are disabled.
2.3 Campaign Composer & Split Editor Modes
Split Mode
Simultaneous code editing on the left and live responsive iframe preview on the right.
Code Only Mode
Full-width distraction-free HTML code editor with gutter line numbers and syntax tabs.
Live Preview Mode
Sandboxed rendering with Desktop (100%), Tablet (600px), and Mobile (375px) device viewport toggles.
Placeholder Variables & Substitution Engine
Personalize emails dynamically using {{name}}, {{email}}, {{unsubscribe_link}}, {{offer_url}}, and {{track_pixel}} tags.
Sandboxed Responsive Viewports
Test responsive mobile (375px), tablet (600px), and desktop (100%) rendering inside an isolated iframe sandbox before launch.
2.4 The 6-Point Automated Preflight Safety Check
To prevent catastrophic mistakes (e.g. sending emails with broken placeholders, missing unsubscribe links, or spam triggers), MarbleSend requires a preflight check before launching.
| Safety Check | Severity | Rule & Detection | Consequence if Failed |
|---|---|---|---|
| 1. Unsubscribe Link | CRITICAL | Checks for presence of {{unsubscribe_link}} token. | Sending is BLOCKED. Mandatory under CAN-SPAM, GDPR, and 2024 Yahoo/Google Sender Requirements. |
| 2. Placeholders Validity | CRITICAL | Detects unknown placeholder tags (e.g. typos like {{first_nam}}). | Sending blocked until unknown tags are corrected or removed. |
| 3. Recipient Audience | CRITICAL | Validates that > 0 active recipients are selected. | Cannot broadcast to empty selection. |
| 4. SMTP Connectivity | CRITICAL | Verifies selected SMTP profile host and credentials. | Prevents queue stalls caused by bad relay credentials. |
| 5. Spam Trigger Words | WARNING | Scans subject and body for spam trigger phrases ("100% free", "act now", "limited time"). | Warning banner shown; admin can review and proceed. |
| 6. Subject Line Length | INFO | Recommends 30–60 characters for optimal mobile subject truncation. | Informational notice for higher open rates. |
Preflight Verification Gate
Evaluates 6 automated safety rules before queue commit. Any CRITICAL check failure disables the launch button.
2.5 Test Send Diagnostic & Confirmation Modal
Before committing thousands of emails to the queue, the Send Test Email button dispatches an instant single test message directly to the administrator or QA mailbox without touching the database recipient list. All placeholder tags ({{first_name}}, {{email}}, {{unsubscribe_link}}) are populated with mock preview values so you can inspect email rendering across Gmail, Apple Mail, and Outlook.
Mock Tag Interpolation
Populates subscriber variables with realistic preview values (e.g. Jane Doe, demo unsubscribe link) for authentic visual verification.
Pre-Launch Verification Modal
Two-step modal confirmation displays recipient count, selected sender profile, and active UTM tags before queue creation.
2.6 Automated UTM Tracking Injection
All outbound anchor tags (<a href="...">) are processed through App\Services\TrackingInjector. UTM parameters (utm_source, utm_medium, utm_campaign, utm_content) are appended automatically before click-redirection wrapping. Mailto links, tel links, and unsubscribes are preserved untouched.
Step 1: UTM Query Injection
Appends utm_source, utm_medium, utm_campaign, and utm_content to every hyperlink without altering existing query parameters.
Step 2: Tracking Pixel Insertion
Injects a transparent 1x1 GIF image before the closing </body> tag to record open events upon email client render.
Step 3: 302 Click Rewrite Engine
Rewrites outbound URLs to /track/click.php?h=... for millisecond-latency redirects with first-touch link telemetry logging.
Strict Compliance Exemptions
Unsubscribe links, mailto/tel protocols, and double opt-in confirmation URLs are strictly exempt from UTM and click wrapping.
2.7 Campaign History & Portfolio (/campaigns/history)
The full history portfolio provides multi-attribute filtering across origin (manual, recurring, rss, resend_unopened), status (completed, sending, failed, draft), and date range. Admins can multi-select campaigns to download aggregated CSV delivery logs.
Origin Tracking Pipeline
Categorizes broadcasts by source: Manual composer, Recurring schedules, RSS blog triggers, or Unopened resend clones.
Multi-Status Filtering & CSV Export
Filter archive by completed, sending, failed, and draft states. Download detailed delivery logs with a single click.
One-Click Broadcast Duplication
Clone any historical broadcast back into the composer with its full HTML layout, subject line, and sender settings preserved.
2.8 Analytics Reports & "Resend to Unopened" Feature
Optionally pick groups to resend only to non-openers who belong to them. A resend never goes beyond the original campaign's recipients.
Under /campaigns/:id/report, MarbleSend presents a comprehensive post-campaign report:
4 Bento Performance Metrics
Tracks Sent volume (100%), Delivery failures, Unique opens with open rate, and Unique clicks with CTR.
Hourly Engagement Curve
Displays hourly open clusters across the initial 72 hours to discover optimal subscriber send timeframes.
Targeted Resend to Non-Openers
Clones the campaign targeting subscribers where opened_at IS NULL, allowing new subject lines to recover 15–25% more opens without emailing active readers twice.
Contacts Hub & Growth Tools (/contacts)
Subscriber lifecycle management, CSV importing, GDPR compliance, custom signup pages, and autoresponders.
Interactive Contacts UI Inspector
Click any component to inspect schema impact and operational tips4-State State Machine
active/pending/unsub/suppSearch & Filter Engine
Tags, status & debounceAudit & Consent Drawer
IP, timestamps & bouncesRFC 4180 CSV Importer
50k rows & dedup guardsHosted Lead Capture Pages
Double opt-in & funnelsTag Autoresponders
Event-driven drip rulesBulk Management Hub
Cross-page segmentationGDPR Privacy & Erasure
JSON export & hard delete3.1 The 4-State Subscriber State Machine
MarbleSend maintains a strict, unified state machine for all contacts. There are no side-boolean flags. Every single sending pipeline filters WHERE status = 'active'.
Active Subscriber
Confirmed opt-in, valid email. Eligible to receive campaigns, schedules, and autoresponders.
Double Opt-In Pending
Signed up via double opt-in form, awaiting click on confirmation email. Excluded from broadcasts until confirmed.
Unsubscribed
Clicked unsubscribe link. Never sent to again. Cannot be manually reactivated without explicit re-opt-in.
Hard Bounce / Complaint
Permanently suppressed due to invalid mailbox (5xx SMTP), spam complaint, or manual admin suppression.
| Status | Transition Trigger | Destination State | Campaign Dispatch Impact |
|---|---|---|---|
| active | Subscriber clicks unsubscribe link or RFC 8058 header | unsubscribed | Instantly excluded from all future broadcast and schedule queues. |
| active | 5xx SMTP permanent bounce or 3rd consecutive soft bounce | suppressed | Quarantined permanently to shield domain sending reputation. |
| pending | Subscriber clicks double opt-in confirmation token link | active | Promoted to active; eligible to receive campaigns and autoresponders. |
| suppressed | Administrative unsuppress override with confirmation | active | Reset soft bounce counter; restored to active delivery queues. |
Architecture Guard: Zero Side-Boolean Flags
MarbleSend enforces a single ENUM column (cm_contacts.status). Other platforms frequently suffer from data corruption when boolean flags like is_active, is_unsubscribed, and is_bounced conflict. In MarbleSend, every dispatch query contains WHERE status = 'active' without ambiguity.
3.2 Real-time Filters, Search & Row Actions
The Contacts table features instant Alpine.js reactive filtering by tag pills, subscriber status (active, pending, unsubscribed, suppressed), and free-text debounce search across email and first/last name.
Every contact belongs to at least one group (General by default). Use the Groups filter to narrow the table and Manage Groups to create, rename or delete groups — deleting a group never deletes its contacts.
JSON Tag Storage & Normalization
Tags are stored as normalized JSON arrays in cm_contacts.tags. Tags are case-insensitive, trimmed, and indexed for rapid filtering and segmentation.
Debounced 300ms Instant Search
Search queries filter simultaneously across email address, first name, and last name with a 300ms debounce to minimize unnecessary database overhead.
Server-Side Pagination & Scale
Effortlessly browses lists exceeding 50,000 subscribers using efficient SQL LIMIT and OFFSET queries with low memory utilization.
Update name, custom metadata, and tag chips inline without reloading.
Instantly suppress a contact to immediately disqualify them from all queues.
Permanently purges contact profile, logs, and queue items pursuant to GDPR Article 17.
3.3 Detail Drawer, GDPR Audit Trail & JSON Export
Clicking any contact row slides open the audit drawer. It presents lifetime metrics (total emails received, open rate, click count), opt-in IP address, opt-in timestamp, signup form slug, and a one-click Export GDPR JSON button to comply with Article 15 (Right of Access) and Article 20 (Data Portability).
Opt-in Timestamp & Consent Source
Records the exact date, time, and acquisition channel (Manual, CSV Import, Signup Page slug, or API) for legal verification.
Verified IP Address Capture
Captures subscriber IP address at the moment of signup form submission, providing indispensable proof of consent under GDPR and CAN-SPAM.
Lifetime Engagement Telemetry
Monitors cumulative broadcast delivery volume alongside verified open timestamps to identify active versus dormant subscribers.
Soft Bounce Auto-Escalation
Tracks consecutive temporary delivery failures (X / 3). Upon encountering the 3rd consecutive soft bounce, status automatically transitions to suppressed.
3.4 CSV Importer & Column Mapping
The importer parses CSV files up to 50,000 rows. It auto-detects delimiters (comma, semicolon, tab) and provides visual dropdowns to map columns to: Email, First Name, Last Name, and Tags.
Choose which groups the imported contacts join (General if none). Existing contacts are added to those groups; they are never removed from groups they already belong to.
Memory-Safe Chunked Upload
Streams files up to 50,000 rows in transactional chunks, maintaining memory usage below 32MB even on restrictive shared hosting.
Auto-Delimiter & Intelligent Mapping
Detects commas, semicolons, and tabs automatically, with interactive column mapping for Email, First Name, Last Name, and Tags.
Batch Tagging & Post-Import Audit
Applies a global tag to all imported contacts in the batch and displays an audit report of inserted, updated, and skipped records.
Skip Duplicates
Preserves existing contact details and tags. New rows only are inserted.
Update Empty Fields
Fills in missing names and appends new tags without overwriting existing data.
Overwrite All
Updates names and tags completely to match the incoming CSV file.
3.5 Public Signup Pages (/s/:slug)
Located under Tab 2 of Contacts. Create standalone branded subscription pages with custom headline, description, input labels, and submit buttons. Features include:
Pick the groups that subscribers from this page join (General if none). Groups deleted later drop out automatically.
Monitors aggregate landing page impressions across all active signup funnels.
Calculates submission-to-view percentage across all hosted lead capture forms.
Shows active landing pages relative to the generous 20-page operational capacity.
Tracks new subscriber acquisitions captured during the active rolling 30-day window.
Multi-Channel Embed Integration
Embed forms anywhere using direct hosted URL (/subscribe?slug=...), clean raw HTML embed code, responsive iframe, or JS modal popup.
Generates HMAC-signed confirmation tokens with automated confirmation email dispatch.
Assigns designated subscriber tags upon signup to trigger tailored drip sequences.
Directs new subscribers to your thank-you page or lead magnet URL after opt-in.
3.6 Autoresponders & Drip Sequences
Located under Tab 3 of Contacts. Configure automated welcome emails that are queued as soon as a new subscriber becomes active — right after signup, or after confirming the double opt-in. A rule matches a tag or a group; there is no delay setting. Emails are delivered by the background worker and respect suppression rules automatically.
A rule can fire on a group instead of a tag. Group rules run when a new active contact is created in that group or confirms by email — not when an existing contact is added to the group later (the same as tag rules). Deleting the group disables the rule.
Tag-Based Event Listener
Fires when a new active subscriber carries the rule's tag or belongs to its group — right after a signup page, API call or manual add (single opt-in), or once the double opt-in is confirmed. It does not fire for CSV imports, nor when a tag or group is added to an existing contact later.
Dedicated SMTP Routing
Routes transactional welcome emails through high-reputation dedicated SMTP profiles separate from marketing bulk broadcasts.
Asynchronous Worker Execution
Delegates email rendering and delivery to worker.php, preventing web timeouts and keeping form submissions instant.
Loop Prevention & Dedup Guard
Prevents duplicate rule triggers, ensuring subscribers never receive duplicate welcome emails if tagged repeatedly.
3.7 Bulk Management & List Segmentation (/contacts/list)
Under /contacts/list, manage high-volume lists with batch operations: select across pages, apply or remove tags in bulk, suppress thousands of cold leads simultaneously, or export targeted segments into UTF-8 CSV files.
Add to Group and Remove from Group apply to every selected contact. Removing a contact from its last group puts it back in General.
Bulk Tag Append
Appends tags to all selected contacts in a single query without removing existing subscriber tags.
Bulk Tag Removal
Surgically strips designated tags from selected contacts across multiple lists.
Filtered CSV Export
Streams filtered subscriber subsets directly to RFC 4180 CSV files with full metadata.
Bulk Suppression
Quarantines cold or unengaged contacts immediately to preserve sender reputation scores.
Bulk Unsubscribe
Processes bulk opt-out requests from external sources or offline campaign channels.
GDPR Hard Purge
Permanently removes subscriber records and associated telemetry logs in compliance with GDPR Article 17.
3.8 GDPR Privacy, Audit Export & Deletion Standards
Built-in privacy controls adhering strictly to European Union General Data Protection Regulation (GDPR), California Consumer Privacy Act (CCPA), and CAN-SPAM requirements.
The GDPR export (JSON) also lists the groups the contact belongs to.
Article 15 & 20: Machine-Readable JSON Export
Generates a complete, structured JSON dossier containing subscriber profile details, consent IP, opt-in timestamps, and full delivery logs.
Article 17: Right to Erasure (Hard Delete)
Executing a permanent delete cascades across cm_contacts, cm_email_logs, and cm_email_queue, destroying all personally identifiable data.
Deliverability Unsuppress Guard
Requires explicit administrative confirmation to restore a suppressed address, preventing accidental mailings to confirmed invalid mailboxes.
Templates Studio & AI Copywriter (/templates)
8 Email-safe table layouts, sandboxed iframe preview with miniature CSS reset, sales-hook AI copywriter, and Markdown news posts for newsletters.
Templates Studio UI Inspector
Click any component below to view database query, schema impact, and operational tipsTemplate Library
Instant search & slug keysStarter Layouts
8 table-based email layoutsRaw Code Editor
Tab indent & beautifierSandboxed Viewport
Desktop & mobile frameDynamic Tokens Palette
1-click placeholder injectionTest Email Sender
Live SMTP verification modalAI Copywriter Drawer
Generate & refine templatesSubject Reviewer
1–100 score & spam auditNews Posts
Markdown articles for newsletters4.1 Template Library, Custom Slugs & Metadata Management
Centralized repository for all marketing, transactional, and autoresponder email templates. Features instant client-side search filtering, immutable URL slug IDs, and live draft status indicators.
Immutable Template Slugs
Slug IDs (e.g. #monthly-newsletter) are defined upon creation and permanently locked when saved. This guarantees automated RSS campaigns and trigger sequences never break due to modified identifiers.
Sub-Second Debounced Search
Fast client-side Alpine filter matches across template titles, subject lines, and unique slug IDs without triggering unnecessary database round-trips.
Live Draft State Synchronization
A dedicated dashed indicator card visually distinguishes active in-memory drafts from saved production templates, preventing accidental overwrites.
Independent Campaign Snapshots
Editing or deleting a library template never alters historical broadcasts. Dispatched campaigns store independent static HTML and subject snapshots in cm_campaigns.
4.2 8 Tested Table-Based Responsive Starter Layouts
Modern desktop email engines (notably Microsoft Outlook using Word HTML) do not support CSS Grid or Flexbox. MarbleSend ships with 8 responsive, table-based layouts battle-tested across Gmail, Outlook, Apple Mail, and Yahoo:
Clean canvas with required unsubscribe footer.
Hero banner, value proposition, bold CTA.
Curated editorial, 2-column post highlights.
Header band plus the {{news_posts}} block for News posts schedules.
Personal onboarding, 3-step getting started.
Date/time badge, speaker bio, RSVP button.
Numbered articles, reading time, author tags.
Urgent discount coupon, loss-aversion copy.
Nested Table Architecture & Inlined CSS
Every starter layout is structured using nested <table> elements with explicit pixel constraints, zero cellpadding/cellspacing, and pre-inlined CSS to ensure layout stability across all email clients.
4.3 Raw HTML Code Editor & Clean Formatter
A focused code editor engineered for email developers, featuring tab indent trapping, real-time character count telemetry, and an automated whitespace normalizer.
Tab Key Indentation Trapping
Pressing Tab inserts 2-space indentation instead of shifting browser focus, allowing seamless code structuring directly within the browser.
One-Click Code Beautifier ("Clean")
Normalizes inconsistent indentation, removes redundant carriage returns, and cleans nested table markup without breaking inline style definitions.
Payload Weight & Truncation Guard
Real-time character counter monitors template size. Keeping HTML weight below 102 KB prevents Gmail from clipping your message with a "[Message clipped]" footer.
Zero Visual WYSIWYG Bloat
Direct HTML control prevents unwanted CSS classes, proprietary web fonts, and hidden span tags generated by visual drag-and-drop builders from corrupting email deliverability.
4.4 Sandboxed Multi-Device Responsive Viewports
Simulates desktop and mobile email clients within an isolated iframe sandbox, protecting the dashboard interface from rogue styles while providing instant visual feedback.
Iframe Security & CSS Isolation
The sandbox="allow-same-origin" attribute completely isolates email CSS rules from bleeding into the MarbleSend admin interface, preventing styling conflicts.
Desktop vs 320px Mobile Switcher
Toggle instantly between full desktop container width and a realistic 320px mobile viewport with device border styling to verify single-column responsiveness.
Real-Time Subject Simulation Bar
Top preview header displays the rendered subject line in real-time, showing how personalization placeholders will look to the recipient before launching.
Dynamic Syntax Warning Banner
Automatically scans draft markup and displays an alert banner if unclosed placeholder tags or unsupported merge tokens are detected.
4.5 Dynamic Personalization Tokens & RFC Compliance
At delivery time, the template engine dynamically replaces tokens with recipient attributes, secure tracking links, syndicated RSS article summaries, and News posts.
Recipient full name (falls back to "there" if empty)
Recipient email address
Mandatory one-click RFC 8058 unsubscribe link
Double opt-in confirmation URL for signup pages
Syndicated blog post loop for automated RSS newsletters
Title of the syndicated RSS/Atom content feed
Headline of the most recently published blog article
Count of new feed posts included in the newsletter
Block of News posts for a Newsletter schedule (cover, title, subtitle, body)
Number of News posts included in this send
Title of the newest News post in this send
Legal Requirement & Deliverability Safeguard
Every marketing broadcast must include {{unsubscribe_link}}. Broadcasts sent without a valid unsubscribe mechanism violate CAN-SPAM, GDPR, and CASL, leading to aggressive domain blacklisting by major ISPs.
4.6 Single Test Send Diagnostic & Pre-Delivery Check
A modal-driven diagnostic tool allowing senders to dispatch test emails to any recipient address via selected SMTP profiles before scheduling or broadcasting.
Multi-SMTP Profile Selection
Route test transmissions through any configured SMTP profile to verify SPF, DKIM, and deliverability status on specific mail routing servers.
Synthetic Placeholder Rendering
Automatically substitutes personalization tokens with realistic mock data (e.g. "John Doe") to inspect visual formatting and alignment accurately.
Synchronous Queue-Bypass Dispatch
Bypasses the background queue (cm_email_queue) to deliver test messages synchronously via MailerFactory, providing immediate pass/fail feedback.
Inbox & Dark Mode Quality Assurance
Send tests directly to personal Gmail, Outlook, and Apple Mail accounts to audit dark-mode color inversions, button contrast, and typography rendering.
4.7 Hooked AI Copywriting Engine BYOK
Integrated AI Copywriter operating directly via BYOK (Bring Your Own Key) to generate and refine high-converting, email-safe templates with zero third-party proxy latency.
Crucial Principle: Output Quality Governed by Prompt & Model
MarbleSend does not restrict you to a single closed AI provider. Because generation runs via your configured BYOK provider, the resulting email layout quality, persuasive strength, HTML table accuracy, and subject critique are fundamentally governed by two variables: the specificity of your prompt instructions and the reasoning power of the selected LLM model.
1. User Prompt Specificity & Context Depth
The AI model reflects the clarity and context of the instructions you provide. Generic prompts such as "write a promo email" produce generic copy. For superior results, provide structured context: target audience, pain points, core offer, desired tone of voice (urgent, conversational, executive), and a specific Call-to-Action (CTA). In the Edit Template tab, give targeted iterative instructions (e.g., "shorten the second paragraph", "emphasize the 20% discount on the CTA", or "add a 3-benefit callout box") to refine your template step-by-step.
2. LLM Model Tier & Reasoning Capability
Under Settings → AI Providers, you can connect frontier reasoning models (such as DeepSeek-V4.1-flash, Claude 5 Sonnet, or GPT-6) or lightweight local models (such as 7B–8B parameters via Ollama or Groq). Frontier models excel at intricate email-safe HTML table nesting, natural placement of personalization tokens ({{name}}, {{email}}, {{unsubscribe_link}}), and nuanced conversion copywriting. On the Subject Options tab, the live 1–100 score, spam risk analysis, and variant suggestions are generated directly by the active model — higher-tier models deliver dramatically sharper critique and more creative alternatives.
MarbleSend Built-in System Prompt Guardrails
Regardless of prompt length or model choice, AiService.php injects robust email-safe architectural guardrails: single-table responsive structure (max 600px width, clean canvas, high-contrast cards), table-based bulletproof CTA buttons compatible with Outlook and Gmail, zero external images to safeguard deliverability, and mandatory {{unsubscribe_link}} token insertion in footers to comply with global email regulations.
Full Template Generation
Produces complete, responsive table-based HTML email layouts tailored to your target audience, core offer, tone of voice, and specified reading length.
Context-Aware Refinement
Analyzes the active template in the editor and applies targeted iterative modifications (e.g. "add a 3-column feature section" or "make the CTA more urgent").
Interactive Visual Diff
Side-by-side sandboxed iframes render the "Before" and "After" versions simultaneously, allowing users to inspect changes visually before applying them.
How to Obtain API Keys Across Major Providers
Once you have obtained your API key and chosen model ID, configure it in MarbleSend with these simple steps:
4.8 Interactive AI Subject Score & Deliverability Inspector
Scans email subject lines across your entire template library, evaluating open-rate probability, spam filter risk, and character truncation with 1-click variant suggestions.
Full Library Subject Loading
Scans all subjects across cm_templates and displays them in a scrollable list, highlighting the subject belonging to the currently edited template.
1–100 Multi-Factor Scoring
The active LLM analyzes curiosity, clarity, value proposition, and urgency, calculating an objective effectiveness score out of 100.
Mobile Truncation & Spam Filter Audit
Flags character counts exceeding mobile viewport limits (recommended <= 50 characters) and identifies spam-trigger words like "FREE", "URGENT $$$", or excessive punctuation.
High-Converting Variant Suggestions
Generates alternative subject lines focused on curiosity, benefit-driven, or loss-aversion angles, applyable directly to the editor with one click.
News Posts — Substack-style Articles for Newsletters
Write articles in the Posts tab of Templates (Markdown with live preview). A Newsletter schedule set to “News posts” sends each Ready post once, oldest first, by filling the {{news_posts}} placeholder of its template. Posts live only in your email — there is no public page.
Write and mark Ready
Use the toolbar or plain Markdown. A post is sent only after you switch it from Draft to Ready.
Template placeholder
Put {{news_posts}} where the articles should appear (the “News Posts Newsletter” starter already has it). {{news_posts_count}} and {{news_latest_post_title}} work in the subject.
Schedule it
In Schedules choose Newsletter, then Content source: News posts. Only posts made Ready after that moment are sent, so your archive is never replayed. Smart Suppression is not used.
Safe by design
Post text is HTML-escaped and only https links and images are allowed. Keep each email under about 100 KB so Gmail does not clip the unsubscribe link — the editor warns you.
Posts workspace at a glance
Open Templates and switch to the Posts tab. The library sits on the left and the editor with its live preview on the right — the same layout as the template studio.
| Part | What it does |
|---|---|
| Library | Lists every post with a Draft or Ready badge, how many times it was sent and when it last changed. The search box filters by title and subtitle; the trash icon appears on hover. |
| Title, subtitle, cover | The title is required (up to 255 characters, one line). The subtitle (up to 500) is shown under it. A cover image must be an https URL — anything else is rejected when you save. |
| Status switch | Draft posts are never sent. Switching a post to Ready stamps the moment it became Ready; that moment decides which schedules may send it. |
| Markdown editor | Toolbar buttons (heading, bold, italic, link, lists, quote, divider, image) insert syntax around your selection. A post can hold up to 100,000 characters. |
| Live preview | Renders the unsaved post inside the template you pick, using the same code as a real send, and shows the email size in KB. Placeholders such as {{name}} use sample values. |
| Placeholder shortcut | A button under the editor copies {{news_posts}} so you can paste it into a template. |
Markdown reference
Post text is written in a small, safe Markdown subset. Anything outside this list stays plain text.
| Syntax | Result |
|---|---|
| ## Heading ### Smaller | Heading (levels 1–3 become email-safe headings) |
| **bold** *italic* | Bold and italic |
| [text](https://example.com) | Link. Only http, https and mailto addresses are linked; any other scheme (such as javascript:) is left as plain text. |
| - item 1. item | Bulleted and numbered lists |
| > quote | Block quote |
| --- | Divider line |
|  | Image. https only; always add a short description for readers who block images. |
| (blank line) | Paragraphs are separated by a blank line; a single line break stays a line break inside the paragraph. |
| <script>alert(1)</script> | Raw HTML is never interpreted: a <script> or <img> tag you type is shown as literal text. |
The {{news_posts}} placeholder family
Place these in a template; a News posts schedule fills them at send time, before the usual per-recipient tokens such as {{name}} are resolved.
| Placeholder | Becomes |
|---|---|
| {{news_posts}} | The block of posts for this send: cover image, title, subtitle and body of each post, oldest first. Required in the template body. |
| {{news_posts_count}} | The number of posts in this send, for example 1. Works in the subject and the body. |
| {{news_latest_post_title}} | The title of the newest post in this send. Ideal for the subject line. |
Required in the body
A schedule set to News posts is refused at save time if its template body has no {{news_posts}}, and a run fails safely (posts stay pending) if the template is edited later.
Never sent as raw text
Used anywhere else — a manual Campaign, an RSS schedule or a template-only schedule — the placeholders are replaced with empty text, and the Campaign pre-flight check warns you before sending.
Personalised and tracked
Post text goes through the normal pipeline: {{name}} inside a post is personalised per recipient, links get UTM parameters and click tracking, and the unsubscribe link in your template stays intact.
Post lifecycle
Every post moves through the same simple states; the library badge always shows the current one.
Draft
Still being written. Never sent. Moving a Ready post back to Draft clears its Ready time.
Ready
Queued for delivery. A schedule sends it only if it became Ready after that schedule's News posts mode was switched on. Saving an already Ready post does not change its place in the queue.
Sent
Tracked per schedule and shown as “Sent 1×” in the library. A schedule never sends the same post twice, even if you edit the post afterwards.
Deleted
Removes the post and its delivery history. Emails that were already delivered are not affected, and a deleted post cannot be recovered.
Email size and safety
Posts carry their full text inside the email — there is no public page — so size and safety matter more than in a link-only digest.
Escape first
Every character you type is HTML-escaped before any formatting is applied, so scripts and tags cannot reach a recipient's inbox.
https only
Images and the cover must be https. Links accept http, https and mailto. Other schemes are shown as plain text.
Stay under about 100 KB
Gmail clips messages over roughly 102 KB and hides the footer, including the unsubscribe link. The preview shows the size in KB and warns above 80 KB; keep posts per email low for long articles.
No public archive
Posts exist only inside the emails you send. There are no web pages, no image uploads and no automatic AI calls — images are linked by URL.
Quick troubleshooting
| Symptom | What to check |
|---|---|
| The preview shows no articles | The selected template has no {{news_posts}} placeholder (it is marked “no {{news_posts}}” in the list). Pick another template or add the placeholder. |
| “Large email” warning | Shorten the post, remove heavy formatting or lower “Posts per email” in the schedule. |
| A link is not clickable | Write it as [text](https://…) with an http, https or mailto address; other schemes are left as text. |
| An image does not appear | The address must start with https and be publicly reachable. Some mail apps also block remote images until the reader allows them. |
Schedules & RSS Automation (/schedules)
Recurring newsletter dispatches (template or News posts), interactive monthly broadcast calendar, cadence engine, dynamic audience targeting, and automated blog RSS/Atom campaigns.
Schedules, RSS & News Posts UI Inspector
Click any component below to view database query, schema impact, and operational tips
Calendar & Month Matrix
Visual schedule map & cadenceRecurrence Engine
Daily, weekly, monthly & customAutomated Blog Poller
Hash deduplication & cacheDynamic Audience
Tag query at exact run secondOccurrence Logs
Run audit & duplicate preventionOn-Demand Run Now
Immediate run without cadence shiftHosting Optimization
8 curated operational guidelinesCron Heartbeat & Lock
Non-blocking concurrency guardNews Posts Mode
Substack-style newsletters5.1 Dual-Mode Operations: Interactive Schedule Manager & Calendar View
MarbleSend provides two synchronized operating views tailored for different workflow needs: a compact Bento-grid Schedule Manager (/schedules) and an expansive interactive Monthly Calendar (/schedules/list).
Dense Tabular & Bento View
Displays all recurring broadcasts with next-run countdowns, active status toggles, selected template previews, and direct execution shortcuts.
Interactive 7-Column Calendar Grid
Maps upcoming deliveries against a standard Sunday–Saturday monthly calendar, featuring a current-day highlight, month switcher, and clickable event chips.
Selected Schedule Detail Drawer
Instant inspection drawer rendering sandboxed template previews, recurrence specs, recipient tag rules, and quick action controls.
5.2 Cadence Engine, Recurrence Rules & UTC Timezone Normalization
Recurring schedules are powered by SchedulerService::computeNextRun(), which eliminates manual timestamp calculation in favor of robust PHP DateTimeImmutable mathematics.
Universal UTC Storage with Localized Presentation
All next_run_at values are stored in UTC within cm_schedules. When rendered in the browser, times are converted to the admin's configured display timezone while preserving recipient delivery windows.
Daily & Weekly Recurrence
Support for daily dispatches at set hours, or specific weekly day selections (e.g. Tuesday and Thursday mornings) using interactive day pills.
Monthly Day-of-Month Clamping
For monthly dispatches on days 29–31, MarbleSend automatically clamps executions to the final valid day of shorter months (e.g. February 28/29).
DST & Timezone Awareness
Uses native PHP DateTimeZone calculations to guarantee broadcasts trigger at the intended local hour across Daylight Saving Time boundaries.
5.3 Automated RSS & Atom Feed Poller Pipeline
MarbleSend connects directly to your blog or content feed using native SimpleXML and cURL, converting new articles into automated subscriber campaigns without third-party integration tools.
Dual-Format Parser (RSS 2.0 & Atom 1.0)
Automatically inspects the XML root element, extracting titles, content summaries, permalinks, and publication timestamps with zero external libraries.
Live Pre-Flight Feed Diagnostic
The "Test Feed" button contacts the feed URL in real time, validating XML schema, verifying HTTP 200 responses, and previewing the latest 3 posts.
Deduplication & Initial Snapshot
Tracks last_rss_item_id and publish dates. On initial schedule creation, MarbleSend records the latest post ID as a baseline to prevent retroactively broadcasting historical archives.
5.4 Dynamic Audience Resolution & Recipient Targeting at Execution Time
A schedule's audience can also be limited to a group (combined with a tag, both must match). If that group is deleted the schedule is disabled instead of silently sending to nobody.
Unlike static campaign snapshots, scheduled broadcasts resolve their recipient lists dynamically at the exact second the occurrence triggers.
Real-Time Audience Expansion
Subscribers who register or receive tags minutes before the scheduled run are automatically included in the upcoming broadcast without manual list re-syncing.
Strict Deliverability Status Filter
The dispatch query enforces WHERE status = 'active', unconditionally excluding pending, unsubscribed, and suppressed contacts from recurring queues.
Tag-Based Segment Isolation
Supports broadcasting to all active contacts or targeting specific audience segments matching JSON tag arrays (e.g. #weekly-digest, #customers).
5.5 Occurrence Execution Logs, Run Audits & The Single Catch-Up Policy
Every automated run creates an immutable audit row in cm_schedule_occurrences, detailing execution timestamps, outcomes, recipient counts, and error telemetry.
3-State Occurrence Telemetry
Distinguishes between 'completed' (emails queued), 'skipped' (no new RSS articles detected), and 'failed' (SMTP disconnect, invalid template, or network timeout).
Single Catch-Up Idempotency Guard
If the cron worker was inactive across multiple missed runtimes, MarbleSend executes at most ONE catch-up broadcast upon recovery and advances next_run_at to the future.
Occurrence History Audit Modal
Click "View Execution History" from the sidebar to inspect past runs, recipient volumes, and diagnostic error logs directly within the UI.
5.6 On-Demand Manual Dispatches ("Run Now") & Emergency Overrides
Administrators can bypass the countdown timer and immediately trigger an unscheduled broadcast of any schedule using the "Trigger Send Now" action.
Immediate Queue Compilation
Instantly compiles the template, resolves active audience members, and dispatches batch items to cm_email_queue for the next worker cycle.
Preserved Recurrence Timeline
Triggering an on-demand broadcast does not shift or postpone the scheduled next_run_at timestamp, maintaining regular calendar cadence.
Diagnostic & Breaking News Utility
Ideal for testing end-to-end template formatting with live subscriber data or distributing urgent bulletins outside normal business hours.
5.7 Fail-Safe Worker Concurrency, Overlap Prevention & Non-Blocking File Locks
MarbleSend features an enterprise concurrency perimeter designed specifically to prevent duplicate broadcasts on budget shared hosting environments.
Non-Blocking flock Concurrency Lock
worker.php acquires an exclusive flock lock on storage/worker.lock. If a previous run is still sending over high-latency SMTP relays, subsequent invocations exit silently.
Dual-Worker Protocol Parity
Both CLI worker.php and HTTP cron.php share identical execution paths and locking mechanics, ensuring consistent behavior across cPanel cron and webhooks.
Atomic Occurrence Claims
Schedules due for execution are reserved using atomic PDO prepared statements, preventing duplicate queueing even in multi-threaded container environments.
5.8 Shared-Hosting Performance Tuning & The 8 Curated Optimization Tips
The dynamic Optimization Tip card on the Schedules page provides 8 actionable, production-proven operational recommendations for reliable shared-hosting delivery:
Schedule broadcasts on Tuesday or Thursday between 09:00 and 10:30 recipients' local time for peak open and click-through rates.
Execute worker.php every minute (* * * * *) with flock protection to guarantee sub-minute accuracy without process collision.
Query cm_email_queue using indexed prepared statements with LIMIT clauses to eliminate race conditions during high-volume spikes.
Segment contacts by region tags (#us-east, #apac, #emea) and assign dedicated staggered schedules to avoid delivering emails at 3:00 AM.
Schedule RSS dispatches 1–2 hours after blog publication in your CMS, allowing CDN edge nodes and XML caches to fully update.
Periodically prune older execution logs from cm_schedule_occurrences to keep database table sizes lean and query indexes optimal on shared hosting.
Schedule a follow-up broadcast 48–72 hours later to subscribers where opened_at IS NULL with a fresh subject line to recapture 15–25% engagement.
Assign distinct SMTP profiles for automated RSS feeds versus high-volume marketing blasts to safeguard domain sender reputation.
5.9 News Posts Schedules (Substack-style Newsletters)
A Newsletter schedule can now send articles instead of one fixed template. Set Content source to “News posts” and each run delivers the next Ready posts from Templates → Posts, inserted into the template through {{news_posts}}. Nothing is sent when no post is waiting.
Which content source should I use?
Template only
The same template on every run — for recurring announcements or reminders. Smart Suppression is available.
RSS digest
New posts of an external blog feed. The first run only marks existing posts as seen; later runs send only new posts.
News posts
Articles you write inside MarbleSend. Each Ready post is sent once, oldest first. Best for a Substack-style newsletter.
Set it up in six steps
- 1Open Templates → Posts and write your article in Markdown.
- 2Create or open a template and place {{news_posts}} where articles should appear (the “News Posts Newsletter” starter already has it).
- 3Open Schedules → New, keep the type on Newsletter and set Content source to “News posts”.
- 4Choose the template, audience, SMTP profile, posts per email (1–20) and the cadence.
- 5Save. Only posts that become Ready after this moment can be sent by this schedule.
- 6Mark the post Ready. The next occurrence — or Run now — delivers it.
Which posts are sent on each run
At every occurrence the schedule picks posts by four rules, in this order.
Ready only
Drafts are ignored, however old they are.
After the cut-off
A post must have become Ready after the schedule's News posts mode was switched on. Your existing archive is never replayed.
Not sent by this schedule yet
Delivery is tracked per schedule, so two schedules can each send the same post once, but never twice.
Oldest first, up to the limit
“Posts per email” (default 1, maximum 20) caps how many are combined. The rest wait for the next run.
What each run logs
Every occurrence appears in the schedule history. Posts are marked as delivered only when an email batch was really queued.
Sent
A Campaign was created and queued, for example “1 post · 5 recipients queued”. The posts are marked as sent by this schedule.
Skipped: no new posts
No Ready post is waiting. Nothing is sent and the schedule simply tries again at the next occurrence.
Skipped: no recipients
The audience was empty. The posts stay pending and are sent as soon as the audience has active contacts.
Failed
The template is missing or no longer contains {{news_posts}}. The posts stay pending; fix the template and run again.
Run now and test sends
Run now
A real occurrence, executed by the worker within about a minute. It sends the pending posts and marks them as delivered.
Send test digest
Sends the next pending posts — or, if none is pending, the newest Ready post — to one address through the schedule's own SMTP profile. It never marks anything as delivered and shares the 5-per-minute test limit.
Editing and pausing
- Switching a schedule from Template only to News posts sets a new cut-off at that moment; switching back clears it.
- Saving the schedule form re-checks that the template contains {{news_posts}}. Pause and Resume only flip the on/off switch and never re-check it, so you can always pause.
- Smart Suppression is switched off automatically in News posts mode: it would exclude everyone who received an earlier issue.
- News posts is available for Newsletter schedules only; RSS schedules keep their own feed logic.
Reading the schedule screens
- The schedule list labels these schedules “News posts” instead of “Newsletter”.
- The detail panel shows how many Ready posts are waiting for this schedule.
- In the form, templates without {{news_posts}} are flagged, and a warning appears if you pick one.
Quick troubleshooting
| Symptom | What to check |
|---|---|
| My Ready post was not sent | Check that it became Ready after the schedule was saved (or switched to News posts), that the schedule is enabled, and look at the history entry for “no recipients” or “failed”. |
| The schedule says “no new posts” right after I wrote one | The post may still be a Draft, or it was already sent by this schedule. Open Templates → Posts and check the badge and “Sent N×”. |
| I cannot save: the template must contain {{news_posts}} | Add the placeholder to the template body (not only to the subject) or choose another template. |
| I want to resend an old post | Copy its text into a new post and mark that one Ready — a schedule never sends the same post twice. |
Settings & Infrastructure (/settings)
SMTP multi-relay management, cron heartbeat, 23 languages, deliverability diagnostics, and license activation.
Settings & Infrastructure UI Inspector
Click any component below to view database schema impact, background processes, and operational guidelines
SMTP Profiles
BYO SMTP & Managed SES RelayWorker & Cron
1-minute execution cadence23 Locales & Theme
RTL mirror & Dark/Light modeREST API Keys
Scoped developer accessEvent Webhooks
HMAC-SHA256 event pushDeliverability Suite
SPF, DKIM, DMARC & BIMIAI Providers
BYOK & MarbleSend CreditsEd25519 License
Cryptographic grant token6.1 Dual Delivery Architecture: BYO SMTP & Managed Amazon SES Relay
MarbleSend decouples campaign execution from delivery relays with two robust transport models: (1) Bring Your Own (BYO) SMTP for connecting your existing mail servers, and (2) Managed Amazon SES Relay via admin.marblesend.com for instant cloud delivery with zero AWS setup. Supports Option 1 Friendly From (0 DNS configuration) and Option 2 Custom Domain Easy DKIM with dynamic pricing and atomic credit management.
Connect any standard SMTP provider including Postmark, Mailgun, Brevo, SendGrid, Resend, or your host's cPanel mailbox. Supports Port 587 with STARTTLS, Port 465 with implicit SSL/TLS, and Port 25 for local relays, with custom authentication and isolated sender headers.
6.1.1 SMTP Server Providers (Free, Commercial & Shared Hosting)
SMTP Directory & ComparisonConnect your own mail transport with standard SMTP credentials. Choose between high-volume dedicated cloud relays or your existing hosting mailboxes, keeping strict sending quotas in mind.
Standard shared hosting mailboxes (cPanel, Webmail) enforce strict hourly and daily rate limits (typically 100–500 emails/day, and script-based php mail() is often throttled to 100/day or 10/min). Sending newsletters or broadcast campaigns through standard hosting mailboxes can cause 'Quota Exceeded' bounce rejections or account suspensions. For campaign marketing, upgrade to dedicated Business Email / Email Marketing packages or connect a cloud relay.
Optimized for high-volume dispatches, robust IP reputation management, and high deliverability.
| Provider / Website | Free Allowance | Commercial Pricing | SMTP Host & Port | Key Rules & Limitations |
|---|---|---|---|---|
| Amazon SES (Direct) | Free tier on EC2 only (62k/mo); Otherwise pay-as-you-go | $0.10 / 1,000 emails ($1 per 10k) | email-smtp.<region>.amazonaws.com Port 587 (TLS) or 465 (SSL) |
Requires AWS account setup, IAM SMTP user credentials, and sandbox exit request. |
| Brevo | 300 emails / day (Free forever) | Starter from ~$9/mo (5k emails) to ~$18/mo (20k emails) | smtp-relay.brevo.com Port 587 (TLS) |
Free tier resets daily. Great for low-volume testing without entering a credit card. |
| Mailgun (Sinch) | Trial (5,000 emails for 30 days) | Foundation: $35/mo (50,000 emails); Scale: $90/mo | smtp.mailgun.org (or smtp.eu.mailgun.org) Port 587 (TLS) or 465 (SSL) |
Robust analytics and deliverability. Supports EU data residency. |
| Twilio SendGrid | 100 emails / day (Free forever) | Essentials: ~$19.95/mo (50k emails); Pro: ~$89.95/mo | smtp.sendgrid.net Port 587 (TLS) (User: apikey) |
Requires Two-Factor Authentication. Username must literally be apikey. |
| Postmark (ActiveCampaign) | Free Developer trial: 100 emails/mo | From $15/mo (10,000 emails), $55/mo (50,000 emails) | smtp.postmarkapp.com Port 587 (TLS) or 2525 |
Industry-leading transactional speed and strict spam filters. Separate broadcast streams required. |
| Resend | 3,000 emails / month (100 / day free) | Pro: $20/mo (50,000 emails/mo) | smtp.resend.com Port 465 (SSL) or 587 (TLS) (User: resend) |
Modern developer experience, clean UI, quick domain verification via DNS. |
Standard cPanel and hosting mailboxes suitable for light correspondence, but strictly capped for mass marketing.
Business Email Plans (Per Mailbox)
1 GB storage, 1 forwarding rule, 1 alias, up to 100 mailboxes/domain. Standard webmail & antispam. Strict sending cap (~100-200/day).
5 GB storage, 5 forwarding rules, 5 aliases, expandable mailbox storage. No mailbox limit per domain.
20 GB storage, 20 forwarding rules, 10 aliases, AI Kodee mailbox assistant & smart replies.
50 GB storage, 50 forwarding rules, 30 aliases, full AI suite & priority deliverability.
Hostinger Reach (Email Marketing)
100 recipients, 200 emails / month. Suitable for basic transactional testing.
500 recipients, 500 emails / month, 10 AI credits, remove Reach branding, automations.
1,000 recipients, 7,000 emails / month, 20 AI credits, custom HTML code, send-time optimization.
• cPanel Shared: Max ~500 emails/hour per cPanel domain.
• Private Email: Starter (~$1.24/mo per mailbox, 5GB). Pro (~$2.82/mo, 30GB).
• SMTP Host: mail.privateemail.com (Port 465 SSL or 587 TLS).
• Shared cPanel: Default cap ~500 emails/hour. Internal mail system may defer large sudden bursts.
• Professional Email: Integrated with Microsoft 365 or Google Workspace ($2.99–$9.99/mo).
• SMTP Host: box###.bluehost.com or mail.yourdomain.com (Port 465 SSL).
• Site Tools Mail: Included in hosting (StartUp, GrowBig, GoGeek).
• Rate Limits: ~400 emails/hour on StartUp; up to ~800/hour on GoGeek. Strictly intended for transactional mail.
• SMTP Host: mail.yourdomain.com (Port 465 SSL).
• Paid Business Email: ~$1.99/mo per mailbox (25GB storage).
• Strict Outbound Policy: Maximum 100 recipients per hour. Mass marketing prohibited via shared SMTP.
• SMTP Host: smtp.dreamhost.com (Port 465 SSL or 587 TLS).
• cPanel Limit: Max 500 emails/hour per domain on shared plans.
• Workspace Option: Google Workspace addon ($6–$18/mo).
• SMTP Host: mail.yourdomain.com (Port 465 SSL or 587 TLS).
• M365 Reseller: Email Essentials from ~$1.99–$5.99/mo (10GB-50GB).
• SMTP Relay Note: Direct SMTP AUTH requires enabling Authenticated SMTP in the M365 Admin Portal.
• SMTP Host: smtp.office365.com (Port 587 TLS).
Select the appropriate port and security protocol based on your server network and firewall rules.
The modern standard for email client-to-relay submission (RFC 6409). Starts in cleartext and immediately upgrades connection to TLS. Supported by almost all cloud relays.
Implicit TLS/SSL wrapper connection (RFC 8314). Required by Hostinger, Namecheap cPanel, and selected enterprise relays. Highly reliable when hosting firewalls restrict 587.
Server-to-server MTA routing. Almost universally blocked on cloud hosts (Hostinger, AWS, DigitalOcean, Hetzner, etc.) to combat spam. Do NOT use for outbound SMTP credentials.
6.1.1 Managed Amazon SES Relay & Custom Sender Domains
MANAGED SESEnterprise cloud delivery infrastructure managed by MarbleSend via api.marblesend.com. Eliminates SMTP port blocking, IAM credential management, and IP reputation risks on shared hosting. Features dual sender modes (Friendly From & Easy DKIM), free starter credits, atomic ledger auditing, and dynamic on-demand scaling.
Enterprise Amazon SES cloud delivery pre-configured and managed by MarbleSend via admin.marblesend.com. Requires zero AWS account setup, zero IAM credential management, and no IP warming. Top up credits on-demand with automatic meter sync and real-time quota visibility.
Instant campaign delivery with zero DNS configuration. Emails are enveloped via relay@marblesend.com displaying your friendly brand name, while recipient replies route directly to your personal mailbox.
Send directly with From: "Your Brand" <you@yourdomain.com>. Add 3 CNAME DNS records generated by AWS SES v2 to authenticate domain ownership and achieve maximum inbox placement.
Each active license includes 1 custom sender domain identity free. Additional domain slots and SES credit packs (10k, 50k, 100k, 250k bulk) are priced dynamically from admin.marblesend.com without needing app updates.
Every email dispatched via MarbleSend SES reserves 1 credit atomically using queue-item UUID idempotency (cm_addon_ledger.uq_addon_ledger_key). If relay transmission fails, the reservation is instantly released and refunded without manual intervention.
6.1.2 Sending rules with the SES Relay
Amazon SES only works for senders who keep their lists clean, so these rules are enforced by the MarbleSend relay itself, not just by this app. They cannot be switched off and they apply to every email sent through the SES Relay.
Before the first send, complete Settings → SMTP → Sender Identity & Acceptable Use: legal name, postal address, country, phone number and acceptance of the Acceptable Use Policy. Sending stays blocked until it is saved. Your name and address appear in every email footer; the phone number is never shown.
Every campaign body must contain the {{unsubscribe_link}} tag, otherwise the send is refused. On top of that, the relay adds the List-Unsubscribe and one-click headers and a footer with your name, address, an unsubscribe link and “Sent with MarbleSend”. The footer cannot be removed.
While the SES Relay is in use, signup pages and the API always use double opt-in, public signup requires Cloudflare Turnstile, and a CSV import needs a tick confirming that everyone gave permission. Someone who unsubscribed or was suppressed cannot re-subscribe through a public form.
Hard bounces and spam complaints reach MarbleSend automatically and turn the contact into suppressed. The relay also refuses to send to a suppressed address, and no credit is used. Soft bounces are retried and only suppress a contact after repeated failures.
Each account has a daily sending limit (2,000 emails by default). If bounce or complaint rates get too high, sending pauses automatically until support reviews the account, and it also stops if the license is inactive. A banner in Settings → SMTP explains why, and queued emails wait instead of being lost.
Emails are signed with DKIM for marblesend.com (or for your own domain once verified). The return path uses mail.marblesend.com, so SPF aligns and DMARC passes. Replies go to the Reply-To address you set.
Easy DKIM 3 CNAME Setup Directory (Multi-Provider)
Comprehensive DNS configuration guide for adding the 3 AWS SES Easy DKIM CNAME records across popular hosting providers and domain registrars, preventing common syntax pitfalls and ensuring fast verification.
Universal DNS Architecture & Essential Rules
Easy DKIM delegates cryptographic signing to Amazon SES via 3 CNAME records. Because AWS manages and automatically rotates the underlying 1024/2048-bit RSA keys without requiring manual DNS updates, CNAME records must remain in place permanently.
| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME | <token1>._domainkey.yourdomain.com | <token1>.dkim.amazonses.com | 300 (or Auto) |
| CNAME | <token2>._domainkey.yourdomain.com | <token2>.dkim.amazonses.com | 300 (or Auto) |
| CNAME | <token3>._domainkey.yourdomain.com | <token3>.dkim.amazonses.com | 300 (or Auto) |
Rule 1: Beware of the "Double Domain" Trap
Most domain registrars and web hosts automatically append your root domain to the Host/Name field. If you paste "token._domainkey.yourdomain.com", the DNS server will resolve "token._domainkey.yourdomain.com.yourdomain.com", causing DKIM verification to fail immediately. Always enter only "token._domainkey" unless your provider explicitly requires the full FQDN.
Rule 2: CNAME, Not TXT
Unlike manual DKIM which uses a TXT record with a static public key (p=MIGf...), Amazon SES Easy DKIM uses 3 CNAME records pointing directly to token.dkim.amazonses.com. Do not select TXT as the record type.
Rule 3: Proxying & Cloudflare Grey Cloud
If you use Cloudflare or an HTTP proxy, the 3 DKIM CNAME records must be set to DNS Only (Grey Cloud / Unproxied). HTTP proxying hides the CNAME target and breaks cryptographic email verification.
# Check resolution via dig (macOS / Linux): dig CNAME <token1>._domainkey.yourdomain.com +short # Check resolution via nslookup (Windows / PowerShell): nslookup -type=CNAME <token1>._domainkey.yourdomain.com
Hostinger (hPanel DNS Zone)
hPanel DNS| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME | <token>._domainkey (hPanel auto-appends .yourdomain.com) | <token>.dkim.amazonses.com | 300 (or 14400) |
Log in to your Hostinger hPanel dashboard and navigate to Domains.
Select your domain and click DNS / Nameservers in the left sidebar.
Under Manage DNS Records, select CNAME from the Type dropdown.
In the Name field, enter only token._domainkey (hPanel automatically appends .yourdomain.com).
In the Target field, enter token.dkim.amazonses.com and set TTL to 300 or default (14400).
Click Add Record. Repeat the process for all 3 tokens provided in MarbleSend.
DreamHost (DreamHost Web Panel)
DreamHost Web Panel| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME | <token>._domainkey (DreamHost appends .yourdomain.com) | <token>.dkim.amazonses.com | Default |
Log in to the DreamHost Web Panel and navigate to Websites > Manage Websites.
Locate your domain, click the three dots or DNS Settings (or Domains > Manage Domains > DNS).
Scroll down to the "Add a custom DNS record" section and click Add Record.
In the Name field, enter token._domainkey (DreamHost shows .yourdomain.com automatically).
Set Type to CNAME and enter token.dkim.amazonses.com in the Value field.
Click Save or Add Record. Repeat for all 3 tokens.
Namecheap (Advanced DNS)
Advanced DNS| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME Record | <token>._domainkey (never paste root domain here) | <token>.dkim.amazonses.com | Automatic (or 1 min) |
Log in to Namecheap, open Domain List from the left sidebar, and click Manage next to your domain.
Switch to the Advanced DNS tab at the top of the management page.
In the Host Records section, click the red ADD NEW RECORD button.
Select CNAME Record from the Type dropdown menu.
In the Host field, enter ONLY token._domainkey (never include your domain name).
In Target, enter token.dkim.amazonses.com, set TTL to Automatic (or 1 min), and click the green checkmark ✓ to save. Repeat for all 3 records.
Bluehost (Account Manager / Zone Editor)
Bluehost DNS| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME | <token>._domainkey (Bluehost handles suffix automatically) | <token>.dkim.amazonses.com | 4 Hours (or Default) |
Log in to Bluehost Account Manager, click Domains in the left navigation, and choose your domain.
Navigate to the DNS tab (or open cPanel Zone Editor if using legacy hosting).
Scroll to the CNAME (Alias) section and click Add Record.
In the Host Record field, enter token._domainkey.
In Points To, enter token.dkim.amazonses.com and select TTL (4 Hours or Default).
Click Save. Repeat for the remaining 2 CNAME tokens.
SiteGround (Site Tools DNS Zone Editor)
Site Tools DNS| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME | <token>._domainkey (Site Tools appends .yourdomain.com) | <token>.dkim.amazonses.com | 1 hour (3600) |
Log in to SiteGround Client Area, go to Websites, and open Site Tools for your domain.
In the left sidebar, expand Domain and click DNS Zone Editor.
Select the CNAME tab under Create New Record.
In the Name field, enter token._domainkey (Site Tools appends .yourdomain.com automatically).
In the Resolves To field, enter token.dkim.amazonses.com and leave TTL at 1 hour (3600).
Click Create. Repeat for all 3 tokens.
HostGator (cPanel Zone Editor)
cPanel Zone Editor| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME | <token>._domainkey (cPanel automatically appends dot) | <token>.dkim.amazonses.com | 14400 (or 3600) |
Log in to HostGator Customer Portal and launch cPanel.
Under the Domains section, click Zone Editor.
Find your domain and click + CNAME Record (or click Manage > Add Record).
In the Name field, enter token._domainkey (cPanel auto-appends your domain with a trailing dot).
In the CNAME / Record field, enter token.dkim.amazonses.com.
Click Add a CNAME Record. Repeat for all 3 tokens.
GoDaddy (Domain Portfolio DNS)
GoDaddy DNS| Record Type | Host / Name | Value / Target / Points To | TTL |
|---|---|---|---|
| CNAME | <token>._domainkey (never include root domain in GoDaddy) | <token>.dkim.amazonses.com | 1/2 Hour (or Default) |
Log in to your GoDaddy account and go to My Products.
Under Domains, find your domain and click DNS (or the three dots > Manage DNS).
In the DNS Records section, click Add New Record.
Select CNAME from the Type dropdown menu.
In the Name field, enter ONLY token._domainkey (do NOT include .yourdomain.com).
In Value, enter token.dkim.amazonses.com, set TTL to 1/2 Hour (or Default), and click Save. Repeat for all 3 records.
After saving the 3 CNAME records, DNS propagation typically takes 5–30 minutes (up to 24–48 hours depending on TTL). Return to MarbleSend Settings > SMTP, open the custom domain card, and click "Verify DNS". Once validated by Amazon SES, the status badge will switch to Verified.
6.2 Worker Background Heartbeat, 1-Minute Cron & Concurrency Guard
MarbleSend sends email in the background. A cron job is a timer in your hosting panel that wakes MarbleSend up every minute so it can run schedules and autoresponders, retry failed deliveries, collect bounces and keep a long Campaign going. A Campaign you send starts at once, but everything that has to happen later depends on this timer. If Settings → System shows Cron Not Detected, the timer is missing, is not running every minute, or points to the wrong place. The three steps below fix that.
Open Settings → System, find Cron Job Command and click Copy Command. The line already holds the right folder and, when your server tells us, the right PHP. Use it exactly as it is; the examples further down this page only show the shape of a line. If Settings says it could not detect PHP, ask your host for the PHP command-line path and replace the word php at the start of the line.
Pick your host below. Menu names change from time to time, so look for the closest match. Whatever the panel, the job must run every minute (* * * * *) and its command is the line from Step 1. Plesk, DirectAdmin and other panels call it Scheduled Tasks or Cron Jobs.
Hostinger (hPanel)
- Open hPanel → Websites → Dashboard for your site, then click Cron Jobs in the sidebar.
- Set Type to Custom. (The PHP type only takes the path of a .php file; your line also contains the PHP command.)
- Paste the line from Step 1 into Command to run.
- Set the schedule to every minute, or enter * * * * *.
- Click Save. The job appears under List of Cron Jobs, where you can also read its last output.
Hostinger runs cron jobs in UTC. The Single plan allows 2 cron jobs; Premium and higher plans have no limit.
Namecheap
Open cPanel from your Namecheap hosting account, then follow these steps.
- Open cPanel and, in the Advanced section, click Cron Jobs.
- Under Add New Cron Job, choose Once Per Minute (* * * * *) in Common Settings.
- Paste the line from Step 1 into Command.
- Click Add New Cron Job. The job runs every minute from now on.
Namecheap does not allow cron jobs that run more often than every 5 minutes, or more than 5 cron jobs, on shared servers. Use Web-Cron below if you need every minute.
Bluehost
- Log in to the Bluehost Portal, click Websites, then Manage Site for your site.
- Open the Advanced tab, find Cron Jobs and click Manage, then Add Cron.
- Choose a frequency in Common Settings (or fill in Minute, Hour, Day, Month and Weekday).
- Paste the line from Step 1 into Command and click Add.
Bluehost's own documentation says shared hosting only runs cron jobs every 15 minutes or longer (one page says 5 minutes). Use Web-Cron below if you need every minute.
HostGator
In the Customer Portal open Websites → Manage Site → Advanced, then under Cron Jobs click Manage and Add Cron. The form has the same fields as cPanel, so the steps below apply. Or open cPanel → Advanced → Cron Jobs.
- Open cPanel and, in the Advanced section, click Cron Jobs.
- Under Add New Cron Job, choose Once Per Minute (* * * * *) in Common Settings.
- Paste the line from Step 1 into Command.
- Click Add New Cron Job. The job runs every minute from now on.
HostGator's help pages do not state the shortest interval allowed on shared hosting. If every minute is refused, use Web-Cron below.
GoDaddy
On your GoDaddy product page, next to Linux Hosting click Manage, then cPanel Admin, then follow these steps.
- Open cPanel and, in the Advanced section, click Cron Jobs.
- Under Add New Cron Job, choose Once Per Minute (* * * * *) in Common Settings.
- Paste the line from Step 1 into Command.
- Click Add New Cron Job. The job runs every minute from now on.
GoDaddy's help pages do not state the shortest interval allowed. If every minute is refused, use Web-Cron below.
DreamHost
- Open the DreamHost panel, go to Cron Jobs and click Add New Cron Job.
- Choose a user (an existing SFTP or shell user), give the job a title, and optionally an email address for its output.
- Paste the line from Step 1 into Command. DreamHost keeps PHP at paths such as /usr/local/php83/bin/php (use your PHP version).
- Set the timing and turn on locking so two runs never overlap, then save.
In custom timing the panel lets you pick at most 12 different minutes per hour, so the shortest regular schedule you can build is every 5 minutes. Times are in Pacific time. Use Web-Cron below if you need every minute.
SiteGround
- Open Site Tools → Devs → Cron Jobs.
- Choose the interval from the list (or set minute, hour, day, month and weekday).
- Paste the line from Step 1 into Command. An email address for the output is optional.
- Click Create.
SiteGround's documentation shows presets such as Twice Per Hour and does not state the shortest interval. If your plan has no every-minute option, use Web-Cron below.
- Open Settings → System and copy the Web-Cron URL. Keep it private: it contains your secret token.
- Create a free account at a cron service such as cron-job.org.
- Create a job, paste the URL and set it to run every minute. Save and activate it.
- Use either Web-Cron or a cron job from your host, not both: one is enough.
Wait one or two minutes, then reload Settings → System. The badge should say Active and Last Heartbeat should show a time from the last minute. Site Health on the same page should show the worker as Passed. If your host only allows a job every 5 or 15 minutes, the badge can switch between Active and Cron Not Detected, because it expects a run within the last 5 minutes; email still goes out whenever the worker runs.
- The PHP command is wrong. Ask your host for the PHP command-line path (for example /opt/alt/php83/usr/bin/php on Hostinger or /usr/local/bin/php on many cPanel hosts) and replace php at the start of the line.
- Only part of the line was pasted. Paste the whole line, including >/dev/null 2>&1 at the end.
- The folder is wrong. worker.php is in the folder that contains public, not inside public.
- To see the error, run the job once without >/dev/null 2>&1 and read its output. A working worker prints a line like worker.php: processed=0.
- The job is for another site on the same account. Check the folder in the command.
- Give it up to 5 minutes after saving before you check again.
For technical users
Email compilation, audience query resolution, delivery dispatch, and webhook processing operate asynchronously via a 1-minute background worker. MarbleSend employs a dual-entrypoint architecture (native CLI worker.php or HTTP cron.php) protected by non-blocking file locks to eliminate overlapping executions on resource-constrained shared hosting.
* * * * * php /home/USER/your-folder/worker.php >/dev/null 2>&1
Executes directly from the operating system shell (* * * * * php /path/to/worker.php >/dev/null 2>&1). Completely bypasses HTTP timeout and gateway limits, making it the ideal choice on cPanel, Plesk, and DirectAdmin.
GET https://your-domain.com/cron.php?token=SECURE_TOKEN
For hosting environments without shell crontab access, pinging GET /cron.php?token=SECURE_TOKEN via third-party services (e.g. cron-job.org or UptimeRobot) triggers the exact same queue processing safely.
The worker locks storage/worker.lock with flock(LOCK_EX | LOCK_NB) on start. If a previous run is still processing a heavy queue, subsequent ticks exit cleanly without race conditions or memory exhaustion.
Claims batches of 50 queue items atomically using indexed status queries. Failed deliveries incur exponential backoff with retry counters up to max_attempts before transitioning to failed.
6.3 Global 23 Locales, RTL Arabic & Admin Security Machine
MarbleSend is engineered as an internationally accessible self-hosted platform. It ships with 23 complete human-curated native language dictionaries, automatic Right-to-Left layout mirroring for Arabic, persistent tri-state theme customization, and an impenetrable single-tenant security perimeter.
Language files in app/lang/*.php are pure PHP arrays loaded into memory in sub-millisecond execution time, requiring zero gettext server binaries or external translation API dependencies.
Selecting Arabic (ar) automatically injects dir='rtl' into the HTML document root, mirroring navigation sidebars, table columns, drawer panels, and chevron iconography across the entire UI.
Allows seamless switching between Dark Palette, Light Palette, and System OS preference. The active selection is saved to cookies (marblesend_theme) to guarantee zero layout shift on page load.
Admin credentials stored in cm_settings.admin_user are encrypted with bcrypt work factor 12. Protects against session fixation via automatic session ID regeneration on login.
6.3.1 Two-Factor Authentication (TOTP) & QR Code Setup
Two-factor authentication adds a second lock to the admin account: after the password, MarbleSend asks for a 6-digit code that changes every 30 seconds and that only your phone can generate. It is optional, takes about a minute to enable, and works with any standard authenticator app such as Google Authenticator, Microsoft Authenticator, 1Password, Bitwarden, Authy or Aegis. You will find it at Settings → Language & Account → Two-factor authentication (TOTP).
MarbleSend uses the open TOTP standard (RFC 6238: HMAC-SHA1, 6 digits, 30-second steps). Enabling it creates a random secret that is shared once with your app. From then on, signing in takes the password and then a current code. A code that was already used cannot be replayed, and the second step expires after 5 minutes.
1) Open Settings → Language & Account and press Enable. 2) In your authenticator app choose “Scan QR code” and scan the square shown on screen. 3) Type the 6-digit code your app now shows and press “Confirm and enable”. 4) Save the eight recovery codes that appear once: copy them into a password manager or print them.
The QR image is drawn by your own installation as a small vector graphic; the secret is never sent to an online QR generator or any other third party. If you cannot scan (for example when the app is on the same phone), use the setup key or the otpauth link shown under the QR code. The secret is stored encrypted (AES-256-GCM) with your ENCRYPTION_KEY, like every other credential.
After the correct password you are asked for the 6-digit code. Wrong codes are rate-limited per account, so guessing is not practical. If codes are rejected even though you type them correctly, check that the phone’s date and time are set to automatic; a wrong clock produces wrong codes.
Eight one-time codes are created when you enable 2FA. Each one works once in the code field instead of the 6-digit code. They are stored only as hashes, so they cannot be displayed again; use “New recovery codes” (it needs a current code) to replace the whole set, which invalidates the old ones.
The emailed link for a forgotten password does not skip two-factor authentication, on purpose, so it cannot help here. On the server, run “php tools/reset-admin.php” (SSH or your hosting terminal): it resets the admin password, clears lockouts, ends every session and turns 2FA off, and you can then enable it again with the new device. To turn it off normally, use Disable in the same card (password plus a current or recovery code).
6.3.2 Signup Protection (Cloudflare Turnstile): Get Your Free Keys
Turnstile is Cloudflare’s free, privacy-friendly alternative to CAPTCHA. When it is enabled, your public signup pages (the /s/your-page forms and /subscribe) check that the visitor is a real person before a subscription is accepted, which stops bots from flooding your list and your confirmation emails. It works on top of the built-in honeypot and the per-IP and per-address limits, and it is optional, except while you send through the SES Relay (see the last note). You need two keys, a Site key (public) and a Secret key (private), from your own free Cloudflare account; your domain does not have to use Cloudflare for DNS.
Go to dash.cloudflare.com and sign up (it is free) or log in. You do not need to add your website to Cloudflare or change your DNS in order to use Turnstile.
In the dashboard menu open “Turnstile” (or search for it), then press “Add widget” (older dashboards say “Add site”). Menu names change from time to time; look for Turnstile under Application security.
Give it any name, for example “MarbleSend signup”. Under Hostname add the domain where MarbleSend runs, without https:// or a path, for example app.yourdomain.com. If your signup pages are shown on another domain, add that one too. Add localhost only while you test on your own computer.
Choose Managed (recommended): most visitors pass without any puzzle and Cloudflare asks for a quick click only when a visit looks suspicious. Non-interactive shows a short progress bar without any click, and Invisible shows nothing at all. Leave pre-clearance off, then press Create.
Cloudflare shows a Site key (public) and a Secret key (private). Copy both. The secret is used only by your server to confirm each visitor with Cloudflare, so never paste it anywhere public. You can view or rotate it later in the widget’s settings.
Open Settings → Language & Account → Signup protection (Cloudflare Turnstile), paste the Site key and the Secret key, and press Save. The badge changes to “Protection is ON”. To turn it off later, clear both fields and save. The saved secret is stored encrypted and never shown again; leave the field blank to keep it.
Open one of your public signup pages in a private browser window: the Turnstile widget appears above the button. Subscribe with a test address and you should receive the confirmation email. A submission that has not passed the check is rejected.
If your server cannot reach Cloudflare’s verification service (an outage or a blocked outbound connection), MarbleSend normally lets the signup through and logs the event instead of locking every visitor out; the honeypot and the per-IP and per-address limits still apply. While you send through the SES Relay it does the opposite and fails closed: signups are refused until Cloudflare is reachable again.
Cloudflare publishes dummy keys for testing: Site key 1x00000000000000000000AA with Secret key 1x0000000000000000000000000000000AA always passes. They never protect a real site, so replace them with your own keys before going live.
The widget does not appear: check that the hostname of the page is listed in the Turnstile widget and that no browser extension blocks challenges.cloudflare.com. “Invalid site key”: the Site key was copied with a space or belongs to another widget. Real visitors are rejected: paste the Secret key again; it must belong to the same widget as the Site key.
If you send through the Managed Amazon SES Relay, Turnstile is mandatory for public signup forms. Until both keys are saved, the signup page shows “sign-up is temporarily unavailable” and Settings → Language & Account shows a notice with a button that takes you to the right card. In that mode every new subscriber must also confirm by email (double opt-in), even if a signup page or an API call asks for single opt-in.
6.4 Scoped REST API Keys & Developer Programmatic Access
The REST API allows external websites, landing pages, mobile apps, and the official MarbleSend Forms WordPress plugin to securely interact with your contact database and broadcast engine. API keys use cryptographic prefixing, one-way SHA-256 storage, and granular capability scoping.
Keys are generated with an ms_ prefix followed by 32 cryptographically secure random bytes via random_bytes(24). The raw key is revealed only once during creation and can never be retrieved again.
Only the SHA-256 hash (hash('sha256', $rawKey)) is persisted in cm_api_keys.key_hash. Even if the database is read directly, active tokens cannot be decrypted or recovered.
Restrict keys to specific capabilities: contacts:read, contacts:write, campaigns:read, campaigns:write, campaigns:send, schedules:read, and schedules:write for tight least-privilege control.
Incoming requests authenticate via Authorization: Bearer ms_... headers evaluated using hash_equals(). Telemetry logs record the exact last_used_at timestamp on every successful API call.
6.5 Outbound Event Webhooks & HMAC-SHA256 Cryptographic Delivery
Contact events (subscribed, confirmed, unsubscribed, suppressed) now carry a groups array of {id, name} next to tags.
Webhooks provide real-time HTTP event streaming to external endpoints, allowing downstream CRM syncs, Slack notifications, or custom automation pipelines to react instantly to subscriber opt-ins, campaign broadcasts, unsubscribes, and hard bounces.
Subscribe to critical lifecycle triggers: contact.created, contact.unsubscribed, campaign.sent, campaign.bounced, and link.clicked with individual webhook toggling.
Every webhook POST request includes X-MarbleSend-Signature: sha256=... generated using hash_hmac('sha256', $payload, $secret), allowing receiving servers to verify authenticity.
Failed webhook deliveries (HTTP 5xx or connection timeouts) trigger exponential backoff retries. Endpoints with consecutive failures are automatically paused to prevent server overhead.
The Test Webhook button sends mock payloads immediately. The delivery history drawer displays HTTP response status codes, execution latency in milliseconds, and error messages.
6.6 The 4-Pillar Deliverability Suite (SPF, DKIM, DMARC, BIMI)
Email deliverability determines whether your messages land in the subscriber inbox or get diverted to spam. MarbleSend features a built-in DNS inspection engine that runs four sequential, non-blocking queries directly against authoritative nameservers without third-party API dependencies.
Queries root TXT records for v=spf1 syntax. Verifies authorized SMTP host mechanisms, flags DNS lookup limits (>10 lookups cause hardfail), and checks enforcement qualifiers (~all or -all).
Inspects selector._domainkey.domain.com to validate RSA or Ed25519 public cryptographic keys. Ensures outbound email headers can be cryptographically verified against sender domains.
Checks _dmarc.domain.com for policy tags (p=none, p=quarantine, or p=reject). In 2026, Gmail and Yahoo bulk sender requirements mandate active DMARC records with rua reporting.
Queries default._bimi.domain.com for SVG Tiny P.S. brand logo files and Verified Mark Certificates (VMC), enabling verified brand avatars directly in Gmail, Yahoo, and Apple Mail inboxes.
6.7 Dual-Mode AI Engine: BYOK Providers & MarbleSend AI Credits
MarbleSend provides a flexible dual-mode AI engine for copywriting, subject line generation, and spam score audits: (1) Bring Your Own Key (BYOK) for connecting directly to major LLM providers with zero markup, and (2) MarbleSend AI Credits for instant, pay-as-you-go access to curated, top-performing models without creating developer accounts or managing API keys.
Connect directly to OpenAI, Anthropic Claude, Google Gemini, DeepSeek, Moonshot, OpenRouter, or private Ollama instances. API keys are encrypted at rest using AES-256-GCM. Requests flow straight from your server to the provider's API with zero middleman fees or rate markups.
Pre-funded AI credits managed directly through MarbleSend. Features curated, high-efficiency models specially benchmarked for email marketing conversion and deliverability. The vendor server dynamically manages optimal model routing and upgrades, deducting 1 credit per generation atomically.
Official add-on pricing is retrieved dynamically from admin.marblesend.com. Purchase top-up credit packs via global and local payment gateways with instant real-time balance activation.
All BYOK credentials are encrypted at rest with hardware-accelerated AES-256-GCM using your installation's unique APP_KEY. No prompt contents, contact lists, or recipient emails are ever stored or logged by the license server.
6.7.1 Default Model Selection & Execution Routing
Defines how MarbleSend routes AI copywriting, subject suggestions, and deliverability reviews across the application. You can switch between curated vendor relays and direct custom BYOK providers.
Zero-configuration access to top-performing curated models specially benchmarked for email conversion. No external developer accounts or API keys required.
Direct server-to-vendor communication with zero markup. Select any configured provider and specific model identifier as your default generation engine.
Templates Studio, AI Subject Score Inspector, and AI Assist automatically execute against the active default selection with graceful fallback on credit depletion.
The active selection is persisted in cm_settings.ai_provider_default, ensuring synchronized execution across all admin sessions and worker tasks.
6.7.2 MarbleSend AI Credits & Managed Relay Hub
MarbleSend AI Credits provide an instant, pay-as-you-go infrastructure for email copywriting without managing third-party AI billing, quotas, or rate limits.
Every activated license receives complimentary starter AI credits dynamically retrieved from the license server, allowing immediate evaluation of AI Assist features.
Generations are debited at exactly 1 credit per request via cm_addon_ledger. Unique transaction UUIDs prevent duplicate deductions during network retries.
Credit packs (e.g. ai_100) are loaded dynamically from the vendor edge catalog. Payments are securely processed by our payment provider.
Webhook events instantly credit your account upon checkout completion. A manual 1-click refresh button provides immediate synchronization with the server.
6.7.3 Configured Providers & BYOK Infrastructure
Connect and orchestrate multiple external LLM providers with encrypted key storage, dual API protocols, and instant connection diagnostics.
Maintain simultaneous connections to OpenAI, Anthropic Claude, Google Gemini, DeepSeek, Kimi/Moonshot, OpenRouter, or private Ollama instances.
Supports standard OpenAI-compatible endpoints (/chat/completions) and Anthropic messages format (/v1/messages) with custom proxy Base URLs.
All BYOK API keys are encrypted at rest with hardware-accelerated AES-256-GCM. Keys are never exposed in HTML responses or system logs.
Test connection button sends a lightweight probe to verify credentials and report round-trip latency (ms) before deploying models into production.
How to Obtain API Keys Across Major Providers
MarbleSend connects directly to AI providers from your server via BYOK without third-party proxies. Below are the official developer portal links, generation steps, and model reference directories for each supported provider:
Local, Self-Hosted & Alternative Providers (Ollama, Groq, Mistral, GLM)
For local zero-cost AI, install Ollama (ollama.com), run your chosen model (e.g. ollama run llama3.1), set Base URL to http://localhost:11434/v1, select OpenAI format, and check "No Auth". For ultra-fast cloud inference, register at console.groq.com to generate free Groq API keys.
Configuring AI Providers in Settings (/settings)
Once you have obtained your API key and chosen model ID, configure it in MarbleSend with these simple steps:
From the main sidebar navigation, click Settings, then switch to the "AI Providers" tab (direct access at /settings).
Click one of the Quick Add preset buttons (OpenAI, DeepSeek, Gemini, Anthropic, OpenRouter, etc.) to auto-fill settings, or click "+ Add Provider" for custom setups.
Select protocol format: "OpenAI format" (standard for OpenAI, DeepSeek, Gemini, Groq, Ollama) or "Anthropic format" (for Anthropic Claude). Confirm the pre-filled Base URL.
Paste your secret API key into the field. MarbleSend stores all keys strongly encrypted with AES-256-GCM via Crypto.php with zero plain-text database exposure.
Enter your desired Model ID and context window limit, then click "Test Connection". MarbleSend performs a live ping and authentication handshake before saving.
Under "Default AI Model" at the top of the tab, select your configured provider and model, then click "Save Default". It is now active across Templates Studio!
6.8 Ed25519 Cryptographic License Grant Token Engine
MarbleSend verifies product authenticity using Ed25519 asymmetric cryptography via PHP's native libsodium extension. A signed License Grant Token is issued upon initial activation and validated locally without constant connection demands.
Validates the digital signature with sodium_crypto_sign_verify_detached(). Any manual tampering with database records immediately breaks the mathematical signature, ensuring absolute integrity.
The signed token embeds the licensed domain hostname, license tier (Regular or Extended), and active entitlements. Multi-domain moves can be initiated directly from the settings interface.
Temporary network dropouts or license server maintenance will never interrupt running email broadcasts or cron execution. MarbleSend provides a 7-day grace window before restricting dispatches.
If a license expires, MarbleSend enforces a graceful soft-lock: existing subscriber data, campaigns, reports, and export tools remain 100% accessible, while launching new campaigns is paused.
Once a day the worker asks the license server whether a newer version exists. It sends your purchase code and the installed version, exactly like the manual check, and nothing else. Turn it off with “Release updates” in this tab: no check is made and no update banner is shown. Development installs (APP_ENV=local) never make this call. Nothing about your contacts or Campaigns is ever sent.
“Changelog” (sidebar, above Documentation) lists every release with its upgrade guide. When a newer release exists you see a banner on the dashboard (a security update shows a red banner on every page and cannot be dismissed), a dot next to “Changelog”, and, if enabled, one email per version to the admin address through your default SMTP profile. Release notes are written in English.
Frequently Asked Questions & Diagnostics
Immediate solutions for the most common shared hosting issues.
7.1 Dashboard & Operational Diagnostics
7.1.1 Why does the Dashboard show "Worker Not Detected" or Inactive status?
The OS crontab is not firing, the PHP CLI path is wrong, or a stale worker lock is preventing execution.
The worker updates cm_settings.worker_last_run with a UTC timestamp on every tick. The dashboard flags inactive if now - worker_last_run > 180 seconds.
Go to Settings → System. Copy the exact crontab line. If CLI crontab is restricted, configure the Web-Cron URL in cron-job.org every 1 minute. If lock is stuck, verify storage/worker.lock.
7.1.2 Why do email open and click rates differ between Dashboard KPI cards and external SMTP provider logs?
Apple Mail Privacy Protection (MPP) pre-fetching, proxy caching, recipient image blocking, or adblockers stripping UTM parameters.
MarbleSend embeds a 1x1 transparent tracking GIF in outgoing emails to record cm_email_logs.opened_at. When recipients block remote images, opens are not recorded; Apple MPP pre-fetches images immediately upon arrival on Apple servers.
Treat Unique Clicks (CTR) as the primary indicator of engagement. MarbleSend routes all template links through public/click.php to record tamper-proof subscriber clicks.
7.2 Campaign Dispatch & Preflight Diagnostics
7.2.1 Why did my campaign pause or stop in the middle of sending?
Shared hosting web server or PHP CLI execution limits (max_execution_time 30s/60s) reached during large broadcast dispatches.
MarbleSend processes the queue in atomic batches of 50 recipients. When time limits approach, the worker gracefully exits and commits cm_email_queue progress to avoid memory exhaustion.
No manual intervention required! The next 1-minute cron heartbeat automatically claims the next batch of pending items from cm_email_queue with zero duplicate emails sent.
7.2.2 Why does the 6-Point Automated Preflight Check block launching my campaign?
Missing mandatory {{unsubscribe_link}}, invalid subject line, zero active recipients selected, or unclosed syntax tokens.
The Preflight Engine evaluates 6 critical compliance and deliverability vectors before allowing status transition to queued or sending.
Review the red warning badges in the campaign preflight modal. Insert {{unsubscribe_link}} (mandated by RFC 8058 and CAN-SPAM) and ensure selected tags match active contacts.
7.2.3 When does "Resend to Non-openers" become available, and how does it prevent subscriber fatigue?
The action button in Campaign Report is disabled while the broadcast is still sending or before a safe cooldown period has elapsed.
MarbleSend requires campaign status = completed and recommends waiting at least 24 to 48 hours to allow subscribers across timezones sufficient time to open.
Once completed, click "Resend to Non-openers". This creates a new child campaign targeting solely contacts where opened_at IS NULL, allowing you to test an alternate subject line.
7.3 Contacts, CSV Import & Bounce Management
7.3.1 Why did the CSV Importer skip rows or fail to import certain subscriber records?
Invalid RFC 5322 email syntax, duplicate email addresses with "Skip Duplicates" enabled, non-UTF-8 character encoding, or suppressed contacts.
MarbleSend inspects each row, validates syntax via filter_var(FILTER_VALIDATE_EMAIL), and checks against cm_contacts to prevent overwriting unsubscribed or bounced states.
Ensure your CSV file is saved as UTF-8 without BOM. Check column mapping (email, name, tags). Review the import progress modal for exact line numbers that were skipped.
7.3.2 How does MarbleSend handle Soft Bounces vs Hard Bounces?
Permanent delivery failures (5xx SMTP error codes: mailbox does not exist) versus temporary delivery failures (4xx SMTP error codes: mailbox full, rate limited).
A 5xx response immediately escalates the contact to status = bounced. A 4xx response records a soft bounce attempt in cm_email_logs and retries up to 3 times before marking bounced.
Do not attempt to reactivate hard-bounced contacts. Doing so will severely penalize your domain reputation with major mailbox providers like Gmail and Yahoo.
7.4 Template Rendering, Outlook & Email-Safe Design
7.4.1 Why does my email template look broken or misaligned in Microsoft Outlook desktop?
Microsoft Outlook for Windows uses the Microsoft Word (MSO) rendering engine, which does not support CSS Flexbox, Grid, margin auto, or HTML5 layout tags.
Word MSO only respects traditional table-based layouts (nested <table>, <tr>, <td>) with explicit pixel widths and inline styles.
Use MarbleSend's 8 built-in email-safe starter layouts. They use nested HTML tables and conditional MSO comments (<!--[if mso]>) to ensure bulletproof rendering across all Outlook versions.
7.4.2 Why does Gmail show "[Message clipped] View entire message" at the bottom of my emails?
The raw HTML file size of the email exceeds Gmail's strict 102 KB (104,448 bytes) payload limit.
When the HTML payload exceeds 102 KB, Gmail truncates the message, cutting off content and hiding the footer unsubscribe link and tracking pixel.
Host images on external HTTPS servers instead of embedding inline base64 image strings. Avoid redundant CSS. In Templates Studio, use the "Clean HTML" formatter before launching.
7.4.3 Why is {{news_posts}} empty or missing in my email?
The placeholder is only filled by a Newsletter schedule whose Content source is “News posts”. In a manual Campaign, an RSS schedule or a template-only schedule it is replaced with empty text.
The Campaign pre-flight check shows a warning when a template uses {{news_posts}} without a news post source; the preview in Templates → Posts always shows the filled block.
Send the template through a Newsletter schedule set to “News posts”, or remove the placeholder from templates used elsewhere.
7.5 Schedules, RSS Feeds & Worker Cadence
7.5.1 Why did my automated RSS schedule not send when new blog posts were published?
The feed URL was unreachable, feed XML syntax was malformed, or the latest post GUID matched cm_schedules.last_rss_item_id.
MarbleSend stores a SHA-256 hash of the latest seen item in last_rss_item_id. If no new <item> or <entry> exists, the worker skips execution without creating queue entries.
Open the schedule in /schedules and click "Test Feed" to inspect the live XML response. Verify the RSS feed produces valid <pubDate> and unique <guid> tags. Ensure next_run_at is due.
7.5.2 What happens to recurring schedules if the server or worker cron was offline for several days?
Multiple recurring occurrence intervals passed while the worker process was unable to run.
MarbleSend enforces a strict "Single Catch-Up Policy" in SchedulerService. Instead of queuing all accumulated missed runs (which would spam inboxes), it executes only the single most recent occurrence.
Once cron resumes, exactly 1 catch-up broadcast is compiled and dispatched. The schedule's next_run_at timestamp is automatically advanced to the next future scheduled date.
7.5.3 Why was my Ready post not sent by the News posts schedule?
The post became Ready before the schedule's News posts mode was switched on, it was already delivered by this schedule, or the audience or template blocked the run.
Open the schedule history: “no new posts” means nothing is eligible, “no recipients” means the audience was empty, and “failed” means the template is missing or lacks {{news_posts}}.
Mark a copy of the post Ready after the schedule exists, fix the audience or template, then use Run now.
7.5.4 Can two schedules send the same post?
Delivery is tracked per schedule, not per post.
Each News posts schedule keeps its own list of which posts it delivered, so the library shows the total “Sent N×”.
Yes — a post that is Ready after both schedules were created is sent once by each; neither schedule sends it twice.
7.6 Settings, Deliverability, Webhooks & Licensing
7.6.1 How do I diagnose SMTP error "535 Authentication Failed" or "550 Relay Access Denied"?
Incorrect SMTP credentials, wrong port and encryption mismatch (Port 587 STARTTLS vs Port 465 SSL), or unverified sender email address.
Settings → SMTP includes a live socket test that displays the raw dialogue between PHPMailer and your SMTP server.
Use Port 587 with TLS for most relays. For Gmail/Google Workspace, generate an App Password. For Amazon SES, verify the sender domain. For cPanel, use the full email address as the username.
7.6.2 Why does the Deliverability Suite report SPF, DKIM, or DMARC validation errors?
Missing DNS records on the sending domain, exceeding the 10-lookup limit in SPF, or selector prefix mismatch in DKIM.
MarbleSend performs sequential non-blocking DNS queries (dns_get_record) directly to authoritative nameservers for SPF (root TXT), DKIM (selector._domainkey), and DMARC (_dmarc).
Add your SMTP relay's include mechanism to your SPF record. Verify DKIM selector matches your relay provider recommendations. Ensure DMARC record contains a valid rua reporting address.
7.6.3 How does the Webhook Circuit Breaker work, and how do I reactivate a paused endpoint?
Your destination endpoint returned repeated HTTP 5xx errors, connection timeouts (>10s), or 404 Not Found responses.
To prevent server queue congestion, MarbleSend tracks delivery attempts and automatically pauses the webhook (enabled = 0) after repeated consecutive failures.
Open the Webhook Delivery History drawer to inspect HTTP status codes and error payloads. Once your receiving server is fixed, click "Test Webhook", verify HMAC signature, and re-enable.
7.6.4 How does the Ed25519 License verification work, and what is the 7-Day Offline Grace window?
Network connectivity dropouts, DNS resolution failures, or temporary downtime on the upstream licensing server.
License authenticity is verified locally in memory using libsodium sodium_crypto_sign_verify_detached(). Tampering with database records breaks the mathematical cryptographic signature.
MarbleSend provides a 7-day offline grace window (grace_until). Active broadcasts continue without disruption. If expired, existing subscriber data and reports remain 100% accessible under soft-lock.
WordPress & Third-Party Form Integrations
Collect subscribers on a WordPress site and send them to MarbleSend. The companion plugin has its own subscribe form and connects WPForms, Contact Form 7, Fluent Forms, Formidable Forms and Forminator.
Form Integration Inspector
Select a component to see its hook, what it sends and a tip for setting it up.
REST API bridge
No shared databaseDelivery queue
Transient storageMarbleSend Form
Shortcode, Block & WidgetWPForms hook
wpforms_process_completeContact Form 7 hook
wpcf7_mail_sentFluent Forms hook
submission_insertedFormidable Forms
frm_after_create_entryForminator hook
submit_before_set_fieldsTag autoresponders
Tags and welcome emailsInstall the companion plugin first (marblesend-forms.zip)
MarbleSend runs on its own server, so WordPress needs a small plugin to send it subscribers. The plugin serves the built-in MarbleSend form and connects WPForms, Contact Form 7, Fluent Forms, Formidable Forms and Forminator. It's free, needs WordPress 6.3 or newer and PHP 7.4 or newer, and talks to MarbleSend only through the REST API.
Where do the Settings menus appear?
After you activate the plugin, WordPress shows two pages under Settings in wp-admin: MarbleSend Forms (connection URL, API key, default tags and the failure log) and MarbleSend Integrations (field mapping for each form plugin). Both live in WordPress. The MarbleSend dashboard doesn't have them. Your own forms, with their group, tags and spam protection, are managed under MarbleSend Forms in the sidebar.
1. Download marblesend-forms.zip
Download marblesend-forms.zip from Settings → License in MarbleSend, or directly from https://marblesend.com/download/marblesend-forms.zip. It is a ready-to-install WordPress plugin zip: upload it as it is, without unzipping it.
2. Install and activate in WordPress
In wp-admin go to Plugins → Add New → Upload Plugin, choose marblesend-forms.zip, click Install Now, then Activate Plugin. To update later, upload the newer zip the same way. WordPress offers to replace the installed version, and your settings stay.
3. Connect the API key (contacts:write)
In MarbleSend, open Settings → API & Webhooks and create an API key with the contacts:write scope. In WordPress, open Settings → MarbleSend Forms, enter your MarbleSend Site URL and the key, save, and click Test Connection. It should show Connected with the scope contacts:write. Then choose a Default Group and Default Tags from the lists loaded from MarbleSend, or create a new group there.
4. Map your forms
In wp-admin open Settings → MarbleSend Integrations. Every form plugin that is active appears with its forms. Tick Enabled, enter the email field and, if the form has one, the name field, add tags if you want, and save. The built-in form needs no mapping.
8.1 How the plugin talks to MarbleSend
The plugin calls the MarbleSend REST API over HTTPS, so WordPress and MarbleSend can run on different servers. It has no access to the MarbleSend database.
WordPress never reads MarbleSend's tables, and MarbleSend never reads wp_posts or wp_users. The two can run on separate hosts with separate databases.
Every request carries a Bearer API key from Settings → API & Webhooks. The contacts:write scope is enough, so the key can't send campaigns or change schedules.
Test Connection in Settings → MarbleSend Forms calls GET /api/verify-key and shows the scopes of the key, so a wrong URL or key shows up right away.
Each submission goes to POST /api/contacts. A new email becomes a contact. An existing one keeps its status and gets the new tags added to its current ones; its name changes only when the form sends one. With double opt-in on, a new contact stays pending until they confirm.
curl -X POST https://your-domain.com/api/contacts \
-H "Authorization: Bearer ms_live_abc123..." \
-H "Content-Type: application/json" \
-d '{"email":"visitor@example.com","name":"John Doe","tags":["newsletter","source:wpforms"]}'
Optional: add "group_ids":[1,2] to put the contact in specific groups (list them with GET /api/groups; omitted = General). GET /api/contacts accepts group_id to filter by group.
8.2 Delivery queue and WP-Cron
A slow or unreachable MarbleSend shouldn't make a visitor's form hang or lose a lead. Submissions from form plugins are queued and sent in the background.
When a form plugin submits, MarbleSend_Integration_Base::dispatch() cleans the fields and stores them in a WordPress transient (set_transient). No request to MarbleSend happens while the visitor waits.
One WP-Cron event, marblesend_forms_process_queue, is scheduled for about a minute later and sends the queued submissions.
WP-Cron sends each queued item once. The built-in form calls MarbleSend directly first and queues the submission for one WP-Cron attempt only if that call fails. A queued item is kept for up to one hour.
If sending fails, or an item expires before WP-Cron could send it, the plugin adds a line to marblesend_forms_log (the last 50 records are kept) and shows an admin notice in wp-admin. Settings → MarbleSend Forms lists the log and the number of pending items.
// Non-blocking submission buffer in WordPress
set_transient('marblesend_forms_queue_' . wp_generate_uuid4(), $payload, 3600);
if (!wp_next_scheduled('marblesend_forms_process_queue')) {
wp_schedule_single_event(time() + 5, 'marblesend_forms_process_queue');
}
8.3 The built-in subscribe form
The plugin includes a subscribe form, so you don't need a form builder. Add it with a shortcode, a Gutenberg block or a widget. All three run the same code.
Use [marblesend_form], the Gutenberg block marblesend/subscribe-form, or the MarbleSend Subscribe Form widget. The shortcode takes tags, show_name (1 or 0) and consent (the text of a required checkbox). The block and the widget offer the same options. Build your own forms under MarbleSend Forms → Add New Form and embed one with [marblesend_form id="12"].
An off-screen honeypot field (marblesend_hp), a nonce requested when the form is submitted so a cached page can never serve an expired one, and a limit of 5 submissions per 60 seconds per IP (stored hashed). A request that fills the honeypot or lacks a valid nonce gets a normal-looking success message and is dropped. Too many requests get a short "please wait" message. Forms built in the editor can also use Cloudflare Turnstile or Google reCAPTCHA, set in Settings → MarbleSend Forms.
The page doesn't reload. The plugin sends the contact to MarbleSend right away (5-second timeout). If MarbleSend can't be reached, or the plugin isn't configured yet, the submission goes to the queue for one WP-Cron attempt and the visitor still sees the success message. If the request itself fails, the form shows an error and can be sent again.
When the visitor gives only an email, a new contact is named after the part before the @, because the API needs a name. An existing contact keeps its name. Every contact gets the tag source:wp-form plus the tags from the form and from Settings → MarbleSend Forms. With double opt-in on, the visitor sees a message asking them to check their email. The contact joins the group of the form, or the Default Group from Settings → MarbleSend Forms.
[marblesend_form tags="newsletter,inbound" show_name="1" consent="I agree to receive email updates and accept the privacy policy."]
8.4 WPForms (Lite & Pro)
WPForms is one of the most used form builders. The plugin reads the submission once WPForms has validated it, so you need no WPForms add-on.
The plugin detects WPForms with function_exists('wpforms') and listens to the wpforms_process_complete action (priority 10), which receives the validated fields.
WPForms identifies fields by number. Select a field in the builder and read its ID at the top of the field options, for example Field ID #2. Enter that number in the mapping. A name field with first and last name is joined into one name.
1. Note the ID of the Email field and, if there is one, the Name field. 2. Open Settings → MarbleSend Integrations and find WPForms. 3. Tick Enabled, enter the IDs and any tags. 4. Save.
Every contact from WPForms gets the tag source:wpforms next to your own tags, so you can send a campaign to just that group.
add_action('wpforms_process_complete', function($fields, $entry, $form_data, $entry_id) {
$email = $fields[1]['value'] ?? ''; // Numeric Field ID #1
$name = $fields[0]['value'] ?? ''; // Numeric Field ID #0
MarbleSend_Integration_Base::dispatch('wpforms', $form_data['id'], $email, $name);
}, 10, 4);
8.5 Contact Form 7 (CF7)
Contact Form 7 defines its fields as tags in a text template. The plugin hooks in after the form's email has been sent.
The plugin detects CF7 with class_exists('WPCF7_ContactForm') and listens to wpcf7_mail_sent (priority 10). A lead is forwarded only after the form passed the spam and validation checks and Contact Form 7 sent its own email. If email sending fails on your hosting, nothing is forwarded.
CF7 names a field in its form tag, for example [email* your-email] and [text your-name]. Enter the name (your-email, your-name) in the Email Field and Name Field boxes.
The plugin reads the posted data with WPCF7_Submission::get_instance()->get_posted_data(), which works with CF7's AJAX submit.
1. In Contact → Contact Forms open the form and note the tag names. 2. In Settings → MarbleSend Integrations find Contact Form 7. 3. Tick Enabled, enter your-email and, if the form has one, your-name, and add any tags. 4. Save.
add_action('wpcf7_mail_sent', function($contact_form) {
$submission = WPCF7_Submission::get_instance();
$data = $submission ? $submission->get_posted_data() : [];
$email = $data['your-email'] ?? '';
$name = $data['your-name'] ?? '';
MarbleSend_Integration_Base::dispatch('cf7', $contact_form->id(), $email, $name);
}, 10, 1);
8.6 Fluent Forms (Free & Pro)
The plugin listens to the action Fluent Forms fires after it saves an entry.
The plugin detects Fluent Forms with function_exists('wpFluent') and listens to fluentform/submission_inserted (priority 10), which receives $entry_id, $form_data and $form.
Fluent Forms identifies fields by their Name Attribute, for example email for Email and names for the Name field (first and last name are joined). Find it in the field settings under Advanced Options.
The Integrations page lists your forms by reading Fluent Forms' own form table, so you don't have to look up form IDs.
1. Check the Name Attribute of the email and name fields. 2. In Settings → MarbleSend Integrations, tick Enabled for the form and enter both. 3. Add tags and save. New contacts get the tag source:fluentforms.
add_action('fluentform/submission_inserted', function($entryId, $formData, $form) {
$email = $formData['email'] ?? '';
$name = is_array($formData['names'] ?? null) ? implode(' ', $formData['names']) : ($formData['first_name'] ?? '');
MarbleSend_Integration_Base::dispatch('fluentforms', $form->id, $email, $name);
}, 10, 3);
8.7 Formidable Forms (Lite & Pro)
The plugin reads the entry right after Formidable Forms saves it.
The plugin detects Formidable with class_exists('FrmForm') and listens to frm_after_create_entry (priority 30), which receives $entry_id and $form_id.
Formidable assigns a numeric Field ID (e.g. 12) to every field. Enter it into the mapping; field keys are not supported. Data is extracted via FrmEntry::getOne($entry_id)->metas.
The plugin runs after the entry is created, so only submissions that passed Formidable's validation are forwarded.
1. In Formidable builder, click the email field and note its Field ID. 2. In Settings → MarbleSend Integrations, enable form and enter the Field ID. 3. Save mapping. Ingested leads receive tag source:formidable.
add_action('frm_after_create_entry', function($entryId, $formId) {
$entry = FrmEntry::getOne($entryId, true);
$email = $entry->metas[12] ?? $entry->metas['email-address'] ?? '';
$name = $entry->metas[11] ?? $entry->metas['full-name'] ?? '';
MarbleSend_Integration_Base::dispatch('formidable', $formId, $email, $name);
}, 30, 2);
8.8 Forminator (Free & Pro)
The plugin listens to the hook Forminator fires while it processes a submission.
The plugin detects Forminator with class_exists('Forminator_API') and listens to forminator_custom_form_submit_before_set_fields (priority 10), which receives $entry, $form_id and $field_data.
Forminator gives every field an element ID such as email-1, name-1 or text-1. Enter these IDs in the mapping. The form editor shows them.
The Integrations page lists your Forminator forms by name, read with Forminator_API::get_forms().
1. Open Forminator and note the element IDs (email-1, name-1). 2. In Settings → MarbleSend Integrations, find Forminator and enter the IDs. 3. Add tags and save. 4. Send a test submission and check that the contact has the tag source:forminator.
add_action('forminator_custom_form_submit_before_set_fields', function($formId, $response, $fieldData) {
$fields = array_column($fieldData, 'value', 'name');
$email = $fields['email-1'] ?? '';
$name = $fields['name-1'] ?? '';
MarbleSend_Integration_Base::dispatch('forminator', $formId, $email, $name);
}, 10, 3);
8.9 Tags, segments and autoresponders
Contacts from the WordPress plugin join the group chosen in Settings → MarbleSend Forms (Default Group) or on the form, and land in General when none is chosen. Use tags to segment them, or move them into groups from Contacts.
Capturing an email is the first step. Tags decide what happens to the contact next.
Besides your own tags, every contact gets source:<slug>: source:wp-form, source:wpforms, source:cf7, source:fluentforms, source:formidable or source:forminator. Use them to send a campaign to the subscribers of one form plugin.
In /contacts → Autoresponders, create a rule for the tag you assigned to your WordPress form. When the contact becomes active (right after submitting, or after confirming the double opt-in), MarbleSend queues your welcome email with the template and SMTP profile you chose.
The plugin sends leads straight to your own MarbleSend installation. A service such as Zapier or Make isn't part of the path.
With double opt-in on, new contacts are stored as pending with consent_source = 'api'. MarbleSend emails a confirmation link ({{confirm_link}}). When the contact clicks it, the status changes to active and the time and IP address are recorded for GDPR.
{
"email": "lead@agency.com",
"name": "Jane Smith",
"tags": ["source:wpforms", "ebook-download", "vip-inbound"],
"consent_source": "api"
}
8.10 Troubleshooting
Common problems when connecting WordPress forms, with the cause, how to check it and how to fix it.
Why do submissions show up late, or not at all, in MarbleSend?
WP-Cron runs only when someone visits the site. On a quiet or staging site, queued submissions wait for the next visit. A queued submission is kept for one hour. If WP-Cron doesn't run in that time, the submission is lost and marblesend_forms_log records that one expired (it can't name the visitor, because the stored data has expired too).
Open Settings → MarbleSend Forms in wp-admin. The pending count shows items waiting for marblesend_forms_process_queue. A log entry saying a queued submission expired means WP-Cron didn't run for over an hour.
Open https://your-site.com/wp-cron.php?doing_wp_cron to run it once, or set up a real server cron job (* * * * * wget -q -O - https://your-site.com/wp-cron.php >/dev/null 2>&1).
Why does the log say "Skipped: no valid email in form submission"?
The email field in the mapping has a typo, or the visitor sent an empty or invalid address that sanitize_email() rejected.
MarbleSend_Integration_Base::log_skip() writes an entry to marblesend_forms_log whenever an enabled form produces no valid email.
Check the identifier your form builder uses: a number for WPForms (for example 2) and Formidable Forms, the tag name for Contact Form 7 (your-email), the Name Attribute for Fluent Forms (email) and the element ID for Forminator (email-1).
Why does WordPress report an invalid API key or a missing scope?
The key in WordPress was revoked, expired, deleted in MarbleSend, or created without the contacts:write scope.
Test Connection calls GET /api/verify-key. An HTTP 401 or 403 answer means authentication failed.
In MarbleSend open Settings → API & Webhooks and create a new key with the scope contacts:write (or admin). Copy the token that starts with ms_ into Settings → MarbleSend Forms in WordPress.
Why do Contact Form 7 leads go missing when the form shows an error?
The plugin forwards a lead only after Contact Form 7 has sent its own email. When the email fails, CF7 shows its error message and the lead isn't forwarded.
Submit the form yourself. If you see "There was an error trying to send your message", CF7 couldn't send its email.
Fix email sending on your WordPress site, for example with an SMTP plugin. Once CF7 can send its email, the lead reaches MarbleSend.
Why is a contact named after its email address?
The form has no name field, or no name field is mapped. MarbleSend's API requires a name, so a new contact gets the part of the email before the @.
Look at the form's row in Settings → MarbleSend Integrations: the Name Field box is empty. A contact that already existed isn't affected, because it keeps the name it has.
Add a name field to the form and enter it in the mapping. Contacts that already exist keep their current name; change it in /contacts if you need to.