Tag Archives: rails

HOWTO: Integrate Adobe Sign API with Ruby on Rails

Adobe Sign is a product that allows companies to send documents to another individual or company to sign. This product is similar to Docusign and Hellosign. Most signing services provide APIs that allow developers to automate the signing process.

This document describes the steps involved to programmatically send a PDF document with dynamically-filled fields to a user to sign, and receive call-backs when the document is signed (or rejected).

  1. Signup for an Adobe Sign developer account at https://www.adobe.io/products/sign.html. Please note that they don’t let you register using the same email as an existing Adobe Sign user (Why? I don’t know..)

  2. Once registered and confirmed, you can goto Adobe Sign’s website, log in, and upload a PDF template into their document library. You can add data fields to various locations. Don’t forget to add a signature field and a signature date field.

  3. Before we can start the OAuth process, we need to create a “webhook” on your web application to receive POSTs from Adobe Sign. We need that during the OAuth process to read an OAuth code from them, and also during the signature process to receive call-back when a documents is signed. In Rails, you can do the following to create a simple webhook that prints out the parameters:

    routes.rb

    
      post '/sign_webhook' => 'adobe_sign#sign_webhook'
      get '/sign_webhook' => 'adobe_sign#sign_webhook'
    

    adobe_sign_controller.rb

    
      def sign_webhook
        puts "---------- sign webhook : params -------------------------"
        puts params
        puts "---------- sign webhook : request body -------------------"
        puts request.body.read
        puts "---------- sign webhook : end ----------------------------"
      end
    

    adobe_sign/sign_webhook.html.erb

    
      <%= debug(params) %>
    
  4. You can test out the webhook by checking: http://localhost:3000/sign_webhook?a=1&b=2. Push this to production. For the rest of the document, I am using “livio.herokuapp.com” as the name of our production machine. Make sure you have SSL enabled on your production server. Adobe Sign won’t allow callbacks to go to unsecured urls.

  5. We can begin the OAuth process to get access token. Here’s some doc provided by them, but it sure requires a lot of trial and error to get the whole workflow to work properly, as the documentation is not very easy to follow.

    https://www.adobe.io/products/sign/docs/overview

  6. Follow the steps described here to create an app for OAuth. Make sure redirect URL is specified. For this example, we are using https://livio.herokuapp.com/sign_webhook. Check all the boxes in the “Scopes” section and use “account” as modifier. To perform what we are trying to accomplish in this document, you will need the following scopes:

    
    user_login:account
    library_read:account
    agreement_send:account
    agreement_write:account
    agreement_read:account
    

    Take down the client ID and client Secret as we will need that in subsequent steps.

  7. We are ready to retrieve an access token to begin making API calls. Monitor your production server log to capture the parameters sent to the webhook. In this example, since we are using Heroku, we can simply run heroku logs --tail. Next, goto a web browser and type in the following URL. Make sure all your URL parameters are properly encoded.

    
    
    https://secure.na1.echosign.com/public/oauth?
    
    redirect_uri=https%3A%2F%2Flivio.herokuapp.com%2Fsign_webhook&
    response_type=code&
    client_id=[your_client_id]&
    scope=user_login%3Aaccount+library_read%3Aaccount+agreement_send%3Aaccount+agreement_write%3Aaccount+agreement_read%3Aaccount
    
  8. When you authenticate and grant access to the app, you should see the following in your production log:

    
    {"code": "[your_oauth_code]",
     "api_access_point": https://api.na1.echosign.com/",
     "web_access_point": https://secure.na1.echosign.com/"}
    

    Capture the “code” in the log file and use it in the following step.

  9. Use “curl” or other tools to make the following POST request. I like a Chrome plug-in called Postman as it is very easy to debug and the UI is very intuitive.

    
    POST: http://api.echosign.com/oauth/token?
       code=[your_oauth_code]&
       client_id=[your_client_id]&
       client_secret=[your_client_secret]&
       redirect_uri=https%3A%2F%2Flivio.herokuapp.com%2Fsign_webhook&
       grant_type=authorization_code
    

    Make sure you keep body empty when you make the POST request. You should get the following response:

    
    {
        "access_token": "[your_access_token]",
        "refresh_token": "[your_refresh_token]",
        "token_type": "Bearer",
        "expires_in": 3600
    }
    
  10. The access token is only valid for an hour. That won’t work for us since we will be making server side call non-deterministically. Luckily, the refresh token is valid forever, and we can use the refresh token to get a new access token. So before we send an agreement, we need to grab a new access token. Here’s some Ruby code to get a new access token:

    
      def self.adobe_sign_access_token
    
        require 'net/http'
        require 'json'
    
        refresh_token = "[your_refresh_token]"
        client_id = '[your_client_id]'
        client_secret = '[your_client_secret]'
    
        @host = 'api.na1.echosign.com'
        @port = '443'
        @path = "/oauth/refresh"
        @body = "refresh_token=#{refresh_token}&client_id=#{client_id}&client_secret=#{client_secret}&grant_type=refresh_token"
    
        request = Net::HTTP::Post.new(@path, initheader = {'Content-Type' => 'application/x-www-form-urlencoded'})
        request.body = @body
    
        http = Net::HTTP.new(@host, @port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    
        response = http.start { |http| http.request(request) }
    
        j_response = JSON.parse(response.body)
        if j_response.present?
          puts "[adobe_sign] Access Token = #{j_response["access_token"]}"
          return j_response["access_token"]
        end
    
        raise "Error getting Adobe Sign Refresh Token"
      end
    
  11. Before we can send an agreement to sign, we need to retrieve the template ID of the template we uploaded in an earlier step. We can do so by making the following GET request.

    
      def self.adobe_sign_get_templates
    
        @host = 'api.na1.echosign.com'
        @port = '443'
        @path = "/api/rest/v5/libraryDocuments"
    
        access_token = self.adobe_sign_refresh_token
        request = Net::HTTP::Get.new(@path, initheader = {"Access-Token" => access_token})
        request.body = @body
    
        http = Net::HTTP.new(@host, @port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    
        response = http.start { |http| http.request(request) }
    
        j_response = JSON.parse(response.body)
        ap j_response
    
      end
    
  12. Finally, we are all set to send an agreement. The following code shows how the form fields can be pre-filled. In this example, we assumed that there are two fields called “cost” and “vendor_name”, whose value will be passed into the method.

    
      def self.adobe_sign_send_agreement (email, cost, vendor_name)
    
        library_document_id = "[your_template_id]"
    
        # get access token
        access_token = self.adobe_sign_refresh_token
    
        @host = 'api.na1.echosign.com'
        @port = '443'
        @path = "/api/rest/v5/agreements"
    
        @body = {
            documentCreationInfo: {
                fileInfos: [{
                                libraryDocumentId: library_document_id
                            }],
                name: "Our First Agreement",
                recipientSetInfos: [{
                                        recipientSetMemberInfos: [{email: email}],
                                        recipientSetRole: "SIGNER"}],
                signatureType: "ESIGN",
                signatureFlow: "SENDER_SIGNATURE_NOT_REQUIRED",
                callbackInfo: "https://livio.herokuapp.com/sign_webhook",
                mergeFieldInfo: [
                    {defaultValue: cost, fieldName: "cost"},
                    {defaultValue: vendor_name, fieldName: "vendor_name"},
                ]
            }
        }.to_json
    
        request = Net::HTTP::Post.new(@path, initheader = {'Content-Type' => 'application/json', "Access-Token" => access_token})
        request.body = @body
    
        http = Net::HTTP.new(@host, @port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    
        response = http.start { |http| http.request(request) }
        j_response = JSON.parse(response.body)
    
        ap j_response
      end
    

    When you run the method, if everything goes well, you will get the following response with the agreement ID.

    
    {
      "agreementId": "3AAABLblqZhCE1yk2iufMtVoHyVtjrfMtCFLmYXXyaWP18_eNdnLq3_ifO2A2XUhRq5XiXA3PXwWgOdiPLtQGj3oFLuBgth62"
    }
    

    You probably want to store it in the database somewhere, and when the agreement is signed by the user, the webhook will get triggered and be able to update the status.

  13. The agreement is sent. When the recipient signs the doc, you will receive a POST to the webhook with the following parameters. Your webhook should grab the status and update your database accordingly.

    
    {"documentKey"=>"[agreement_id]", "eventType"=>"ESIGNED", "status"=>"SIGNED"}
    
  14. And that’s it! We’ve now sent an agreement out for signature with pre-filled form fields. When the agreement is signed, our web app receives a callback which could update the database or notify the administrator. Can you think of other use cases for Adobe Sign? Let me know!

Email Open Tracking using AWS CloudFront

At Plum District, we send out a lot of marketing emails – on some days more than 5 millions daily. It’s a big part of our business, and we spend a lot of our time tracking open and send rates, and running analytics on this data. Luckily, SendGrid is able to funnel all that data via their Event Notification API where all the events (processed, opened, clicked, etc) are sent to us. We recently acquired another company, but unfortunately they send their emails via Amazon SES which doesn’t have any tracking.

In this article, I’ll discuss an innovative way to track email open rate using Amazon CloudFront. Well, the basic mechanism is really just pixel tracking. CloudFront provides detailed access log that’s dumped directly into S3. Hence, we can host the pixel in CloudFront, put the pixel in the email (plus any optional HTTP params that you want to track) and be able to track how many times that pixel is loaded, and finally track open count plus a bunch of other information including demographic, most active timeframe, etc.

Here are the steps:

  1. Create an S3 bucket if you don’t have one to store the pixel. In this case, I’ve put the pixel under s3n://plum-mms/images/1.gif. Feel free to borrow the pixel here. It’s just a 1×1 transparent gif. Also, make sure that bucket has the appropriate permission, i.e. Everyone → Open / Download.

  2. Create another S3 bucket for logs. In our case, we’ve created a bucket named plum-mms-logs.

  3. Configure a CloudFront distribution using the S3 bucket you created in step 1. Make sure you have the following configured when creating:

    Logging
    On. This tells CloudFront to enable logging.
    Cookie Logging
    Off. We don’t really need that information.
    Log Bucket
    This tells CloudFront where to dump the access log. In my case, I selected plum-mms-logs.s3.amazonaws.com which corresponds to the bucket I created in step 2.
  4. Capture the domain name associated with your new CloudFront distribution. In our case, it’s d2x9v85k2ohcuy.cloudfront.net. You can test that http://d2x9v85k2ohcuy.cloudfront.net/images/1.gif returns the GIF file in a browser.

  5. Insert the pixel in your email template. We want to capture who has opened an email, so we’ve included the subscriber ID, as well as the email category as GET parameters. Here’s a bit of Ruby code to generate the pixel:

    
    pixel_tracking_url = nil
    if subscriber && category
      pixel_tracking_url = "http://d2x9v85k2ohcuy.cloudfront.net/images/1.gif?sid=#{subscriber.id}&category=#{category}"
    end
    
    
  6. And in your email template (.erb file), you can add the code anywhere in the email:

    
    <% if @pixel_tracking_url %>
      <img src="<%= @pixel_tracking_url%>" width="1" height="1" alt=""/>
    <% end %>
    
    

Now we are ready to roll! You can send the email to a few test email accounts, and see if you are getting the logs. It usually takes a few hours for CloudFront to push the log files out to your logging bucket.