I’ve been busy building software lately.
One of the main things I’ve been working on is getting Gumroad purchases connected to my web application.
Why Gumroad? Because the people who have supported my book are already there. It allows me to put all of the content and training videos in one place as well.
Achieving an Automated Gumroad Workflow
The workflow is straightforward.
- Customer purchases a membership via Gumroad
- Gumroad notifies my application via the Gumroad Ping webhook
- If the product matches my membership product identifier, then my app saves the user with the email, license key info, and timestamp
- User is sent an email invitation to sign up for the web application
Gumroad Ping
Gumroad Ping is a webhook that posts sale date to your application. It includes enough identifying information to trigger a full invitation workflow.
You can set up a ping by visiting https://gumroad.com/settings/advanced in your Gumroad seller dashboard.

I set up a simple Ruby on Rails API to consume the data and secured it with a secret token that i pasted in the provided Ping
field.
Below is my GumroadPing
model. In it there is an after_create
hook that sends an invitation if the sale type is a subscription. I save the license_key
and sale_timestamp
to the User
model and send out the invitation.
# == Schema Information
#
# Table name: gumroad_pings
#
# id :bigint not null, primary key
# data :json not null
# created_at :datetime not null
# updated_at :datetime not null
#
class GumroadPing < ApplicationRecord
APP_PRODUCT_ID = "MY_PRODUCT_ID==".freeze
validates :data, presence: true
after_create_commit :send_invitation, if: :subscription?
private
def subscription?
data.dig("product_id") == APP_PRODUCT_ID
end
def send_invitation
user = User.invite!(
email: data.dig("email"),
name: data.dig("full_name"),
invited_by: User.first)
apply_license_key_to_user(user)
end
def apply_license_key_to_user(user)
user.update_attributes(
sale_timestamp: data.dig("sale_timestamp"),
license_key: data.dig("license_key")
)
end
end
The controller is relatively straightforward as well. I validate the token and 401 if it fails, then attempt to save the model, and if that doesn’t work, then I return a 422 with the error message back to Gumroad (not that they care).
# GumroadPingsController
class GumroadPingsController < ApplicationController
skip_before_action :verify_authenticity_token, :authenticate_user!
def create
return gumroad_unauthorized if invalid_gumroad_token?
@gumroad_ping = GumroadPing.new(data: gumroad_ping_params)
if @gumroad_ping.save
render json: {}, status: :created
else
render json: @gumroad_ping.errors, status: :unprocessible_entity
end
end
private
def invalid_gumroad_token?
params[:token] != Rails.application.credentials.dig(:gumroad, :token)
end
def gumroad_unauthorized
render json: { message: "unauthorized" }, status: :unauthorized
end
def gumroad_ping_params
params.except(:token, :controller, :action)
end
end
Gumroad Ping Json Structure
This is where I got a bit hung up and wish Gumroad had slightly better documentation. The json is highly variable so in the initial implementations I had to do some experimentation to see exactly what was coming from Gumroad and stored the payload as a json blob in my database.
Here is an anonymized example of the membership sale data structure.
Subscription with no affiliate sales
{
"seller_id": "A_LONG_STRING==",
"product_id": "A_LONG_STRING==",
"product_name": "Dividend Cultivator Web App",
"permalink": "mHSuk",
"product_permalink": "https://gum.co/mHSuk",
"email": "anonymous@gmail.com",
"price": "1249",
"currency": "usd",
"quantity": "1",
"order_number": "A_STRINGIFIED_INTEGER",
"sale_id": "A_LONG_STRING==",
"sale_timestamp": "2020-06-09T18:11:22Z",
"subscription_id": "kjletlkjeqasdf==",
"variants": {
"Tier": "Base Subscription"
},
"offer_code": "an_discount_code_you_have_set_up",
"license_key": "C14341FC-C14341FC-C14341FC-C14341FC",
"ip_country": "Canada",
"recurrence": "quarterly",
"is_gift_receiver_purchase": "false",
"refunded": "false",
"resource_name": "sale",
"disputed": "false",
"dispute_won": "false"
}
Fin
That’s really it. I’m haven’t found ping events for refunds, cancellations, or other product events, but would be interested if anyone finds them.
Let me know about your experience with it as I found it pretty pleasant to get set up and working once I knew what the data structure was.
Here’s a 50% off link to the dividend investing software if you’re interested in it.
Looks like you can get those other transaction events via : https://api.gumroad.com/api#resource-subscriptions
LikeLike
Thanks Andrew. Yes you’re right. It’s a bit more complex than I wanted it to be, but will likely have to go down that route. I think there are several wrapper libraries for it too.
LikeLike