GDPR Compliance

Right To Be Forgotten

Please see Deleting User Data.

Privacy Records

To help with GDPR compliance, we've added a new "privacy records" feature. There's new template, privacy.html, you can use to show Europeans and UK residents their privacy choices on action forms. Our default version displays opt-in/out buttons and a notice, and saves the text of the notice and the option they chose.

We realize groups will apply GDPR differently. We're trying to provide a toolkit, not a prescription for how to comply. We'll cover ways you can override our default behaviors below, and you can build custom setups with templating, custom fields, JavaScript, and the API if our defaults aren't what you want.

We also offer tools to create records when you update user data via uploads, the admin, or the user API.

We'd like to hear via Support if you think you can't do what you need within the current framework.

Getting Started

Enable the Privacy Records feature. Go to Configure ActionKit (under the gear menu at top right), search "Privacy Records" and click Edit.

Finish reading this section before you do anything else. Checking any of the boxes (like "Require on actions") can break your pages and other functionality. Read the "Getting Started" section and the descriptions of what each checkbox does before checking.

Once you've read everything, enter the countries where you'd like to collect privacy records in the Countries Requiring Privacy Records box.

Enter one country per line with no commas or semicolons. We use "Europeans," "Europe," etc. in this documentation as shorthand for "countries requiring privacy records," though the UK has kept similar GDPR requirements after leaving the EU, and nothing in ActionKit stops you from adding or removing other countries from the list as you wish.

The built-in templates save the English spelling of country names, even for forms in other langauges, so normally you'll only need to enter those. Here are ActionKit's default names for European countries plus the UK:

Czech Republic
United Kingdom

