
Offering an affiliate program is one of the most effective marketing channels for growing your SaaS (Software as a Service) business, especially in the early stages.
The idea is simple: your existing users (or affiliates) refer your product to their audience through a unique link, and in return, they earn a small commission for every new paying customer they bring.
Benefits:
- High conversion rate (people buy from people that they know, like, and trust)
- You spend money ONLY when you make money (unlike paid ads, influencers, and SEO)
The commissions vary, but for SaaS, they are usually recurring and around 30% of the amount paid by the referred customer.
Many third-party providers, such as Rewardful, Tolt, FirstPromoter, PartnerStack, and Dub Partners, allow you to launch your affiliate program instantly, but if you’re looking to build a custom one in-house, keep reading.
Why build the affiliate program in-house
While no-code solutions are great, they often come with fees and many limitations.
One of the main factors that led us to build our affiliate program from scratch was the inability to customize 3rd party solutions to our needs.
Pros for building your affiliate program
- No 3rd party fees, thus higher commission rates
- Highly customizable, including an employee rewards program
Cons
- Building the infrastructure
- Handling payouts to affiliates
The cons are not as bad as they look, so let’s dive in.
Creating affiliate links
I personally dislike affiliate links that look very generic and/or belong to a different (sub)domain than yours.
Instead, I wanted our affiliate links to feel like part of our website and be customizable by the user, similar to how someone can choose their @ handle on Instagram, X, or any other social network.
Specific usernames that we use as routes for our website, such as /plans or /blog, are restricted, but other than that, affiliates can be as creative as they can, and the username can be changed at any moment.
Someone has chosen the username freetrial -> publer .com/freetrial
Our goal is to convert our existing customers into brand ambassadors, rather than offering yet another affiliate program; therefore, we have made it mandatory to have an account on Publer.

We took it a step further and now manually approve affiliate access for users in the free plan.
Multiple affiliate links
Sharing the product’s homepage usually works. Still, if you have a niche community, it may be more effective to share a specific landing page or article that directly targets your community’s pain points.
Despite using different technologies, such as Framer, Vercel, and WordPress, we invested a significant amount to make it possible for every link on our website to be an affiliate link.
Affiliates can share any link from our website as an affiliate link simply by appending /USERNAME at the end of the URL
Here’s the WordPress 404.php code that catches blog links with /USERNAME at the end, verifies the USERNAME is valid through a cURL request, and redirects the visitor back to the original link by passing the ?referral=ID param in the URL.
<?php
$url = rtrim(strtok($_SERVER["REQUEST_URI"], '?'), '/');
$LINK_WITHOUT_AMBASSADOR = "https://publer.com/blog" . substr($url, 0, strrpos($url, '/'));
// Don't honor clicks from ads
if (!$_GET['gclid'] && !$_GET['gad_source'] && !$_GET['via']) {
preg_match("/[^\/]+$/", $url, $matches);
$AMBASSADOR = $matches[0];
$referrer = isset($_GET['referrer']) ? $_GET['referrer'] : $_SERVER['HTTP_REFERER'];
// The data to send to the API
$postData = array(
'username' => $AMBASSADOR,
'link' => 'https://publer.com/blog' . $LINK_WITHOUT_AMBASSADOR,
'referrer' => $referrer
);
// Setup cURL
$ch = curl_init('https://app.publer.com/ambassador');
curl_setopt_array($ch, array(
CURLOPT_POST => TRUE,
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_HTTPHEADER => array('Content-Type: application/json'),
CURLOPT_POSTFIELDS => json_encode($postData)
));
$response = curl_exec($ch);
if ($response) {
$data = json_decode($response, TRUE);
$ambassador_id = $data['id'];
header("Location: $LINK_WITHOUT_AMBASSADOR/?referral=$ambassador_id");
exit();
} else {
header("Location: $LINK_WITHOUT_AMBASSADOR/");
exit();
}
} else {
header("Location: $LINK_WITHOUT_AMBASSADOR/");
exit();
}
get_header();
?>
<div>
<h3>Oops! That page canβt be found.</h3>
<p>Try searching or go to the <a href="/blog">homepage</a></p>
</div>
In header.php, the referral ID, if it exists, is stored as a cookie, which is then used for tracking purposes.
<?php
// Don't honor clicks from ads
if ($_GET['referral'] && !$_GET['gclid'] && !$_GET['gad_source'] && !$_GET['via']) {
$expiration = 30 * 24 * 60 * 60; // 30 days
echo "<script>document.cookie = 'referral=" . $_GET['referral'] . ";path=/;domain=.publer.com;max-age=$expiration;samesite=none;secure=true';</script>";
echo "<script>window.history.replaceState({}, document.title, window.location.href.split('?')[0]);</script>";
}
?>
The solutions for Vercel and Framer routes differ slightly and/or are more complex, but the underlying logic remains the same. Feel free to ping me if you would like the code snippets for those.
Tracking affiliate links
The easiest way to track referrals is through time-based cookies, which are typically valid for 30 or 60 days.
Cookies are small text files that websites send to your browser to store information about your visit.
We store the referral cookie on the .domain level so that the app subdomain can access it.
Whenever someone signs up on our platform, we run this Ruby helper method to check if they came from an affiliate link, a.k.a., the referral cookie exists.
def check_referral(user)
referral = cookies[:referral]
return if referral.blank?
# Mark referred user as such and offer a sign up commission
Ambassador::CommissionsService.new(user, referral).registration_commission
cookies.delete(:referral)
end
Tracking mobile app installs
Cookies are great for tracking on web-based platforms, but things get tricky if you offer a mobile app.
You cannot pass custom tracking params to Google Play or Apple App Store, so this was our workaround:
- Before redirecting visitors to Google Play or Apple App Store from our website
- We copy to the clipboard the referral cookie (if it exists)
- And use the copied ID when the visitor installs the app and signs up
While good on paper, this workaround is not bulletproof. The visitor may copy something else before signing up through our mobile app, or the operating system may not allow copying to the clipboard without permission.
Missing referrals
It’s normal for someone to click an affiliate link from their phone, but decide to learn more and sign up through the web.
Cookies are not cross-device, nor cross-browser
To avoid unnecessary customer support tickets and manual database changes, we allow users to voluntarily specify if someone referred them to Publer.
Metrics tracked
While insights have not been our primary focus, we do keep track of the following essential metrics:
- Number of daily and total clicks
- Number of daily and total signups
- Number of daily upgrades
- Most clicked (affiliate) links
- Top referring sites
The affiliate dashboard was built back in 2016 using jQuery, Chartist, Bootstrap, and DataTables. Nothing fancy, but it still does the job 10 years later with no maintenance required.
Commissions and payouts
For each payment made by a referred customer, the affiliate is entitled to a commission of approximately 30%.
For someone paying $10/month, the affiliate would earn $3/month
RATE = 0.3 # 30% commission
def payment_commission(payment, next_bill_date)
monthly_price = payment.unit_price.to_f
commission = (monthly_price * RATE).round(2)
Ambassador::Commission.create(created_at: Time.current, commission: commission, user: @user, ambassador: @ambassador, payment: payment)
Notification.create(reason: 'commission_pending', user_id: @ambassador.user.id.to_s)
end
Earned commissions are typically placed on hold for 30 days to accommodate any refund requests that may arise.
This is a daily job that automatically marks commissions as ‘approved’ or ‘declined’ depending on the payment status, and once it has been 30 days.
scheduler.cron '7 18 * * *' do # Every day at 18:07 UTC
Ambassador::CommissionsTask.new.execute
end
module Ambassador
class CommissionsTask
def execute
pipeline = [
{ '$match': { state: 'pending', created_at: { '$lte': 30.days.ago.beginning_of_day } } },
{ '$project': { _id: 1 } }
]
ids = Ambassador::Commission.collection.aggregate(pipeline).map { |oid| oid.as_json['_id']['$oid'] }
ids.each_slice(BATCH_SIZE) do |bulk|
# Mark as 'approved' or 'declined' depending on the payment status
Ambassador::CommissionsWorker.perform_async(bulk)
end
end
end
end
The approved commissions are then grouped together and paid once the sum reaches a specific threshold, usually $20 or $50.
Tracking commissions
Affiliates can see in real-time every commission earned and every paying customer they have brought, along with some details regarding their plan.

Paying affiliates
This is probably the biggest con when it comes to running an in-house affiliate program:
- You need to figure out how to pay the affiliates, i.e., PayPal, Stripe, etc
- Your business needs to be registered in a country supported by PayPal or Stripe
- You need to handle tax forms, withholding, bookkeeping, and reporting
However, using online services like FirstBase and Online Taxman is very easy to incorporate in the United States and maintain its legal status.
π How to incorporate and open a bank account in the United States
Payouts can be fully automated on a monthly or biweekly schedule.
scheduler.cron '7 13 1 * *' # First day of the month at 13:07 UTC
Ambassador::PayoutsTask.new.execute
end
PayPal Payouts
PayPal is the most straightforward method, as the affiliate only needs to provide their PayPal email address.
module Ambassador
class PaypalPayoutsWorker
include Sidekiq::Worker
def perform(params)
commission_ids = []
payout_items = params.map do |item|
ambassador = Ambassador::Detail.find(item['ambassador'])
receiver = ambassador.payout_method&.[](:email)
next unless receiver
commission_ids.concat(item['commissions'])
{
recipient_type: 'EMAIL',
amount: {
value: item['total'].round(2),
currency: 'USD'
},
note: <<~NOTE,
Thank you for being an Ambassador of #PublerNation.
Login to your dashboard: <a href='https://app.publer.com/ambassador'>https://app.publer.com/ambassador</a><br>
NOTE
sender_item_id: ambassador.username,
receiver: receiver
}
end
payout_params = {
sender_batch_header: {
sender_batch_id: SecureRandom.hex(8),
email_subject: 'Your payment from the Publer Ambassador program!'
},
items: payout_items.compact
}
payout = PayPal::SDK::REST::DataTypes::Payout.new(payout_params)
Ambassador::Commission.in(id: commission_ids.map { |id| BSON::ObjectId(id) }).update_all(state: 'sending')
payout.create
rescue => ex
Sentry.capture_exception(ex, extra: { params: params })
end
end
end
The only downside with PayPal is that you need to set up an endpoint and listen for PayPal’s webhooks, which notify you whether the payouts were successful or not.
Stripe Connect
Stripe is another excellent alternative for those who cannot or prefer not to use PayPal.
Unlike PayPal, Stripe-powered affiliates need to complete a few onboarding steps to provide their personal information and bank account details.
Once the affiliate completes onboarding, paying out their approved commissions is a one-step process.
module Ambassador
class StripePayoutsWorker
include Sidekiq::Worker
include AmbassadorHelpers
def perform(payout)
ambassador = Ambassador::Detail.find(payout['ambassador'])
commissions = Ambassador::Commission.where(_id: { '$in': payout['commissions'] })
commissions.update_all(state: 'sending')
params = { currency: 'usd',
amount: (payout['total'] * 100).to_i,
destination: ambassador.payout_method[:id],
description: 'Thank you for being an Ambassador of #PublerNation!' }
transfer = Stripe::Transfer.create(params)
notify_payout(payout['total'], transfer.id, ambassador, :stripe)
rescue => ex
commissions&.update_all(state: 'approved')
Sentry.capture_exception(ex, extra: { payout: payout })
end
end
end
While PayPal payouts are sent via PayPal, Stripe payouts are deposited directly into the affiliate’s connected bank account.
Incentivizing the affiliate program
If you think setting up an affiliate program is all it takes for influencers and customers to start promoting your product, you’re in for a disappointment.
Below are some of the custom incentives we have implemented for our affiliate program.
Progressive commission rates
The standard commission rates start as follows:
- $0.25 for each legitimate new sign-up, regardless of whether they upgrade their plan
- 50% commission on the first monthly payment for each new paying customer
- 20% commission on the recurring payments for actively paying customers
And based on the sales the affiliate made the previous month, commission rates can go up:
- $0 – $500 β 50% commission for first payment, 20% on recurring payments
- $500 – $1,000 β 60% and 25% respectively
- > $1,000 β 70% and 30% respectively
For big influencers we offer fixed, but higher commission rates
Early access to new features
Those who actively promote Publer, regardless of their success, get to test new features before everyone else, leave feedback, and thus, directly become part of the decision-making process.
Payout frequency
Similar to early access to new features, the hardest-working affiliates are paid biweekly, rather than monthly.
Prize rewards
We have allocated a monthly budget of $175 for the top 3 affiliates (based on the monthly revenue brought from new paying customers).
Testimonial popup
People buy from people (or brands) that they know, like, and trust, so we wanted to make our affiliates part of our website – literally.
Pro-tip: Popups support markdown so affiliates can link their business or personal website in their testimonial for some additional brand awareness.
Discount sharing
Why would someone sign up for a service using an affiliate link instead of just going directly to the website? Discounts are always a great motive!

Building a community around the affiliate program
Lastly, you need to establish communication channels between you and your affiliates.
- Automatic emails to share reports and leaderboards
- Dedicated Facebook Group to share product updates and news
And it’s always a great idea to prepare ready-made graphics, tutorials, and posts.
SEO optimization
Having affiliate links as part of your main domain is a significant SEO advantage, but it can also backfire on you.
Indexing
You don’t want affiliate links to show up on Google Search results whenever someone searches for your product, right?
You need to make sure that search engines do not index affiliate links.
<Header image={image} description={description} title={title} url={url}>
<meta name="robots" content="noindex" />
</Header>
Canonical URL
You also do not want to have multiple links for the same landing page, as that would lead to keyword cannibalization (links fighting one another for traffic).
Instead, the canonical URL should always be the unaffiliated one.
<link rel="canonical" href="https://publer.com"> β
<link rel="canonical" href="https://publer.com/affiliate"> β
<link rel="canonical" href="https://publer.com/anotherone"> β
<link rel="canonical" href="https://publer.com/random123"> β
URL cleanup
Once you’ve stored the affiliate cookie in the browser, it’s always a good idea to clear the tracking params from the address bar URL.
removeURLParameter('username');
removeURLParameter('referrer');
const removeURLParameter = useCallback(
(parameter) => {
let params = new URLSearchParams(window.location.search);
params.delete(parameter);
const url = constructURL(params.toString());
window.history.replaceState({}, document.title, url);
},
[constructURL]
);
Otherwise, visitors may accidentally reshare the affiliate link and cause unnecessary affiliate fees for you, unless you’re offering a multi-tier affiliate program (a commission structure where an affiliate earns not only from their direct sales but also from sales generated by their direct sales).
Preventing ads
In almost every affiliate program, the first term you have to agree on is the following:
By continuing you agree to not use paid ads for promoting your affiliate links, our website, or our brand keywords
That’s because you don’t want affiliates to bid for keyword traffic to your website or brand.
You also don’t want random affiliate ads to show up on Google when someone searches for your product. You lose from keyword bidding, and you lose on affiliate commissions.
But rules are made to be broken, so the only way to entirely prevent affiliates from running ads is by not honoring clicks and commissions from such ads.
const router = useRouter();
const { ambassador, referrer, gclid, gad_source, via } = router.query;
const isAffiliateMarketingNetwork = (referrer) => {
return /shareasale\.com|googleadservices\.com|google\.com/.test(referrer || '');
};
useEffect(() => {
// Don't honor clicks from ads
router.push({
pathname: '/',
query:
gclid || gad_source || via || isAffiliateMarketingNetwork(referrer)
? {}
: { username: ambassador, referrer },
});
}, [ambassador, referrer, gclid, gad_source, via, router]);
Turning the affiliate program into an employee rewards program
No matter how much you pay in commissions, the best ambassadors of a brand will always be its employees
It would be a shame not to offer similar commissions to your marketing and support team, especially if you already have the infrastructure in place.
Whenever someone decides to upgrade their plan on Publer, this modal appears.
The selected employee, except me, will receive a 10% commission on payments for up to one year, an excellent incentive for anyone to go the extra mile.
Just like any other affiliate, using the same dashboard, employees can also share their unique link to attract new customers from social media and beyond.
And there’s an additional prize of $50 for the top employee of the month (based on the revenue brought from new paying customers)
Return on investment
Although this affiliate program is turning 10 years old, its initial development took only a few weeks, with some minor updates made over the years. It’s a fully autonomous revenue-generating machine.
To date, we have paid our affiliates, including employees, over $208K in commissions.
In return, we have earned $742K in profit (excluding revenue from my personal branding).
At 355% ROI, this affiliate program outperforms every other marketing channel we have tested so far by a wide margin
Plans for the future
Given the success of these few lines of code, I’m seriously considering offering what we have built as a standalone SaaS product.
It would be a great addition to our mission of empowering your online presence.
Please let me know if this is something that would interest you.
And if you have read this far, I hope it has been worth it.
Thank you!
Other updates you might have missed: