Skip to content

PAYAPPS Example

The code below is an example of a process connecting to PAYAPPS. PAYAPPS authenticates using an OAuth password grant (client credentials over Basic Auth, plus a username/password, with automatic token refresh) rather than MODLR's built-in OAuth 2.0 flow, so this is an Advanced Integration.

INFO

This process requires PAYAPPS client credentials and a username/password, stored as Secrets or Model Variables rather than hardcoded in the process.

Library Code

A small client for the PAYAPPS API, handling authentication (including refreshing an expired token and retrying once) and wrapping authenticated GET requests. Save this as a Process Library and load it with script.library.

js
class PayAppsApi {
    /**
     * @param {string} basicToken - The base64-encoded client credentials for Basic Authorization.
     * @param {string} username - The username for the OAuth password grant.
     * @param {string} password - The password for the OAuth password grant.
     * @param {string} [baseUrl="https://api.payapps.com/v1"] - The base URL of the PAYAPPS API.
     */
    constructor(basicToken, username, password, baseUrl = "https://api.payapps.com/v1") {
        this.username = username;
        this.password = password;
        this.baseUrl = baseUrl;
        this.token = null;
        this.refreshToken = null;
        this.basicToken = datasource.atob(basicToken);
    }

    /**
     * Helper method to create a form-encoded string.
     * @private
     */
    _encodeForm(params) {
        return Object.keys(params)
            .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
            .join('&');
    }

    /**
     * Retrieves and sets the OAuth token for the API.
     * @private
     */
    _getToken() {
        const url = `${this.baseUrl}/oauth/token`;

        const headers = {
            "Authorization": `Basic ${this.basicToken}`,
            "Content-Type": "application/x-www-form-urlencoded"
        };

        const body = this._encodeForm({
            grant_type: "password",
            username: this.username,
            password: this.password
        });

        const response = web.request('POST', url, headers, body, false);

        if (response.status >= 200 && response.status < 300) {
            const data = JSON.parse(response.body);
            this.token = data.accessToken;
            this.refreshToken = data.refreshToken;
        } else {
            console.log("Failed to obtain access token. Response body:", response.body);
            throw new Error(`Token request failed with status code ${response.status}`);
        }
    }

    /**
     * Refreshes the OAuth token using the refresh token.
     * @private
     */
    _refreshAccessToken() {
        const url = `${this.baseUrl}/oauth/refresh-token`;

        const headers = {
            "Authorization": `Basic ${this.basicToken}`,
            "Content-Type": "application/x-www-form-urlencoded"
        };

        const body = this._encodeForm({
            grant_type: "refresh_token",
            refresh_token: this.refreshToken
        });

        const response = web.request('POST', url, headers, body, false);

        if (response.status >= 200 && response.status < 300) {
            const data = JSON.parse(response.body);
            this.token = data.accessToken;
            this.refreshToken = data.refreshToken;
        } else {
            console.log("Failed to refresh token. Response body:", response.body);
            throw new Error(`Refresh token request failed with status code ${response.status}`);
        }
    }

    /**
     * Makes an HTTP request to the PAYAPPS API, refreshing the token and retrying once if it has expired.
     * @private
     */
    _fetch(url, method = 'GET', body = null, retryAttempt = 0) {
        if (!this.token) {
            this._getToken();
        }

        const absoluteUrl = `${this.baseUrl}/${url}`;
        const headers = {
            "Authorization": `Bearer ${this.token}`,
            "Content-Type": "application/json"
        };

        const response = web.request(method, absoluteUrl, headers, body ? JSON.stringify(body) : null, false);

        if (response.status === 401 && retryAttempt === 0) {
            console.log("Access token expired, attempting to refresh...");
            this._refreshAccessToken();
            return this._fetch(url, method, body, retryAttempt + 1);
        }

        if (response.status >= 200 && response.status < 300) {
            return JSON.parse(response.body);
        } else {
            console.log("Request failed. Response body:", response.body);
            throw new Error(`Request failed with status code ${response.status}`);
        }
    }

    /**
     * Makes a GET request to the specified endpoint.
     */
    get(url) {
        return this._fetch(url, 'GET');
    }
}

Process Code

js
script.library("api/payapps.js");

function pre() {
    // This function is called once before the process is executed.
    // Use this to setup prompts.
    script.log('process pre-execution parameters parsed.');
}

function begin() {
    // This function is called once at the start of the process
    script.log('process execution started.');

    const basicToken = security.getSecret("PAYAPPS_BASIC_TOKEN");
    const username = script.variableGet("PAYAPPS_EMAIL");
    const password = script.variableGet("PAYAPPS_PASSWORD");

    const api = new PayAppsApi(basicToken, username, password);

    try {
        const projects = api.get("projects");
        console.log(`Projects returned: ${projects.length}`);
        console.log(projects);
    } catch (e) {
        console.log("Something went wrong");
        console.log(e);
    }
}

function data(record) {
    // This function is called once for each line of data on the second cycle
    // Use this to build dimensions and push data into cubes
}

function end() {
    // This function is called once at the end of the process
    script.log('process execution finished.');
}