MailOdds

Salesforce

Salesforce + MailOdds

Native Apex integrations for email validation, sending, campaigns, deliverability monitoring, and content classification. No middleware required. 35+ production-ready patterns with Named Credential authentication.

Setup time: 15-30 min
Difficulty: Advanced
1,000 free validations included

Prerequisites

  • MailOdds account with API key
  • Salesforce Enterprise Edition or higher
  • Named Credential configured for api.mailodds.com
  • Apex development access (System Administrator or Developer profile)

Getting Started

MailOdds is an email validation, sending, and deliverability API. Connect it to Salesforce to validate lead emails, send transactional messages, monitor sender reputation, and protect revenue.

1

Create a MailOdds account

Sign up at signup.mailodds.com/register. The free tier includes 1,000 validations per month.

2

Copy your API key

Go to Dashboard > Settings > API Keys. Copy the key starting with mo_live_.

3

Create a Named Credential in Salesforce

A Named Credential is Salesforce's secure way to store API URLs and authentication. Configure it once, then every Apex class references it as callout:MailOdds. See the detailed setup steps below.

4

Deploy your first pattern

Start with the Lead Intake Gate pattern. It validates emails on lead creation and auto-corrects typos. Copy the Apex code, deploy it, and create a trigger to call it.

Data Flow: Salesforce and MailOdds

Salesforce to MailOdds

  • Lead/Contact emails for validation
  • Bounce events for suppression sync
  • Campaign members for list sync
  • Email content for spam checks
  • Case content for classification
Authentication
callout:MailOdds
Named Credential
HMAC-SHA256 webhooks
REST JSON TLS

MailOdds to Salesforce

  • Validation results with action/status
  • Webhook events (open/click/bounce)
  • Sender health and reputation scores
  • DMARC compliance and trend data
  • Blacklist alerts and event timelines

Named Credential Setup

All integrations below use a single Named Credential. Configure it once, then reference it as callout:MailOdds in every Apex class.

1

Create a Named Credential

Setup > Named Credentials > New. Label: MailOdds. URL: https://api.mailodds.com. Identity Type: Named Principal. Authentication Protocol: No Authentication (we pass the key in the header).

2

Add the Authorization Header

In the Named Credential, enable "Generate Authorization Header" = false. Instead, add a Custom Header: Authorization = Bearer mo_live_YOUR_KEY.

3

Add a Remote Site Setting

Setup > Remote Site Settings > New. Name: MailOdds_API. URL: https://api.mailodds.com. Active: checked.

4

Create Custom Fields

On Lead/Contact: Email_Validation_Status__c (Picklist), Email_Validation_Action__c (Text), Email_Sub_Status__c (Text), Email_Validated_At__c (DateTime).

Field Mapping Reference

How Salesforce fields map to MailOdds API fields. Use this as a reference when creating custom fields and writing Apex code.

Salesforce FieldMailOdds API FieldDirection
Lead.Email / Contact.Emailemail in POST /v1/validateSF to MO
Email_Validation_Status__cresult.statusMO to SF
Email_Validation_Action__cresult.actionMO to SF
Email_Sub_Status__cresult.sub_statusMO to SF
Email_Validated_At__cSet to Datetime.now() on responseSF internal
Engagement_Score__c (Lead)Computed from webhook eventsMO to SF
Last_Upsell_Message_Id__c (Opp)result.message_id from POST /v1/deliverMO to SF
MailOdds_Config__c.Webhook_SecretHMAC verification keyShared secret

API Rate Limits by Plan

Free
200/min
Starter
500/min
Growth
750/min
Pro
1,000/min
Business
2,000/min
Enterprise
5,000/min

Every response includes X-RateLimit-Limit and X-RateLimit-Remaining headers. On 429, honor the Retry-After header. Salesforce governor limits (100 callouts/transaction) will usually be your bottleneck before hitting API rate limits.

Webhook Signature Verification

All webhook handlers in the patterns below reference this shared utility. Deploy it once and every @RestResource endpoint gets HMAC-SHA256 (a cryptographic signature that proves the webhook came from MailOdds and has not been tampered with) verification. Store your webhook secret in MailOdds_Config__c.Webhook_Secret__c (Protected Custom Setting).

Apex: WebhookVerifier Utility

