Help Needed: Retrieving Clicked, Unsubscribed, and Bounced Stats via Mautic API

Your software
My Mautic version is: 4.4.12

Hello Mautic Community,

We’re using a Python script to pull email stats from the Mautic API and inject them into a Google Sheet. So far, we’ve successfully retrieved the Email ID, Sent Count, and Read Count. However, we’re struggling to locate the data for Clicked, Unsubscribed, and Bounced metrics.

Could anyone guide us on where to find these specific stats via the API? Any advice on how to correctly access and extract these metrics would be greatly appreciated.

Thank you in advance for your help!

If it does not exist , just create your own endpoint in custom plugin that exposes that info.

I would think it exist because it part of the email stats in Mautic

What I am trying to do is list all emails sent to that one segment … date published , subject line, (stats) sent / read / failed / clicked / unsub / bounced for each row

But stats keep showing up as a zero no matter what data I tried to pull . Below is a screenshot from Google Sheet where a script I create successfully inject the data from Mautic into Sheet but missing the stats or it not pulling or finding it.

I tried different dataset suggest by document over at https://developer.mautic.org/

I am still having no luck … I know stats are working because it does exist as shown in previous post screenshot. What I am overlooking??

This only shows that there is a function in mautic that can calculate the stats. There is no gurantee its acutally accessible via api endpoint (it could be, but its unlikely).

Reverse engineer how the stats in the graph are calculated and write a custom endpoint that you can call that would call this function and return the result.

At the moment I cannot say for sure where or what that function is, but with a liitle bit of browsing the code I am sure you can find it. On the other hand if you would like a helping hand in developing this solution, please reach out, I am happy to look into the issue for you.

1 Like

I found the total clicks and I was told that total unsub and bounce would be in the lead_donotcontact which I do not even see it in there or how to link it to an email id. If I had some idea how it all linked then I could be able to code a function to tally it up and output it to Google Sheet.

Why don’t you tap in directly via MYSQL?

select s.email_id, e.subject, s.date_sent, count(s.lead_id) as sent, sum(s.is_read) as isread from email_stats s, emails e WHERE s.email_id = e.id group by email_id
1 Like

Hi,

the unsub and bounce you would potentially get with something like this.

Bounce (soft or hard):

SELECT 
    channel_id, 
    COUNT(lead_id) 
FROM 
    lead_donotcontact 
WHERE 
    reason = 2 
    AND channel = 'email' 
GROUP BY 
    channel_id 
ORDER BY 
    channel_id;

Unsub:

SELECT 
    channel_id, 
    COUNT(lead_id) 
FROM 
    lead_donotcontact 
WHERE 
    reason = 1
    AND channel = 'email' 
GROUP BY 
    channel_id 
ORDER BY 
    channel_id;

It should be noted that this might not be accurate due to the unsufficent database model to properly handle soft and hard bounce and the fact that its entiely up to the transport to map the data properly (as much as its possible with the given table).

Hope this helps.

Best, M.

1 Like

My issue is not the sent or read. I am able to pull this data just fine. I am trying to also pull the total click, total unsub and total bounce.

The query you mentioned only pulled the sent and read information. It doesn’t pull the other stats I wanted included into the table unless I am doing something wrong.

I ran the query and got a list of emails subject line, send, and read result. No other stats.

Using @mzagmajster and @joeyk help above, I think I managed to get the data pulled using python.

Here a script and right now it’s only pulling one email id into a log to make sure the data result was correct before going further.

I ran it and the result was correct matching the data in Mautic so I am happy camper now.

Thoughts?

import mysql.connector
import logging
from datetime import datetime

# Database connection details
db_config = {
    'user': 'your_db_user',
    'password': 'your_db_password',
    'host': 'your_db_host',
    'database': 'your_db_name'
}

# Set up logging
log_filename = f"email_stats_279_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
logging.basicConfig(filename=log_filename, level=logging.INFO, format='%(message)s')

try:
    # Connect to the database
    conn = mysql.connector.connect(**db_config)
    cursor = conn.cursor(dictionary=True)

    # SQL query to retrieve email stats for email ID 279 and use the first sent date
    query = """
    SELECT 
        s.email_id, 
        e.subject, 
        MIN(s.date_sent) AS date_sent, 
        COUNT(DISTINCT s.lead_id) AS sent, 
        SUM(s.is_read) AS isread,
        (SELECT COUNT(ph.id) FROM page_hits ph WHERE ph.email_id = s.email_id) AS clicks,
        IFNULL((SELECT COUNT(ldnc.lead_id) FROM lead_donotcontact ldnc WHERE ldnc.channel = 'email' AND ldnc.reason = 1 AND ldnc.channel_id = s.email_id), 0) AS unsubscribes,
        IFNULL((SELECT COUNT(ldnc.lead_id) FROM lead_donotcontact ldnc WHERE ldnc.channel = 'email' AND ldnc.reason = 2 AND ldnc.channel_id = s.email_id), 0) AS bounces
    FROM 
        email_stats s
    JOIN 
        emails e ON s.email_id = e.id
    WHERE 
        s.email_id = %s
    GROUP BY 
        s.email_id, e.subject
    """

    cursor.execute(query, (279,))
    result = cursor.fetchone()

    if result:
        # Format the date as MM-DD-YYYY directly from the datetime object
        formatted_date_sent = result['date_sent'].strftime('%m-%d-%Y')
        
        # Log the aggregated result with the adjusted format
        logging.info(f"Email ID: {result['email_id']}, Date Sent: {formatted_date_sent}, Subject: {result['subject']}, Sent: {result['sent']}, Read: {result['isread']}, Clicks: {result['clicks']}, Unsubscribes: {result['unsubscribes']}, Bounces: {result['bounces']}")
        print(f"Aggregated email stats for email ID 279 have been logged to {log_filename}")
    else:
        print("No data found for email ID 279.")

except mysql.connector.Error as err:
    print(f"Error: {err}")
    logging.error(f"Error: {err}")

finally:
    if cursor:
        cursor.close()
    if conn:
        conn.close()

Updated script to pull all emails from a segment and input it into a CVS format file. But it pulling all emails from database. I just want all email from one segment id 56

What I am doing wrong?

import mysql.connector
import csv
from datetime import datetime

# Database connection details
db_config = {
    'user': 'your_db_user',
    'password': 'your_db_password',
    'host': 'your_db_host',
    'database': 'your_db_name'
}

# Output CSV file
csv_filename = f"segment_56_email_stats_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"

try:
    # Connect to the database
    conn = mysql.connector.connect(**db_config)
    cursor = conn.cursor(dictionary=True)

    # Find all emails associated with segment ID 56
    query_emails = """
    SELECT DISTINCT e.id AS email_id, e.subject, MIN(es.date_sent) AS date_sent
    FROM emails e
    JOIN email_stats es ON e.id = es.email_id
    JOIN lead_lists_leads lll ON es.lead_id = lll.lead_id
    WHERE lll.leadlist_id = 56
    GROUP BY e.id, e.subject
    """
    cursor.execute(query_emails)
    emails = cursor.fetchall()

    # Open CSV file for writing
    with open(csv_filename, mode='w', newline='') as file:
        writer = csv.writer(file)
        writer.writerow(['Email ID', 'Date Sent', 'Subject', 'Sent', 'Read', 'Clicks', 'Unsubscribes', 'Bounces'])

        for email in emails:
            # SQL query to retrieve stats for each email
            query_stats = """
            SELECT 
                COUNT(DISTINCT s.lead_id) AS sent, 
                SUM(s.is_read) AS isread,
                (SELECT COUNT(ph.id) FROM page_hits ph WHERE ph.email_id = s.email_id) AS clicks,
                IFNULL((SELECT COUNT(ldnc.lead_id) FROM lead_donotcontact ldnc WHERE ldnc.channel = 'email' AND ldnc.reason = 1 AND ldnc.channel_id = s.email_id), 0) AS unsubscribes,
                IFNULL((SELECT COUNT(ldnc.lead_id) FROM lead_donotcontact ldnc WHERE ldnc.channel = 'email' AND ldnc.reason = 2 AND ldnc.channel_id = s.email_id), 0) AS bounces
            FROM 
                email_stats s
            WHERE 
                s.email_id = %s
            """
            cursor.execute(query_stats, (email['email_id'],))
            stats = cursor.fetchone()

            if stats:
                # Write the stats to the CSV file
                writer.writerow([
                    email['email_id'], 
                    email['date_sent'].strftime('%m-%d-%Y'),
                    email['subject'], 
                    stats['sent'], 
                    stats['isread'], 
                    stats['clicks'], 
                    stats['unsubscribes'], 
                    stats['bounces']
                ])

    print(f"Email stats for segment ID 56 have been logged to {csv_filename}")

except mysql.connector.Error as err:
    print(f"Error: {err}")

finally:
    if cursor:
        cursor.close()
    if conn:
        conn.close()

I been struggling to pull all email just from one segment because if contact is in that segment “56” and also in other different segments then it’ll pull those emails which is driving me crazy. I only want email that was in this one specific segment . Spent most of the day trying to figure out how to filter out so I gave up on this and put it on hold.

Instead, I went back to all my email and added a category to place all those email into and filter it out by using category which seem to be the only way at the moment. So far it worked for me … If anyone know how to filter email using specific segment please do let me know.

Here the python script to output email state into cvs file based a category.

import mysql.connector
import csv
from datetime import datetime

# Database connection details
db_config = {
    'user': 'your_username',
    'password': 'your_password',
    'host': 'your_host',
    'database': 'your_database'
}

# Category title to filter by
category_title = 'your_category_title'

# Output CSV file
csv_filename = f"formatted_email_stats_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"

try:
    # Connect to the database
    conn = mysql.connector.connect(**db_config)
    cursor = conn.cursor(dictionary=True)

    # Step 1: Identify the category_id for the specified category
    query_category_id = """
    SELECT id FROM categories WHERE title = %s
    """
    cursor.execute(query_category_id, (category_title,))
    category = cursor.fetchone()
    
    if not category:
        print(f"No category found with the title '{category_title}'.")
    else:
        category_id = category['id']
        
        # Step 2: Identify all emails in the specified category
        query_emails = """
        SELECT e.id AS email_id, e.subject, DATE_FORMAT(e.date_added, '%m-%d-%Y') AS date_added
        FROM emails e
        WHERE e.category_id = %s
        """
        cursor.execute(query_emails, (category_id,))
        emails = cursor.fetchall()

        # Check if emails were found
        if not emails:
            print(f"No emails found in the category '{category_title}'.")
        else:
            # Step 3: Open CSV file for writing
            with open(csv_filename, mode='w', newline='') as file:
                writer = csv.writer(file)
                writer.writerow(['Email ID', 'Date Added', 'Subject', 'Sent', 'Read', 'Clicks', 'Unsubscribes', 'Bounces'])

                for email in emails:
                    # Step 4: Retrieve stats for each email
                    query_stats = """
                    SELECT 
                        COUNT(DISTINCT s.lead_id) AS sent, 
                        SUM(s.is_read) AS isread,
                        (SELECT COUNT(ph.id) FROM page_hits ph WHERE ph.email_id = s.email_id) AS clicks,
                        IFNULL((SELECT COUNT(ldnc.lead_id) FROM lead_donotcontact ldnc 
                                WHERE ldnc.channel = 'email' AND ldnc.reason = 1 AND ldnc.channel_id = s.email_id), 0) AS unsubscribes,
                        IFNULL((SELECT COUNT(ldnc.lead_id) FROM lead_donotcontact ldnc 
                                WHERE ldnc.channel = 'email' AND ldnc.reason = 2 AND ldnc.channel_id = s.email_id), 0) AS bounces
                    FROM 
                        email_stats s
                    WHERE 
                        s.email_id = %s
                    """
                    cursor.execute(query_stats, (email['email_id'],))
                    stats = cursor.fetchone()

                    # Write the results, including those without stats
                    if stats:
                        writer.writerow([
                            email['email_id'],
                            email['date_added'],
                            email['subject'],
                            stats['sent'],
                            stats['isread'],
                            stats['clicks'],
                            stats['unsubscribes'],
                            stats['bounces']
                        ])
                    else:
                        # Write emails without any stats
                        writer.writerow([
                            email['email_id'],
                            email['date_added'],
                            email['subject'],
                            0,  # Sent
                            0,  # Read
                            0,  # Clicks
                            0,  # Unsubscribes
                            0   # Bounces
                        ])

except mysql.connector.Error as err:
    print(f"Error: {err}")
finally:
    cursor.close()
    conn.close()


Just to understand: you only want the members of segment 56’s stats? Or any email total stat that was sent to segment 56?

Email stats that was sent to segment 56.

We have weekly newsletter that members can join in more than one different type of newsletters so we create segment for each newsletters group they want to receive weekly from.

I wanted to clarify something about how our newsletters work. Our members can subscribe to multiple newsletters, and some of them do. We separate these newsletters using segments. For example, if a member is subscribed to Newsletter A, they’re in segment 56. If they’re also subscribed to Newsletter B, they’re in segment 35.

While developing a script to pull email stats for just Newsletter A (segment 56), I noticed it was also pulling stats from segment 35. This happens because some members in segment 56 are also part of segment 35, so the script ends up pulling stats from all segments those members are in.

Does that make sense?

Here a post about the python3 script I been working on and where you can look it over at Github. It been working for me so far.