All Ruby on Rails Node JS Android iOS React Native Frontend

Active Campaign Integration - What Do You Need to Remember About?

Automated e-mails is a big business nowadays, and quite a  topic to discuss. Usually when developing a rails application a simple SMTP provider like Sendgrid or Mandrill with an addition of Sidekiq and Cron is more than enough. Nevertheless, you may encounter clients who already used some tools and have a large mailing list in, ActiveCampaign being one of them. Here are a few tips about ActiveCampaign and Rails implementation with the use of CloudFlare.

Basic Architecture

Obviously, we are not going to reinvent the wheel. There is a nice active-campaign-rails gem, which is a wrapper for their API. You may also generate form the inside of ActiveCampaign itself - and then paste iframe inside your view/template, but since we want to have more control over it (e.g. adding styling to iframe is not possible) it’s not the way we will proceed. The other gem I used is reform for form objects. I don’t have a related Enquiry model, hence OpenStruct.new is passed to EnquiryForm instance. After a user submits this form, we just send the information to AC to let it deal with it. Here is the form object I used.

class EnquiryForm < Reform::Form

 properties :first_name, :email, :topic, :question

 validates :first_name, :email, :topic, :question, presence: true

 validates :email, email: true

end

I also wrote a Service Object that will handle submitting logic. I wrapped it in an Operations module (Trailblazer Book suggests it should be done this way, but whatever fits your needs).

module Operations

 module ActiveCampaign

   class EnquiryManager

     attr_reader :form


     def initialize(params)

       @params = params

       @form = EnquiryForm.new(OpenStruct.new)

       @list_id = AppConfig.active_campaign.api_enquiry_list_id

       @active_campaign = ::ActiveCampaign.new(

         api_endpoint: AppConfig.active_campaign.api_endpoint,

         api_key: AppConfig.active_campaign.api_key

       )

     end


     def call

       return unless form.validate(@params)

       sync_form(form)

     end


     private


     def sync_form(form)

       form.sync do |hash|

         result = parsed_form(hash)

         result['result_code'].to_i.positive?

       end

     end


     def parsed_form(hash)

       JSON.parse(

         @active_campaign.contact_sync(

           hash.merge(

             "status[#{@list_id}]" => 1,

             "p[#{@list_id}]" => [@list_id],

             'first_name' => hash['first_name'],

             'field[%TOPIC%,0]' => hash['topic'],

             'field[%QUESTION%,0]' => hash['question']

           )

         )

       )

     end

   end

 end

end

That I’m am verifying status with the use of the positive? method - AC doesn’t respond with 200 or 404 here, it’s just 1 or 0. Since, we wanted the user to be able to submit the form many times (and update the question field each time), I used contact_sync  instead of contact_add.

Ok, good to go, now some html form to handle it.

    .row

     .col-xs-6

       = f.field_container :first_name, class: ['form-group'] do

         = f.label :first_name

         = f.text_field :first_name, class: 'form-control'

         = error_message_on @enquiry_form, :first_name

     .col-xs-6

       = f.field_container :email, class: ['form-group'] do

         = f.label :email

         = f.email_field :email, class: 'form-control'

         = error_message_on @enquiry_form, :email

     .col-xs-12

       = f.field_container :topic, class: ['form-group'] do

         = f.label :topic

         = f.select :topic, @topics,    

         = error_message_on @enquiry_form, :topic

     .col-xs-12

       = f.field_container :question, class: ['form-group'] do

         = f.label :question

         = f.text_field :question, class: 'form-control'

         = error_message_on @enquiry_form, :first_name

And we are good to go. After a few tests I went to the ActiveCampaign console, displayed users list and bam! My e-mail was there. Let’s play some fifa and go home then... Well, not so fast!

Tip no. 1 Always add IP.

So in the documentation for contact_sync there is ip4 field.

 

IP address of the contact. Example: '127.0.0.1' If not supplied, it will default to '127.0.0.1'

