1. Home
  2. Documentation
  3. Selling with Freemius
  4. SaaS Integration

SaaS Integration

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

  1. Sign-up to Freemius
  2. Create a new SaaS product
  3. 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.

Freemius SaaS One Subscription Per User Restriction Config

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:

  1. Sending an API request from your app with the license_id to generate the upgrade Checkout URL.
  2. Passing the URL to your user or triggering the modal Checkout (depending on your integration).
  3. 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