APEX
// Reusable HMAC-SHA256 Webhook Verification Utility
// All webhook handlers call WebhookVerifier.verify(req)
public class WebhookVerifier {
    public static Boolean verify(RestRequest req) {
        String signature =
            req.headers.get('X-MailOdds-Signature');
        String secret = MailOdds_Config__c.getOrgDefaults()
            .Webhook_Secret__c;
        if (String.isBlank(signature)
            || String.isBlank(secret)) return false;

        Blob expectedMac = Crypto.generateMac(
            'HmacSHA256',
            req.requestBody,
            Blob.valueOf(secret)
        );
        String expected =
            EncodingUtil.convertToHex(expectedMac);
        return constantTimeEquals(signature, expected);
    }

    // Prevent timing attacks
    private static Boolean constantTimeEquals(
        String a, String b
    ) {
        if (a.length() != b.length()) return false;
        Integer diff = 0;
        for (Integer i = 0; i < a.length(); i++) {
            diff |= a.charAt(i) ^ b.charAt(i);
        }
        return diff == 0;
    }
}

Integration Patterns

15 production-ready patterns, from lead intake gating to content classification and blacklist monitoring. Each includes working Apex code.

Validate and Gate

Lead Intake Gate with Typo Auto-Correction

Sales Dev Intermediate

Validates emails on lead creation using @future(callout=true) (Apex annotation that runs HTTP calls in the background, outside the triggering transaction), auto-corrects typos via the suggested_email field, and quarantines rejects to prevent bad data from reaching your pipeline.

POST /v1/validateenhanced depthsuggested_email field

Division-Specific Validation Policies

RevOps Advanced

Apply different validation rules per business unit using Custom Metadata Type mappings and a policy_id (a reference to a validation rule set you create in MailOdds to customize pass/fail criteria). Enterprise divisions can enforce strict reject-on-risky while SMB can allow catch-all (a mail server that accepts messages for any address at the domain, even ones that do not exist) addresses.

POST /v1/validatepolicy_id parameterCustom Metadata mapping
Bulk Operations

Nightly Database Scrub

Marketing Ops Advanced

Schedule a nightly bulk validation job via the API, then use Platform Events (Salesforce's real-time publish-subscribe messaging system for decoupled integrations) to write results back as they complete. Webhook-driven writeback eliminates polling.

POST /v1/jobscallback_url webhooksPlatform Events
Suppression and Hygiene

Bounce-to-Suppression Pipeline

Deliverability Intermediate

Automatically sync hard bounces from Pardot/MCAE into the MailOdds suppression list, creating a shared blocklist across your email infrastructure.

POST /v1/suppressionbounce event sync

Account Email Health Score

RevOps / CS Advanced

Compute an aggregate email health score for each Account by batch-validating all Contact emails. Surface the score on the Account record for CSMs and AEs.

POST /v1/validate/batchenhanced depthaggregate scoring

Two-Tier Cost-Optimized Validation

Finance / Ops Intermediate

Run standard (free) validation on all inbound leads. Only spend credits on enhanced SMTP validation (standard checks syntax and DNS only; enhanced adds SMTP mailbox verification for higher accuracy) for leads that pass qualification. Typical savings: 72%.

POST /v1/validatedepth parameter (standard/enhanced)
Domain and Content

Domain Blocklist via Policies

Sales Ops Intermediate

Block competitor and freemail domains at the gate. Apply different filtering rules per lead source using validation policies with domain filter rules.

POST /v1/validatepolicy_iddomain_filter rules

Data Quality Command Center

RevOps Leadership Intermediate

Pull telemetry data into native Salesforce dashboards. Track validation volume, hit rates, and credit consumption with ETag caching for efficiency.

GET /v1/telemetry/summaryETag caching
Pipeline Protection

Conversion Guard with Suppression Gate

Sales Ops Advanced

Double-gate at Lead conversion: check the suppression list first, then re-validate the email. Prevents converting leads with known-bad addresses into Contacts and Opportunities.

POST /v1/suppression/checkPOST /v1/validateconversion blocking

Campaign Pre-Send Audit

Marketing Ops Advanced

Run a pre-flight validation audit on campaign member lists before sending. Generate a risk report with a Go/No-Go recommendation based on valid-email ratios.

POST /v1/validate/batch or POST /v1/jobsrisk scoring
Monitoring

Blacklist Monitor with Auto-Case Creation

Deliverability / IT Intermediate

Monitor your sending IPs and domains against DNS blacklists. A scheduled Apex job checks monitor status and auto-creates a Case when a listing is detected, with full blacklist details for remediation.

POST /v1/blacklist-monitorsGET /v1/blacklist-monitorsPOST .../checkGET .../history

Spam Check Pre-Flight Gate

Marketing Ops Intermediate

Run a spam score check before sending any transactional or campaign email from Salesforce. Checks domain reputation, link safety, and subject line quality. Blocks sends that score above a configurable threshold.

POST /v1/spam-checksdomain_reputationlink_safetysubject_analysis

Content Classification for Case Routing

Service / Support Advanced

When an Email-to-Case arrives, run LLM-powered content classification to detect purchase intent, technical issues, or complaints. Auto-route Cases to the right queue and set priority based on content signals.

POST /v1/content-checkstatus: clean/warning/riskyflag / reasons / signals

SMTP Server Health Audit

IT / Deliverability Intermediate

Run SMTP handshake tests and MX configuration audits for your sending domains. A scheduled job checks domain health weekly and creates Tasks when STARTTLS, certificate, or MX issues are detected.

POST /v1/server-testsGET /v1/server-testsSMTP handshake + MX audit

Message Event Timeline Query

Sales / AE Intermediate

Query the full delivery and engagement timeline for any message_id stored on a Salesforce record. Returns delivered/bounced status, human vs. bot opens, clicks with URLs, and unsubscribe status. Useful for debugging delivery issues without parsing webhook logs.

GET /v1/message-events?message_id=summary: delivered/bounced/human_opens/bot_opens/clicksbot detection

Revenue Intelligence Patterns

10 patterns that turn email delivery signals into pipeline actions. Webhook-driven engagement scoring, bounce forensics, deferral monitoring, and transactional sending with structured data.

Understanding is_bot and is_mpp in Webhook Events

is_bot = true when the event came from a security scanner, link prefetcher, or corporate email gateway (not a human). Common with Barracuda, Proofpoint, and Mimecast.

is_mpp = true when the event came from Apple Mail Privacy Protection, which pre-fetches all images and inflates open counts. Affects roughly 50% of iOS/macOS mail users.

Both fields are Booleans on engagement events (opened, clicked). Always guard with == true since they may be absent on non-engagement events.

Additional Custom Fields for Revenue Intelligence

Lead: Engagement_Score__c, Last_Engagement_Message_Id__c, Last_Click_URL__c
Contact: Emails_Sent_Count__c, Human_Opens_Count__c, Last_Human_Open_At__c
Opportunity: Email_Policy_Id__c, Last_Upsell_Message_Id__c, Proposal_View_Count__c, Proposal_Click_Count__c, Proposal_Last_Viewed_At__c, Last_Clicked_Link__c, Sending_Domain_Id__c
Campaign: MailOdds_List_Id__c
Custom Settings: MailOdds_Config__c (Webhook_Secret, Deferral_Threshold), Active_Sending_Config__c (Primary_Domain_Id, Fallback_Domain_Id, Min_Grade_Threshold)
Custom Objects: Email_Bounce_Log__c, Email_Deferral_Log__c
Engagement Scoring

AI-Bot Filtered "Hot Lead" Routing

Sales Dev Advanced

Receive webhook click events, verify HMAC-SHA256 (a cryptographic signature that proves the webhook came from MailOdds and has not been tampered with) signatures, filter bot and Apple MPP noise, then bump engagement scores on Leads. Creates a high-priority Task when the score crosses a configurable threshold.

Webhook: message.clickedis_bot / is_mpp filterHMAC-SHA256 signature verification
Suppression and Reactivation

Programmatic "Win-Back" via Suppression Audit

Sales Ops Advanced

Poll the suppression audit log on a schedule, find hard-bounced contacts, cross-reference Accounts for alternate contacts, and create win-back Tasks for the sales team.

GET /v1/suppression/auditevent_type / event_category filterpagination
Sending

Schema-Driven Transactional Upsell

RevOps / E-Commerce Advanced

Fire a transactional upsell email with JSON-LD structured data when an Opportunity closes. Uses campaign_type, schema_data, and ai_summary for rich inbox rendering. Stores the returned message_id for engagement tracking.

POST /v1/delivercampaign_typeschema_dataai_summaryoptions.validate_first

Dynamic Sender Reputation Balancing

Deliverability / IT Intermediate

Poll your sending domain identity score on a schedule. When the grade drops below a configurable threshold, auto-switch to a fallback domain and alert the team with a full breakdown.

GET /v1/sending-domains/{id}/identity-scoregrade / score / breakdown
Bounce Forensics

Pre-Emptive Revenue Protection

Sales Leadership Advanced

Block Opportunity Close-Won on high-value deals when the primary contact email is unvalidated or rejected. Creates an smtp_required policy via preset, validates with policy_id, and creates an urgent Task on rejection.

POST /v1/policies/from-preset (smtp_required)POST /v1/validate with policy_idtrigger gate

Bounce Forensics Pipeline

Deliverability Advanced

Receive bounce webhooks with full SMTP forensics: bounce_type, smtp_code, enhanced_status_code, smtp_response, mx_host, and isp. Logs to a custom object and auto-suppresses hard bounces.

Webhook: message.bouncedbounce_type / smtp_code / enhanced_status_codemx_host / isp / smtp_response
Engagement Scoring

Engagement-Decay Re-Validation

Marketing Ops Advanced

Track delivered and human-opened events via webhook (filtering is_bot and is_mpp). A scheduled job finds "zombie" contacts with sends but zero human opens, then re-validates to detect stale addresses.

Webhook: message.delivered + message.openedis_bot / is_mpp filterPOST /v1/validate re-check
Sending

Campaign List Sync

Marketing Ops Intermediate

When a Campaign becomes Active, create a MailOdds subscriber list and sync all members. Uses Queueable (Salesforce async job pattern that allows chaining multiple jobs with separate governor limit budgets) chaining to handle lists larger than the 100-callout governor limit.

POST /v1/listsPOST /v1/lists/{id}/subscribersQueueable chaining
Engagement Scoring

Proposal Engagement Tracker

Sales / AE Advanced

Companion to the Transactional Upsell pattern. Matches incoming webhook open/click events by message_id to the Opportunity record. Filters bots and creates a high-priority Task for the AE on first human open.

Webhook: message.opened + message.clickedmessage_id join keyis_bot / is_mpp filter
Bounce Forensics

Deferral Early Warning System

Deliverability / IT Intermediate

Receive deferral webhooks with smtp_code, smtp_response, mx_host, isp, and attempts. Logs to a custom object and fires an alert when any ISP exceeds a configurable hourly deferral threshold.

Webhook: message.deferredsmtp_code / smtp_response / isp / attemptsISP threshold alerting

Send, Measure, and Protect: Full Platform Access

MailOdds is a full-cycle email platform. Use these Apex patterns to send transactional and campaign email, monitor sender health and DMARC compliance, run spam checks, classify content, and track engagement directly from Salesforce.

Send Transactional Email

Trigger transactional emails from Salesforce events (new opportunity, case update, approval) through the MailOdds delivery API. Uses Named Credentials, domain_id for DKIM signing, and validate_first option for pre-send email checks. Returns message_id for engagement tracking.

POST /v1/deliver domain_id options.validate_first message_id

Apex: Send Transactional Email

APEX
// Apex: Send Transactional Email via Named Credential
public class MailOddsSendService {
    @future(callout=true)
    public static void sendEmail(
        String toEmail, String subject,
        String htmlBody, String domainId
    ) {
        try {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(
                'callout:MailOdds/v1/deliver');
            req.setMethod('POST');
            req.setHeader('Content-Type',
                'application/json');
            req.setTimeout(120000);

            Map<String, Object> body =
                new Map<String, Object>{
                    'to' => new List<Map<String, String>>{
                        new Map<String, String>{
                            'email' => toEmail
                        }
                    },
                    'from' => 'notifications@'
                        + getDomainName(domainId),
                    'domain_id' => domainId,
                    'subject' => subject,
                    'html' => htmlBody,
                    'tags' => new List<String>{
                        'salesforce-triggered'
                    },
                    'track_opens' => true,
                    'track_clicks' => true,
                    'options' => new Map<String, Object>{
                        'validate_first' => true
                    }
                };
            req.setBody(JSON.serialize(body));

            HttpResponse res = new Http().send(req);
            if (res.getStatusCode() == 200) {
                Map<String, Object> result =
                    (Map<String, Object>)
                    JSON.deserializeUntyped(
                        res.getBody());
                // Store result.message_id for
                // engagement tracking via
                // GET /v1/message-events
            }
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'SendEmail error: ' + ex.getMessage());
        }
    }

    private static String getDomainName(String id) {
        Sending_Domain_Config__mdt cfg = [
            SELECT Domain_Name__c
            FROM Sending_Domain_Config__mdt
            WHERE Domain_Id__c = :id LIMIT 1
        ];
        return cfg?.Domain_Name__c ?? 'mail.example.com';
    }
}

Batch Delivery to Campaign Members

Send a single message to up to 100 Campaign members in one API call. Shares the same message body across all recipients, each processed independently. For campaigns over 100 members, chain Queueable jobs.

POST /v1/deliver/batch max 100 recipients 202 Accepted

Apex: Batch Deliver to Campaign Members

APEX
// Apex: Batch Deliver to Campaign Members (max 100)
public class BatchDeliverService {
    @future(callout=true)
    public static void sendToCampaignMembers(
        Id campaignId, String domainId,
        String subject, String htmlBody
    ) {
        try {
            List<CampaignMember> members = [
                SELECT Contact.Email, Contact.Name,
                    Lead.Email, Lead.Name
                FROM CampaignMember
                WHERE CampaignId = :campaignId
                LIMIT 100
            ];

            List<Map<String, String>> recipients =
                new List<Map<String, String>>();
            for (CampaignMember cm : members) {
                String email = cm.Contact?.Email != null
                    ? cm.Contact.Email : cm.Lead?.Email;
                String name = cm.Contact?.Name != null
                    ? cm.Contact.Name : cm.Lead?.Name;
                if (email == null) continue;
                recipients.add(new Map<String, String>{
                    'email' => email,
                    'name' => name
                });
            }

            HttpRequest req = new HttpRequest();
            req.setEndpoint(
                'callout:MailOdds/v1/deliver/batch');
            req.setMethod('POST');
            req.setHeader('Content-Type',
                'application/json');
            req.setTimeout(120000);
            req.setBody(JSON.serialize(
                new Map<String, Object>{
                    'to' => recipients,
                    'from' => 'campaigns@'
                        + getDomainName(domainId),
                    'domain_id' => domainId,
                    'subject' => subject,
                    'html' => htmlBody,
                    'track_opens' => true,
                    'track_clicks' => true
                }
            ));

            HttpResponse res = new Http().send(req);
            // 202 Accepted: batch queued for delivery
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'BatchDeliver error: ' + ex.getMessage());
        }
    }

    private static String getDomainName(String id) {
        Sending_Domain_Config__mdt cfg = [
            SELECT Domain_Name__c
            FROM Sending_Domain_Config__mdt
            WHERE Domain_Id__c = :id LIMIT 1
        ];
        return cfg?.Domain_Name__c ?? 'mail.example.com';
    }
}

