DEV Community

Cover image for Webhook your weight with Withings API
Gustav Ehrenborg
Gustav Ehrenborg

Posted on

Webhook your weight with Withings API

This is a short walkthrough on how to authenticate with and use the Withings API. Its purpose is to explain how to get going with the code in this GitHub repo. The goal of the code is to be lightweight and have few dependencies, making it possible to run on weaker computers, often used in home automation.

Prerequisites

  • You'll need a Withings product. I use a scale. The webhook is the same for all products.
  • You'll also need a domain name pointing towards your server and an open port, or use ngrok. This will be your callback url.
  • I write cURL commands below, if you prefer to use Postman, it has an Import button to convert from cURL.

Withings account setup

Click here to create a partner account. You’ll receive a client id and a consumer secret, hereafter called client secret.

Withings authentication

The authentication flow contains quite many steps, and we'll cheat by running some requests manually, instead of implementing the whole process in code. This links leads to the authentication documentation.

Using your client id, client secret and callback url, complete the following URL and enter it in a browser.

https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=<your client id>&state=whatever&scope=user.info,user.metrics&redirect_uri=<your callback url>
Enter fullscreen mode Exit fullscreen mode

Click Allow this app, which will redirect to your callback url with a query param called code. This is an authorization code and it is valid for 30 seconds.

Then, fill out your values in this cURL command and run it.

curl --data "action=requesttoken&grant_type=authorization_code&client_id=<your client id>&client_secret=<client secret>&code=<the authorization code from above>&redirect_uri=<your callback url>" 'https://wbsapi.withings.net/v2/oauth2'
Enter fullscreen mode Exit fullscreen mode

You will receive a JSON response, containing a user id, an access token and a refresh token. The access token is needed to fetch data, and it's valid for three hours. The refresh token is used to get a new access token, and every time the access token is renewed, the refresh token is also renewed.

The webhook server

Persisting the user id and refresh tokens

As mentioned above, the refresh token is also renewed, thus demanding a way both to persist and update it. A database would be the best option, but this would require more dependencies. Instead, we'll persist the user id and refresh tokens in a file on disk.

Two really simple methods is all that is needed, one for reading and one for writing.

const getRefreshToken = async (userId) => fs.readFile(`./${userId}`, 'utf8');

const persistRefreshToken = async (userId, refreshToken) => fs.writeFile(`./${userId}`, refreshToken);
Enter fullscreen mode Exit fullscreen mode

The last step is also to create a file for your user id and current refresh token. Use this command: echo '<refresh token>' >> <userid>, for example: echo 'xxyyzz112233' >> 1234.

Getting an access token (and a refresh token)

Axios is used to make the API request. An axios instance for communicating with the Withings API is made, setting the defaults of the API. Note that the data is not sent as JSON, it is form-data.

const withingsAxios = axios.create({
  baseURL: 'https://wbsapi.withings.net',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
Enter fullscreen mode Exit fullscreen mode

The refreshAccessToken function fetches the current refresh token from file, uses it to obtain both an access and a new refresh token, and then persists the new refresh token. The Withings endpoint used is described more here.

const refreshAccessToken = async (userId) => {
  const currentRefreshToken = await getRefreshToken(userId);

  const data = {
    action: 'requesttoken',
    grant_type: 'refresh_token',
    client_id: WITHINGS_CLIENT_ID,
    client_secret: WITHINGS_CLIENT_SECRET,
    refresh_token: currentRefreshToken,
  };
  const body = qs.stringify(data);
  const response = await withingsAxios.post('/v2/oauth2', body);

  const newRefreshAccessToken = response.data.body.refresh_token;
  await persistRefreshToken(userId, newRefreshAccessToken);

  return response.data.body.access_token;
};
Enter fullscreen mode Exit fullscreen mode

Fetching a measurement

The getMeasures function fetches mesurements from your Withings appliances. An access token must be supplied, as well as a start and an end date. The dates should be Unix timestamps. However, we don't have to worry about those, because the callback will include timestamps.

Again, the axios instance is used, the data is form-data. The access token is included in the header. This links to the documentation.

const getMeasures = async (accessToken, startDate, endDate) => {
  const data = {
    action: 'getmeas',
    meastype: 1,
    category: 1,
    startdate: startDate,
    enddate: endDate,
  };
  const body = qs.stringify(data);

  const response = await withingsAxios.post('/measure', body,
    { headers: { Authorization: `Bearer ${accessToken}` } },
  );

  return response.data.body.measuregrps;
};
Enter fullscreen mode Exit fullscreen mode

Webhook server using Express

A simple Express endpoint is used to receive the webhook call. The call does not include any measurements, only timestamps, but your code will get a new access token and fetch the measurement data using the functions above, using the timestamps that were included in the webhook.

app.post('/withingsWebhook', async (request, response) => {
  response.send('ok');

  const userId = request.body.userid;
  const startDate = request.body.startdate;
  const endDate = request.body.enddate;

  const accessToken = await refreshAccessToken(userId);
  const measures = await getMeasures(accessToken, startDate, endDate);
  const weightInGrams = measures[0].measures[0].value;
  return weightInGrams;
});
Enter fullscreen mode Exit fullscreen mode

Subscribing to the Withings webhook

The last step is to tell Withings to post to your webhook. We will do this manually as well.

curl --header "Authorization: Bearer <your access token>" --data "action=subscribe&callbackurl=<your callback url>&appli=1&comment=comment" 'https://wbsapi.withings.net/notify'
Enter fullscreen mode Exit fullscreen mode

Finalizing this will have your webhook endpoint be called when a new measurement is made. There is, unfortunately, about 30 seconds delay from Withings.

The optional last steps

  • Add it to cron to start at system reboot
  • Publish your weight on mqtt
  • Have Home Assistant receive it
  • ...and configure Google Home to say it out load!
  • Add error handling, and tests

For full code, head over here: https://github.com/GuTsaV/trumpet

Latest comments (0)