Homeyscript log in to Bol

Hi guys,

I am trying to use Homeyscript to log in to Bol so I can get notifications from Bol. I have API information and I think I’ve got the code correct to log in, but for some reason I get the message that I am not authorised. Could one of you help me out in figuring out what I am doing wrong?

//constants
const clientid="secret client id"
const secret="secret client password"
const loginurl="https://login.bol.com/token"
const orderlisturl="https://api.bol.com/retailer/orders"

/**
 * Base64 encode a string without using btoa()
 * Supports full UTF-8 characters (e.g., emojis, accents)
 * Works in Node.js and browsers
 * @param {string} str - The input string
 * @returns {string} - Base64 encoded string
 */
function base64Encode(str) {
    if (typeof str !== 'string') {
        throw new TypeError('Input must be a string');
    }

    // Browser environment
    if (typeof window !== 'undefined' && typeof window.TextEncoder === 'function') {
        const encoder = new TextEncoder();
        const bytes = encoder.encode(str);
        let binary = '';
        bytes.forEach(byte => binary += String.fromCharCode(byte));
        return base64FromBinary(binary);
    }

    // Node.js environment
    if (typeof Buffer !== 'undefined') {
        return Buffer.from(str, 'utf-8').toString('base64');
    }

    throw new Error('No suitable Base64 encoding method found');
}

/**
 * Convert binary string to Base64 manually
 * @param {string} binary - Binary string
 * @returns {string} - Base64 encoded string
 */
function base64FromBinary(binary) {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    let result = '';
    let padding = '';

    // Pad binary length to multiple of 3
    const remainder = binary.length % 3;
    if (remainder > 0) {
        padding = '='.repeat(3 - remainder);
        binary += '\0'.repeat(3 - remainder);
    }

    for (let i = 0; i < binary.length; i += 3) {
        const chunk = (binary.charCodeAt(i) << 16) |
                      (binary.charCodeAt(i + 1) << 8) |
                      (binary.charCodeAt(i + 2));
        result += chars[(chunk >> 18) & 63] +
                  chars[(chunk >> 12) & 63] +
                  chars[(chunk >> 6) & 63] +
                  chars[chunk & 63];
    }

    return result.substring(0, result.length - padding.length) + padding;
}

auth=clientid+":"+secret
auth=base64Encode(auth)

const res = await fetch(loginurl, {
  method: "POST",
  headers: {
    'Content-Type':'application/x-www-form-urlencoded',
    'Accept':'application/json',
    "Authorization": "Basic "+auth
  }
});
// Get the JSON
log(res)
return -1

When using ncat to listen what Homey is sending, it seems to be ok:

ncat -k -v --listen 4430
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4430
Ncat: Listening on 0.0.0.0:4430
Ncat: Connection from homey:41884.
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Accept: application/json
Authorization: Basic base64encodedstuff=
Content-Length: 0
User-Agent: node-fetch/1.0 (+https://github.com/bitinn/node-fetch)
Accept-Encoding: gzip,deflate
Host: myhost:4430
Connection: keep-alive

I get the following back from bol:

Response { size: 0, timeout: 0, [Symbol(Body internals)]: { body: PassThrough { _events: [Object], _readableState: [ReadableState], _writableState: [WritableState], allowHalfOpen: true, _maxListeners: undefined, _eventsCount: 2, [Symbol(shapeMode)]: true, [Symbol(kCapture)]: false, [Symbol(kCallback)]: null }, disturbed: false, error: null }, [Symbol(Response internals)]: { url: '``https://login.bol.com/token``', status: 401, statusText: 'Unauthorized', headers: Headers { [Symbol(map)]: [Object: null prototype] }, counter: 0 } }

This is the docs I am using to get this:

You should read the body also, not the entire response object. You can do `await res.json()` to retrieve the JSON object.

Thanks for the tip, I tried it but only got this back:

{ error: 'invalid_request' }

That’s an actual response from Bol, so the res.json() part is working, your request is just rejected :slight_smile:

Another tip, I see that you create a base64 function yourself. You can use this as well: Buffer.from(‘My string / binary’).toString(‘base64’);

EDIT: I see that you check for Node, in a browser you could use the function `btoa`

The token request requires a grant_type parameter, which you’re not passing.

Thanks, that put me in the right direction. The Bol docs are a quite unclear, stating that that parameter should be in the body. But it should have been in the URL-parameter (saw that in a third party python api for bol).

So instead of:

const loginurl=“https://login.bol.com/token

it should have been:

const loginurl=“https://login.bol.com/token?grant_type=client_credentials

(for future people who want to do stuff with Bol)

1 Like

Hi @JF_Bethlehem

Little tip, you can edit the first post with this new information, can save people some time :wink:

I was able to finish the script, thanks to all the help by the people above this post. To celebrate, here’s the finished (working for me) Homeyscript. You only need to set your own clientid and clientsecret values that you can get from the Bol partner website (if you sell stuff).

const clientid=“”

const secret=“”

const loginurl=“https://login.bol.com/token”

const orderlisturl=“https://api.bol.com/retailer/orders”

const status=“OPEN”

/**

* Base64 encode a string without using btoa()

* Supports full UTF-8 characters (e.g., emojis, accents)

* Works in Node.js and browsers

* @paramparam {string} str - The input strin@returns

* @returns {string} - Base64 encoded string

*/

function base64Encode(str) {

if (typeof str !== 'string') {

    throw new TypeError('Input must be a string');

}



// Browser environment

if (typeof window !== 'undefined' && typeof window.TextEncoder === 'function') {

    const encoder = new TextEncoder();

    const bytes = encoder.encode(str);

    let binary = '';

    bytes.forEach(byte => binary += String.fromCharCode(byte));

    return base64FromBinary(binary);

}



// Node.js environment

if (typeof Buffer !== 'undefined') {

    return Buffer.from(str, 'utf-8').toString('base64');

}



throw new Error('No suitable Base64 encoding method found');

}

/**

* Convert binary string to Base64 @paramanually

* @param {string} binary - @returnsinary string

* @returns {string} - Base64 encoded string

*/

function base64FromBinary(binary) {

const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

let result = '';

let padding = '';



// Pad binary length to multiple of 3

const remainder = binary.length % 3;

if (remainder > 0) {

    padding = '='.repeat(3 - remainder);

    binary += '\\0'.repeat(3 - remainder);

}



for (let i = 0; i < binary.length; i += 3) {

    const chunk = (binary.charCodeAt(i) << 16) |

                  (binary.charCodeAt(i + 1) << 8) |

                  (binary.charCodeAt(i + 2));

    result += chars\[(chunk >> 18) & 63\] +

              chars\[(chunk >> 12) & 63\] +

              chars\[(chunk >> 6) & 63\] +

              chars\[chunk & 63\];

}

return result.substring(0, result.length - padding.length) + padding;

}

auth=clientid+“:”+secret

auth=base64Encode(auth)

var requestOptions = {

method: ‘POST’,

headers: {

"Accept":"application/json",

"AUthorization": 'Basic '+auth,

"Content-type":"application/x-www-form-urlencoded",

"Accept-Encoding": "gzip, deflate",

"Connection": "keep-alive",

},

redirect: ‘follow’,

body: new URLSearchParams({

grant_type: "client_credentials"

})

};

var response = ‘’

var result = ‘’

var error=‘’

var res = await fetch(loginurl, requestOptions)

.then((response) => response.json())

.then((result) => { return result} )

.catch(error => log('error', error));

var requestOptions = {

method: ‘GET’,

headers: {

"Accept":"application/vnd.retailer.v10+json",

"Authorization": res.token_type+' '+res.access_token,

"Accept-Encoding": "gzip, deflate",

"Connection": "keep-alive",

},

redirect: ‘follow’,

};

var url = orderlisturl+“?status=”+status

var orderlist = await fetch(url, requestOptions)

.then(response => response.json())

.then(result => {log(result);return result;})

.catch(error => log('error', error));

const ordercount = Array.isArray(orderlist?.orders) ? orderlist.orders.length : 0;

await tag(“bol_ordercount”, ordercount)

return ordercount
1 Like