While documentation stands that this field is not really required, I strongly recommend adding it. Collecting geo-location data might be one of the reasons, but in our case, the more serious one was that we were experiencing problems with setting up automation for some contacts that were added without providing the ip4 field in request. I e-mailed ActiveCampaign support about this issue, but it is hard to recreate it, as only part of our contacts had this problem. Ok then, let’s add the above mentioned field then. I modified my service object a little bit.

      def parsed_form(hash)

       JSON.parse(

         @active_campaign.contact_sync(

           hash.merge(

             "status[#{@list_id}]" => 1,

             'ip4' => @params[:client_ip],
)


...

Perfect. The line I added is 'ip4' => @params[:client_ip] (yup, you need to use hash rocket syntax here). Also client_ip is added to params in controller like this:

      private

     def enquiry_params

       params

         .require(:enquiry)

         .permit(:first_name, :email, :topic, :preferred_city, :question)

         .merge(client_ip: request.remote_ip)

     end
   

Great, I pushed changes to staging, tested it a few times, and moved this task to ‘Waiting for qa’ column in our Jira. Not for long though.

Tip no.2 Make sure your IP isn’t overwritten

We started getting information that some clients still weren’t added properly (once again, it looked like they were added, but automation wasn’t triggered). The reason was CloudFlare, which works as reverse proxy and is overriding default user IP (the one you access via request.remote_ip) with its own IP addresses. There are many IP addresses which CF uses, but if they start duplicating inside of AC, which will happen after some time, users won’t get the automated campaign. How to fix it? Easy.

 private

     def enquiry_params

       params

         .require(:enquiry)

         .permit(:first_name, :email, :topic, :preferred_city, :question)

         .merge(client_ip: Rails.env.production? ? request.headers['Cf-Connecting-IP'] : request.remote_ip))

     end

Fortunately, Cloudflare uses Cf-Connecting-IP field and stores real IP there. Once again, I added the above line, pushed changes, checked it - it looked like everything is alright. Once again, it wasn’t.

Tip no.3 Make sure you handle IPv6.

The last problem was connected with proper handling of IPv6.  Active Campaign doesn't support adding contacts with IPv6. Someone mentioned it here, but also in API specification there is no info about IP6v support, only ip4. How to solve this issue? Well, CloudFlare complicated things a little bit in the previous case, but was very helpful in this one, so I can’t be mad. Let’s get straight to the solution:

  • Cloudflare provides a service called `Pseudo IPv4` which, when enabled, adds custom headers that can be used for tools that don't support IP6. The easiest way would be to switch it on with the 'Overwrite headers' option, here is the info.
  • After enabling it, Cf-Connecting-IP and X-Forwarded-For will have either
    • original IPv4 from a user if he uses IPv4 or
    • pseudo-IPv4 if user uses IPv6.

and that should fix the problem with AC non handling IPv6 users. At this point I extracted the IP fetching logic to another method called real_client_ip. Remember that IP may be used also by other tools, like Spree or devise (e.g. last_sign_in_ip) so if you use reverse proxy like CF, you need to update your code (and sometimes even overwrite gem code).

def real_client_ip

 (Rails.env.production? || Rails.env.beta?) ? request.headers['Cf-Connecting-IP'] : request.remote_ip

end

And updated enquiry_params method with     

def enquiry_params

  params

    .require(:enquiry)

    .permit(:first_name, :email, :topic, :preferred_gym, :question)

    .merge(client_ip: real_client_ip)

end

In the meantime we also added a `beta` environment, where CF is enabled, hence Rails.env.beta? is used above. In our case it was something between staging and production.

Additional debugging tip

Note that last case is hard to be tested if you don’t use IPv6 yourself, or don’t know anyone who uses it in your company. And I don’t. There are solutions like online proxy - but it didn’t work for me (I couldn’t submit a form when using it). We could try some paid tools to fake it, but not to complicate things too much, and not to waste more time on it, we did something different. At netguru we are using Rollbar. And rollbar is a good tool not only for reporting errors, but also debugging. So instead of recreating the case by ourselves, we added something like this:

  def real_client_ip

   Rollbar.error("DEBUGGING IPv6”, extra: params[:enquiry][:email]) if debugging_ipv6_users?

   (Rails.env.production? || Rails.env.beta?) ? request.headers['Cf-Connecting-IP'] : request.remote_ip

 end

 def debugging_ipv6_users?

   request.headers['Cf-Connecting-IPv6'].present? && controller_path.starts_with?('api/active_campaign')

 End

If Cf-Connecting-IPv6 headers are present - that means that this specific user uses IPv6. We waited 2 days and some people with IPv6 occurred in rollbar logs. Since we printed params to rollbar, we could check manually if a user is properly added to the AC list and automation is triggered. This time it worked. Yay!

Conclusion

While setting basic API integration with AC seems to be a piece of cake, there are still some things that may surprise you. Additionally, those cases can be hard to spot in the short-term which can cost you some time and anger. To avoid that, make sure to go through that list and take care of your clients IPs when dealing with ActiveCampaign. Good luck!

Code stories CTA
Need a successful project?
Estimate project or contact us