Campaign Management with A/B Testing

Create MailOdds campaigns from Salesforce, add A/B test variants with different subjects and weights, and trigger sending. Use the analytics endpoints (ab-results, funnel, delivery-confidence) to measure performance.

POST /v1/campaigns POST .../variants POST .../send GET .../ab-results GET .../funnel

Apex: Campaign Create, Variant, and Send

APEX
// Apex: Create and Send MailOdds Campaign from SF
public class CampaignSendService {
    @future(callout=true)
    public static void createAndSendCampaign(
        Id sfCampaignId, String listId,
        String domainId
    ) {
        try {
            Campaign camp = [
                SELECT Name, Description
                FROM Campaign WHERE Id = :sfCampaignId
                LIMIT 1
            ];

            // 1. Create campaign
            HttpRequest createReq = new HttpRequest();
            createReq.setEndpoint(
                'callout:MailOdds/v1/campaigns');
            createReq.setMethod('POST');
            createReq.setHeader('Content-Type',
                'application/json');
            createReq.setTimeout(15000);
            createReq.setBody(JSON.serialize(
                new Map<String, Object>{
                    'name' => camp.Name,
                    'list_id' => listId,
                    'domain_id' => domainId
                }
            ));
            HttpResponse createRes =
                new Http().send(createReq);
            if (createRes.getStatusCode() != 201) return;

            Map<String, Object> result =
                (Map<String, Object>)
                JSON.deserializeUntyped(
                    createRes.getBody());
            Map<String, Object> campaign =
                (Map<String, Object>)
                result.get('campaign');
            String campaignId =
                (String) campaign.get('id');

            // 2. Add A/B variant
            HttpRequest varReq = new HttpRequest();
            varReq.setEndpoint(
                'callout:MailOdds/v1/campaigns/'
                + campaignId + '/variants');
            varReq.setMethod('POST');
            varReq.setHeader('Content-Type',
                'application/json');
            varReq.setTimeout(10000);
            varReq.setBody(JSON.serialize(
                new Map<String, Object>{
                    'subject' => camp.Name,
                    'html' => '<h1>' + camp.Name + '</h1>'
                        + '<p>' + camp.Description + '</p>',
                    'weight' => 100
                }
            ));
            new Http().send(varReq);

            // 3. Send campaign
            HttpRequest sendReq = new HttpRequest();
            sendReq.setEndpoint(
                'callout:MailOdds/v1/campaigns/'
                + campaignId + '/send');
            sendReq.setMethod('POST');
            sendReq.setTimeout(15000);
            new Http().send(sendReq);
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'CampaignSend error: '
                + ex.getMessage());
        }
    }
}

