diff --git a/bbb-webhooks/application.js b/bbb-webhooks/application.js
index 6b1e641e26443064677e3981f7e7fbc10d581767..32bf0bcfe006f4d8221ff6516230714f541c23ae 100644
--- a/bbb-webhooks/application.js
+++ b/bbb-webhooks/application.js
@@ -4,6 +4,7 @@ const IDMapping = require("./id_mapping.js");
 const WebHooks = require("./web_hooks.js");
 const WebServer = require("./web_server.js");
 const redis = require("redis");
+const UserMapping = require("./userMapping.js");
 
 // Class that defines the application. Listens for events on redis and starts the
 // process to perform the callback calls.
@@ -19,10 +20,12 @@ module.exports = class Application {
 
   start() {
     Hook.initialize(() => {
-      IDMapping.initialize(() => {
-        this.webServer.start(config.server.port);
-        this.webServer.createPermanent();
-        this.webHooks.start();
+      UserMapping.initialize(() => {
+        IDMapping.initialize(()=> {
+          this.webServer.start(config.server.port);
+          this.webServer.createPermanent();
+          this.webHooks.start();
+        });
       });
     });
   }
diff --git a/bbb-webhooks/config.js b/bbb-webhooks/config.js
index 21de8e01ed5d574354cb93cd85633e76264e0137..e97a25bca48a50622642d33282e34ca43260dc03 100644
--- a/bbb-webhooks/config.js
+++ b/bbb-webhooks/config.js
@@ -44,6 +44,8 @@ config.redis.keys.hooks = "bigbluebutton:webhooks:hooks";
 config.redis.keys.mappings = "bigbluebutton:webhooks:mappings";
 config.redis.keys.mapping = id => `bigbluebutton:webhooks:mapping:${id}`;
 config.redis.keys.events = id => `bigbluebutton:webhooks:events:${id}`;
+config.redis.keys.userMaps = "bigbluebutton:webhooks:userMaps";
+config.redis.keys.userMap = id => `bigbluebutton:webhooks:userMap:${id}`;
 
 config.api = {};
 config.api.responses = {};
diff --git a/bbb-webhooks/id_mapping.js b/bbb-webhooks/id_mapping.js
index 2c2ecb603952a514de2250dc4fffcbf0d389da59..f16d5fc18ce7a95cb0768083d145929bae11406c 100644
--- a/bbb-webhooks/id_mapping.js
+++ b/bbb-webhooks/id_mapping.js
@@ -4,6 +4,7 @@ const redis = require("redis");
 
 const config = require("./config.js");
 const Logger = require("./logger.js");
+const UserMapping = require("./userMapping.js");
 
 // The database of mappings. Uses the internal ID as key because it is unique
 // unlike the external ID.
@@ -163,7 +164,10 @@ module.exports = class IDMapping {
     const toRemove = _.filter(all, mapping => mapping.lastActivity < (now - config.mappings.timeout));
     if (!_.isEmpty(toRemove)) {
       Logger.info("[IDMapping] expiring the mappings:", _.map(toRemove, map => map.print()));
-      toRemove.forEach(mapping => mapping.destroy());
+      toRemove.forEach(mapping => {
+        UserMapping.removeMappingMeetingId(mapping.internalMeetingID);
+        mapping.destroy()
+      });
     }
   }
 
diff --git a/bbb-webhooks/userMapping.js b/bbb-webhooks/userMapping.js
new file mode 100644
index 0000000000000000000000000000000000000000..6770b0ae6b530e1409661aac895fb88aa0834bca
--- /dev/null
+++ b/bbb-webhooks/userMapping.js
@@ -0,0 +1,190 @@
+const _ = require("lodash");
+const async = require("async");
+const redis = require("redis");
+
+const config = require("./config.js");
+const Logger = require("./logger.js");
+
+// The database of mappings. Uses the internal ID as key because it is unique
+// unlike the external ID.
+// Used always from memory, but saved to redis for persistence.
+//
+// Format:
+//   {
+//      internalMeetingID: {
+//       id: @id
+//       externalMeetingID: @externalMeetingID
+//       internalMeetingID: @internalMeetingID
+//       lastActivity: @lastActivity
+//     }
+//   }
+// Format on redis:
+//   * a SET "...:mappings" with all ids (not meeting ids, the object id)
+//   * a HASH "...:mapping:<id>" for each mapping with all its attributes
+const db = {};
+let nextID = 1;
+
+// A simple model to store mappings for user extIDs.
+module.exports = class UserMapping {
+
+  constructor() {
+    this.id = null;
+    this.externalUserID = null;
+    this.internalUserID = null;
+    this.meetingId = null;
+    this.redisClient = config.redis.client;
+  }
+
+  save(callback) {
+    this.redisClient.hmset(config.redis.keys.userMap(this.id), this.toRedis(), (error, reply) => {
+      if (error != null) { Logger.error("[UserMapping] error saving mapping to redis:", error, reply); }
+      this.redisClient.sadd(config.redis.keys.userMaps, this.id, (error, reply) => {
+        if (error != null) { Logger.error("[UserMapping] error saving mapping ID to the list of mappings:", error, reply); }
+
+        db[this.internalUserID] = this;
+        (typeof callback === 'function' ? callback(error, db[this.internalUserID]) : undefined);
+      });
+    });
+  }
+
+  destroy(callback) {
+    this.redisClient.srem(config.redis.keys.userMaps, this.id, (error, reply) => {
+      if (error != null) { Logger.error("[UserMapping] error removing mapping ID from the list of mappings:", error, reply); }
+      this.redisClient.del(config.redis.keys.userMap(this.id), error => {
+        if (error != null) { Logger.error("[UserMapping] error removing mapping from redis:", error); }
+
+        if (db[this.internalUserID]) {
+          delete db[this.internalUserID];
+          (typeof callback === 'function' ? callback(error, true) : undefined);
+        } else {
+          (typeof callback === 'function' ? callback(error, false) : undefined);
+        }
+      });
+    });
+  }
+
+  toRedis() {
+    const r = {
+      "id": this.id,
+      "internalUserID": this.internalUserID,
+      "externalUserID": this.externalUserID,
+      "meetingId": this.meetingId
+    };
+    return r;
+  }
+
+  fromRedis(redisData) {
+    this.id = parseInt(redisData.id);
+    this.externalUserID = redisData.externalUserID;
+    this.internalUserID = redisData.internalUserID;
+    this.meetingId = redisData.meetingId;
+  }
+
+  print() {
+    return JSON.stringify(this.toRedis());
+  }
+
+  static addMapping(internalUserID, externalUserID, meetingId, callback) {
+    let mapping = new UserMapping();
+    mapping.id = nextID++;
+    mapping.internalUserID = internalUserID;
+    mapping.externalUserID = externalUserID;
+    mapping.meetingId = meetingId;
+    mapping.save(function(error, result) {
+      Logger.info(`[UserMapping] added user mapping to the list ${internalUserID}:`, mapping.print());
+      (typeof callback === 'function' ? callback(error, result) : undefined);
+    });
+  }
+
+  static removeMapping(internalUserID, callback) {
+    return (() => {
+      let result = [];
+      for (let internal in db) {
+        var mapping = db[internal];
+        if (mapping.internalUserID === internalUserID) {
+          result.push(mapping.destroy( (error, result) => {
+            Logger.info(`[UserMapping] removing user mapping from the list ${internalUserID}:`, mapping.print());
+            return (typeof callback === 'function' ? callback(error, result) : undefined);
+          }));
+        } else {
+          result.push(undefined);
+        }
+      }
+      return result;
+    })();
+  }
+
+  static removeMappingMeetingId(meetingId, callback) {
+    return (() => {
+      let result = [];
+      for (let internal in db) {
+        var mapping = db[internal];
+        if (mapping.meetingId === meetingId) {
+          result.push(mapping.destroy( (error, result) => {
+            Logger.info(`[UserMapping] removing user mapping from the list ${mapping.internalUserID}:`, mapping.print());
+            return (typeof callback === 'function' ? callback(error, result) : undefined);
+          }));
+        } else {
+          result.push(undefined);
+        }
+      }
+      callback();
+    })();
+  }
+
+  static getExternalUserID(internalUserID) {
+    if (db[internalUserID]){
+      return db[internalUserID].externalUserID;
+    }
+  }
+  
+  static allSync() {
+    let arr = Object.keys(db).reduce(function(arr, id) {
+      arr.push(db[id]);
+      return arr;
+    }
+    , []);
+    return arr;
+  }
+
+  // Initializes global methods for this model.
+  static initialize(callback) {
+    UserMapping.resync(callback);
+  }
+
+  // Gets all mappings from redis to populate the local database.
+  // Calls `callback()` when done.
+  static resync(callback) {
+    let client = config.redis.client;
+    let tasks = [];
+
+    return client.smembers(config.redis.keys.userMaps, (error, mappings) => {
+      if (error != null) { Logger.error("[UserMapping] error getting list of mappings from redis:", error); }
+
+      mappings.forEach(id => {
+        tasks.push(done => {
+          client.hgetall(config.redis.keys.userMap(id), function(error, mappingData) {
+            if (error != null) { Logger.error("[UserMapping] error getting information for a mapping from redis:", error); }
+
+            if (mappingData != null) {
+              let mapping = new UserMapping();
+              mapping.fromRedis(mappingData);
+              mapping.save(function(error, hook) {
+                if (mapping.id >= nextID) { nextID = mapping.id + 1; }
+                done(null, mapping);
+              });
+            } else {
+              done(null, null);
+            }
+          });
+        });
+      });
+
+      return async.series(tasks, function(errors, result) {
+        mappings = _.map(UserMapping.allSync(), m => m.print());
+        Logger.info("[UserMapping] finished resync, mappings registered:", mappings);
+        return (typeof callback === 'function' ? callback() : undefined);
+      });
+    });
+  }
+};
diff --git a/bbb-webhooks/web_hooks.js b/bbb-webhooks/web_hooks.js
index 8461e23c59e9a8745e9432b05f9cd0fcee0accdf..2744de96d228ca106c87e953d87f4a3c9e01540a 100644
--- a/bbb-webhooks/web_hooks.js
+++ b/bbb-webhooks/web_hooks.js
@@ -7,6 +7,7 @@ const Hook = require("./hook.js");
 const IDMapping = require("./id_mapping.js");
 const Logger = require("./logger.js");
 const MessageMapping = require("./messageMapping.js");
+const UserMapping = require("./userMapping.js");
 
 // Web hooks will listen for events on redis coming from BigBlueButton and
 // perform HTTP calls with them to all registered hooks.
@@ -38,27 +39,31 @@ module.exports = class WebHooks {
         messageMapped.mapMessage(JSON.parse(message));
         message = messageMapped.mappedObject;
         if (!_.isEmpty(message) && !config.hooks.getRaw) {
-          const id = message.data.attributes.meeting["internal-meeting-id"];
-          IDMapping.reportActivity(id);
+          const intId = message.data.attributes.meeting["internal-meeting-id"];
+          IDMapping.reportActivity(intId);
 
           // First treat meeting events to add/remove ID mappings
-          if ((message.data != null ? message.data.id : undefined) === "meeting-created") {
-            Logger.info(`[WebHooks] got create message on meetings channel [${channel}]:`, message);
-            IDMapping.addOrUpdateMapping(message.data.attributes.meeting["internal-meeting-id"], message.data.attributes.meeting["external-meeting-id"], (error, result) =>
+          switch (message.data.id) {
+            case "meeting-created":
+              Logger.info(`[WebHooks] got create message on meetings channel [${channel}]:`, message);
+              IDMapping.addOrUpdateMapping(intId, message.data.attributes.meeting["external-meeting-id"], (error, result) => {
               // has to be here, after the meeting was created, otherwise create calls won't generate
               // callback calls for meeting hooks
-              processMessage()
-            );
-
-          // TODO: Temporarily commented because we still need the mapping for recording events,
-          //   after the meeting ended.
-          // else if message.header?.name is "meeting_destroyed_event"
-          //   Logger.info "[WebHooks] got destroy message on meetings channel [#{channel}]", message
-          //   IDMapping.removeMapping message.payload?.meeting_id, (error, result) ->
-          //     processMessage()
-
-          } else {
-            processMessage();
+                processMessage();
+              });
+              break;
+            case "user-joined":
+              UserMapping.addMapping(message.data.attributes.user["internal-user-id"],message.data.attributes.user["external-user-id"], intId, () => {
+                processMessage();
+              });
+              break;
+            case "user-left":
+              UserMapping.removeMapping(message.data.attributes.user["internal-user-id"], () => { processMessage(); });
+              break;
+            case "meeting-ended":
+              UserMapping.removeMappingMeetingId(intId, () => { processMessage(); });
+              break;
+            default: processMessage();
           }
         } else {
           this._processRaw(raw);