Do you manage multiple instances of Marketo? Keeping lead information synchronized across instances can be challenging. Here is a way to sync email unsubscribes across instances using a webhook that calls an external web service. The external web service loops through each instance looking for the known lead that triggered the unsubscribe event. When a matching lead is found the “unsubscribed” field in the corresponding lead record is updated.
Here is a diagram illustrating the idea.
It’s up to you to implement the web service, but the code below should help you jumpstart the process!
External Web Service
Here is the proposed REST endpoint specification for the external web service:
URI: /unsubscribe
Method: POST
Parameter | Description | Example |
id | Account id that triggered the unsubscribe event. | AAA-111-BBB |
Email address of the lead that triggered the unsubscribe event. | jdoe@marketo.com |
The external web service performs the following steps for each Marketo instance that needs to be synchronized:
- Composes instance-specific REST API Endpoint URL
- Obtains access token using Identity
- Obtains list of lead records that match email address using Get Multiple Leads by Filter Type
- Updates “unsubscribed” field of each lead record using Create/Update Leads
Here is another diagram showing the external web service call and Marketo REST API calls in detail.
The sample code below is not an out of the box web service. Rather, it is a console mode program that you can pass arguments to via the command line. The intent here is to show how to call the appropriate Marketo APIs to update lead records across instances. Implementing the web service is left as an exercise for the reader 😉
Sample Code
To get the sample code up and running, you will need to create a Java project in your favorite IDE. After that, you will need to make the following changes:
1. The sample code uses json-simple to parse JSON strings. Add the json-simple jar to your Java project.
2. The sample code has a structure that holds metadata for each Marketo instance. Place actual values from your instances into the structure as follows:
1 2 3 4 5 |
public static String instanceInfo[][] = { { "AccountId1", "ClientId1","ClientSecret1" }, // Instance 1 metadata { "AccountId2", "ClientId2","ClientSecret2" }, // Instance 2 metadata { "AccountId3", "ClientId3","ClientSecret3" } // Instance 3 metadata }; |
You can find the metadata for the instance in the Marketo Admin panel:
- Account Id
Admin > Integration > Munchkin > Munchkin Account - Client Id & Client Secret
Admin > Integration > LaunchPoint > Email Unsubscribe Sync > View Details
3. The sample code takes two command line arguments that simulate the “id” and “email” query parameters for external web service described above.
- args[0] = Account Id
- args[1] = Email Address
Pass actual values from your instance as arguments to the program. Here is a project configuration screenshot from Intellij IDEA.
SyncEmailUnsubscribe.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
package com.marketo; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import javax.net.ssl.HttpsURLConnection; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Scanner; public class SyncEmailUnsubscribe { // Define Marketo instance meta data here. // Each row contains three elements: Account Id, Client Id, Client Secret. // For example: // public static String instanceData[][] = { // {"111-AAA-222", "2f4a4435-f6fa-4bd9-3248-098754982345", "asdf6IVE9h4Jjcl59cOMAKFSk78ut12W"}, // {"222-BBB-333", "5f4a6657-f6fa-4cd9-4356-123083238821", "gfjgfIVE9h4Jjcl59cOMAKFSk78ut12W"}, // {"444-CCC-444", "9f4a4678-f6fa-4dd9-7735-908713247721", "xzcxvbVE9h4Jjcl59cOMAKFSk78ut12W"} // }; // public static String instanceData[][] = { // ADD YOUR INSTANCE META DATA HERE }; public static void main(String[] args) { String accountId = args[0]; // Account id that processed the unsubscribe String emailAddress = args[1]; // Email address of lead that unsubscribed SyncEmailUnsubscribe seu = new SyncEmailUnsubscribe(); // Loop through each Marketo instance for (int i = 0; i < instanceData.length; i++) { // Make sure we skip instance that triggered the webhook if (!accountId.equals(instanceData[i][0])) { String endpointUrl = String.format("https://%s.mktorest.com", instanceData[i][0]); // Generate access token String identityUrl = String.format("%s/identity/oauth/token?grant_type=client_credentials&client_id=%s&client_secret=%s", endpointUrl, instanceData[i][1], instanceData[i][2]); String token = seu.getToken(identityUrl); // Get lead records for given email address (may be duplicates) String getLeadsUrl = String.format("%s/rest/v1/leads.json?access_token=%s&filterType=email&filterValues=%s", endpointUrl, token, emailAddress); String leads = seu.getLeads(getLeadsUrl); // Update unsubscribed field in lead record String updateLeadsUrl = String.format("%s/rest/v1/leads.json?access_token=%s", endpointUrl, token); seu.updateLeads(updateLeadsUrl, leads, accountId); } } System.exit(0); } // Call Identity Service to generate access token public String getToken(String url) { // Call Identity Service String tokenData = getData(url); // Convert response into JSONObject JSONParser parser = new JSONParser(); Object obj = null; try { obj = parser.parse(tokenData); } catch (ParseException pe) { System.out.println("position: " + pe.getPosition()); System.out.println(pe); } // Retrieve access_token JSONObject jsonObject = (JSONObject)obj; return jsonObject.get("access_token").toString(); } // Call Get Multiple Leads by Filter Type Service to get lead records public String getLeads(String url) { return getData(url); } // Call Create/Update Lead Service to update "unsubscribed" flag in lead record public void updateLeads(String url, String leads, String account) { JSONObject body = composeBody(leads, account); if (body != null) { postData(url, body); } } // Compose JSON body for Create/Update Leads Service private JSONObject composeBody(String leads, String account) { JSONObject body = new JSONObject(); // Convert leads into JSONObject JSONParser parser = new JSONParser(); Object obj = null; try { obj = parser.parse(leads); } catch (ParseException pe) { System.out.println("position: " + pe.getPosition()); System.out.println(pe); } JSONObject leadsObj = (JSONObject)obj; Object success = leadsObj.get("success"); if (success.equals(true)) { body.put("action", "updateOnly"); body.put("lookupField", "id"); body.put("asyncProcessing", "true"); // Build array of lead objects JSONArray input = new JSONArray(); JSONArray result = (JSONArray) leadsObj.get("result"); Iterator<JSONObject> iterator = result.iterator(); while (iterator.hasNext()) { JSONObject leadIn = (JSONObject)iterator.next(); JSONObject lead = new JSONObject(); lead.put("id", leadIn.get("id")); lead.put("unsubscribed", "true"); lead.put("unsubscribedReason", "Cross instance synch triggered by webhook from: " + account); input.add(lead); } body.put("input", input); } return body; } // HTTP POST request private String postData(String endpoint, JSONObject body) { String data = ""; try { // Make request URL url = new URL(endpoint); HttpsURLConnection urlConn = (HttpsURLConnection) url.openConnection(); urlConn.setRequestMethod("POST"); urlConn.setAllowUserInteraction(false); urlConn.setDoOutput(true); urlConn.setRequestProperty("Content-type", "application/json"); urlConn.setRequestProperty("accept", "application/json"); urlConn.connect(); OutputStream os = urlConn.getOutputStream(); os.write(body.toJSONString().getBytes()); os.close(); int responseCode = urlConn.getResponseCode(); if (responseCode == 200) { System.out.println("Status: 200"); InputStream inStream = urlConn.getInputStream(); data = convertStreamToString(inStream); System.out.println(data); } else { System.out.println(responseCode); data = "Status:" + responseCode; } } catch (MalformedURLException e) { System.out.println("URL not valid."); } catch (IOException e) { System.out.println("IOException: " + e.getMessage()); e.printStackTrace(); } return data; } // HTTP GET request private String getData(String endpoint) { String data = ""; try { URL url = new URL(endpoint); HttpsURLConnection urlConn = (HttpsURLConnection) url.openConnection(); urlConn.setRequestMethod("GET"); urlConn.setAllowUserInteraction(false); urlConn.setDoOutput(true); int responseCode = urlConn.getResponseCode(); if (responseCode == 200) { System.out.println("Status: 200"); InputStream inStream = urlConn.getInputStream(); data = convertStreamToString(inStream); System.out.println(data); } else { System.out.println(responseCode); data = "Status:" + responseCode; } } catch (MalformedURLException e) { System.out.println("URL not valid."); } catch (IOException e) { System.out.println("IOException: " + e.getMessage()); e.printStackTrace(); } return data; } private String convertStreamToString(InputStream inputStream) { try { return new Scanner(inputStream).useDelimiter("A").next(); } catch (NoSuchElementException e) { return ""; } } } |
Marketo Setup
Perform the following steps for each Marketo instance that you would like to sync.
- Create a Custom Service with role permission: Read-Write Lead. If you are unfamiliar with creating a Custom Service, click here.
- Create a Webhook that calls your external web service. If you are unfamiliar with creating Webhooks, click here.
- Add Webhook as flow step in Smart Campaign.
The screenshot below shows how to create a webhook to invoke the service specified above using tokens to automatically populate the query parameters.
Now that we have created our webhook, we can add it to a Smart Campaign as a flow action.
The Smart List should contain an “Unsubsubscribes from Email” trigger.
Validation
To test this all out, create a lead with the same email address in several Marketo instances. Make sure that you own the email address! In one instance trigger a send email flow action, open the resultant email, and click unsubscribe. To validate results, log into each of the other instances and inspect the lead records associated with the email address. The “Unsubscribed” checkbox should be checked, and the “Unsubscribed Reason” field should contain a note with the source account id that initiated the sync.