Sender Health, Stats, and Trend Dashboard

Three @AuraEnabled (Apex annotation that exposes a method to Lightning Web Components for UI display) methods for a Lightning Web Component dashboard: current sender health (score, grade, bounce/complaint rates), historical trend (daily data points), and sending statistics (delivery/open/click rates by period). All use Named Credentials.

GET /v1/sender-health GET /v1/sender-health/trend GET /v1/sending-stats

Apex: Sender Health + Stats + Trend Controller

APEX
// Apex: Fetch Sender Health + Trend for LWC Dashboard
public with sharing class SenderHealthController {
    @AuraEnabled(cacheable=true)
    public static Map<String, Object> getSenderHealth() {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(
            'callout:MailOdds/v1/sender-health');
        req.setMethod('GET');
        req.setTimeout(10000);
        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() != 200) {
            throw new AuraHandledException(
                'API returned ' + res.getStatusCode());
        }
        return (Map<String, Object>)
            JSON.deserializeUntyped(res.getBody());
    }

    @AuraEnabled(cacheable=true)
    public static Map<String, Object>
        getSenderHealthTrend(String period) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(
            'callout:MailOdds/v1/sender-health/trend'
            + '?period=' + (period ?? '30d'));
        req.setMethod('GET');
        req.setTimeout(10000);
        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() != 200) {
            throw new AuraHandledException(
                'API returned ' + res.getStatusCode());
        }
        // Returns: period, data_points array with
        // daily score, grade, delivery_rate, bounce_rate
        return (Map<String, Object>)
            JSON.deserializeUntyped(res.getBody());
    }

    @AuraEnabled(cacheable=true)
    public static Map<String, Object>
        getSendingStats(String period) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(
            'callout:MailOdds/v1/sending-stats'
            + '?period=' + (period ?? '7d'));
        req.setMethod('GET');
        req.setTimeout(10000);
        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() != 200) {
            throw new AuraHandledException(
                'API returned ' + res.getStatusCode());
        }
        // Returns: stats.sent, delivered, bounced,
        // open_rate, click_rate, complaint_rate
        return (Map<String, Object>)
            JSON.deserializeUntyped(res.getBody());
    }
}

DMARC Monitoring Full Suite

Register domains, verify DNS records, check trends (daily pass/fail), get sending source analysis (which IPs send for your domain with DKIM/SPF/DMARC alignment, email authentication DNS standards that prove messages came from authorized servers), and receive policy upgrade recommendations. All via scheduled Apex.

POST /v1/dmarc-domains POST .../verify GET .../sources GET .../trend GET .../recommendation

Apex: DMARC Full Suite

APEX
// Apex: Full DMARC Suite from Salesforce
public class MailOddsDmarcService {
    // Register a domain for DMARC monitoring
    @future(callout=true)
    public static void registerDomain(String domain) {
        try {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(
                'callout:MailOdds/v1/dmarc-domains');
            req.setMethod('POST');
            req.setHeader('Content-Type',
                'application/json');
            req.setTimeout(10000);
            req.setBody(JSON.serialize(
                new Map<String, String>{
                    'domain' => domain
                }
            ));
            HttpResponse res = new Http().send(req);
            if (res.getStatusCode() == 201) {
                Map<String, Object> data =
                    (Map<String, Object>)
                    JSON.deserializeUntyped(
                        res.getBody());
                // Store domain_id for verify/sources/trend
            }
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'DMARC register error: '
                + ex.getMessage());
        }
    }

    // Verify DMARC DNS records are configured
    @future(callout=true)
    public static void verifyDomain(String domainId) {
        try {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(
                'callout:MailOdds/v1/dmarc-domains/'
                + domainId + '/verify');
            req.setMethod('POST');
            req.setTimeout(10000);
            new Http().send(req);
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'DMARC verify error: '
                + ex.getMessage());
        }
    }

    // Scheduled: check DMARC trend and alert on failures
    @future(callout=true)
    @TestVisible
    public static void checkDmarcHealth(
        String domainId
    ) {
        try {
            // 1. Get trend data
            HttpRequest trendReq = new HttpRequest();
            trendReq.setEndpoint(
                'callout:MailOdds/v1/dmarc-domains/'
                + domainId + '/trend?days=7');
            trendReq.setMethod('GET');
            trendReq.setTimeout(10000);
            HttpResponse trendRes =
                new Http().send(trendReq);

            // 2. Get policy recommendation
            HttpRequest recReq = new HttpRequest();
            recReq.setEndpoint(
                'callout:MailOdds/v1/dmarc-domains/'
                + domainId + '/recommendation');
            recReq.setMethod('GET');
            recReq.setTimeout(10000);
            HttpResponse recRes =
                new Http().send(recReq);

            // 3. Get sending sources
            HttpRequest srcReq = new HttpRequest();
            srcReq.setEndpoint(
                'callout:MailOdds/v1/dmarc-domains/'
                + domainId + '/sources?days=7');
            srcReq.setMethod('GET');
            srcReq.setTimeout(10000);
            HttpResponse srcRes =
                new Http().send(srcReq);

            // Aggregate findings into Task
            if (trendRes.getStatusCode() == 200) {
                Map<String, Object> trend =
                    (Map<String, Object>)
                    JSON.deserializeUntyped(
                        trendRes.getBody());
                List<Object> points =
                    (List<Object>) trend.get(
                        'data_points');
                // Check last 7 days for failures
                if (points != null
                    && !points.isEmpty()) {
                    Map<String, Object> latest =
                        (Map<String, Object>)
                        points[points.size() - 1];
                    Integer failCount =
                        (Integer) latest.get(
                            'fail_count');
                    if (failCount > 0) {
                        insert new Task(
                            Subject = 'DMARC failures '
                                + 'detected ('
                                + failCount + ' in 7d)',
                            Description =
                                'Trend: '
                                + trendRes.getBody()
                                    .left(2000)
                                + '\nSources: '
                                + srcRes.getBody()
                                    .left(2000)
                                + '\nRecommendation: '
                                + recRes.getBody()
                                    .left(1000),
                            Priority = 'High',
                            ActivityDate = Date.today()
                        );
                    }
                }
            }
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'DMARC health check error: '
                + ex.getMessage());
        }
    }
}

Bounce Analysis and Contact Sync

Fetch bounce analyses, drill into per-record bounce details (email, bounce_type, smtp_code), and update matching Salesforce Contacts with bounce status. Cross-references the full analysis chain.

GET /v1/bounce-analyses GET .../records GET .../cross-reference

Apex: Bounce Analysis Sync

