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.
For plan changes, you’ll need to specify the new plan ID as part of the path and set the customer’s license key (more details on license keys below):
https://checkout.freemius.com/mode/page/product/{product_id}/plan/{new_plan_id}/?license_key={license_key}
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.
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
https://checkout.freemius.com/mode/page/product/{product_id}/?license_key={license_key}&is_payment_method_update=true
Plan change
https://checkout.freemius.com/mode/page/product/{product_id}/plan/{new_plan_id}/?license_key={license_key}
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