Consuming SendGrid and Twilio webhooks in Rails
If you’re looking for services that handle the delivery of your emails and SMSs in your app, SendGrid and Twilio are some of the most complete options out there.
In this article we are going to focus on a common scenario when using those services: How can we have a real time status of the emails and text messages that we send from our Rails app.
Let’s say we have a contacts
table with the following columns: name
, email
, phone_number
, email_status
and sms_status
.
When we send an email or SMS with Sendgrid and Twilio, there are a set of possible outcomes that can happen, such as “failed”, “delivered”, “open” and many more. Here you can see the full list of them:
What we want to achieve is to update our email_status
and sms_status
columns when one of these events happen. To do that we’ll have to use webhooks.
A webhook is an HTTP callback that allows a web application to POST a message to a URL when certain events take place. Often called “Reverse APIs,” Webhooks can be used to receive data in real time, pass it on to another application, or process the data faster than traditional APIs.
A clean way to implement that would be to create two separate endpoints, one for each service:
# config/routes.rb
post "sendgrid_webhook", to: "sendgrid#webhook"
post "twilio_webhook", to: "twilio#webhook"
# app/controllers/sendgrid_controller.rb
class SendgridController < ApplicationController
skip_before_action :verify_authenticity_token
def webhook
end
end
# app/controllers/twilio_controller.rb
class TwilioController < ApplicationController
skip_before_action :verify_authenticity_token
def webhook
end
end
We skipped the verify_authenticity_token
action so it doesn’t raise an InvalidAuthenticityToken
exception when the endpoint is accessed from outside of our app. But to keep the security in place it’s important to add a custom verification so only requests with a specific token can access the endpoints:
# config/routes.rb
post "sendgrid_webhook/:token", to: "sendgrid#webhook"
post "twilio_webhook/:token", to: "twilio#webhook"
# app/controllers/sendgrid_controller.rb
class SendgridController < ApplicationController
skip_before_action :verify_authenticity_token, if: :valid_webhook_token?
def webhook
end
private
def valid_webhook_token?
params[:token] == ENV["SENDGRID_WEBHOOK_TOKEN"]
end
end
# app/controllers/twilio_controller.rb
class TwilioController < ApplicationController
skip_before_action :verify_authenticity_token, if: :valid_webhook_token?
def webhook
end
private
def valid_webhook_token?
params[:token] == ENV["TWILIO_WEBHOOK_TOKEN"]
end
end
You can store the tokens as environment variables or whatever method you use to store your credentials:
# .env
SENDGRID_WEBHOOK_TOKEN = 1234567890qwertyuiop
TWILIO_WEBHOOK_TOKEN = 0987654321poiuytrewq
Once we have the two new endpoints we should add their URL to the Sendgrid and Twilio configuration. For SendGrid that has to be done in their platform :
And for Twilio it has to be done using the status_callback
parameter when we send the text message:
def send!
client = Twilio::REST::Client.new
client.api.account.messages.create(
from: "+15005550006",
to: contact.phone_number,
body: body,
status_callback: "https://www.ombulabs.com/twilio_webhook/#{ENV['TWILIO_WEBHOOK_TOKEN']}"
end
If everything went well, when we send an email or SMS, the SendGrid and Twilio API will hit our endpoints every time there is a change in the status. So now let’s add the logic to update the status in our database:
# app/controllers/sendgrid_controller.rb
class SendgridController < ApplicationController
skip_before_action :verify_authenticity_token, if: :valid_webhook_token?
before_action :set_contact
def webhook
@contact.update_column(:email_status, sendgrid_params[:event])
render json: {}, status: :ok
end
private
def valid_webhook_token?
params[:token] == ENV["SENDGRID_WEBHOOK_TOKEN"]
end
def set_contact
@contact = Contact.find_by(email: sendgrid_params[:email])
end
def sendgrid_params
params.require(:_json).first.permit(:email, :event)
end
end
# app/controllers/twilio_controller.rb
class TwilioController < ApplicationController
skip_before_action :verify_authenticity_token, if: :valid_webhook_token?
before_action :set_contact
def webhook
@contact.update_column(:sms_status, params[:SmsStatus])
render json: {}, status: :ok
end
private
def valid_webhook_token?
params[:token] == ENV["TWILIO_WEBHOOK_TOKEN"]
end
def set_contact
@contact = Contact.find_by(email: params[:To])
end
end
And that’s pretty much it. I recommend that you put a debugger in the webhook
method so you can clearly see the parameters you receive so you can tweak it to your needs.
I hope this quick tutorial has been of value to you. Feel free to ask any question in the comments section below.