diff --git a/package.json b/package.json
index dd914f1..8a796c4 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
     "telegraf": "^4.3.0",
     "typescript": "^4.2.3",
     "url": "^0.11.0",
+    "vk-io": "^4.2.0",
     "winston": "^3.3.3",
     "yargs": "^17.0.0-candidate.10",
     "yup": "^0.32.9"
diff --git a/src/api/http/index.ts b/src/api/http/index.ts
index 92cf1af..b34744f 100644
--- a/src/api/http/index.ts
+++ b/src/api/http/index.ts
@@ -56,7 +56,8 @@ export class HttpApi {
       this.app.get(url.pathname, this.testWebhook);
     }
 
-    this.app.post(this.vk.endpoint, this.handleVkEvent);
+    // VK event handler
+    this.app.post(this.vk.endpoint, this.vk.handle);
   }
 
   /**
@@ -73,12 +74,4 @@ export class HttpApi {
   private testWebhook = async (req: Request, res: Response) => {
     res.sendStatus(200);
   };
-
-  /**
-   * Handles VK events
-   */
-  private handleVkEvent = async (req: Request, res: Response) => {
-    await this.vk.handle(req.body);
-    res.sendStatus(200);
-  };
 }
diff --git a/src/service/vk/handlers/MessageNewHandler.ts b/src/service/vk/handlers/MessageNewHandler.ts
new file mode 100644
index 0000000..c8a1f55
--- /dev/null
+++ b/src/service/vk/handlers/MessageNewHandler.ts
@@ -0,0 +1,12 @@
+import { VkEventHandler } from "./types";
+import { ContextDefaultState, MessageContext } from "vk-io";
+import { NextMiddleware } from "middleware-io";
+
+export class MessageNewHandler extends VkEventHandler<
+  MessageContext<ContextDefaultState>
+> {
+  public execute = async (context, next: NextMiddleware) => {
+    console.log("received message!");
+    await next();
+  };
+}
diff --git a/src/service/vk/handlers/index.ts b/src/service/vk/handlers/index.ts
new file mode 100644
index 0000000..ac7b8af
--- /dev/null
+++ b/src/service/vk/handlers/index.ts
@@ -0,0 +1,11 @@
+import { VkEvent } from "../types";
+import { VkEventHandler } from "./types";
+import { MessageNewHandler } from "./MessageNewHandler";
+
+export const vkEventToHandler: Record<VkEvent, typeof VkEventHandler> = {
+  [VkEvent.GroupJoin]: VkEventHandler,
+  [VkEvent.GroupLeave]: VkEventHandler,
+  [VkEvent.MessageNew]: MessageNewHandler,
+  [VkEvent.PostSuggestion]: VkEventHandler,
+  [VkEvent.WallPostNew]: VkEventHandler,
+};
diff --git a/src/service/vk/handlers/types.ts b/src/service/vk/handlers/types.ts
new file mode 100644
index 0000000..96ce7e1
--- /dev/null
+++ b/src/service/vk/handlers/types.ts
@@ -0,0 +1,13 @@
+import { NextMiddleware } from "middleware-io";
+import { ConfigGroup } from "../types";
+
+export class VkEventHandler<T = any> {
+  public constructor(protected config: ConfigGroup) {}
+  public execute: (context: T, next: NextMiddleware) => Promise<void> = async (
+    ctx,
+    next
+  ) => {
+    console.log(`vk received unknown event`, ctx);
+    await next();
+  };
+}
diff --git a/src/service/vk/index.ts b/src/service/vk/index.ts
index f1f59e5..60f8283 100644
--- a/src/service/vk/index.ts
+++ b/src/service/vk/index.ts
@@ -1,7 +1,16 @@
-import { VkConfig } from "./types";
+import { ConfigGroup, GroupInstance, VkConfig, VkEvent } from "./types";
+import { API, Upload, Updates } from "vk-io";
+import logger from "../logger";
+import { Request, Response } from "express";
+import { flatten, has, keys } from "ramda";
+import { NextFunction } from "connect";
+import { VkEventHandler } from "./handlers/types";
+import { vkEventToHandler } from "./handlers";
 
 export class VkService {
   public endpoint: string = "/";
+  private readonly instances: Record<string, GroupInstance>;
+  private readonly groups: Record<number, ConfigGroup>;
 
   constructor(private config: VkConfig) {
     if (!config.groups.length) {
@@ -9,12 +18,87 @@ export class VkService {
     }
 
     this.endpoint = config.endpoint;
+
+    this.groups = config.groups.reduce(
+      (acc, group) => ({
+        ...acc,
+        [group.id]: group,
+      }),
+      {}
+    );
+
+    this.instances = config.groups.reduce(
+      (acc, group) => ({
+        ...acc,
+        [group.id]: this.createGroupInstance(group),
+      }),
+      {}
+    );
   }
 
   /**
    * Handles incoming VK events
    */
-  public handle = async (event: any) => {
-    // TODO: handle events
+  public handle = async (req: Request, res: Response, next: NextFunction) => {
+    try {
+      const { body } = req;
+      const { groups } = this;
+      const groupId = body?.group_id;
+
+      if (!groupId || !has(groupId, groups) || !has(groupId, this.instances)) {
+        logger.warn(`vk received unknown call`, { body });
+        res.sendStatus(200);
+        return;
+      }
+
+      logger.debug(`received vk event`, { body });
+
+      const inst = this.instances[groupId] as GroupInstance;
+      inst.updates.getWebhookCallback(this.config.endpoint)(req, res, next);
+    } catch (e) {
+      next(e);
+    }
   };
+
+  private createGroupInstance = (group: ConfigGroup): GroupInstance => {
+    const api = new API({
+      token: group.apiKey,
+      apiBaseUrl: this.config.endpoint,
+    });
+    const upload = new Upload({ api });
+    const updates = new Updates({
+      api,
+      upload,
+      webhookConfirmation: group.testResponse,
+      webhookSecret: group.secretKey,
+    });
+
+    const handlers = this.setupHandlers(group);
+    handlers.forEach((channel) => {
+      keys(channel).forEach((event) => {
+        console.log(`updates in ${String(event)}`);
+        updates.on(event as any, channel[event].execute);
+      });
+    });
+
+    return {
+      api,
+      upload,
+      updates,
+    };
+  };
+
+  /**
+   * Setups handlers
+   */
+  private setupHandlers(group: ConfigGroup): Record<VkEvent, VkEventHandler>[] {
+    return flatten(
+      group.channels.map((chan) =>
+        chan.events.reduce((acc, event) => {
+          const handler = vkEventToHandler[event];
+          return { ...acc, [event]: new handler(group) };
+        }, {} as Record<VkEvent, VkEventHandler>[])
+      )
+    );
+  }
 }
diff --git a/src/service/vk/types.ts b/src/service/vk/types.ts
index 030f33f..62ee3e8 100644
--- a/src/service/vk/types.ts
+++ b/src/service/vk/types.ts
@@ -1,9 +1,11 @@
+import { API, Upload, Updates } from "vk-io";
+
 export interface VkConfig extends Record<string, any> {
   groups: ConfigGroup[];
   endpoint?: string;
 }
 
-interface ConfigGroup {
+export interface ConfigGroup {
   id: number;
   name: string;
   testResponse: string;
@@ -18,10 +20,15 @@ interface GroupChannel {
 }
 
 export enum VkEvent {
-  Confirmation = "confirmation",
   WallPostNew = "wall_post_new",
   PostSuggestion = "post_suggestion",
   GroupJoin = "group_join",
   GroupLeave = "group_leave",
   MessageNew = "message_new",
 }
+
+export interface GroupInstance {
+  api: API;
+  upload: Upload;
+  updates: Updates;
+}
diff --git a/yarn.lock b/yarn.lock
index 67d53ac..40f9d69 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -177,6 +177,11 @@ async@^3.1.0:
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
   integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
 
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -298,6 +303,13 @@ colorspace@1.1.x:
     color "3.0.x"
     text-hex "1.0.x"
 
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -357,6 +369,11 @@ debug@4, debug@^4.3.1:
   dependencies:
     ms "2.1.2"
 
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
 depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@@ -481,6 +498,15 @@ fn.name@1.x.x:
   resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
   integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
 
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 forwarded@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -577,6 +603,11 @@ inherits@2.0.3:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
+inspectable@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/inspectable/-/inspectable-1.1.1.tgz#7a9ecc277e483b00ca9a4eadd08bf22659b23585"
+  integrity sha512-dMT0Qj7iXzWt4pjAVRkWBdq8u1Yro5BEHDwv5EdNrIcbDp1wfhrYrWu4P48eAK5VA5MmVWPtKUB/xvBQA92rXw==
+
 ip@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
@@ -673,12 +704,17 @@ methods@~1.1.2:
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
 
+middleware-io@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/middleware-io/-/middleware-io-2.6.0.tgz#bce019eabf14ad0adb8c7ec328cb57ec37b8eb84"
+  integrity sha512-o/Aa6ZtufvXGFCKurGSu7QBCud4b2OAzj7LMR6eKMFX9IuC7UqDMDszKjo1pNIelhDX4TbAQVFobRRzLD1IgDQ==
+
 mime-db@1.47.0:
   version "1.47.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c"
   integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==
 
-mime-types@~2.1.24:
+mime-types@^2.1.12, mime-types@~2.1.24:
   version "2.1.30"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d"
   integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==
@@ -1187,6 +1223,18 @@ vary@~1.1.2:
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
 
+vk-io@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/vk-io/-/vk-io-4.2.0.tgz#ff16cd94ef8f96eacf3deac95eb448137bd8bbea"
+  integrity sha512-jBoS3NhJU5qHULcd2uYKvS3sT6aYxj66F0CD7ryeHci4jMpHVzmuT8NCueJrygoJDccm3pX+DzbGY6JeVaB8FQ==
+  dependencies:
+    abort-controller "^3.0.0"
+    debug "^4.3.1"
+    form-data "^4.0.0"
+    inspectable "^1.1.1"
+    middleware-io "^2.6.0"
+    node-fetch "^2.6.1"
+
 winston-transport@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"