Provide required text To provide organization-wide text for checkbox labels,notices, or whatever other details you want to record, use the language editor. On the Pages tab, expand the Language menu in the right-side navigation, then click Languages in the expanded menu (or, if you're viewing these docs from your ActionKit domain, click here), then choose a language. There's no default or placeholder text, so you have to do this step to use the feature!

Scroll to the bottom, click "Translate another piece of text". For the name (the smaller textbox), with our default privacy.html you can use:

  • privacy_notice to show Europeans a notice.
  • privacy_optin and privacy_optout to show radio buttons with an opt in/out choice.
  • privacy_really_optout to show additional content to those who opt out for example, "Please sign up!".

The content (which you type in the longer textbox) may contain HTML like links to policies, but it doesn't need to contain form inputs.

To change or translate the error message when privacy= is required but missing, click "Translate another piece of text" again. Use error_privacy:missing as the name, and enter whatever text you want to replace the default message, "We need more information to process your data."

If you don't specify privacy text in every language, text lookup falls back on English to at least display something. Make sure your translations are complete to avoid this!

Changing behavior. While you're in the language admin, you can specify additional "text" to tweak our default behaviors.

If you'd like to unsubscribe current subscribers who choose No, make a string called privacy_unsub_optouts and enter any text. Click Save. Remove the text you entered to blank the string out. Click Save. This will turn that off.

If you'd like to show the privacy UI to recognized users with a European country on file, add a string called privacy_show_if. Use the value in_country to enable it for all Europeans, or missing to enable it just for users without an active privacy record. If you don't provide either, recognized users won't be shown anything.

You only need to set these flags in the English language.

An alternative to configuring the language strings is to edit the privacy.html template directly. If it's not clear what to change, look at "New action-processing options" below for more help or ask us via Support.

Adding privacy.html to your templateset. Add {% include "./privacy.html" %} to the very end of user_form_wrapper.html, after the closing div <div> tag. Then, if you preview the change on a petition with a country dropdown and select a European country, you can see the privacy UI.

Donation forms and the user update form at /me/update collect user info without using user_form_wrapper. For donation pages, you can include the privacy template under the first <div> that includes country_select.html. If you're using the three-step donation feature, we've updated function validate_step and function submit_paypal in Original/donate.html to add privacy to some field lists; validation still works without the changes, but copying them over will make the UX smoother when the user forgets to opt in or out.

For the user update screen at /me/update/, a few changes are needed; you might be able to just copy user_update.html code from Original to your templateset, or look for 'privacy' in Original/user_update.html to see the relevant parts. Users who opt in there are subscribed to whichever list you've chosen as your default in the list admin, or list 1 if there's no default chosen. To reach the list admin, choose the Mailings tab, then expand the Data menu in the right-hand navigation, then click "Mailing Lists", or, if viewing these docs on your ActionKit domain, click here.

If your standard opt-in/out doesn't work for /me/update or other particular page types, more options for customization are below.

Viewing the records. Users' privacy records appear in their user records, and action-related records appear in their action history.

If seeing records in the user admin isn't enough, you can use a report to query the database. The query builder has support for retrieving privacy records for users and for actions. If you are writing custom SQL, the database table core_privacyrecord has records that can be looked up by user_id or action_id. Its version_id links to the core_privacytext table, which contains the text associated with the record (like notice or opt-in text), the type (like notice or optin), and the hash used as the value for the privacy input. You can't yet access them through mailing targeting.

There's a lot more to know! You can customize privacy text per page and page type, revamp privacy.html to your liking, and more. Even if you're satisfied with the defaults for now, we suggest at least scanning the sections below to get an idea how things work.

Customizing Privacy Options on Action Forms

Overriding privacy text. You can override privacy text for specific page types: for example, maybe you want the notice event pages to mention that hosts see some user info. You could do that in the Language admin by adding a string named privacy_notice_eventsignup. The general format is privacy_[text_type]_[page_type]. The page type names used must be lowercase, and are petition, letter, survey, donation (not donate), signup, call, whipcount, lte, unsubscribe, eventcreate, and eventsignup. For the user update screen at /me/update/, use update.

You can override privacy text for individual pages. For that, you add a custom page field with the same name as the language string, like privacy_notice. You'll have to add an "allowed page field" first. To do that, click the Pages tab, then expand the Other menu in the right-hand navigation, then click Custom Page Fields, or click here if viewing these docs on your ActionKit domain.

Custom HTML. You can edit privacy.html like any other template. Things to know:

From the browser's side, privacy.html adds a div with class="ak-privacy". Whenever a European country is picked from the country dropdown, actionkit.js unhides .ak-privacy and enables all of the inputs under it. When a non-European country is picked (or is the default when the page loads) ActionKit hides .ak-privacy and disables the inputs inside it.

Values of disabled inputs aren't submitted to the server or checked by validation, so it's much as if .ak-privacy were removed from the form entirely when it's hidden and disabled.

You can put whatever you want inside the div. For example, you could add a custom user field input to the div, and require it using <input type="hidden" name="required" value="user_xyz">.

Treating some page types specially. As with any template, you can use code like {% if page.type == "Donation" %}...{% endif %} to use different code with certain page types. You can also use {% if not page.type %} for code that should apply to the user update screen at /me/update/.

Handling confirmation emails. There are a few approaches to after-action emails. Some organizations have made no changes for European users, treating opt-outs as not subscribing to ongoing updates, but not requiring any change to transactional emails.

Another option is to add text to your transactional or thank you emails reminding opted-out users that this message is a one-off and doesn't mean they'll get more email from you. One way to do that is by adding code to your email wrapper:

{% if not mailing_id %}
  {% if user.subscription_status != "subscribed" %}
     <i>We want you to know you're <b>not</b> subscribed to our email
     list. This is just a one-off message to thank you for taking action
     and offer some next steps.</i>
  {% endif %}
{% endif %}

Or, if you want, you can skip the "thank you" email on actions where the user opts out. Go to the language admin and add a string called privacy_optouts_skip_thanks, with text 1. That doesn't keep the user from ever receiving thanks emails; it just skips sending the email on the particular action where they chose to opt out. Keep in mind that suppressing confirmations could have some odd effects: donors might wonder where their receipt is, for example.

If you want to make it so that your unsubscribe page doesn't send a confirmation to recognized European users, you can add HTML to unsubscribe.html to do that:

<div class="ak-privacy">
  <input type="hidden" name="privacy_show_if" value="in_country">
  <input type="hidden" name="skip_confirmation" value="1">

If you just want to stop transactional emails for a particular user, you can use the Blackholed Emails feature; see Blackholing Email. To get to Blackholing Email, click the Mailings Tab, then expand the List Hygiene menu in the right-hand navigation, then click Blackholed Emails, or click here if viewing these docs on your ActionKit domain.

Custom privacy record types. Our default template creates privacy records of types "notice", "optin", and "optout". Those names aren't magic or hard-coded in the backend, you can add other types.

For example, if you add a language string called privacy_sms_optin, you can use {{ privacy_text.sms_optin }} to access the text from privacy.html, and submit an input with a value of {{ privacy_text.sms_optin_hash }}.

"Extra" privacy inputs. If you want to require users pick a "main" privacy choice when they an action, but also save an extra record (like to save text of a privacy notice or record an opt-in to text messages), name your additional input privacy_extra or privacy_hidden. That saves another record while making sure that required="privacy" still only refers to the main choice you're showing the user.

New action-processing options. The default privacy.html uses some new action processing options to link opt-ins and opt-outs to subscriptions and to control whether recognized users see anything. You can use them in your custom code, too.

An input like <input type="hidden" name="privacy_optin_lists" value="5"> will subscribe the user to list 5 if they submit a privacy= value of type 'optin'. The variable {{ list_id }} is now the page's list ID on action pages, and the default list ID on /me/update.

An input like <input type="hidden" name="privacy_optout_unsub_all" value="1"> will unsubscribe the user from all lists if they submit a privacy= value of type 'optout'.

You can also now submit unsub_all=1 with any action to make it unsubscribe the user from all lists.

An input like <input type="hidden" name="privacy_show_if" value="in_country"> shows the privacy UI if a recognized user is in a European country. A value of missing only shows the UI if there's no existing record (of any type: optin, optout, or something else) on file.

Using standard country names is important. The system currently doesn't recognize that, for example, "Deutschland" means "Germany", so if you aren't consistent in the country names you use, users might not be recognized as Europeans. This is especially key in the user admin, where there's no dropdown to push users to use the correct values.

JavaScript hook. You can handle a jQuery event when .ak-privacy is shown or hidden. After initForm has been called, use $(actionkit.form).on('actionkit.privacyRequirementChanged', handler) to add a handler. The handler will be passed an event object and the new privacy-required status.

You must use resources/actionkit.js. Code to show and hide the .ak-privacy div is in an update to resources/actionkit.js. If you're loading your JavaScript from any other location, including custom forks of actionkit.js (we don't recommend forking!), our code to hide and disable the privacy UI won't run. Updates to the feature will require you have the latest actionkit.js too.

