Building an Email Referral Rewards Program API in Rails
Tags: rails, ruby, webdev
Published: 2019-10-10 13:45:23

---
title: Building an Email Referral Rewards Program API in Rails
published: true
description: How to build an email referral rewards program using rails as an API.
tags: rails, ruby, webdev
---


One of the most effective means of growing an audience is through email referrals. The rise of newsletters like *Morning Brew* and companies like *Harry’s* showcase that offering a tiered rewards structure for referring friends can grow a list to astronomical numbers. That is, provided your newsletter or product is actually something people want. The following describes how you can build a simple version of this program yourself using Ruby on Rails API. 

### Program Requirements

1. After a user is subscribed through mailchimp they are able to refer their friends to the program.
2. Subscribers can track their confirmed referrals and see their progress toward the referral count goal.
3. We will start with a single tier of five referrals to receive a prize.
4. Referral emails need to be validated and not existing subscribers.
5. Subscribers will not be required to create any sort of account with a username and password to participate since this can be a barrier to entry.

## Technical Architecture and User Flow
At a high level, subscribers will be added to the database when a subscriber is successfully subscribed to our mailchimp list. This allows us to check these against our existing subscribers as well as control sends through our unsubscribe list as well.

Now the call to mailchimp could be done inside our  Rails API controller or a model hook, or it could live on our web server and we can make the call to our referral program API in a promise when the mailchimp subscription succeeds.  I opted for the latter approach for two reasons:

1. Keeping external dependencies to mailchimp out of the API itself would allow us to more easily change email service providers should that happen in the future.
2. A failure in our referral program API server would not cause users to stop being able to subscribe to the mailing list. While this is a slim case it does give us an added layer of redundancy by keeping these decoupled and is easy to recover from should this ever happen.

Upon signing up, subscribers are issued a `user_key` and `share_key`. The `user_key` is a unique key where a subscriber can check their progress toward a reward and find their unique share link that will allow them to receive attribution for referrals. The `share_key` is a unique key that gets appended to a  subscribers’ referral url that they will share with friends.

When the `share_key` of another user is seen in a new subscription that will queue up a validation email. When the link in that validation email is clicked (ensuring it is a real email address) that referral will get attributed to the referee.

When a subscribers referral count reaches five, they are sent an email letting them know they have won a reward and to add their address to have it sent to them. When that subscriber adds their address this triggers an alert to the team to fulfill their reward.

When the reward is fulfilled that user is notified via email that their prize has shipped.

*Now for those of you thinking, but what if they have two email addresses?You could just make five unique email addresses to get the prize! This is where business sense has to step in and say, if you are trying to give away a Ferrari, this is not the program for you. If you want a coffee mug or hoodie so bad that you will go through the process of creating five email addresses and referring them, then that is a risk we were willing to accept and reward. In general it’s hard enough to get people to open your newsletters, no less go through all the hoops of gaming the system.*

## Getting Started
Without further ado let’s get going…

First spin up a rails app in API only mode.

`rails new referral_program_api --api`

To learn more about why I like using rails for this type of thing check out their documentation: [Using Rails for API-only Applications — Ruby on Rails Guides](https://guides.rubyonrails.org/api_app.html).

And that’s it we now have our API ready to go.

## Defining The Model
For this we only need to models, one for **Subscribers** the other for **Referrals**.

### The Rules

1. Each referral can belong to only one subscriber.
2. Every email address can only subscribe once.
3. Every email address can only be a referral once.
4. Every subscriber and referral must have an email address.

Let’s start by creating the subscribers. We can do so with our rails generator commands. 

`rails g model subscriber`

You can also add all of the attributes to this command as well, however I find it a bit easier to add them to the migration file afterwards, but that’s just a personal preference. So head on over to your  `/db/migrate/<timestamp>_create_subscribers.rb` file and create the following attributes.

```ruby
class CreateSubscribers < ActiveRecord::Migration[5.2]
  def change
    create_table :subscribers do |t|
      t.string :email
      t.string :first_name
      t.string :last_name
      t.string :share_key
      t.string :user_key
      t.string :street_address_one
      t.string :street_address_two
      t.string :city
      t.string :state 
      t.string :zip
      t.boolean :prize_sent
      t.boolean :sent_to_fulfillment
		t.references :referrer, index: true
      t.timestamps
    end
  end
end
```

Now we can hop over to our  `/app/models/subscriber.rb` file where we can setup some rules and callbacks to generate our unique keys for subscribers.

```ruby
class Subscriber < ApplicationRecord
  before_save { email.downcase! }
  has_many :referrals, class_name: "Subscriber", 
						  foreign_key: "referrer_id"
  validates :first_name, length: { maximum: 50 }
  validates :last_name, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 50 }, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }, uniqueness: {case_sensitive: false}

  # Assign a share key
  before_create do |subscriber|
    subscriber.share_key = generate_unique_key('share_key')
    subscriber.user_key = generate_unique_key('user_key')
    subscriber.prize_sent = false
    subscriber.sent_to_airtable = false
  end

  private
  
  def generate_unique_key(field_name)
    loop do
      key = SecureRandom.urlsafe_base64(9).gsub(/-|_/,('a'..'z').to_a[rand(26)])
      break key unless Subscriber.exists?("#{field_name}": key)
    end
  end
end
```

The first thing we are doing here is converting all email addresses to lowercase  and then setting up our association to the referrals model with the line  `has_many :referrals...`. This is called a self join and will allow us to associate our referrals to a subscriber all in one table. 

We then create a few simple validations for  `first_name`, `last_name`, and `email_address` .  

We then use the `before_create` callback to assign a unique `share_key` and `user_key` to the subscriber. These are generated using a private function that makes sure that each of these keys are unique independently. The likelihood that two keys are the same using the  `SecureRandom.urlsafe_base64(9)` function is highly unlikely, but as a safeguard we do this in a loop and in the case that the key is already used we generate another.

Now that we have a solid subscriber model we can go ahead and create our  controller.

## Creating Subscriber Controller
Now that we have the proper model in place we can setup our endpoints and controller.

Run the command:

`rails g controller subscriber`

Now head over to `/app/controllers/subscriber_controller.rb` and we can create actions for `show`, `create`, `confirm`, and `update`. 

| Action | Description |
|————|——————|
| Show | For fetching the subscriber information along with associated referrals. |
| Create | For creating a new subscriber, can take in a `referrer_key` parameter, which triggers a confirmation email to the subscriber to confirm the email is truly valid. |
| Confirm | For confirming an email address and giving credit to the referrer via `referrer_id` field. |
| Update | For updating the subscriber, used for when prize is won for entering address, triggering notifications to the fulfillment team. |

We are not going to be using a delete action here. In general we flag users as unsubscribed in email but we like to never forget them. We don’t have to create any unsubscribe flags in this program since this is still going to be managed by mailchimp and all subscribes and referrals will be passing through their management system before ever hitting our API.

```ruby
class SubscriberController < ApplicationController

  def show
    
  end

  def create    
    
  end

  def confirm
    
  end

  def update
    
  end

  private

  def subscriber_params
    params
      .require(:subscriber)
      .permit(
        :first_name,
        :last_name,
        :email,
        :street_address_one,
        :street_address_two,
        :city,
        :state,
        :zip,
        :referrer_key
      )
  end
end
```

We want to be sure to only allow  the parameters we have set forth at this stage. You’ll notice that we are not allowing parameters that are not going to be updatable by the user and we have included one additional parameter of `referrer_key`, which will have the `share_key` of the referring subscriber if this is a referral.

It’s important to remember that all of our subscribers, whether they are referred to us or not need to pass through the referral program to be properly assigned a `user_key` and `share_key`. 

### Our  Private Functions

Here we’re going to be adding a few more private function to our controller in order to set the subscriber and referrer prior to our actions. I’m using `...` for brevity here, this builds upon the boilerplate laid out above.

```ruby
class SubscriberController < ApplicationController
	before_action :set_subscriber, only: [:show, :update, :confirm]
  before_action :set_referrer, only: [:create, :confirm]

  ...

  private

	def set_subscriber
    @subscriber = Subscriber.find_by_user_key(params[:id])
  end

  def set_referrer
    @referrer = Subscriber.find_by_share_key(params[:referrer_key])
  end

  ...

end
```

These functions set our subscriber and referrer. We are also adding some `before_action` hooks here to set these instance variables in the functions we need them in.

### Defining Routes

Now that we have our base actions setup we can head over to  `/config/routes.rb` and define our routes. I like to setup the actions as follows:

```ruby
Rails.application.routes.draw do
  get '/subscriber/:id' => 'subscriber#show'
  post '/confirm-subscriber/:id' => 'subscriber#confirm'
  post '/subscriber' => 'subscriber#create'
  put '/subscriber/:id' => 'subscriber#update'
end
```

The `:id` field in this case is going to be our `user_key` so as users can only be looked up by their unique key that will only be sent to their personal emails. Again, we are going to take privacy seriously here and even if a user key was somehow compromised the only thing that we are going to return is the users’ email address, user key, and share key. Sensitive information like address and name are going to be used only or our internal administrators and do not need to be accessible at any endpoint.

### Completing Our Actions

Now let’s fill in our actions.

```ruby
class SubscriberController < ApplicationController

...

def show
    render json: @subscriber, :only => ['email', 'user_key', 'share_key', 'sent_to_airtable', 'prize_sent'], :include => { :referrals => { :only => :email } }
  end

  def create    
    @subscriber = Subscriber.new(subscriber_params)

    if @subscriber.save
      if @referrer
        # Send email here to confirm the referral email
      end

      render json: @subscriber, status: :created, location: @subscriber, :only => ['email', 'user_key', 'share_key']
    else
      render json: @subscriber.errors, status: :unprocessable_entity
    end
  end

  def confirm
    if @subscriber && @referrer 
      @subscriber.update(
        referrer_id: @referrer.id
      )

      if get_referral_count(@referrer.id) == 5
        # send reward notification
      end

      render json: @subscriber, status: :created, location: @subscriber, :only => ['email', 'user_key', 'share_key']
    else
      render json: { message: 'Bad user key or share key.' }, status: :unprocessable_entity
    end
  end

  def update
    if !@subscriber
      create
    elsif @subscriber.update(subscriber_params)
      if get_referral_count(@subscriber.id) >= 5 && has_complete_address?
        # Sent to fulfillment service
      end

      render json: @subscriber, status: :created, location: @subscriber, :only => ['email', 'user_key', 'share_key']
    else
      render json: @subscriber.errors, status: :unprocessable_entity
    end
  end

	private

	...

	def get_referral_count(id)
    Subscriber.where(referrer_id: id).count
  end

  def has_complete_address?
    @subscriber.street_address_one != nil && @subscriber.city != nil && @subscriber.state != nil && @subscriber.zip != nil
  end

	...

end
```


Here, we’ve filled in our controllers except for three key notification points.

1. Sending the confirmation email to validate the email address is real.
2. Sending the notification that a subscriber has achieved five referrals.
3. Sending a notification to  the fulfillment team that a subscriber has achieved a reward and submitted their address.

To complete these we will need to setup some mailers in a later section.

We also add two more private functions here to `get_referral_count` of a referee, and `has_complete_address?` to see if a subscriber has entered a complete address.

At this point we have to setup our connections with mailchimp in two places. 

1. To send a subscribers’  `user_key` and `share_key` back to mailchimp as **merge_vars**.
2. To send notifications emails via mandrill using ActionMailer.

*If you do not want to use mailchimp or gibbon you may skip the next section and integrate whichever email service provider and transactional emails service you are comfortable with.*

## Integrating Mailchimp and Mandrill
### Gibbon Setup
I find Gibbon to be the best gem for interacting with mailchimp via a rails application.  We’ll also be using the figaro gem to allow for environment variables so we don’t commit our mailchimp api keys to source code.

Add the following to your `Gemfile`.

```ruby
# use for environment variables
gem "figaro"
# mailchimp
gem "gibbon", '~>2.2.4'
```

Then run  `bundle install`.

Create an `application.yml` file in your  `/config` folder.

 `touch ./config/application.yml` 

Then head over to Mailchimp and get your API key and list id. *Be advised these are in two different locations inside of Mailchimp* I also like to setup a variable used to signal that we are in the development or staging environment for testing purposes.

```yaml
MAILCHIMP_API_KEY: '<your_mailchimp_api_key>'
MAILCHIMP_LIST_ID: '<your_mailchimp_list_id>'
MAILCHIMP_ENV: 'development'
```

#### Initialize Gibbon

Create a `gibbon.rb` file inside `./config/initializers`. 

`touch ./config/initializers/gibbon.rb`

Inside this file you will setup the connection to Mailchimp via the Gibbon gem when the rails server starts. Hence, being in the *initializers* folder.

```ruby
Gibbon::Request.api_key = ENV["MAILCHIMP_API_KEY"]
Gibbon::Request.timeout = 15
Gibbon::Request.throws_exceptions = false unless ENV["MAILCHIMP_ENV"] == "development"

if ENV["CONTENTFUL_ENV"] == "development"
    puts "MailChimp API key: #{Gibbon::Request.api_key}"
end
```

At this point you can fire up your rails server and you should see the above line printed to the console letting you know our connection to Mailchimp is up and running.

#### Adding to the Model

We can now add Gibbon to our subscriber model callbacks in order to send the `share_key` and `user_key` back to mailchimp. But first, we have to allow for these in our subscriber list.

If you go to your mailchimp audiences tab and select  the list we used for the list id above you can click *settings* and select **Audience fields and *|MERGE|* tags**. Here you will be able to add two new fields with the labels *Share Key* and *User Key* and the tag names `SHAREKEY` and `USERKEY` to be used inside templates.

We can now use gibbon to send over the proper values in the `after_save` callback.

```ruby
...

after_save do |subscriber|
    unless Rails.env.test?
      begin
        gibbon = Gibbon::Request.new

	gibbon.lists(ENV['MAILCHIMP_LIST_ID']).members(Digest::MD5.hexdigest(subscriber.email))
          .update(body: {
            merge_fields: {
              SHAREKEY: subscriber.share_key, 
              USERKEY: subscriber.user_key
            }
          })
      rescue Gibbon::MailChimpError => e  
			# This is a good place to add your error alerting but is beyond the scope of this
        puts "Houston, we have a problem: #{e.raw_body}"
      end
    end
  end

...

```

The key thing here is that if our connection to mailchimp fails for whatever reason, we are still subscribing the user to be corrected later. Inside of the `rescue` statement a pro tip would be to put your preferred error alerting system to alert you of an issue sending the keys over to mailchimp without affecting the user experience.

### Mandrill Setup
Next we can setup mandrill, mailchimp’s transactional email service. I’m not going to go into how to set this up on the mailchimp side in this tutorial but mailchimp has great help docs and it takes about five minutes to connect mandrill to you mailchimp account.

Inside you rails app you are going to add the `mandrill-api` package to your Gemfile.

```ruby
#mandrill
gem ‘mandrill-api’
```

Run `bundle install`.

Pull up your `/config/application.yml` file again and add the following environment variables.

```yaml
SMTP_ADDRESS: 'smtp.mandrillapp.com'
SMTP_DOMAIN: 'localhost'
SMTP_PASSWORD: '<your_mandrill_smtp_password>'
SMTP_USERNAME: '<your_mandrill_username>'
```

The password and username can be obtained inside of mandrill for use in this file.

Next we will create our `base_mandrill_mailer` that will setup all of our default values for our notification emails to inherit.

Create our `base_mandrill_mailer.rb` file:

`touch ./app/mailers/base_mandrill_mailer.rb`

And add the following:

```ruby
require "mandrill"

class BaseMandrillMailer < ActionMailer::Base
  default(
    from: "<your_from_email_address>",
    reply_to: "<your_reply_to_email_address>"
  )

  private

    def send_mail(email, subject, body)
      mail(to: email, subject: subject, body: body, content_type: "text/html")
    end

    def mandrill_template(template_name, attributes)
      mandrill = Mandrill::API.new(ENV["SMTP_PASSWORD"])

      merge_vars = attributes.map do |key, value|
        { name: key, content: value }
      end

      mandrill.templates.render(template_name, [], merge_vars)["html"]
    end
end
```

Be sure to fill in your  *from* and *reply to* email values above.

Now we can create two mailers, one for confirmation emails the other for rewards notifications.

```
touch ./mailers/confirmation_mailer.rb ./mailers/reward_mailer.rb
```

```ruby
class ConfirmationMailer < BaseMandrillMailer
  def confirm_email(subscriber, referrer_key)
    subject = "Confirm Your Email"
    merge_vars = {
      "USERKEY" => subscriber.user_key,
      "REFERRERKEY" => referrer_key,
      "EMAIL" => subscriber.email
    }
    body = mandrill_template("Confirm Email", merge_vars)

    send_mail(subscriber.email, subject, body)
  end
end
```

```ruby
class RewardMailer < BaseMandrillMailer
  def reward_achieved(subscriber)
    subject = "Your Gift Awaits!"
    merge_vars = {
      "USERKEY" => subscriber.user_key,
      "EMAIL" => subscriber.email
    }
    body = mandrill_template("Reward Achieved", merge_vars)

    send_mail(subscriber.email, subject, body)
  end

  def reward_sent(subscriber)
    subject = "Your Gift Is On The Way"
    merge_vars = {
      "EMAIL" => subscriber.email
    }
    body = mandrill_template("Reward Sent", merge_vars)

    send_mail(subscriber.email, subject, body)
  end

  def notify_fulfillment_team(subscriber)
    subject = "Someone just earned themselves a free tote!"
    merge_vars = {
      "EMAIL" => subscriber.email
    }
    body = mandrill_template("Notify Fulfillment Team", merge_vars)

    send_mail('<the_email_of_whoever_fulfills_rewards>', subject, body)
  end
end
```

You’ll notice that these mailers reference some templates that are not in mandrill yet. You are correct. What we have to do next is go over to mailchimp and create four templates inside their editor named the following:

1. `Confirm Email`
2. `Reward Achieved`
3. `Reward Sent`
4. `Notify Fulfillment Team`

I like to keep them simple and inside each of these templates you need to use your merge_vars to create links to your front end that will pass the proper keys to your application. 

We’ll just be focusing on the API layer since use of merge vars and how this interacts with the front end of the site can vary based upon configurations.

At last we can add our email notifications to our controller actions.

## Adding Mail Notifications
Now we can head back into our subscriber controller and add the following notifications.

```ruby
class SubscriberController < ApplicationController

...

  def create   
 
     ...

      if @referrer
        # Send email here to confirm the referral email
			ConfirmationMailer.confirm_email(@subscriber, @referrer.share_key).deliver_now
      end

     ...

  end

  def confirm

     ...

      if get_referral_count(@referrer.id) == 5
        # send reward notification
			RewardMailer.reward_achieved(@referrer).deliver_now
      end

     ...

  end

  def update

    ...

      if get_referral_count(@subscriber.id) >= 5 && has_complete_address?
        # Sent to fulfillment service
		@subscriber.update(sent_to_fulfillment: true)
		RewardMailer.notify_fulfillment_team(self).deliver_now

      end

    ...

  end

	...

end
```

We now have all that we need to fire up the program. There’s just one mailer that we created that we did not use. That’s the one notifying the user that their order has shipped. For brevity I decided to leave out how you want to manage that since that can vary by what you want to do.

What I decided to do is send the orders over to Airtable and have a field that marks them as pending. I then run a cron job that checks the table each day and if fulfillments are marked as fulfilled since that last run we send an email letting the subscriber know their reward has shipped. 

## Authentication
This is the final part of our api. While we don’t want users to have to log in to view their progress and to get their share links, we don’t just want these endpoints accessible to the public.

What we are going to do here is just integrate basic token based authentication with our API, using a key we generate and store in our `application.yml` file. On our server this is going to be a part of our configuration file that should not be shared with anyone. When we implemented the figaro gem before this `application.yml` file should be a part of our `.gitignore` and never checked into source control. 

First let’s jump over to our `/app/controllers/application_controller.rb` file and add the following.

```ruby
class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate

  protected

  def authenticate
    authenticate_token || render_unauthorized
  end

  def authenticate_token
    authenticate_with_http_token do |token, options|
      token == ENV['SECRET_KEY_BASE']
    end
  end

  def render_unauthorized(realm = "Application")
    self.headers["WWW-Authenticate"] = %(Token realm="#{realm}")
    render json: 'Bad credentials', status: :unauthorized
  end
end

```


Here we are using Action Controller’s built in http token authentication method. We setup a `before_action` hook that ensures that any requested resource in any controller passes through our `authenticate` method. 

We then create a set of `protected` methods, which are similar to private methods except these are accessible by any subclass of our main `ApplicationController` class. 

In our `authenticate_token` method we just do a simple comparison to check if the token is equal to the token in our environment variables.

### Generating the Token

To generate our token I like to just use a variant of our `generate_token` function in our controller in the rails console.

Open a new terminal, navigate to your rails project, and run the command `rails c`

Enter the following in the rails console: 

```
SecureRandom.urlsafe_base64(32).gsub(/-|_/,('a'..'z').to_a[rand(26)])
```

Anytime you run this you will end up with a string that looks something like this:

`c3OcKh1J8a8ich9bQC0TNsdP7LdCqcWL57cJKoch4a0`

Take the unique key you have generated and add it to your `application.yml` file as `SECRET_KEY_BASE` like so:

```yaml
SECRET_KEY_BASE: 'c3OcKh1J8a8ich9bQC0TNsdP7LdCqcWL57cJKoch4a0'
```

Now if you try to access a resource you should get an unauthorized response.

In order to get a valid response again you need to pass the header `Authorization: “Token token= c3OcKh1J8a8ich9bQC0TNsdP7LdCqcWL57cJKoch4a0”`

Make sure that you are always doing this over `https` so that this is encrypted. When you build out your front end you must ensure that this key is NOT exposed on the client nor checked into version control like github. If you are using a node based front end, make sure to use a package like `DOTENV` which gives you similar functionality to figaro for ruby. You can then create a route in your node server to handle these responses and keeps this key hidden from your front end code. 

## Conclusion
And that’s all folks. Our API is complete and you can deploy this to heroku or wherever you like to deploy your rails apps to. This is just an API and you can design the front end however you like. As always appreciate any feedback on the approach. Happy coding.