This post was updated on January 6, 2021 to use Bitly’s API v4, which changed how to authenticate to their API.
This post is inspired by Jared Baker who asked about how to integrate Salesforce and Bitly to create short urls to include in email alerts.
This was a fun challenge and I ended up using @InvocableMethod apex class so that the URL shortening could be kicked off by Process Builder. In my example below, I added a custom field “Short URL” to the Case object. I have a single Process that kicks off when a Case is created or updated and monitors when the “Short URL” field changes. If the field is blank then it calls out to Bitly to generate the url asynchronously (the async part is important and I’ll discuss that later). When the “Short URL” field becomes non-blank then Process Builder sends an email alert to the case owner with the shortened link.
Step 1. Sign-up for Bitly (FREE)
To use the Bitly API you will need to register. You can sign-up using Twitter, Facebook, or create a new login with their service. I chose to create a Bitly specific login because I don’t like sharing my social logins with websites.
Step 2. Generate a Bitly API Access Token
Go to https://bitly.is/accesstoken to generate an access token you can use with the Bitly API.
You’ll be prompted for your Bitly password, enter it then click Generate Token. A pop-up will appear with your new access token, copy it and store it someplace secure.
Step 3. Find Your Bitly Group ID
When using the Bitly API, instead of authenticating with your username and password you’ll instead use a Bitly group id and an access token. You generated an access token in the prior step. Now you need to find your group id. The group id is displayed in the URL when logged in to Bitly:
https://app.bitly.com/[group_id]/bitlinks/
If you have a paid account, you might consider creating a test group as described here.
Step 4. Create Named Credential in Salesforce
In Salesforce, go to Setup and enter “Named Credential” in the quick find box, then click Named Credentials. Create a new Named Credential to securely store your Bitly group id and access token.
- Label: Bitly
- Name: Bitly
- URL: https://api-ssl.bitly.com
- Identity Type: Named Principal
- Authentication Protocol: Password Authentication
- Username: <your bitly group id>
- Password: <your bitly access token>
- Allow Merge Fields in HTTP Header: ✅
- Allow Merge Fields in HTTP Body: ✅
Step 5. Create Custom Field “Short URL”
Create a custom URL field on your objects of interest to store the Bitly generated short urls. Depending on your situation you may have one or more. The sample code I share only supports one on the Case object, but you can easily adapt the code to your needs.
Step 6. Create Bitly Apex Classes
I developed two classes, one that encapsulates the Bitly http callouts and the other to expose the service as @InvocableMethod so it can be invoked by Process Builder or Flow Builder. An important point to note is that the BitlyShortenURLInvocable class actually calls a @Future method to generate the short urls asynchronously. This is required because if you don’t then you’ll get an error that you have uncommitted work and you must either commit or rollback before making the http callout to the Bitly service. When the async process completes then it updates the Case objects with the new short urls.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Simple service to make http callout to Bitly url shortener service. | |
*/ | |
public class BitlyService { | |
/** | |
* Given a long URL will return the shortened version. | |
* https://dev.bitly.com/api-reference#createBitlink | |
*/ | |
public String shorten(String url) { | |
HttpRequest req = new HttpRequest(); | |
req.setEndpoint('callout:Bitly/v4/shorten'); | |
req.setMethod('POST'); | |
req.setHeader('Authorization', 'Bearer {!$Credential.Password}'); | |
req.setHeader('Accept', 'application/json'); | |
req.setHeader('Content-Type', 'application/json'); | |
req.setBody(JSON.serialize(new Map<String, Object>{ | |
'group_guid' => '{!$Credential.UserName}', | |
'long_url' => url | |
})); | |
HttpResponse res = new Http().send( req ); | |
Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody()); | |
return (String) response.get('link'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class BitlyShortenURLInvocable { | |
@InvocableMethod( | |
label = 'shorten' | |
description = 'Given case IDs then generates a bitly short url for them' | |
) | |
public static void shorten(List<ID> caseIds) { | |
// You can't invoke http callouts from Process Builder or Flow | |
// because the database transaction has not committed yet, you will get error: | |
// "System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out" | |
// To get around this then we'll actually make the call in a @Future method asynchronously. | |
shortenAsync( caseIds ); | |
} | |
@Future(callout = true) | |
private static void shortenAsync(List<ID> caseIds) { | |
// Fetch your data and a field where you want to store the generated short url | |
List<Case> cases = new List<Case>([ SELECT Id, Short_URL__c FROM Case WHERE Id IN :caseIds ]); | |
// Service to actually call out to bitly and get a shortened url | |
BitlyService service = new BitlyService(); | |
// Bitly does not support bulk url shortening | |
// so you must make each call individually. | |
// Do be aware of Bitly's rate limiting if you try | |
// to mass create a bunch of records in short succession | |
// https://dev.bitly.com/docs/getting-started/rate-limits | |
for ( Case caseObj : cases ) { | |
// in this trivial example, we're just creating short urls to the record itself | |
caseObj.Short_URL__c = service.shorten( 'https://login.salesforce.com/' + caseObj.id ); | |
} | |
// update the records with their short urls | |
// use workflow or trigger to fire off the short url field being populated | |
// to then send email alerts, etc. including the short url | |
if ( cases.size() > 0 ) { | |
update cases; | |
} | |
} | |
} |
Test classes are available here. For more information, check out Testing HTTP Callouts.
Step 7. Create Process to Automate URL Shortening
In our simple example, navigate to Process Builder to create a new Process on the Case object whenever it is created or edited.
- The first decision criteria should check if the custom “Short URL” field is blank.
- If true then call Apex class BitlyShortenURLInvocable, passing in the case record’s ID
- The second decision criteria should check if the custom “Short URL” field is not blank and was changed.
- If true then send an email alert or some other action to use the now populated “Short URL” field
Step 8. Create a New Case!
Activate your Process then go to create a new Case. Once saved, you may need to refresh the page after a couple seconds and then you’ll notice the “Short URL” field has been populated. Voila!
In this example, I setup a simple email alert to the Case owner with the short url link as shown in this screen shot:
Thank you so much for posting! Could you give an example of everything that would need to be chanced to make the BitlyShortenURLInvocable Class specify a different object? For example, if I wanted to reference the “short_url__c” field on the following object: Custom_Object__c.
Also – I didn’t see any specific references to the cases object on the BitlyService Class. Is there anything that would need to be changed there?
Thanks again! This is a great start to what I need to accomplish!
LikeLiked by 1 person
Hi Doug! Thank you for posting! I am sorry if I am pinging you twice… I thought I commented on this but I am not seeing it.
Is there any way that you could help me with some instruction on how to customize the BitlyShortenURLInvocable class so that it is specific to a custom object?
Also – would the BitlyService class need to be modified? I didn’t see any specific object references there.
Lastly, could I use this same approach to take the value from a formula field and convert to a short URL?
Thanks again for posting this! Super helpful!
LikeLiked by 1 person
Hi Derek!
Thanks for your interest in this solution. By the way, comments have a delay due to moderation to avoid spam (stupid bots!)
To customize this solution to work with objects other than Case you will need to alter the BitlyShortenURLInvocable apex class. The BitlyService is object agnostic and won’t have to change. When creating your process with Process Builder, you will of course choose your custom object for the workflow logic to trip on rather than Case.
To get the BitlyShortenURLInvocable class to work with a custom object you need to change the ‘shortenAsync’ method, specifically line 22 to query your custom object and fields instead of Case. At line 32 then iterate your custom object instead of Case. Line 35 makes the call out to bitly to create a shortened url. In my example I create a url to the case record, but you could pass as the string argument any URL you wanted even referencing a URL or constructing a URL from a formula field.
Let me know if you have any other questions,
Doug
LikeLike
Thanks so much! I got it working as desired. 🙂
And now for the test class…
LikeLiked by 1 person
Hi Doug!
I am having same scenario in my case also..I have to deploy the same to PROD environment.Could you please help me with some sample test classes for the above classes so that i can deploy it to PROD.
Thanks in advance!
Anup
LikeLike
Hi Anup, I now have some sample test classes to get code coverage: https://gist.github.com/DouglasCAyers/de978590b97e235a96fc
LikeLike
Hey Doug,
Did you by chance ever create a test class? I worked on this a couple months ago, but it got tabled due to other projects. Now trying to work out the final steps here for deployment. I haven’t created a test class the Mock Http Callouts before. I have reviewed the following resources but didn’t have much luck trying to implement:
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http_testing_httpcalloutmock.htm
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http_testing_dml.htm
Would you happen to have an example of test class?
LikeLiked by 1 person
Hi Derek, I have not yet created a test class. A lot of people have asked for one, it’s still on my todo list when I get some time to work on it.
LikeLike
Hi Derek, I now have some sample test classes to get code coverage: https://gist.github.com/DouglasCAyers/de978590b97e235a96fc
LikeLike
Hi Doug,
I just had a go at your code but I get the following error: Error Occurred: An Apex error occurred: System.AsyncException: Future method cannot be called from a future or batch method: BitlyShortenURLInvocable.shortenAsync(List)
Help would be appreciated, thanks.
LikeLiked by 1 person
Sorry I fixed the above issue by just adding a proper criteria condition in the process builder. I now get a different issue with the generated URL. The populated field now comes out as: INVALID_URI
LikeLiked by 1 person
never mind again. my URL needed ‘https://’ just for the API even though it works without it normally
LikeLiked by 1 person
Hi Michael,
Thanks for the follow up and I’m glad you solved the problem. Maybe this will help someone else running into similar situation.
Doug
LikeLike
Hey Doug
I have ran into an issue while pushing this through to production now, as my trigger for the process above is also a trigger point in my business process at multiple stages I have multiple tests now failing as they have a dml statement in them. I keep reading to do ‘Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());’ and put the dml statememnt outside of test.starttest() and test.stoptest() but that just defeats the purpose of the test method. How can I keep my dml statements in my tests without it running into the error: Methods defined as TestMethod do not support Web service callouts
Much appreciated.
LikeLiked by 1 person
Hi Michael,
I know it’s not ideal when doing HTTP callouts and DML, but I think we just have to configure the unit test that way to get it to run per the docs, https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http_testing_dml.htm
If it makes you feel any better, the Test.startTest() and Test.stopTest() simply reset governor limits so that when you are building up the sample data ahead of time that those DML and CPU limits don’t count against you so you get a more realistic idea of if the code you’re actually testing would exceed a governor limit. It also ensures that async code actually completes at the Test.stopTest() point.
Doug
LikeLike
Nice…
LikeLiked by 1 person
Thanks Mohsin!
LikeLike
Hi Doug
Not sure if I posted my comment/Query correctly yesterday – But I’ve now found the answer – a bit of googling and finding a post that referred to your Bitly shortener and gave a variation that matched what I was trying to do.
LikeLiked by 1 person
Hi Greg,
I did not see you earlier comment but glad that you got something working!
Doug
LikeLike
I was excited to find this post but an unable to get it to work. It looks like Bitly has recently changed its access policies. If you are able to update the apex it would be very much appreciated!
LikeLiked by 1 person
Hi Allen, are you getting any specific errors?
LikeLike
Hi Dough,
When I am following the same process to update Short_URL__c field on case by case trigger then I am getting error “System.CalloutException: java.security.cert.CertificateException: No subject alternative DNS name matching api.ssl.bitly.com found.”
Please help me on this.
Thanks,
Rahul Mittal
LikeLiked by 1 person
Hi Rahul, Bitly may have changed their API endpoints and/or certificates for their domains. You may need to modify the apex code making the callout per https://dev.bitly.com/api.html
LikeLike
Hi Doug, Thanks for your quick reply.
This URL should be update in Named Credential or in apex class? Because when I modified URL into Named Credential setting with ‘https://dev.bitly.com/api.html’ then we are getting “System.CalloutException: The URI is invalid.” exception.
Thanks,
Rahul
LikeLike
I have a requirement where I have to create a VF page and then the user will enter any of the URL and on click of a button the URL entered by User should be shortened and should be displayed on the same page
LikeLike
FLOW_ELEMENT_ERROR|An Apex error occurred: System.AsyncException: Future method cannot be called from a future or batch method: BitlyShortenURLInvocable.shortenAsync(List)
The error is true but how every one is not getting. Am i miss something?
LikeLike
Hi Krish,
By chance is the BitlyShortenURLInvocable class being triggered by other asynchronous code? For example, a scheduled job or batch job trying to call the invocable class, or async jobs updating records that cause process builder or flows to try and call the invocable class?
Per the error message, this invocable class can’t be invoked in an asynchronous context because it itself makes a @Future call. Meaning, as the invocable code is written now, you’d need to avoid having it invoked from scheduled, queueable, batch, or other async code.
LikeLike
Hey Doug, is there a way where we can use tinyURL instead of bitly here?
LikeLiked by 1 person
I’m not aware of an API for TinyURL like Bitly has. I did discover that you can just make a request to one of their URLs to shorten one:
Here’s how to do it from the command line in macOS, Linux, or Windows if using git-bash:
curl -X POST -H "Content-Length: 0" "https://tinyurl.com/api-create.php?url=https://salesforce.com"
When making the request from Apex I never got result back, but I suspect that might be due to IP rate limiting.
LikeLike
Hi
I got a problem.
I follow your code, and build my shorten URL.
Everything is good, thank for your effort.
But this month (Sep.24), the code was failed.
The error message like below:
Failed to invoke future method ‘private static void shortenAsync(List)’ on class ‘BitlyShortenURLInvocable’ for job id ‘**********’
caused by: System.CalloutException: The URI is invalid.
Class.BitlyService.shorten: line 31, column 1
Class.BitlyShortenURLInvocable.shortenAsync: line 44, column 1
Any idea for this?
LikeLike
I got solution by checking log.
The getAccessToken() need Content-Length
I modify the code as below:
private String getAccessToken() {
LikeLiked by 1 person
@haily
@doug
This is how I have it set up, no matter what I do, I keep getting 403 status and forbidden as message. I have used named credential as well as tried having the header in apex itself. Any idea what am I doing wrong?
HttpRequest req = new HttpRequest();
req.setEndpoint(
‘callout:Bitly/v4/shorten’ +
‘?access_token=’ + this.accessToken +
‘&longUrl=’ + EncodingUtil.urlEncode( url, ‘UTF-8’ ) +
‘&format=txt’ );
req.setMethod(‘POST’);
req.setHeader(‘Content-Length’, ‘0’); // add this line
req.setHeader(‘Accept’, ‘application/json’);
Http http = new Http();
HttpResponse res = http.send(req);
System.debug(res);
return res.getBody();
LikeLike
Doug,
Thanks so much for this (and all you’re other work that I’ve benefitted from). I’ve taken what you’ve created. Got initial test (without Process) running and succeeding, all classes compiled (confirmed via a List View in Apex Classes filtered by Classes IsValid=false), @InvokableMethod assigned, etc and can’t get the Method to show up in the Apex Classes dropdown in the Process Builder Actions.
@InvocableMethod(
label = ‘Shorten Digital Assessment Tracking Url’
description = ‘Given case IDs then generates a bitly short url for them’)
public static List ShortenDigitalAssessmentTrackingUrl( List dacIds ) {
ShortenDigitalAssessmentTrackingUrlAsync( dacIds );
return new List();
}
Thoughts? Thanks in advance.
Peter
LikeLike
Doug, Well, funny enough, I created another simple class with an empty void method, no parameters with the @InvocableMethod attribute and that showed up in Process Builder, I slowly, one change at a time reconstructed this method (ie: adding method param, next adding changing void to return List) all the while changing the label (ie: Test Invoke Method 1 then Test Invoke Method 2) to ensure that each time I made a change that Process Builder was actually showing the most recent version. Made it all the way to reconstruct the original Method and it still showed in PB. Then, strange enough on the last try, the original method then also showed up in PB. I had compiled classes, ran test, etc. Don’t understand what happened, but I can now access it in PB so . . . . . whatever 🙂
Peter
LikeLiked by 1 person
Hi everyone, a bit overdue but I’ve just updated the blog post and code to support Bitly’s API v4.0, which has a new authentication flow which is likely why you all were getting HTTP 403 errors.
LikeLike
As of Jan-7-2021, I confirm that upon following all the steps mentioned by Doug, I was able to generate short urls.
LikeLiked by 1 person
Hi Doug,
This is a great implementation. I was curious what if a salesforce user wants to share the short URL with a non-salesforce user. Will the page be accessible without any security concerns?
If yes, can you advice if any additional steps required.
LikeLike