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.

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 /v1/products/{product_id}/users/{user_id}/payments.json

When selling add-ons:
GET /v1/stores/{store_id}/users/{user_id}/payments.json

Invoice download
GET /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 /v1/products/{product_id}/users/{user_id}/subscriptions.json

Cancel subscription
DELETE /v1/products/{product_id}/subscriptions/{subscription_id}.json

Get licenses
GET /v1/products/{product_id}/users/{user_id}/licenses.json

When selling add-ons:
GET /v1/stores/{store_id}/users/{user_id}/licenses.json