Staying in the loop: tracking email advocacy actions without breaking the experience
I previously wrote about helping your supporters email decision-makers straight from their inbox with mailto:
links — skipping the “load a web page, submit a form” steps altogether, and instead asking constituents to email decisionmakers directly.
Of course, the drawback of this mailto:
approach is that you aren’t tracking who actually takes the action, or who the messages are going to.
A prefilled BCC line fixes this, and a bit of automation lets campaigners fully track advocacy actions for integrated reporting, analysis, and personalization.
The basic version
For a very low-tech zero-setup version, you can:
Pick an inbox that you want to track advocacy emails in
This might be your
info@yourdomain.org
inbox with some filters set up, or a dedicated new inbox likeactions@yourdomain.org
.
Add a
&bcc=actions@yourdomain.org
parameter to themailto:
links you send out in mailingsKeep an eye on what comes in to that inbox
And that’s it! Here’s what it might look like in an ActionKit mailing:
<a href="mailto:decisionmaker@example.com?bcc=actions@yourdomain.org&subject={% filter urlencode %}Please support saving our streams!{% endfilter %}&body={% filter urlencode %}New research is revealing just how important small streams are to the survival of brown bears in Alaska, which are some of the world's biggest bears.
We're asking everyone to join us in saving our streams. We want streams to be there for everyone.{% endfilter %}">Send an email</a>
This basic version gives you visibility into who’s actually doing the action … as long as you have someone monitoring that actions@yourdomain.org
inbox and keeping track of the emails.
With a bit of setup and custom code, you can instead ingest these emails automatically & directly into ActionKit using an inbound email processing platform.
Inbound email processing
An inbound email processing platform gives you an email address that anyone can send mail to, just like an inbox.
When that email address receives a message, instead of putting it in an inbox, it runs some custom code against the contents of the message that was just received.
While it’s possible to self-host an inbound email processor, the maintenance overhead makes it pretty impractical. It’s often not really cost-effective either. Instead you’ll want to choose a cloud-hosted platform. Some of the primary choices here include:
SendGrid’s Inbound Parse
Mailgun’s Routes
Postmark’s Inbound Email
Cloudflare’s Email Workers
Zapier’s Email and Email Parser
Picking a platform
SendGrid, Mailgun, or Postmark (they’re all pretty much equivalent to each other on pricing and features) provide the most flexibility. These platforms let you set up as many email addresses as you want, at custom domains or subdomains, so you can set up automations against any number of email addresses — inbox@yourdomain.org, inbox+anything@yourdomain.org,
inbox+anything@actions.yourdomain.org
, and so on.
For all three of these platforms, month-to-month plans start in the $10-$20/mo range, for up to ~10-100K emails processed. (That said, Mailgun and Postmark both offer a free tier that doesn’t expire, which can be great for tinkering & internal testing.) They also won’t actually run custom code for you1, so you’ll need to connect them to an additional free or low-cost platform2 to finish wiring up your “pushing the emails into ActionKit” logic.
Zapier can host your custom code as well as processing inbound emails, and offers a free plan (with very low limits) — so it can be a fit if you’re testing out mailto actions, you expect to have pretty limited activity, and/or you will only run a mailto action once every few months. But it gets costly very fast if you have more than a few hundred action-takers per month — you can easily end up spending $50-$200/mo if you have a few popular list-wide mailto actions.
With Zapier Email it’s also a bit of a pain to set up with your own custom domain at all. By default you’ll end up with an ugly email address like actions.a5b3op@zapiermail.com
address, which you definitely won’t want your supporters to see in a draft message’s BCC line!3
Cloudflare gives you excellent value for both inbound email processing and custom code hosting: their very generous free plan will cover almost any realistic level of mailto activity unless you have an extremely large & active audience, and if you opt for their $5/mo plan you’ll virtually never need to spend more than that.
The only drawback is that their email routing system is fairly basic. You can set up custom email addresses at your domain, but:
They do not support plus-addressing as a core feature:
inbox@yourdomain.org
andinbox+anything@yourdomain.org
are treated as separate routes and need to be set up separately, so you can’t have arbitrary dynamic email addressesInstead, you can set a single catch-all route, meaning “any email sent to my domain will run my custom code, unless it’s matched by a more specific email route”
You can only set up email routing domain-wide — you cannot set it up for a subdomain without also impacting the primary domain, which can be risky and disruptive. In practice, this means that unless you already have Cloudflare set up for your domain’s DNS hosting and email routing, you probably don’t want to use it just for this — unless you do it at a different dedicated domain.
Overall, we recommend Cloudflare for most organizations. You’ll just need to set it up against a dedicated custom domain, like yourdomain-actions.org
, instead of your primary domain.
If you don’t want to purchase a domain name just for this, we’d recommend Mailgun or Postmark, for around $15/mo.
Getting the emails into ActionKit
If you’re using Cloudflare, we’ve published some code that you can drop in: https://github.com/thethirdbearsolutions/email-to-actionkit
Setup is straightforward, but there are a few configuration steps you'll need to handle first.
First, you'll need to configure Cloudflare Email Routing for your dedicated domain. Follow Cloudflare's email routing setup guide to get the basics in place.
Next, clone the repository and update the configuration in wrangler.jsonc
:
Set
FORWARD_TO_EMAIL
to whatever inbox you want copies of the advocacy emails to land in after they’ve been processed (this needs to be verified as a destination address in your Cloudflare account)Set
ACTIONKIT_DOMAIN_NAME
to your ActionKit domain (just the domain part, likeyourdomain.actionkit.com
)
Then deploy with npx wrangler deploy
.
Lastly, go back to the Cloudflare Email Routing configuration and set up a catch-all route for your custom domain, bound to your newly-deployed worker instead of a destination address.
How it works
The worker looks for plus-addressing in the BCC field of incoming emails. When someone sends an email to decisionmaker@example.com
with a BCC to actions+akid=12345.67890.abcde&page=save-our-streams@yourdomain-actions.org
, the worker:
Extracts the plus-addressed suffix from the email address, and interprets it as URL parameters (
akid=12345.67890.abcde&page=save-our-streams
)Parses the full email to get the subject, body (both HTML and text), and recipient information
Sends all of this data to ActionKit's REST API as a new action record
Includes custom action fields so you can track which targets have been emailed and review the content that was emailed:
action_email_subject
action_email_to
action_email_cc
action_email_body_html
action_email_body_text
Forwards the original email to your specified inbox for backup/monitoring
The beauty of this approach is that ActionKit gets all the data it needs to properly track the action — who took it (via the AKID), what page it was associated with, and the full content of what was sent to the decision-maker.
You can then use standard ActionKit page configuration to decide how you want to process new actions — e.g. whether the sender should get subscribed to any mailing lists, be added to any groups, or receive an after-action confirmation email.
How to use it
To use it, just extend the “basic version” by encoding a bit more information (for example, akid={{ user.token }}&page=save-our-streams&source=bcc-email-to-members
) in the mailto:
links that you render in a mailing template or elsewhere. Here’s what it might look like in an ActionKit mailing:
<a href="mailto:decisionmaker@example.com?bcc=actions+akid={{ user.token }}&page=save-our-streams@yourdomain.org&subject={% filter urlencode %}Please support saving our streams!{% endfilter %}&body={% filter urlencode %}New research is revealing just how important small streams are to the survival of brown bears in Alaska, which are some of the world's biggest bears.
We're asking everyone to join us in saving our streams. We want streams to be there for everyone.{% endfilter %}">Send an email</a>
Plus, ActionKit will automatically generate a version of the user’s AKID token scoped to the mailing4, so when a BCC’ed email comes in and gets ingested, it will automatically attribute the resulting action to the right mailing.
Advanced considerations
If you want to allow people to take actions without pre-generated AKIDs (say, from a mailto link on your website rather than from an ActionKit mailing), you can uncomment the lines in the code that set the email parameter based on the sender's address. ActionKit will then try to match or create a user record based on the email address.
The worker currently sends data to ActionKit and forwards the email regardless of whether the ActionKit API call succeeds. You could enhance this to check the ActionKit response and handle errors differently — maybe by adding a custom header to forwarded emails that indicates whether the action was successfully recorded, or by sending a reply to the original sender if something went wrong.
The current target-tracking is quite simple: it’s just a custom action field with the target’s email address. If you plan to use this frequently, you may want to more fully integrate it with a custom target database in ActionKit. That would allow you to assign different targets to different recipients and create more structured relationships between actions and target records in the database.
One limitation to keep in mind: this setup works great for individual advocacy emails, but if someone forwards your mailto:
link to a bunch of people who all send the same message, you'll get duplicate actions in ActionKit. Depending on your campaign goals, you might want to add some validation logic to confirm that the sender’s email address matches the email on file for the AKID, use the AKID as a referring_akid
parameter (for fwd-tracking), and/or include the sender’s email address in the action payload.
Instead, they all work by sending a JSON representation of an inbound email to a URL of your choice. You then set up your own code to listen at that URL as a standard HTTP endpoint.
Like Netlify, Vercel, Cloudflare, Zapier, Heroku, and many others.
To work around this, you need to set up another inbox somewhere and configure mail forwarding. This adds cost and/or complexity and can be somewhat brittle.
This is version with three components (12345.12345.abcde
) rather than two (.12345.abcde
)