Webhooks overview

Webhooks allow your application to receive real-time data from Fuse. This guide will walk you through how to work with webhooks in Fuse, including how to implement the sync data endpoint and listen for transaction updates and account disconnections.

Overview
The general flow for webhooks in Fuse is as follows:

  1. An aggregator sends a webhook to Fuse.
  2. Fuse forwards the webhook to your application with a type financial_connection.sync_data.
  3. Your application should then hit the sync endpoint to update the financial connection data. This allows Fuse to verify the webhook, perform any updates and then send you follow up webhooks (ie transactions.updates)

Syncing Financial Connection Data

When you receive a financial_connection.sync_data webhook, you should make a request to the sync endpoint. This request should include the body you received in the webhook and headers populated as you would for a normal request.

In addition to your regular headers, for this endpoint specifically, add fuse-verification to the headers. The value for fuse-verification will be included in the headers of the financial_connection.sync_data webhook when Fuse sends it.

You can see an example of how to call this endpoint here.

Here is an example of a financial_connection.sync_data webhook:

{
    "type": "financial_connection.sync_data",
    "financial_connection_id": "67bcb452-225e-11ee-be56-0242ac120002",
    "environment": "sandbox",
    "source": "mx",
    "remote_data": "{\\"type\\":\\"AGGREGATION\\",\\"action\\":\\"member_data_updated\\",\\"user_guid\\":\\"USR-67bcb452-225e-11ee-be56-0242ac120002\\",\\"user_id\\":\\"67bcb452-225e-11ee-be56-0242ac120002\\",\\"member_guid\\":\\"MBR-67bcb452-225e-11ee-be56-0242ac120002\\",\\"job_guid\\":\\"JOB-ebdbcc48-1823-41da-96ef-adc2dc2fd6ec\\",\\"transactions_created_count\\":2,\\"transactions_updated_count\\":0,\\"is_background\\":true,\\"completed_at\\":1688527757,\\"completed_on\\":\\"2023-07-05\\"}"
}

After you have synced the data, Fuse will send follow up webhooks to let you know if there are new transactions or if the account got disconnected.

Listening for Transaction Updates

When there are new transactions, Fuse will send a transactions.updates webhook. Here is an example:

{
    "type": "transactions.updates",
    "financial_connection_id": "67bcb452-225e-11ee-be56-0242ac120002",
    "environment": "sandbox",
    "source": "fuse",
    "historical_transactions_available": true,
    "remote_data": "{\\n  \\"environment\\": \\"production\\",\\n  \\"error\\": null,\\n  \\"item_id\\": \\"67bcb452-225e-11ee-be56-0242ac120002\\",\\n  \\"new_transactions\\": 3,\\n  \\"webhook_code\\": \\"DEFAULT_UPDATE\\",\\n  \\"webhook_type\\": \\"TRANSACTIONS\\"\\n}"
}

Listening for Account Disconnections

When an account is disconnected, Fuse will send a financial_connection.disconnected webhook. Users can reconnect these accounts. Here is an example:

{
    "type": "financial_connection.disconnected",
    "financial_connection_id": "67bcb452-225e-11ee-be56-0242ac120002",
    "environment": "sandbox",
    "source": "fuse",
    "remote_data": "{\\n  \\"environment\\": \\"sandbox\\",\\n  \\"error\\": {\\n    \\"error_code\\": \\"ITEM_LOGIN_REQUIRED\\",\\n    \\"error_message\\": \\"the login details of this item have changed (credentials, MFA, or required user action) and a user login is required to update this information. use Link's update mode to restore the item to a good state\\",\\n    \\"error_type\\": \\"ITEM_ERROR\\",\\n    \\"status\\": 400\\n  },\\n  \\"item_id\\": \\"67bcb452-225e-11ee-be56-0242ac120002\\",\\n  \\"webhook_code\\": \\"ERROR\\",\\n  \\"webhook_type\\": \\"ITEM\\"\\n}"
}

When an account is in a finished state (i.e., users can't reconnect), Fuse will send a financial_connection.finished webhook. Here is an example:

{  
    "type": "financial_connection.finished",  
    "financial_connection_id": "67bcb452-225e-11ee-be56-0242ac120002",  
    "environment": "sandbox",  
    "source": "fuse",  
    "remote_data": "{...}"  
}

Example webhook handler in nodejs

import express from 'express';
import bodyParser from 'body-parser';
import crypto from 'crypto';
import {FuseApi, Configuration} from "fuse-node";

const app = express();
app.use(bodyParser.json());

const configuration = new Configuration({
    basePath: 'https://sandbox-api.letsfuse.com',
    baseOptions: {
        headers: {
            'Fuse-Client-Id': 'my-fuse-client-id',
            'Fuse-Api-Key': 'my-fuse-api-key',
            'Content-Type': 'application/json',
            // ... other headers ...
        },
    },
});

const fuseApi = new FuseApi(configuration);

const hmacSignature = (key, msg, algorithm) => {
  return crypto.createHmac("sha256", key).update(msg).digest(algorithm);
};

const requestIsFromFuse = (fuseApiKey, webhook, fuseVerificationHeader) => {
  const replacer = (key, value) =>
    value instanceof Object && !(value instanceof Array)
      ? Object.keys(value)
          .sort()
          .reduce((sorted, key) => {
            sorted[key] = value[key];
            return sorted;
          }, {})
      : value;

  const requestJson = JSON.stringify(webhook, replacer);
  const dataHmac = hmacSignature(fuseApiKey, requestJson, "base64");

  return crypto.timingSafeEqual(
    Buffer.from(fuseVerificationHeader),
    Buffer.from(dataHmac)
  );
};

app.post('/webhook', async (req, res) => {
  const headers = req.headers;
  const body = req.body;

  const fuseVerificationHeader = headers['fuse-verification'];
  if (!requestIsFromFuse(configuration.baseOptions.headers['Fuse-Api-Key'], body, fuseVerificationHeader)) {
    res.status(401).send('Unauthorized');
    return;
  }

  const handlers = {
    'financial_connection.sync_data': async (headers, body) => {
      const fuseVerificationHeader = headers['fuse-verification'] as string;
      await fuseApi.syncFinancialConnectionsData(fuseVerificationHeader, body);
      console.log('Syncing financial connection data...');
    },
    'transactions.updates': async (headers, body) => {
      // Here, you would handle transaction updates.
      // This could involve syncing historical transactions, removing transactions,
      // or updating transactions based on the data in the webhook.
      console.log('Handling transaction updates...');
      //Make a fetch of the last two years if it's available and it has not already been fetched
      if (body.historical_transactions_available && this.historyNotFetched()) {
         await storeTransactionsFromLastTwoYears(
           body
         );
      } else if (body.removed_transaction_ids && body.removed_transaction_ids.length > 0) {
        await deleteRemovedTransactions(body)
      }) else {
        //Sync new and updated transactions from the get transactions endpoint with our database
        await retrieveAndUpdateTransactionsFromLastWeek(body)
      }
    },
    'financial_connection.disconnected': async (headers, body) => {
      // Here, you would handle a financial connection being disconnected.
      // This could involve updating the status of the connection in your database.
      console.log('Handling financial connection disconnection...');
    },
    'financial_connection.finished': async (headers, body) => {
      // Here, you would handle a financial connection being finished.
      // This could involve logging an error or notifying the user that a new connection needs to be added.
      console.log('Handling financial connection finished...');
    },
  };

  const handler = handlers[body.type];
  if (!handler) {
    console.log(`Unhandled webhook: ${body.type}`);
    res.status(200).send();
    return;
  }

  await handler(headers, body);
  res.status(200).send();
});

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});