APEX
// Apex: Cross-Reference Bounces with Contacts
public class MailOddsBounceSync {
    @future(callout=true)
    public static void syncBounces() {
        try {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(
                'callout:MailOdds/v1/bounce-analyses');
            req.setMethod('GET');
            req.setTimeout(15000);
            HttpResponse res = new Http().send(req);
            if (res.getStatusCode() != 200) return;

            Map<String, Object> data =
                (Map<String, Object>)
                JSON.deserializeUntyped(res.getBody());
            List<Object> analyses =
                (List<Object>) data.get('analyses');
            Set<String> bouncedEmails =
                new Set<String>();

            for (Object a : analyses) {
                Map<String, Object> analysis =
                    (Map<String, Object>) a;
                // Fetch bounce records for each analysis
                String analysisId =
                    (String) analysis.get('id');
                HttpRequest recReq = new HttpRequest();
                recReq.setEndpoint(
                    'callout:MailOdds/v1/bounce-analyses/'
                    + analysisId + '/records');
                recReq.setMethod('GET');
                recReq.setTimeout(10000);
                HttpResponse recRes =
                    new Http().send(recReq);
                if (recRes.getStatusCode() != 200)
                    continue;

                Map<String, Object> recData =
                    (Map<String, Object>)
                    JSON.deserializeUntyped(
                        recRes.getBody());
                List<Object> records =
                    (List<Object>) recData.get(
                        'records');
                for (Object r : records) {
                    Map<String, Object> rec =
                        (Map<String, Object>) r;
                    bouncedEmails.add(
                        (String) rec.get('email'));
                }
            }

            if (bouncedEmails.isEmpty()) return;

            List<Contact> contacts = [
                SELECT Id, Email FROM Contact
                WHERE Email IN :bouncedEmails
            ];
            for (Contact c : contacts) {
                c.MailOdds_Bounce_Status__c = 'Bounced';
                c.MailOdds_Bounce_Synced_At__c =
                    Datetime.now();
            }
            update contacts;
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'BounceSync error: ' + ex.getMessage());
        }
    }
}

Suppression Stats Dashboard

Fetch suppression list statistics for an LWC dashboard widget. Shows total entries, breakdown by type, and recent additions. Complements the Suppression Audit pattern in Revenue Intelligence.

GET /v1/suppression/stats

Apex: Suppression Stats Controller

APEX
// Apex: Fetch Suppression Stats for Dashboard
public with sharing class SuppressionStatsController {
    @AuraEnabled(cacheable=true)
    public static Map<String, Object>
        getSuppressionStats() {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(
            'callout:MailOdds/v1/suppression/stats');
        req.setMethod('GET');
        req.setTimeout(10000);
        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() != 200) {
            throw new AuraHandledException(
                'API returned ' + res.getStatusCode());
        }
        // Returns: total_entries, by_type breakdown,
        // recent additions
        return (Map<String, Object>)
            JSON.deserializeUntyped(res.getBody());
    }
}

Inactive Contact Report and Re-Engagement

Find contacts with no engagement (opens, clicks) in a configurable period. Flag matching Salesforce Contacts and create re-engagement Tasks for the sales team. Run as a scheduled job for automated list hygiene.

GET /v1/contacts/inactive-report days parameter (1-365)

Apex: Inactive Contact Report

APEX
// Apex: Identify Inactive Contacts for Re-Engagement
public class InactiveContactReportService {
    @future(callout=true)
    public static void findInactiveContacts(
        Integer inactiveDays
    ) {
        try {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(
                'callout:MailOdds/v1/contacts'
                + '/inactive-report?days='
                + inactiveDays);
            req.setMethod('GET');
            req.setTimeout(15000);
            HttpResponse res = new Http().send(req);
            if (res.getStatusCode() != 200) return;

            Map<String, Object> data =
                (Map<String, Object>)
                JSON.deserializeUntyped(res.getBody());
            List<Object> inactive =
                (List<Object>) data.get('contacts');
            if (inactive == null
                || inactive.isEmpty()) return;

            Set<String> emails = new Set<String>();
            for (Object c : inactive) {
                Map<String, Object> contact =
                    (Map<String, Object>) c;
                emails.add(
                    (String) contact.get('email'));
            }

            // Flag matching Salesforce Contacts
            List<Contact> sfContacts = [
                SELECT Id, Email FROM Contact
                WHERE Email IN :emails
            ];
            List<Task> tasks = new List<Task>();
            for (Contact c : sfContacts) {
                c.Engagement_Status__c = 'Inactive';
                tasks.add(new Task(
                    WhoId = c.Id,
                    Subject = 'Re-engage: No activity '
                        + 'in ' + inactiveDays + ' days',
                    Priority = 'Normal',
                    ActivityDate = Date.today().addDays(7)
                ));
            }
            update sfContacts;
            if (!tasks.isEmpty()) insert tasks;
        } catch (Exception ex) {
            System.debug(LoggingLevel.ERROR,
                'InactiveReport error: '
                + ex.getMessage());
        }
    }
}

Frequently Asked Questions

Troubleshooting

Need more help?

Can't find what you're looking for? We're here to help you get Salesforce working.

Build Production-Grade Salesforce Integrations

Get 1,000 free validations. Start with one pattern, scale to the full platform.