FYI: Users with disabled or broken JavaScript are treated like Europeans. Since we rely on JavaScript to hide the privacy UI when it's not needed, it's inevitable that this won't work quite right if JS is broken or disabled. To try to err on the safe side, .ak-privacy is visible and enabled if the JS is missing or doesn't run.

Non-Action User Updates

User info doesn't always come through actions: import pages, the REST user API, and the user admin can also make changes. We've added ways you can create (and, if you want, require) privacy records for those updates too.

When editing a user profile or import page, you'll see a "Privacy notes" field once you've configured a set of countries. By default, there's a dropdown of options you can edit. Under the dropdown you have links to edit the choices in the dropdown or enter one-off custom privacy notes. The type for privacy records from import pages is 'import_notes' and the type for user admin updates is 'admin_notes'. The user admin's subscription features don't create privacy records, and the form to subscribe a user through the admin is disabled for Europeans. (You can subscribe the user through a special action page as a workaround.)

On the user admin page, you'll only see the "Privacy notes" dropdown when you're going to save a European country for the user. On import pages, the dropdown's always visible, and records are saved even for non-Europeans. (If those extra records cause you problems, contact us via Support.)

If you want to bulk-add records with a different type from 'import_notes', you can also provide 'privacy' as a column in an upload, and set its value to the same hash value that's submitted from an action form (not raw text).

In the user API, you can now submit a privacy= value with PUT or POST requests. The value can either be a hash like the ones submitted with actions, or other text that will be saved like custom notes. The type for these is always 'api_notes'. To encourage third-party integrators to work with you on correct privacy compliance rather than just submitting something like privacy=1 to try to get past the error, we'll reject "1", "true", and a handful of other values.

Also, via the Config screen you can configure privacy records as required in any or all of these contexts:

  • Requiring notes in the user admin will require staff to pick privacy notes before saving any updates to the user's profile, if the user's country after the changes is European.
  • Requiring notes on import pages for uploads through the API and admin. Old import pages will no longer work until you specify notes for them, and any integrations that don't set privacy notes will break. It requires notes on all import pages, since any import page could update European users.
  • Requiring notes on the user API will break any integrations unprepared to pass privacy= with any updates that either include a European country or don't include country but affect a user with a European country already assigned. It also disables the legacy XML-RPC User API, which doesn't support the new feature.

We're not advising you to use these "Require on..." features or not. You can still save privacy records on some imports, for example, without requiring them everywhere.

Requiring Records on All Actions

Checking the "Require on actions" box requires a privacy record with every action submitted with a European value for country=, excluding import and unsubscribe pages, and including actions submitted through the API. It also requires privacy values on the profile update form at /me/update/.

We're not advising you to use this feature or not. When our default privacy UI is visible on an action page, it already requires the user to make a choice. What "Require on actions" changes is that if some user doesn't see the privacy UI after choosing a European country from a dropdown (maybe because of a bug or an outdated form), we won't accept their action.

If you want to use this, remember it will intentionally break actions from sources that don't collect privacy records. It can break actions even when they are coming in through third-party integrations, through old forms hosted on third-party sites that you can't feasibly update, or through forms that don't subscribe users. If you enable this option and it rejects actions you wanted to be accepted, we won't be able to recover the rejected data, because rejecting the actions was intentional behavior.

If you believe you need to take that risk, we still suggest that, if feasible, you don't flip the switch immediately. Instead, update your forms without enabling "Require on actions", then look for any submits missing records by querying for actions where created_user=1 and the user's country is in Europe, but there is no privacy record. That can reveal straggler forms that need updating.

Some things to remember about this feature:

  • This only requires records when a European country is submitted. If a user has a European country previously stored but no country submitted with the action (maybe because the form only asks for email, or because the user's recognized) that doesn't trigger the requirement.
  • It requires only one record, which has to come in with the name privacy (not privacy_hidden or privacy_extra).

Marking Records as Withdrawn

Privacy records have a status column that's initially active when a record is created. By default, when a user unsubscribes from all lists, all of their records are marked status="withdrawn". That includes opt-outs through unsubscribe pages, through the user admin, or through choosing the opt-out radio button if you've enabled the privacy_unsub_optouts option.

Unsubscribes through bounce processing and reengagement don't mark records withdrawn, because they weren't requested by the user.

You'll see the withdrawn status in the lists of privacy records in the user admin and action history.

The default behavior is meant to cover the common situation of using privacy records for email opt-ins, but if you want to customize things, the new withdraw_privacy action-processing option may help.

If you don't want an unsubscribe to withdraw privacy records, submit withdraw_privacy=no_auto. This might make sense if you're tracking something like SMS opt-ins and you want them to be independent of email opt-ins. If you want an action to withdraw all privacy records when it wouldn't by default, you can submit withdraw_privacy=all.

If you want to withdraw only certain records, you can submit withdraw_privacy=[hash]. To get the hashes for existing records from a Django template, you can use a for loop like {% for record in user.privacyrecord_set.all %} and use record.version.type, record.version.status, and record.version.hash to filter down to records of interest and get the hashes you need to submit.