This example covers a SaaS that has a sign-up process, allows a single account-level per user, and can also support account add-ons/extras.
Initial Steps
- Sign-up to Freemius
- Create a new SaaS product
- Go to the Plans and configure the plans and prices
Checkout Integration
You can integrate the Checkout as a modal dialog triggered using JavaScript, or by using direct checkout links. Regardless of the method you choose, ensure that you set user_email
to the logged-in user’s email. To enforce the purchase is made with the same customer email as in your system, you can direct the checkout to set the email input as read-only by setting readonly_user
to true
.
Here’s an example of a direct checkout link:
https://checkout.freemius.com/mode/page/product/{product_id}/plan/{plan_id}/?user_email={email}&readonly_user=true
When readonly_user
is set in a direct link, the checkout will auto redirect to https://checkout.freemius.com/mode/page/product/{product_id}/plan/{plan_id}/
, to ensure the user can’t easily tinker with the email address.
If you’re using the hosted Checkout, we recommend setting up a redirection URL so your app can immediately process the purchase information.
If you’re using the modal integration, use the success
or purchaseCompleted
callback handler to receive the purchase data.
Restricting or Relaxing Single Subscription Per User
Please note that by default, for SaaS products, users can have only one active subscription at a time. You can relax this limitation if needed by going to Settings and disabling the Restrict Single Subscription Per User toggle.
To learn how to facilitate upgrades, please continue reading.
Database Updates
Your database likely have a user
table with the following columns:
id
email
- …
Alter the table to add a new external_id
column (an unsigned 8-bytes integer; default to 0
or null
), which will hold the mapping between your user entities and Freemius’s.
If your SaaS is freemium, i.e., not every user is a customer and therefore, won’t have a Freemius user ID, then you can store the mapping in a separate table named
user_freemius
.
With Freemius, a user’s License
is what governs their account level, providing you with significant flexibility. This setup allows you to maintain feature access even if a subscription is canceled mid-term, or restrict access even if a subscription remains active. To effectively manage user account levels, ensure that your webhook is configured to handle license-related events (a webhook implementation can be found below).
We recommend following the same practice by creating a license
table in your database:
id
user_id
plan_id
external_id
external_key
expiration
is_canceled
created
updated
If you prefer not to store canceled or deleted licenses, you can remove the is_canceled
column and instead add a license_id
column to the user
table, establishing a 1:1 relationship. For the purposes of this example, however, we’ll assume that you want to preserve the entire license history.
Checking User’s Plan
To determine the account level of a logged-in user, search for an active/non-canceled license associated with the user. Ensure the license has not expired (via expiration
) to confirm the user is on the plan indicated by license.plan_id
.
<?php function getUserPlanId($userId) { $license = loadLicense([ 'user_id' => $userId, 'is_canceled' => false, ]); if ( ! is_object($license)) return null; // In case you want to support a concept of non-expiring licenses, you can set their expiration to `null`. if (is_null($license->expiration)) return true; $expiration = new DateTime($license->expiration); $now = new DateTime('now'); return ($now < $expiration) ? $license->plan_id : null; }
Freemius includes a built-in dunning mechanism. If a subscription renewal fails, the system will automatically attempt to process the payment through a series of emails sent over several days following the original renewal date. The subscription will remain active during this period, even though the license may expire, until the recovery process is complete or the subscription is eventually canceled.
Handling License or Subscription Upgrades
Freemius allows your users to upgrade the plan or license quota of their subscription. The upgrade process goes through our Checkout again, which collects up-to-date information from the user. After a successful purchase, the license object will be updated with the new information.
The flow involves:
- Sending an API request from your app with the
license_id
to generate the upgrade Checkout URL. - Passing the URL to your user or triggering the modal Checkout (depending on your integration).
- Synchronizing your SaaS with the up-to-date information.
Below is the API endpoint and an example of how to call it:
POST /v1/products/{product_id}/licenses/{license_id}/checkout/link.json HTTP/1.1 Content-Type: application/json Accept: application/json Authorization: Bearer 123 Host: api.freemius.com Content-Length: 127 { "plan_id": "planID", "billing_cycle": "annual", "quota": 1, "currency": "usd" }
The API will return an object with three properties:
url
– The upgrade link of the hosted checkout that you can share.settings
– Configuration parameter that you can use with the modal Checkout.expires
– Expiration date of the link.
You will need to use a Bearer Token to authenticate the request from your backend. More information about our REST API is available here.
Once the upgrade is complete, depending on your integration, the Checkout will either redirect to the success URL on your SaaS website or pass the data through the modal integration. From there, you can update your own system to reflect the changes in real time.
We strongly recommend setting up webhooks to ensure your backend reliably receives upgrade and license change notifications, even if the user’s browser fails to complete the redirect or if network interruptions occur during the Checkout process.
Creating a Webhook Listener
<?php // Retrieve the request's body payload. $input = @file_get_contents("php://input"); $hash = hash_hmac('sha256', $input, '<PRODUCT_SECRET_KEY>'); $signature = $_SERVER['HTTP_X_SIGNATURE'] ?? ''; if ( ! hash_equals($hash, $signature)) { // Invalid signature, don't expose any data to attackers. http_response_code(200); exit; } $event_json = json_decode($input); function loadUser($fsUser) { $user = loadUserByExternalId($fsUser->id); if (is_object($user)) { // A matching user already exists in the DB. return $user; } else { /** * There's no user matching the Freemius user ID, so we need to link locate the local user by email and link it to the corresponding Freemius user ID. */ $user = loadUserByEmail($fsUser->email); // You'll need to implement `loadUserByEmail($email)` // Link local user with Freemius's user ID. $user->external_id = $fsUser->id; $user->update(); } return $user; } function getPlanID($fsPlanId) { $plansMap = [ // Freemius plan ID => Your plan ID '678' => '1', '789' => '2', ]; return $plansMap[$fsPlanId]; } function createLicense($user, $fsLicense) { return storeLicense([ 'user_id' => $user->id, 'plan_id' => getPlanID($fsLicense->plan_id), 'external_id' => $fsLicense->id, // Using the license key you'll be able to open the checkout for plan changes and payment method updates. 'external_key' => $fsLicense->secret_key, 'expiration' => $fsLicense->expiration, ]); } function setUserPlan($fsUser, $fsLicense) { $user = loadUser($fsUser); return createLicense($user, $fsLicense); } function updateLicenseExpiration($fsUser, $fsLicense) { $license = loadLicenseByExternalId($fsLicense->id); if ( ! is_object($license)) { // Likely the 'license.created' event failed processing. setUserPlan($fsUser, $fsLicense); } else { $license->expiration = $fsLicense->expiration; $license->update(); } return $license; } function updateUserPlan($fsUser, $fsLicense) { if ( ! is_object($license)) { // Likely the 'license.created' event failed processing. setUserPlan($fsUser, $fsLicense); } else { $license->plan_id = getPlanID($fsLicense->plan_id); $license->update(); } return $license; } function downgradeUserPlan($fsUser, $fsLicense) { return updateLicenseExpiration($fsUser, $fsLicense); } switch ($fs_event->type) { case 'license.created': setUserPlan( $fs_event->objects->user, $fs_event->objects->license ); break; case 'license.plan.changed': updateUserPlan( $fs_event->objects->user, $fs_event->objects->license ); break; case 'license.extended': case 'license.shortened': updateLicenseExpiration( $fs_event->objects->user, $fs_event->objects->license ); break; case 'license.expired': downgradeLicenseByExternalId($fs_event->objects->license->id); break; case 'license.deleted': case 'license.cancelled': cancelLicneseByExternalId($fs_event->objects->license->id); break; } http_response_code(200);
To trigger custom emails for specific subscription events, you can handle events such as subscription.created
and subscription.canceled
.
Customer Dashboard (aka Customer Portal)
Freemius comes with a self-service customer dashboard out-of-the-box. Allowing your customers to easily access their order history, subscriptions, billing information, license keys, and more. They can change plans, update payment methods, and cancel subscriptions, putting control directly in their hands.
If you’d like to implement your own, within your SaaS, here are the API endpoints that will help you out:
Payments history
GET https://api.freemius.com/v1/products/{product_id}/users/{user_id}/payments.json
When selling add-ons:
GET https://api.freemius.com/v1/stores/{store_id}/users/{user_id}/payments.json
Invoice download
GET https://api.freemius.com/v1/products/{product_id}/users/{user_id}/payments/{payment_id}/invoice.pdf
Payment method update
POST https://api.freemius.com/v1/products/{product_id}/licenses/{license_id}/checkout/link.js { "is_payment_method_update": true }
Plan change
POST https://apicheckout.freemius.com/v1/products/{product_id}/licenses/{license_id}/checkout/link.js { "plan_id": "newPlanID” }
Get subscriptions
GET https://api.freemius.com/v1/products/{product_id}/users/{user_id}/subscriptions.json
Cancel subscription
DELETE https://api.freemius.com/v1/products/{product_id}/subscriptions/{subscription_id}.json
Get licenses
GET https://api.freemius.com/v1/products/{product_id}/users/{user_id}/licenses.json
When selling add-ons:
GET https://api.freemius.com/v1/stores/{store_id}/users/{user_id}/licenses.json