Connect Salesforce to Productboard’s Notes API

Many companies have different Salesforce configurations, making it challenging to integrate natively with Salesforce. To address your specific needs and configuration, you can use the Productboard Notes API. You can share the sample setup below to help your Salesforce Admins and Developers set up an API integration faster.

Note: Knowledge of the Salesforce Developer Console, APEX, and APIs will be very useful during the setup of this integration.

How the sample integration works

The sample connects a custom object in Salesforce to Productboard (PB) through Apex and our Notes API and creates a new Productboard note every time a custom object is created. For context, Apex is a programming language that “allows developers to execute flow and transaction control statements on Salesforce servers in conjunction with calls to the API.” In other words, our customers can add custom scripts that send requests to Productboard’s API based on events that occur in their own Salesforce instance and host these scripts in Salesforce.

Setting up the integration

Step 1: Setting up the custom object in Salesforce

In Setup > Object Manager, create a new Object titled Productboard Note with the following labels and API names. Make note that the API names will be used in the Apex files to set up the trigger, and ignore the “__c” at the end of the API names when creating yours - it is automatically added at the end for you. Here’s a table of the labels that are created in the sample and what part of the Productboard note (in Productboard) they are mapped to:

Field LabelField Name (used in API)Data TypeConnection to PB
AccountProductboard_Note_SFDC_Account__cMaster-Detail(Account)None - used to connect to account in Salesforce)
Additional ContextAdditional_Notes__cLong Text Area(32768)Added to body of note
ContactProductboard_Note_SFDC_Account_Contact__cLookup(Contact)Email is taken from contact to be applied to the email at the user level of note
Created ByCreatedByIdLookup(User)Added to body of Productboard note
Customer RequestCustomer_Request__cLong Text Area(32768)Added to body of Productboard note
Last Modified ByLastModifiedByIdLookup(User)Added to note object in Salesforce
OpportunityProductboard_Note_SFDC_Opportunity__cLookup(Opportunity)None - used to connect to opportunity in Salesforce)
Opportunity RiskOpportunity_Risk__cPicklistAdded to body of Productboard note
Productboard Note TitleNameText(80)Added to title of Productboard note
Productboard Note URLProductboard_Note_URL__cURL(255)Added to title of Productboard note
TagsProductboard_Tags__cPicklist (Multi-Select)Added as tags to the Productboard note

Note: You can change the fields of your Productboard Note in Salesforce; however, you must update the Apex classes, trigger, and test to reflect those changes.

Step 2: Create Apex triggers and classes

In the Salesforce Developer Console, create the following files and copy in the code provided to you. We will be creating one trigger and five classes.

This script will work for the custom object we just created above. You may already have an object in Salesforce you want to send to Productboard. In that case, the files below can be used as a blueprint for setting up your own integration.

New Trigger titled: ProductboardTrigger

// Trigger to run when new Productboard Note is created in Salesforce
trigger ProductboardTrigger on Productboard_Note__c (after insert) {
    // Use a for loop here in case of multiple Note submissions at the same time. The for loop will allow the trigger to iterate over each submission
    for (Productboard_Note__c note_details: Trigger.new){
        system.debug('New PB Note: ' + note_details);        
        // Create new note based on structure in the ProductboardNoteStructure class
        ProductboardNoteStructure note_payload = new ProductboardNoteStructure();
        // Use the source object in ProductboardNoteStructure to meet the requirements of payload of source and ID
        ProductboardNoteStructure.Source_Object source_obj = new ProductboardNoteStructure.Source_Object();
        // Grab the contact i.e. the customer/end user
        Contact note_contact = [SELECT Email FROM Contact WHERE ID = :note_details.Productboard_Note_SFDC_Account_Contact__c LIMIT 1];
        // Grab the user who is creating the note i.e. our colleague in SFDC
        User created_by = [SELECT Id, Name, ProfileId FROM User WHERE ID = :note_details.CreatedById];
        // Grab the URL to the note to add as the Display URL in Productboard
        String ProductboardNoteSFDC_URL = URL.getSalesforceBaseUrl().toExternalForm() + '/' + note_details.id;
				system.debug('ProductboardNoteURL:' + ProductboardNoteSFDC_URL);        
        
        // Parse through the tags to add commas in the array
        String[] note_tags_list = new String[]{};
        String note_tags_string = note_details.Productboard_Tags__c;
        String[] note_tags = note_tags_string.split(';');
        for (String tag: note_tags) {
            note_tags_list.add(tag);
        }
        
        // Construct the note payload
        note_payload.title = note_details.Name + ' - via Salesforce';
        note_payload.display_url = ProductboardNoteSFDC_URL;
        note_payload.content = note_payload.FormatNoteContent(note_details.Customer_Request__c, note_details.Additional_Notes__c, note_details.Opportunity_Risk__c, created_by.Name);
        note_payload.customer_email = note_contact.Email;
        note_payload.tags = note_tags_list;
        source_obj.origin = 'Salesforce';
        source_obj.record_id = note_details.Id;
        note_payload.source = source_obj;
        String payload = JSON.serializePretty(note_payload);
        system.debug('JSONd payload: ' + payload);
        
        ProductboardNote.sendPBNote(payload, note_details.id);
    }
}

New Class titled: ProductboardNoteStructure

// Class to help structure the note properly to send a payload 
public class ProductboardNoteStructure {
	Public String title;
    Public String content;
    Public String customer_email;
    Public String display_url;
    Public Source_Object source;
    Public List <String> tags;
    
    public class Source_Object {
       Public String origin;
       Public String record_id;
    }
    // Body of the Productboard note is formatted here
    public String FormatNoteContent(String Customer_Request, String Additional_Context, String Opportunity_Risk, String Name){
    	String content_start = string.format(
            '<h2>Customer Request</h2><p>{0}</p><br><h2>Additional Context</h2><p>{1}</p><br><h2>Opportunity Risk</h2><p>{2}</p><br><h2>Logged By</h2><ul><li><b>Name: </b>{3}</li></ul>', 
            new String []{Customer_Request, Additional_Context, Opportunity_Risk, Name}
        );
        return content_start;
    }
}

New Class titled: ProductboardNote

Changes needed:

  • Add your Bearer Token on line 10.
// Class to structure the payload with information from the custom object and send the HTTP request to Productboard's API
public class ProductboardNote {
	@future(callout=true)
    public static void sendPBNote(String payload, ID note_id) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Productboard_sandbox');
        req.setMethod('POST');
        
				// Setting up headers to authorize the POST requests
        String bearerToken = '<Add Bearer Token Here>';
        req.setHeader('Authorization', 'Bearer ' + bearerToken);
        req.setHeader('Content-Type', 'application/json');
      	
        req.setBody(payload);
        system.debug('Class payload: '+payload);

				// Receiving response from Productboard's API
				// Updating the Salesforce object with the link to the Productboard Note
        try {
        Http http = new Http();
        HttpResponse res = http.send(req);
        ProductboardNoteResponse responseBody = (ProductboardNoteResponse)JSON.deserialize(res.getBody(), ProductboardNoteResponse.class);
				Productboard_Note__c[] current_note = [SELECT ID FROM Productboard_Note__c WHERE ID = :note_id LIMIT 1];
            for (Productboard_Note__c note :current_note) {
                // system.debug('PRE FILL URL Field: ' + note.Productboard_Note_URL__c);
                note.Productboard_Note_URL__c = ResponseBody.links.html;
                system.debug('Note: ' + note.Productboard_Note_URL__c);
                
            }
            update current_note;
            system.debug('Current Note: ' + current_note);
        }
        
        catch (Exception e) {
            system.debug('Error is: ' + e);
        }
    }
}

New Class titled: ProductboardNoteResponse

// Structuring the response from Productboard's API after submission
public class ProductboardNoteResponse {
	Public Data_Object data;
    Public Links_Object links;
    
    public class Links_Object {
       Public String html;
    }
    
    public class Data_Object {
       Public String id;
    }
}

New Class titled: Productboard_Test

Changes needed:

  • Add your admin emails on lines 15 - 16
  • Make changes to test data on lines 19 - 35
  • Add your Productboard workspace name on line 101
// Test that will need to run in order to deploy to production
@isTest
private class Productboard_Test {
    static testMethod void testPostiveNoteCreation() {
        String test_title = 'Productboard Test Note Title';
        String Customer_Request = 'TEST - This is the customer request';
        String Additional_Context = 'TEST - This is the additional context';
        String Opp_risk = 'Churn Risk';
        String Productboard_Note_Tags = 'Product 1; Product 2';
        List<String> tags_final = Productboard_Note_Tags.split(';');
       	date startDate = date.today();
        date closeDate = startDate.addMonths(3);
        
        // Add test_admin and admin emails to comply with test requirements 
        User[] test_admin = [SELECT Id, Name, ProfileId FROM User WHERE email = '[email protected]'];
        User admin = [SELECT Id, Name, ProfileId FROM User WHERE email = '[email protected]'];
        
        // Make changes to test Account, Contact, and Opp data as needed
        Account Test_Account = new Account();
        Test_Account.Name = 'Acme';
        insert Test_Account;
        
        Contact Test_Contact = new Contact();
        Test_Contact.FirstName = 'Bob';
        Test_Contact.LastName = 'Smith';
        Test_Contact.Email = '[email protected]';
        Test_Contact.AccountId = Test_Account.Id;
        insert Test_Contact;
        
        Opportunity Test_Opp = new Opportunity();
        Test_Opp.Name = 'AcmeOpp';
        Test_Opp.CloseDate = System.today();
        Test_Opp.AccountId = Test_Account.Id;
        Test_Opp.StageName = 'Prospecting';
        insert Test_Opp;
        
    for (User tester : test_admin){
        system.runAs(tester) {
        String admin_name = [SELECT Id, Name FROM Profile WHERE ID = :tester.ProfileId].Name;
        
        String content_start = string.format(
            '<h2>Customer Request</h2><p>{0}</p><br><h2>Additional Context</h2><p>{1}</p><br><h2>Opportunity Risk</h2><p>{2}</p><br><h2>Logged By</h2><ul><li><b>Name: </b>{3}</li></ul>', 
            new String []{Customer_Request, Additional_Context, Opp_risk, admin_name}
        );
        
        Productboard_Note__c Test_Productboard_Note = new Productboard_Note__c();        
        Test_Productboard_Note.Name = test_title;
        Test_Productboard_Note.Customer_Request__c = Customer_Request;
        Test_Productboard_Note.Opportunity_Risk__c = Opp_risk;
        Test_Productboard_Note.Additional_Notes__c = Additional_Context;
        Test_Productboard_Note.Productboard_Tags__c = Productboard_Note_Tags;
        Test_Productboard_Note.Productboard_Note_SFDC_Account__c = Test_Account.Id;
        Test_Productboard_Note.Productboard_Note_SFDC_Account_Contact__c = Test_Contact.Id;
        Test_Productboard_Note.Productboard_Note_SFDC_Opportunity__c = Test_Opp.Id;
        
		Test.startTest();
        	insert Test_Productboard_Note;
        	
        	Productboard_Note__c note_details = [SELECT Name, Customer_Request__c, Opportunity_Risk__c, Additional_Notes__c, Productboard_Tags__c, Productboard_Note_SFDC_Account__c, Productboard_Note_SFDC_Account_Contact__c, Productboard_Note_SFDC_Opportunity__c FROM Productboard_Note__C WHERE ID = :Test_Productboard_Note.Id];
			
        	System.assertEquals(test_title, note_details.Name);
        	System.assertEquals(Customer_Request, note_details.Customer_Request__c);
        	System.assertEquals(Opp_risk, note_details.Opportunity_Risk__c);
        	System.assertEquals(Additional_Context, note_details.Additional_Notes__c);
        	System.assertEquals(Test_Account.Id, note_details.Productboard_Note_SFDC_Account__c);
        	System.assertEquals(Test_Opp.Id, note_details.Productboard_Note_SFDC_Opportunity__c);
        	System.assertEquals(Test_Contact.Id, note_details.Productboard_Note_SFDC_Account_Contact__c);
            
            String[] note_tags_list = new String[]{};
        	String note_tags_string = note_details.Productboard_Tags__c;
        	String[] note_tags = note_tags_string.split(';');
        		for (String tag: note_tags) {
            	note_tags_list.add(tag);
            }
        
           	ProductboardNoteStructure note_payload = new ProductboardNoteStructure();
        	ProductboardNoteStructure.Source_Object source_obj = new ProductboardNoteStructure.Source_Object();
        	Contact note_contact = [SELECT Email FROM Contact WHERE ID = :note_details.Productboard_Note_SFDC_Account_Contact__c LIMIT 1];
        	// User created_by = [SELECT Id, Name, ProfileId FROM User WHERE ID = :note_details.CreatedById];
        
        	String ProductboardNoteSFDC_URL = URL.getSalesforceBaseUrl().toExternalForm() + '/' + note_details.id;
						system.debug('ProductboardNoteURL:' + ProductboardNoteSFDC_URL);   
            note_payload.title = note_details.Name + ' - via Salesforce';
            note_payload.display_url = ProductboardNoteSFDC_URL;
            note_payload.content = note_payload.FormatNoteContent(note_details.Customer_Request__c, note_details.Additional_Notes__c, note_details.Opportunity_Risk__c, admin_name);
            note_payload.customer_email = note_contact.Email; 
            note_payload.tags = note_tags_list;
            source_obj.origin = 'Salesforce';
            source_obj.record_id = note_details.Id;
            note_payload.source = source_obj;
            String payload = JSON.serializePretty(note_payload);
        
        	Test.setMock(System.HttpCalloutMock.class, new ProductboardMockResponse());
            ProductboardNote.sendPBNote(payload, Test_Productboard_Note.id);
        Test.stopTest();
        
        Productboard_Note__c note_final = [SELECT Productboard_Note_URL__c FROM Productboard_Note__c WHERE ID = :Test_Productboard_Note.Id];
        
        System.assertEquals(content_start, note_payload.content);
        // Add your Productboard workspace name below
        System.assertEquals('https://workspace-name.productboard.com/inbox/notes/123456', note_final.Productboard_Note_URL__c);
        }
    }
  }
}

Add Class titled: ProductboardMockResponse

Changes needed:

  • Add your Productboard workspace name on line 7
// The mock response when running the test
global class ProductboardMockResponse implements HttpCalloutMock {
    global HttpResponse respond(HttpRequest req) {
        HTTPResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        // Add workspace name below
        res.setBody('{"links": {"html": "https://workspace-name.productboard.com/inbox/notes/123456"},"data": {"id": "123456789-abcd-1234-abcd-123456789101"}}');
        res.setStatusCode(201);
        return res;
    }
}

Step 3: Test a trigger

Now that the files are set up, we can create a Productboard Note in Salesforce to test the trigger we created. To see the logs, click on the ProductboardTrigger file, double-click on the FutureHandler operation, and select the Debug Only checkbox.

Step 4: Test the trigger

This may be the trickiest part of the integration. To deploy this integration to production, the trigger needs to “pass” a test. Luckily, the test is set up for you already with the Productboard_Test and ProductboardMockResponse files. To run the test, click the Productboard_Test file, then the Run Test button at the top right of the Developer Console. You can click on the Tests tab below to see the Overall Code Coverage on the bottom right of your screen. You will need at least 75% code coverage to deploy to production.

Step 5: Deploy the trigger

After the test is completed, you will need to create an outbound change set to upload and deploy the trigger to production. Here’s a link to the documentation on deploying components to production.

Conclusion

By setting up an API connection between Salesforce and Productboard, you can greatly improve the efficiency and effectiveness of your organization's sales and product management processes. With this integration, you can seamlessly transfer data from Salesforce to Productboard, allowing you to have a comprehensive overview of customer interactions and feedback within Productboard.

Once the integration is set up, you will begin to receive notes in Productboard like the one below:

And in Salesforce, like: