Repository: line/line-bot-sdk-python
Pull request #151 (2019-03-29 04:02:01)

Fix flask-kitchensink
* Fix LocationSendMessage (location from user doesn't include title)
* Add user_id in follow/unfollow events
* Change environment variables error messages


Patch:
diff --git a/.travis.yml b/.travis.yml
index bda8308..e30565b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,5 @@
+dist: xenial
+
 language: python
 
 python:
@@ -5,6 +7,7 @@ python:
   - "3.4"
   - "3.5"
   - "3.6"
+  - "3.7"
 
 install: pip install tox-travis
 
diff --git a/README.rst b/README.rst
index 9573063..4f24f81 100644
--- a/README.rst
+++ b/README.rst
@@ -1,21 +1,31 @@
-line-bot-sdk-python
-===================
+LINE Messaging API SDK for Python
+=================================
 
 |Build Status| |PyPI version| |Documentation Status|
 
 SDK of the LINE Messaging API for Python.
 
-About the LINE Messaging API
-----------------------------
+Introduction
+------------
+The LINE Messaging API SDK for Python makes it easy to develop bots using LINE Messaging API, and you can create a sample bot within minutes.
 
-See the official API documentation for more information.
 
-English: https://developers.line.me/en/docs/messaging-api/reference/
+Documentation
+-------------
 
-Japanese: https://developers.line.me/ja/docs/messaging-api/reference/
+See the official API documentation for more information
 
-Install
--------
+English: https://developers.line.biz/en/docs/messaging-api/overview/
+
+Japanese: https://developers.line.biz/ja/docs/messaging-api/overview/
+
+Requirements
+------------
+
+-  Python >= 2.7 or >= 3.4
+
+Installation
+------------
 
 ::
 
@@ -310,6 +320,17 @@ https://developers.line.me/en/docs/messaging-api/reference/#link-rich-menu-to-us
 
     line_bot_api.link_rich_menu_to_user(user_id, rich_menu_id)
 
+link\_rich\_menu\_to\_users(self, user\_ids, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Links a rich menu to multiple users.
+
+https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-users
+
+.. code:: python
+
+    line_bot_api.link_rich_menu_to_users(<user_ids>, <rich_menu_id>)
+
 unlink\_rich\_menu\_from\_user(self, user\_id, timeout=None)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -321,6 +342,17 @@ https://developers.line.me/en/docs/messaging-api/reference/#unlink-rich-menu-fro
 
     line_bot_api.unlink_rich_menu_from_user(user_id)
 
+unlink\_rich\_menu\_from\_users(self, user\_ids, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Unlinks rich menus from multiple users.
+
+https://developers.line.biz/en/reference/messaging-api/#unlink-rich-menu-from-users
+
+.. code:: python
+
+    line_bot_api.unlink_rich_menu_from_users(<user_ids>)
+
 get\_rich\_menu\_image(self, rich\_menu\_id, timeout=None)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -360,6 +392,74 @@ https://developers.line.me/en/docs/messaging-api/reference/#get-rich-menu-list
     for rich_menu in rich_menu_list:
         print(rich_menu.rich_menu_id)
 
+set\_default\_rich\_menu(self, rich\_menu\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Sets the default rich menu.
+
+https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu
+
+.. code:: python
+
+    line_bot_api.set_default_rich_menu(<rich_menu_id>)
+
+get\_default\_rich\_menu(self, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Gets the ID of the default rich menu set with the Messaging API.
+
+https://developers.line.biz/en/reference/messaging-api/#get-default-rich-menu-id
+
+.. code:: python
+
+    line_bot_api.get_default_rich_menu()
+
+cancel\_default\_rich\_menu(self, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Cancels the default rich menu set with the Messaging API.
+
+https://developers.line.biz/en/reference/messaging-api/#cancel-default-rich-menu
+
+.. code:: python
+
+    line_bot_api.cancel_default_rich_menu()
+
+issue\_link\_token(self, user\_id, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Issues a link token used for the account link feature.
+
+https://developers.line.biz/en/reference/messaging-api/#issue-link-token
+
+.. code:: python
+
+    link_token_response = line_bot_api.issue_link_token(<user_id>)
+    print(link_token_response)
+
+issue\_channel\_token(self, client_id, client_secret, grant_type='client_credentials', timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Issues a short-lived channel access token.
+
+https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
+
+.. code:: python
+
+    channel_token_response = line_bot_api.issue_channel_token(<client_id>, <client_secret>)
+    print(access_token_response)
+
+revoke\_channel\_token(self, access_token, timeout=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Revokes a channel access token.
+
+https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token
+
+.. code:: python
+
+    line_bot_api.revoke_channel_token(<access_token>)
+
 ※ Error handling
 ^^^^^^^^^^^^^^^^
 
@@ -451,6 +551,17 @@ ImagemapSendMessage
         base_url='https://example.com/base',
         alt_text='this is an imagemap',
         base_size=BaseSize(height=1040, width=1040),
+        video=Video(
+            original_content_url='https://example.com/video.mp4',
+            preview_image_url='https://example.com/video_preview.jpg',
+            area=ImagemapArea(
+                x=0, y=0, width=1040, height=585
+            ),
+            external_link=ExternalLink(
+                link_uri='https://example.com/see_more.html',
+                label='See More',
+            ),
+        ),
         actions=[
             URIImagemapAction(
                 link_uri='https://example.com/',
@@ -481,7 +592,7 @@ TemplateSendMessage - ButtonsTemplate
             actions=[
                 PostbackAction(
                     label='postback',
-                    text='postback text',
+                    display_text='postback text',
                     data='action=buy&itemid=1'
                 ),
                 MessageAction(
@@ -508,7 +619,7 @@ TemplateSendMessage - ConfirmTemplate
             actions=[
                 PostbackAction(
                     label='postback',
-                    text='postback text',
+                    display_text='postback text',
                     data='action=buy&itemid=1'
                 ),
                 MessageAction(
@@ -535,7 +646,7 @@ TemplateSendMessage - CarouselTemplate
                     actions=[
                         PostbackAction(
                             label='postback1',
-                            text='postback text1',
+                            display_text='postback text1',
                             data='action=buy&itemid=1'
                         ),
                         MessageAction(
@@ -555,7 +666,7 @@ TemplateSendMessage - CarouselTemplate
                     actions=[
                         PostbackAction(
                             label='postback2',
-                            text='postback text2',
+                            display_text='postback text2',
                             data='action=buy&itemid=2'
                         ),
                         MessageAction(
@@ -585,7 +696,7 @@ TemplateSendMessage - ImageCarouselTemplate
                     image_url='https://example.com/item1.jpg',
                     action=PostbackAction(
                         label='postback1',
-                        text='postback text1',
+                        display_text='postback text1',
                         data='action=buy&itemid=1'
                     )
                 ),
@@ -593,7 +704,7 @@ TemplateSendMessage - ImageCarouselTemplate
                     image_url='https://example.com/item2.jpg',
                     action=PostbackAction(
                         label='postback2',
-                        text='postback text2',
+                        display_text='postback text2',
                         data='action=buy&itemid=2'
                     )
                 )
@@ -626,18 +737,25 @@ WebhookParser
 
     parser = linebot.WebhookParser('YOUR_CHANNEL_SECRET')
 
-parse(self, body, signature)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+parse(self, body, signature, as_payload=False)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Parses the webhook body and builds an event object list. If the signature does NOT
-match, InvalidSignatureError is raised.
+Parses the webhook body, and returns a list of Event objects or a WebhookPayload object (depending on as_payload).
+If the signature does NOT match, ``InvalidSignatureError`` is raised.
 
 .. code:: python
 
     events = parser.parse(body, signature)
 
     for event in events:
-        # Do something
+        do_something(event)
+
+.. code:: python
+
+    payload = parser.parse(body, signature, as_payload=True)
+
+    for event in payload.events:
+        do_something(payload.event, payload.destination)
 
 WebhookHandler
 ~~~~~~~~~~~~~~
@@ -654,19 +772,18 @@ WebhookHandler
 handle(self, body, signature)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Handles webhooks. If the signature does NOT match,
-InvalidSignatureError is raised.
+Handles webhooks with **handlers** added
+by the decorators `add <#add-self-event-message-none>`__ and `default <#default-self>`__.
+If the signature does NOT match, ``InvalidSignatureError`` is raised.
 
 .. code:: python
 
     handler.handle(body, signature)
 
-Add handler method
-^^^^^^^^^^^^^^^^^^
-
-You can add a handler method by using the ``add`` decorator.
+add(self, event, message=None)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-``add(self, event, message=None)``
+Add a **handler** method by using this decorator.
 
 .. code:: python
 
@@ -676,15 +793,30 @@ You can add a handler method by using the ``add`` decorator.
             event.reply_token,
             TextSendMessage(text=event.message.text))
 
-When the event is an instance of MessageEvent and event.message is an instance of
-TextMessage, this handler method is called.
+When the event is an instance of MessageEvent and event.message is an instance of TextMessage,
+this handler method is called.
+
+.. code:: python
+
+    @handler.add(MessageEvent)
+    def handle_message(event, destination):
+        # do something
+
+If the arity of the handler method is more than one,
+a destination property in a webhook request is passed to it as the second argument.
 
-Set default handler method
-^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. code:: python
+
+    @handler.add(FollowEvent)
+    def handle_follow():
+        # do something
+
+If the arity of the handler method is zero, the handler method is called with no arguments.
 
-You can set the default handler method by using the ``default`` decorator.
+default(self)
+^^^^^^^^^^^^^
 
-``default(self)``
+Set the default **handler** method by using this decorator.
 
 .. code:: python
 
@@ -694,6 +826,15 @@ You can set the default handler method by using the ``default`` decorator.
 
 If there is no handler for an event, this default handler method is called.
 
+WebhookPayload
+~~~~~~~~~~~~~~~
+
+https://developers.line.biz/en/reference/messaging-api/#request-body
+
+- WebhookPayload
+    - destination
+    - events: list[`Event <#event>`__]
+
 Webhook event object
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -828,10 +969,25 @@ API documentation
 
 OR |Documentation Status|
 
-Requirements
-------------
+Help and media
+--------------
+FAQ: https://developers.line.biz/en/faq/
 
--  Python >= 2.7 or >= 3.4
+Community Q&A: https://www.line-community.me/questions
+
+News: https://developers.line.biz/en/news/
+
+Twitter: @LINE_DEV 
+
+Versioning
+----------
+This project respects semantic versioning
+
+See http://semver.org/
+
+Contributing
+------------
+Please check `CONTRIBUTING <CONTRIBUTING.md>`__ before making a contribution.
 
 For SDK developers
 ------------------
@@ -851,6 +1007,7 @@ Test by using tox. We test against the following versions.
 -  3.4
 -  3.5
 -  3.6
+-  3.7
 
 To run all tests and to run ``flake8`` against all versions, use:
 
@@ -876,5 +1033,24 @@ And more... TBD
    :target: https://travis-ci.org/line/line-bot-sdk-python
 .. |PyPI version| image:: https://badge.fury.io/py/line-bot-sdk.svg
    :target: https://badge.fury.io/py/line-bot-sdk
-.. |Documentation Status| image:: https://readthedocs.org/projects/line-bot-sdk-python/badge/?version=latest
-   :target: http://line-bot-sdk-python.readthedocs.io/en/latest/?badge=latest
+.. |Documentation Status| image:: https://readthedocs.org/projects/line-bot-sdk-python/badge/?version=stable
+   :target: http://line-bot-sdk-python.readthedocs.io/en/stable
+
+License
+--------
+
+::
+
+    Copyright (C) 2016 LINE Corp.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
diff --git a/docs/source/conf.py b/docs/source/conf.py
index a008ec4..89604a8 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -16,9 +16,9 @@
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../../'))
 
 # -- General configuration ------------------------------------------------
 
@@ -341,6 +341,11 @@ texinfo_documents = [
 # texinfo_no_detailmenu = False
 
 # enable __init__ method
-autoclass_content = 'both'
-
+autoclass_content = 'class'
+autodoc_default_options = {
+    'special-members': '__init__',
+    'members': True,
+    'undoc-members': True,
+    'show-inheritance': True
+}
 autodoc_member_order = 'bysource'
diff --git a/docs/source/linebot.models.rst b/docs/source/linebot.models.rst
index fb2f4dc..de051a4 100644
--- a/docs/source/linebot.models.rst
+++ b/docs/source/linebot.models.rst
@@ -1,74 +1,63 @@
 linebot.models package
 ======================
 
+linebot.models.actions module
+------------------------------
+
+.. automodule:: linebot.models.actions
+    :exclude-members: get_action, get_actions
+
 linebot.models.base module
 --------------------------
 
 .. automodule:: linebot.models.base
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.models.error module
 ---------------------------
 
 .. automodule:: linebot.models.error
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.models.events module
 ----------------------------
 
 .. automodule:: linebot.models.events
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.models.imagemap module
 ------------------------------
 
 .. automodule:: linebot.models.imagemap
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.models.messages module
 ------------------------------
 
 .. automodule:: linebot.models.messages
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.models.responses module
 -------------------------------
 
 .. automodule:: linebot.models.responses
-    :members:
-    :undoc-members:
-    :show-inheritance:
+
+linebot.models.rich_menu module
+-------------------------------
+
+.. automodule:: linebot.models.rich_menu
 
 linebot.models.send_messages module
 -----------------------------------
 
 .. automodule:: linebot.models.send_messages
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.models.sources module
 -----------------------------
 
 .. automodule:: linebot.models.sources
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.models.template module
 ------------------------------
 
 .. automodule:: linebot.models.template
-    :members:
-    :undoc-members:
-    :show-inheritance:
+
+linebot.models.things module
+------------------------------
+
+.. automodule:: linebot.models.things
diff --git a/docs/source/linebot.rst b/docs/source/linebot.rst
index 64e0c71..1f91321 100644
--- a/docs/source/linebot.rst
+++ b/docs/source/linebot.rst
@@ -5,41 +5,22 @@ linebot.api module
 ------------------
 
 .. automodule:: linebot.api
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.webhook module
 ----------------------
 
 .. automodule:: linebot.webhook
-    :members:
-    :undoc-members:
-    :show-inheritance:
+    :exclude-members: compare_digest
 
 linebot.exceptions module
 -------------------------
 
 .. automodule:: linebot.exceptions
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 linebot.http_client module
 --------------------------
 
 .. automodule:: linebot.http_client
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-linebot.utils module
---------------------
-
-.. automodule:: linebot.utils
-    :members:
-    :undoc-members:
-    :show-inheritance:
 
 Subpackages
 -----------
diff --git a/examples/flask-kitchensink/app.py b/examples/flask-kitchensink/app.py
index 08a41a8..c11d447 100644
--- a/examples/flask-kitchensink/app.py
+++ b/examples/flask-kitchensink/app.py
@@ -40,6 +40,7 @@ from linebot.models import (
     StickerMessage, StickerSendMessage, LocationMessage, LocationSendMessage,
     ImageMessage, VideoMessage, AudioMessage, FileMessage,
     UnfollowEvent, FollowEvent, JoinEvent, LeaveEvent, BeaconEvent,
+    MemberJoinedEvent, MemberLeftEvent,
     FlexSendMessage, BubbleContainer, ImageComponent, BoxComponent,
     TextComponent, SpacerComponent, IconComponent, ButtonComponent,
     SeparatorComponent, QuickReply, QuickReplyButton,
@@ -51,11 +52,8 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1, x_proto=1)
 # get channel_secret and channel_access_token from your environment variable
 channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
 channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
-if channel_secret is None:
-    print('Specify LINE_CHANNEL_SECRET as environment variable.')
-    sys.exit(1)
-if channel_access_token is None:
-    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
+if channel_secret is None or channel_access_token is None:
+    print('Specify LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN as environment variables.')
     sys.exit(1)
 
 line_bot_api = LineBotApi(channel_access_token)
@@ -108,13 +106,57 @@ def handle_text_message(event):
             line_bot_api.reply_message(
                 event.reply_token, [
                     TextSendMessage(text='Display name: ' + profile.display_name),
-                    TextSendMessage(text='Status message: ' + profile.status_message)
+                    TextSendMessage(text='Status message: ' + str(profile.status_message))
                 ]
             )
         else:
             line_bot_api.reply_message(
                 event.reply_token,
                 TextSendMessage(text="Bot can't use profile API without user ID"))
+    elif text == 'quota':
+        quota = line_bot_api.get_message_quota()
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='type: ' + quota.type),
+                TextSendMessage(text='value: ' + str(quota.value))
+            ]
+        )
+    elif text == 'quota_consumption':
+        quota_consumption = line_bot_api.get_message_quota_consumption()
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='total usage: ' + str(quota_consumption.total_usage)),
+            ]
+        )
+    elif text == 'push':
+        line_bot_api.push_message(
+            event.source.user_id, [
+                TextSendMessage(text='PUSH!'),
+            ]
+        )
+    elif text == 'multicast':
+        line_bot_api.multicast(
+            [event.source.user_id], [
+                TextSendMessage(text='THIS IS A MULTICAST MESSAGE'),
+            ]
+        )
+    elif text == 'broadcast':
+        line_bot_api.broadcast(
+            [
+                TextSendMessage(text='THIS IS A BROADCAST MESSAGE'),
+            ]
+        )
+    elif text.startswith('broadcast '):  # broadcast 20190505
+        date = text.split(' ')[1]
+        print("Getting broadcast result: " + date)
+        result = line_bot_api.get_message_delivery_broadcast(date)
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='Number of sent broadcast messages: ' + date),
+                TextSendMessage(text='status: ' + str(result.status)),
+                TextSendMessage(text='success: ' + str(result.success)),
+            ]
+        )
     elif text == 'bye':
         if isinstance(event.source, SourceGroup):
             line_bot_api.reply_message(
@@ -317,6 +359,13 @@ def handle_text_message(event):
                             action=LocationAction(label="label6")
                         ),
                     ])))
+    elif text == 'link_token' and isinstance(event.source, SourceUser):
+        link_token_response = line_bot_api.issue_link_token(event.source.user_id)
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='link_token: ' + link_token_response.link_token)
+            ]
+        )
     else:
         line_bot_api.reply_message(
             event.reply_token, TextSendMessage(text=event.message.text))
@@ -327,7 +376,7 @@ def handle_location_message(event):
     line_bot_api.reply_message(
         event.reply_token,
         LocationSendMessage(
-            title=event.message.title, address=event.message.address,
+            title='Location', address=event.message.address,
             latitude=event.message.latitude, longitude=event.message.longitude
         )
     )
@@ -393,13 +442,14 @@ def handle_file_message(event):
 
 @handler.add(FollowEvent)
 def handle_follow(event):
+    app.logger.info("Got Follow event:" + event.source.user_id)
     line_bot_api.reply_message(
         event.reply_token, TextSendMessage(text='Got follow event'))
 
 
 @handler.add(UnfollowEvent)
-def handle_unfollow():
-    app.logger.info("Got Unfollow event")
+def handle_unfollow(event):
+    app.logger.info("Got Unfollow event:" + event.source.user_id)
 
 
 @handler.add(JoinEvent)
@@ -436,6 +486,20 @@ def handle_beacon(event):
                 event.beacon.hwid, event.beacon.dm)))
 
 
+@handler.add(MemberJoinedEvent)
+def handle_member_joined(event):
+    line_bot_api.reply_message(
+        event.reply_token,
+        TextSendMessage(
+            text='Got memberJoined event. event={}'.format(
+                event)))
+
+
+@handler.add(MemberLeftEvent)
+def handle_member_left(event):
+    app.logger.info("Got memberLeft event")
+
+
 @app.route('/static/<path:path>')
 def send_static_content(path):
     return send_from_directory('static', path)
diff --git a/linebot/__about__.py b/linebot/__about__.py
index 04f5347..7854322 100644
--- a/linebot/__about__.py
+++ b/linebot/__about__.py
@@ -16,7 +16,7 @@
 
 from __future__ import unicode_literals
 
-__version__ = '1.8.0'
+__version__ = '1.11.0'
 __author__ = 'LINE Corporation'
 __copyright__ = 'Copyright 2016, LINE Corporation'
 __license__ = 'Apache 2.0'
diff --git a/linebot/__init__.py b/linebot/__init__.py
index 2598d05..35fb9b2 100644
--- a/linebot/__init__.py
+++ b/linebot/__init__.py
@@ -31,4 +31,5 @@ from .webhook import (  # noqa
     SignatureValidator,
     WebhookParser,
     WebhookHandler,
+    WebhookPayload,
 )
diff --git a/linebot/api.py b/linebot/api.py
index 70f7faf..c345e75 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -22,7 +22,9 @@ from .__about__ import __version__
 from .exceptions import LineBotApiError
 from .http_client import HttpClient, RequestsHttpClient
 from .models import (
-    Error, Profile, MemberIds, Content, RichMenuResponse
+    Error, Profile, MemberIds, Content, RichMenuResponse, MessageQuotaResponse,
+    MessageQuotaConsumptionResponse, MessageDeliveryBroadcastResponse, IssueLinkTokenResponse,
+    IssueChannelTokenResponse,
 )
 
 
@@ -57,10 +59,10 @@ class LineBotApi(object):
         else:
             self.http_client = RequestsHttpClient(timeout=timeout)
 
-    def reply_message(self, reply_token, messages, timeout=None):
+    def reply_message(self, reply_token, messages, notification_disabled=False, timeout=None):
         """Call reply message API.
 
-        https://devdocs.line.me/en/#reply-message
+        https://developers.line.biz/en/reference/messaging-api/#send-reply-message
 
         Respond to events from users, groups, and rooms.
 
@@ -77,6 +79,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :param timeout: (optional) How long to wait for the server
             to send data before giving up, as a float,
             or a (connect timeout, read timeout) float tuple.
@@ -88,17 +92,18 @@ class LineBotApi(object):
 
         data = {
             'replyToken': reply_token,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/reply', data=json.dumps(data), timeout=timeout
         )
 
-    def push_message(self, to, messages, timeout=None):
+    def push_message(self, to, messages, notification_disabled=False, timeout=None):
         """Call push message API.
 
-        https://devdocs.line.me/en/#push-message
+        https://developers.line.biz/en/reference/messaging-api/#send-push-message
 
         Send messages to users, groups, and rooms at any time.
 
@@ -107,6 +112,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :param timeout: (optional) How long to wait for the server
             to send data before giving up, as a float,
             or a (connect timeout, read timeout) float tuple.
@@ -118,17 +125,18 @@ class LineBotApi(object):
 
         data = {
             'to': to,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/push', data=json.dumps(data), timeout=timeout
         )
 
-    def multicast(self, to, messages, timeout=None):
+    def multicast(self, to, messages, notification_disabled=False, timeout=None):
         """Call multicast API.
 
-        https://devdocs.line.me/en/#multicast
+        https://developers.line.biz/en/reference/messaging-api/#send-multicast-message
 
         Send messages to multiple users at any time.
 
@@ -139,6 +147,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :param timeout: (optional) How long to wait for the server
             to send data before giving up, as a float,
             or a (connect timeout, read timeout) float tuple.
@@ -150,17 +160,70 @@ class LineBotApi(object):
 
         data = {
             'to': to,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/multicast', data=json.dumps(data), timeout=timeout
         )
 
+    def broadcast(self, messages, notification_disabled=False, timeout=None):
+        """Call broadcast API.
+
+        https://developers.line.biz/en/reference/messaging-api/#send-broadcast-message
+
+        Send messages to multiple users at any time.
+
+        :param messages: Messages.
+            Max: 5
+        :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
+            list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        if not isinstance(messages, (list, tuple)):
+            messages = [messages]
+
+        data = {
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
+        }
+
+        self._post(
+            '/v2/bot/message/broadcast', data=json.dumps(data), timeout=timeout
+        )
+
+    def get_message_delivery_broadcast(self, date, timeout=None):
+        """Get number of sent broadcast messages.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-number-of-broadcast-messages
+
+        Gets the number of messages sent with the /bot/message/broadcast endpoint.
+
+        :param str date: Date the messages were sent. The format is `yyyyMMdd` (Timezone is UTC+9).
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        response = self._get(
+            '/v2/bot/message/delivery/broadcast?date={date}'.format(date=date),
+            timeout=timeout
+        )
+
+        return MessageDeliveryBroadcastResponse.new_from_json_dict(response.json)
+
     def get_profile(self, user_id, timeout=None):
         """Call get profile API.
 
-        https://devdocs.line.me/en/#bot-api-get-profile
+        https://developers.line.biz/en/reference/messaging-api/#get-profile
 
         Get user profile information.
 
@@ -183,7 +246,7 @@ class LineBotApi(object):
     def get_group_member_profile(self, group_id, user_id, timeout=None):
         """Call get group member profile API.
 
-        https://devdocs.line.me/en/#get-group-room-member-profile
+        https://developers.line.biz/en/reference/messaging-api/#get-group-member-profile
 
         Gets the user profile of a member of a group that
         the bot is in. This can be the user ID of a user who has
@@ -209,7 +272,7 @@ class LineBotApi(object):
     def get_room_member_profile(self, room_id, user_id, timeout=None):
         """Call get room member profile API.
 
-        https://devdocs.line.me/en/#get-group-room-member-profile
+        https://developers.line.biz/en/reference/messaging-api/#get-room-member-profile
 
         Gets the user profile of a member of a room that
         the bot is in. This can be the user ID of a user who has
@@ -235,7 +298,7 @@ class LineBotApi(object):
     def get_group_member_ids(self, group_id, start=None, timeout=None):
         """Call get group member IDs API.
 
-        https://devdocs.line.me/en/#get-group-room-member-ids
+        https://developers.line.biz/en/reference/messaging-api/#get-group-member-ids
 
         Gets the user IDs of the members of a group that the bot is in.
         This includes the user IDs of users who have not added the bot as a friend
@@ -264,7 +327,7 @@ class LineBotApi(object):
     def get_room_member_ids(self, room_id, start=None, timeout=None):
         """Call get room member IDs API.
 
-        https://devdocs.line.me/en/#get-group-room-member-ids
+        https://developers.line.biz/en/reference/messaging-api/#get-room-member-ids
 
         Gets the user IDs of the members of a group that the bot is in.
         This includes the user IDs of users who have not added the bot as a friend
@@ -293,7 +356,7 @@ class LineBotApi(object):
     def get_message_content(self, message_id, timeout=None):
         """Call get content API.
 
-        https://devdocs.line.me/en/#get-content
+        https://developers.line.biz/en/reference/messaging-api/#get-content
 
         Retrieve image, video, and audio data sent by users.
 
@@ -316,7 +379,7 @@ class LineBotApi(object):
     def leave_group(self, group_id, timeout=None):
         """Call leave group API.
 
-        https://devdocs.line.me/en/#leave
+        https://developers.line.biz/en/reference/messaging-api/#leave-group
 
         Leave a group.
 
@@ -335,7 +398,7 @@ class LineBotApi(object):
     def leave_room(self, room_id, timeout=None):
         """Call leave room API.
 
-        https://devdocs.line.me/en/#leave
+        https://developers.line.biz/en/reference/messaging-api/#leave-room
 
         Leave a room.
 
@@ -436,8 +499,12 @@ class LineBotApi(object):
 
         https://developers.line.me/en/docs/messaging-api/reference/#link-rich-menu-to-user
 
-        :param str user_id: ID of an uploaded rich menu
-        :param str rich_menu_id: ID of the user
+        :param str user_id: ID of the user
+        :param str rich_menu_id: ID of an uploaded rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
         :type timeout: float | tuple(float, float)
         """
         self._post(
@@ -448,6 +515,30 @@ class LineBotApi(object):
             timeout=timeout
         )
 
+    def link_rich_menu_to_users(self, user_ids, rich_menu_id, timeout=None):
+        """Links a rich menu to multiple users.
+
+        https://developers.line.biz/en/reference/messaging-api/#link-rich-menu-to-users
+
+        :param user_ids: user IDs
+            Max: 150 users
+        :type user_ids: list[str]
+        :param str rich_menu_id: ID of an uploaded rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/richmenu/bulk/link',
+            data=json.dumps({
+                'userIds': user_ids,
+                'richMenuId': rich_menu_id,
+            }),
+            timeout=timeout
+        )
+
     def unlink_rich_menu_from_user(self, user_id, timeout=None):
         """Call unlink rich menu from user API.
 
@@ -465,6 +556,28 @@ class LineBotApi(object):
             timeout=timeout
         )
 
+    def unlink_rich_menu_from_users(self, user_ids, timeout=None):
+        """Unlinks rich menus from multiple users.
+
+        https://developers.line.biz/en/reference/messaging-api/#unlink-rich-menu-from-users
+
+        :param user_ids: user IDs
+            Max: 150 users
+        :type user_ids: list[str]
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/richmenu/bulk/unlink',
+            data=json.dumps({
+                'userIds': user_ids,
+            }),
+            timeout=timeout
+        )
+
     def get_rich_menu_image(self, rich_menu_id, timeout=None):
         """Call download rich menu image API.
 
@@ -519,7 +632,7 @@ class LineBotApi(object):
             or a (connect timeout, read timeout) float tuple.
             Default is self.http_client.timeout
         :type timeout: float | tuple(float, float)
-        :rtype: list(T <= :py:class:`linebot.models.reponse.RichMenuResponse`)
+        :rtype: list(T <= :py:class:`linebot.models.responses.RichMenuResponse`)
         :return: list[RichMenuResponse] instance
         """
         response = self._get(
@@ -533,6 +646,171 @@ class LineBotApi(object):
 
         return result
 
+    def set_default_rich_menu(self, rich_menu_id, timeout=None):
+        """Set the default rich menu.
+
+        https://developers.line.biz/en/reference/messaging-api/#set-default-rich-menu
+
+        :param str rich_menu_id: ID of an uploaded rich menu
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/bot/user/all/richmenu/{rich_menu_id}'.format(
+                rich_menu_id=rich_menu_id,
+            ),
+            timeout=timeout
+        )
+
+    def get_default_rich_menu(self, timeout=None):
+        """Get the ID of the default rich menu set with the Messaging API.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-default-rich-menu-id
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        response = self._get(
+            '/v2/bot/user/all/richmenu',
+            timeout=timeout
+        )
+
+        return response.json.get('richMenuId')
+
+    def cancel_default_rich_menu(self, timeout=None):
+        """Cancel the default rich menu set with the Messaging API.
+
+        https://developers.line.biz/en/reference/messaging-api/#cancel-default-rich-menu
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._delete(
+            '/v2/bot/user/all/richmenu',
+            timeout=timeout
+        )
+
+    def get_message_quota(self, timeout=None):
+        """Call Get the target limit for additional messages.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-quota
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.MessageQuotaResponse`
+        :return: MessageQuotaResponse instance
+        """
+        response = self._get(
+            '/v2/bot/message/quota',
+            timeout=timeout
+        )
+
+        return MessageQuotaResponse.new_from_json_dict(response.json)
+
+    def get_message_quota_consumption(self, timeout=None):
+        """Get number of messages sent this month.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-consumption
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.MessageQuotaConsumptionResponse`
+        :return: MessageQuotaConsumptionResponse instance
+        """
+        response = self._get(
+            '/v2/bot/message/quota/consumption',
+            timeout=timeout
+        )
+
+        return MessageQuotaConsumptionResponse.new_from_json_dict(response.json)
+
+    def issue_link_token(self, user_id, timeout=None):
+        """Issues a link token used for the account link feature.
+
+        https://developers.line.biz/en/reference/messaging-api/#issue-link-token
+
+        :param str user_id: User ID for the LINE account to be linked
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.IssueLinkTokenResponse`
+        :return: IssueLinkTokenResponse instance
+        """
+        response = self._post(
+            '/v2/bot/user/{user_id}/linkToken'.format(
+                user_id=user_id
+            ),
+            timeout=timeout
+        )
+
+        return IssueLinkTokenResponse.new_from_json_dict(response.json)
+
+    def issue_channel_token(self, client_id, client_secret,
+                            grant_type='client_credentials', timeout=None):
+        """Issues a short-lived channel access token.
+
+        https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
+
+        :param str client_id: Channel ID.
+        :param str client_secret: Channel secret.
+        :param str grant_type: `client_credentials`
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.IssueChannelTokenResponse`
+        :return: IssueChannelTokenResponse instance
+        """
+        response = self._post(
+            '/v2/oauth/accessToken',
+            data={
+                'client_id': client_id,
+                'client_secret': client_secret,
+                'grant_type': grant_type,
+            },
+            headers={'Content-Type': 'application/x-www-form-urlencoded'},
+            timeout=timeout
+        )
+
+        return IssueChannelTokenResponse.new_from_json_dict(response.json)
+
+    def revoke_channel_token(self, access_token, timeout=None):
+        """Revokes a channel access token.
+
+        https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token
+
+        :param str access_token: Channel access token.
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        self._post(
+            '/v2/oauth/revoke',
+            data={'access_token': access_token},
+            headers={'Content-Type': 'application/x-www-form-urlencoded'},
+            timeout=timeout
+        )
+
     def _get(self, path, params=None, headers=None, stream=False, timeout=None):
         url = self.endpoint + path
 
diff --git a/linebot/exceptions.py b/linebot/exceptions.py
index 58c9b84..9f28c35 100644
--- a/linebot/exceptions.py
+++ b/linebot/exceptions.py
@@ -32,17 +32,13 @@ class BaseError(with_metaclass(ABCMeta, Exception)):
         self.message = message
 
     def __repr__(self):
-        """repr.
-
-        :return:
-        """
+        """repr."""
         return str(self)
 
     def __str__(self):
         """str.
 
         :rtype: str
-        :return:
         """
         return '<{0} [{1}]>'.format(
             self.__class__.__name__, self.message)
@@ -78,7 +74,6 @@ class LineBotApiError(BaseError):
         """str.
 
         :rtype: str
-        :return:
         """
         return '{0}: status_code={1}, error_response={2}'.format(
             self.__class__.__name__, self.status_code, self.error)
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
index 819a993..1a3346a 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -28,6 +28,7 @@ from .actions import (  # noqa
     MessageAction as MessageTemplateAction,  # backward compatibility
     URIAction as URITemplateAction,  # backward compatibility
     DatetimePickerAction as DatetimePickerTemplateAction,  # backward compatibility
+    AltUri,
 )
 from .base import (  # noqa
     Base,
@@ -45,7 +46,10 @@ from .events import (  # noqa
     LeaveEvent,
     PostbackEvent,
     AccountLinkEvent,
+    MemberJoinedEvent,
+    MemberLeftEvent,
     BeaconEvent,
+    ThingsEvent,
     Postback,
     Beacon,
     Link,
@@ -74,6 +78,8 @@ from .imagemap import (  # noqa
     URIImagemapAction,
     MessageImagemapAction,
     ImagemapArea,
+    Video,
+    ExternalLink,
 )
 from .messages import (  # noqa
     Message,
@@ -90,7 +96,12 @@ from .responses import (  # noqa
     MemberIds,
     Content,
     RichMenuResponse,
-    Content as MessageContent,  # backward compatibility
+    MessageQuotaResponse,
+    MessageQuotaConsumptionResponse,
+    MessageDeliveryBroadcastResponse,
+    Content as MessageContent,  # backward compatibility,
+    IssueLinkTokenResponse,
+    IssueChannelTokenResponse,
 )
 from .rich_menu import (  # noqa
     RichMenu,
@@ -125,3 +136,10 @@ from .template import (  # noqa
     ImageCarouselTemplate,
     ImageCarouselColumn,
 )
+from .things import (  # noqa
+    DeviceLink,
+    DeviceUnlink,
+    ScenarioResult,
+    ActionResult,
+    Things,
+)
diff --git a/linebot/models/actions.py b/linebot/models/actions.py
index 07143dd..e1bed60 100644
--- a/linebot/models/actions.py
+++ b/linebot/models/actions.py
@@ -126,12 +126,14 @@ class URIAction(Action):
     the URI specified in the uri property is opened.
     """
 
-    def __init__(self, label=None, uri=None, **kwargs):
+    def __init__(self, label=None, uri=None, alt_uri=None, **kwargs):
         """__init__ method.
 
         :param str label: Label for the action
             Max: 20 characters
         :param str uri: URI opened when the action is performed.
+        :param alt_uri: URI opened when the desktop app.
+        :type alt_uri: T <= :py:class:`linebot.models.actions.AltUri`
         :param kwargs:
         """
         super(URIAction, self).__init__(**kwargs)
@@ -139,6 +141,29 @@ class URIAction(Action):
         self.type = 'uri'
         self.label = label
         self.uri = uri
+        self.alt_uri = self.get_or_new_from_json_dict(alt_uri, AltUri)
+
+
+class AltUri(with_metaclass(ABCMeta, Base)):
+    """AltUri.
+
+    https://github.com/line/line-bot-sdk-python/issues/155
+
+    URI opened when the desktop app.
+    """
+
+    def __init__(self, desktop=None, **kwargs):
+        """__init__ method.
+
+        :param str desktop: URI opened on LINE for macOS and Windows
+            when the action is performed.
+            If the altUri.desktop property is set,
+            the uri property is ignored on LINE for macOS and Windows.
+        :param kwargs:
+        """
+        super(AltUri, self).__init__(**kwargs)
+
+        self.desktop = desktop
 
 
 class DatetimePickerAction(Action):
diff --git a/linebot/models/base.py b/linebot/models/base.py
index 457ea1b..e786f1c 100644
--- a/linebot/models/base.py
+++ b/linebot/models/base.py
@@ -35,24 +35,17 @@ class Base(object):
         pass
 
     def __str__(self):
-        """__str__ method.
-
-        :return:
-        """
+        """__str__ method."""
         return self.as_json_string()
 
     def __repr__(self):
-        """__repr__ method.
-
-        :return:
-        """
+        """__repr__ method."""
         return str(self)
 
     def __eq__(self, other):
         """__eq__ method.
 
         :param other:
-        :return:
         """
         return other and self.as_json_dict() == other.as_json_dict()
 
@@ -60,7 +53,6 @@ class Base(object):
         """__ne__ method.
 
         :param other:
-        :return:
         """
         return not self.__eq__(other)
 
@@ -68,7 +60,6 @@ class Base(object):
         """Return JSON string from this object.
 
         :rtype: str
-        :return:
         """
         return json.dumps(self.as_json_dict(), sort_keys=True)
 
@@ -100,8 +91,6 @@ class Base(object):
         """Create a new instance from a dict.
 
         :param data: JSON dict
-        :rtype:
-        :return:
         """
         new_data = {utils.to_snake_case(key): value
                     for key, value in data.items()}
@@ -119,7 +108,6 @@ class Base(object):
         :param data:
         :param cls:
         :rtype: object
-        :return:
         """
         if isinstance(data, cls):
             return data
@@ -142,7 +130,6 @@ class Base(object):
         :param cls_map:
         :param type_key:
         :rtype: object
-        :return:
         """
         if isinstance(data, tuple(cls_map.values())):
             return data
diff --git a/linebot/models/error.py b/linebot/models/error.py
index aab3a52..20db5ad 100644
--- a/linebot/models/error.py
+++ b/linebot/models/error.py
@@ -22,7 +22,7 @@ from .base import Base
 class Error(Base):
     """Error response of LINE messaging API.
 
-    https://devdocs.line.me/en/#error-response
+    https://developers.line.biz/en/reference/messaging-api/#error-response
     """
 
     def __init__(self, message=None, details=None, **kwargs):
@@ -31,7 +31,6 @@ class Error(Base):
         :param str message: Summary of the error
         :param details: ErrorDetail instance list
         :type details: list[T <= :py:class:`linebot.models.error.ErrorDetail`]
-        :type
         :param kwargs:
         """
         super(Error, self).__init__(**kwargs)
@@ -48,16 +47,11 @@ class Error(Base):
 
 
 class ErrorDetail(Base):
-    """ErrorDetail response of LINE messaging API.
-
-    https://devdocs.line.me/en/#error-response
-    """
+    """ErrorDetail response of LINE messaging API."""
 
     def __init__(self, message=None, property=None, **kwargs):
         """__init__ method.
 
-        https://devdocs.line.me/en/#error-response
-
         :param str message: Details of the error message
         :param str property: Related property
         :param kwargs:
diff --git a/linebot/models/events.py b/linebot/models/events.py
index bc7255d..0eb4b98 100644
--- a/linebot/models/events.py
+++ b/linebot/models/events.py
@@ -20,8 +20,8 @@ from abc import ABCMeta
 
 from future.utils import with_metaclass
 
-from .base import Base
-from .messages import (
+from linebot.models.base import Base
+from linebot.models.messages import (
     TextMessage,
     ImageMessage,
     VideoMessage,
@@ -30,13 +30,19 @@ from .messages import (
     StickerMessage,
     FileMessage
 )
-from .sources import SourceUser, SourceGroup, SourceRoom
+from linebot.models.sources import SourceUser, SourceGroup, SourceRoom
+from linebot.models.things import (
+    DeviceUnlink,
+    DeviceLink,
+    ScenarioResult,
+)
+from linebot.models.things import Things  # noqa
 
 
 class Event(with_metaclass(ABCMeta, Base)):
     """Abstract Base Class of Webhook Event.
 
-    https://devdocs.line.me/en/#webhook-event-object
+    https://developers.line.biz/en/reference/messaging-api/#webhook-event-objects
     """
 
     def __init__(self, timestamp=None, source=None, **kwargs):
@@ -63,7 +69,7 @@ class Event(with_metaclass(ABCMeta, Base)):
 class MessageEvent(Event):
     """Webhook MessageEvent.
 
-    https://devdocs.line.me/en/#message-event
+    https://developers.line.biz/en/reference/messaging-api/#message-event
 
     Event object which contains the sent message.
     The message field contains a message object which corresponds with the message type.
@@ -103,7 +109,7 @@ class MessageEvent(Event):
 class FollowEvent(Event):
     """Webhook FollowEvent.
 
-    https://devdocs.line.me/en/#follow-event
+    https://developers.line.biz/en/reference/messaging-api/#follow-event
 
     Event object for when your account is added as a friend (or unblocked).
     You can reply to follow events.
@@ -129,7 +135,7 @@ class FollowEvent(Event):
 class UnfollowEvent(Event):
     """Webhook UnfollowEvent.
 
-    https://devdocs.line.me/en/#unfollow-event
+    https://developers.line.biz/en/reference/messaging-api/#unfollow-event
 
     Event object for when your account is blocked.
     """
@@ -152,7 +158,7 @@ class UnfollowEvent(Event):
 class JoinEvent(Event):
     """Webhook JoinEvent.
 
-    https://devdocs.line.me/en/#join-event
+    https://developers.line.biz/en/reference/messaging-api/#join-event
 
     Event object for when your account joins a group or talk room.
     You can reply to join events.
@@ -178,7 +184,7 @@ class JoinEvent(Event):
 class LeaveEvent(Event):
     """Webhook LeaveEvent.
 
-    https://devdocs.line.me/en/#leave-event
+    https://developers.line.biz/en/reference/messaging-api/#leave-event
 
     Event object for when your account leaves a group.
     """
@@ -201,7 +207,7 @@ class LeaveEvent(Event):
 class PostbackEvent(Event):
     """Webhook PostbackEvent.
 
-    https://devdocs.line.me/en/#postback-event
+    https://developers.line.biz/en/reference/messaging-api/#postback-event
 
     Event object for when a user performs an action on
     a template message which initiates a postback.
@@ -233,7 +239,7 @@ class PostbackEvent(Event):
 class BeaconEvent(Event):
     """Webhook BeaconEvent.
 
-    https://devdocs.line.me/en/#beacon-event
+    https://developers.line.biz/en/reference/messaging-api/#beacon-event
 
     Event object for when a user detects a LINE Beacon. You can reply to beacon events.
     """
@@ -261,6 +267,68 @@ class BeaconEvent(Event):
         )
 
 
+class MemberJoinedEvent(Event):
+    """Webhook MemberJoinedEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-joined-event
+
+    Event object for when a user joins a group or room that the bot is in.
+
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None,
+                 joined=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param joined: Joined object
+        :type joined: :py:class:`linebot.models.events.Joined`
+        :param kwargs:
+        """
+        super(MemberJoinedEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'memberJoined'
+        self.reply_token = reply_token
+        self.joined = self.get_or_new_from_json_dict(
+            joined, Joined
+        )
+
+
+class MemberLeftEvent(Event):
+    """Webhook MemberLeftEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-left-event
+
+    Event object for when a user leaves a group or room that the bot is in.
+
+    """
+
+    def __init__(self, timestamp=None, source=None,
+                 left=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param left: Left object
+        :type left: :py:class:`linebot.models.events.Left`
+        :param kwargs:
+        """
+        super(MemberLeftEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'memberLeft'
+        self.left = self.get_or_new_from_json_dict(
+            left, Left
+        )
+
+
 class AccountLinkEvent(Event):
     """Webhook AccountLinkEvent.
 
@@ -294,10 +362,45 @@ class AccountLinkEvent(Event):
         )
 
 
+class ThingsEvent(Event):
+    """Webhook ThingsEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#device-link-event
+    https://developers.line.biz/en/reference/messaging-api/#device-unlink-event
+    https://developers.line.biz/en/reference/messaging-api/#scenario-result-event
+
+    Event sent from LINE Things Webhook service.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, things=None, **kwargs):
+        """__init__ method.
+
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param things: Things object
+        :type things: T <= :py:class:`linebot.models.things.Things`
+        :param kwargs:
+        """
+        super(ThingsEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'things'
+        self.reply_token = reply_token
+        self.things = self.get_or_new_from_json_dict_with_types(
+            things, {
+                'link': DeviceLink,
+                'unlink': DeviceUnlink,
+                'scenarioResult': ScenarioResult,
+            }
+        )
+
+
 class Postback(Base):
     """Postback.
 
-    https://devdocs.line.me/en/#postback-event
+    https://developers.line.biz/en/reference/messaging-api/#postback-event
     """
 
     def __init__(self, data=None, params=None, **kwargs):
@@ -318,7 +421,7 @@ class Postback(Base):
 class Beacon(Base):
     """Beacon.
 
-    https://devdocs.line.me/en/#beacon-event
+    https://developers.line.biz/en/reference/messaging-api/#beacon-event
     """
 
     def __init__(self, type=None, hwid=None, dm=None, **kwargs):
@@ -340,11 +443,54 @@ class Beacon(Base):
         """Get dm(device_message) as bytearray.
 
         :rtype: bytearray
-        :return:
         """
         return bytearray.fromhex(self.dm) if self.dm is not None else None
 
 
+class Joined(Base):
+    """Joined.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-joined-event
+    """
+
+    def __init__(self, members=None, **kwargs):
+        """__init__ method.
+
+        :param dict members: Member of users who joined
+        :param kwargs:
+        """
+        super(Joined, self).__init__(**kwargs)
+
+        self._members = members
+
+    @property
+    def members(self):
+        """Get members as list of SourceUser."""
+        return [SourceUser(user_id=x['userId']) for x in self._members]
+
+
+class Left(Base):
+    """Left.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-left-event
+    """
+
+    def __init__(self, members=None, **kwargs):
+        """__init__ method.
+
+        :param dict members: Member of users who joined
+        :param kwargs:
+        """
+        super(Left, self).__init__(**kwargs)
+
+        self._members = members
+
+    @property
+    def members(self):
+        """Get members as list of SourceUser."""
+        return [SourceUser(user_id=x['userId']) for x in self._members]
+
+
 class Link(Base):
     """Link.
 
diff --git a/linebot/models/imagemap.py b/linebot/models/imagemap.py
index cd06f4b..75d5415 100644
--- a/linebot/models/imagemap.py
+++ b/linebot/models/imagemap.py
@@ -27,13 +27,14 @@ from .send_messages import SendMessage
 class ImagemapSendMessage(SendMessage):
     """ImagemapSendMessage.
 
-    https://devdocs.line.me/en/#imagemap-message
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
 
     Imagemaps are images with one or more links. You can assign one link for the entire image
     or multiple links which correspond to different regions of the image.
     """
 
-    def __init__(self, base_url=None, alt_text=None, base_size=None, actions=None, **kwargs):
+    def __init__(self, base_url=None, alt_text=None, base_size=None,
+                 video=None, actions=None, **kwargs):
         """__init__ method.
 
         :param str base_url: Base URL of image.
@@ -41,6 +42,8 @@ class ImagemapSendMessage(SendMessage):
         :param str alt_text: Alternative text
         :param base_size: Width and height of base image
         :type base_size: :py:class:`linebot.models.imagemap.BaseSize`
+        :param video: Video in imagemap message
+        :type video: :py:class:`linebot.models.imagemap.Video`
         :param actions: Action when tapped
         :type actions: list[T <= :py:class:`linebot.models.imagemap.ImagemapAction`]
         :param kwargs:
@@ -53,6 +56,9 @@ class ImagemapSendMessage(SendMessage):
         self.base_size = self.get_or_new_from_json_dict(
             base_size, BaseSize
         )
+        self.video = self.get_or_new_from_json_dict(
+            video, Video
+        )
 
         new_actions = []
         if actions:
@@ -71,14 +77,12 @@ class ImagemapSendMessage(SendMessage):
 class BaseSize(Base):
     """BaseSize.
 
-    https://devdocs.line.me/en/#imagemap-message
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
     """
 
     def __init__(self, width=None, height=None, **kwargs):
         """__init__ method.
 
-        https://devdocs.line.me/en/#imagemap-message
-
         :param int width: Width of base image (set to 1040px）
         :param int height: Height of base image（set to the height
             that corresponds to a width of 1040px
@@ -93,7 +97,7 @@ class BaseSize(Base):
 class ImagemapAction(with_metaclass(ABCMeta, Base)):
     """ImagemapAction.
 
-    https://devdocs.line.me/en/#imagemap-message
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
     """
 
     def __init__(self, **kwargs):
@@ -109,7 +113,7 @@ class ImagemapAction(with_metaclass(ABCMeta, Base)):
 class URIImagemapAction(ImagemapAction):
     """URIImagemapAction.
 
-    https://devdocs.line.me/en/#imagemap-message
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
     """
 
     def __init__(self, link_uri=None, area=None, **kwargs):
@@ -130,7 +134,7 @@ class URIImagemapAction(ImagemapAction):
 class MessageImagemapAction(ImagemapAction):
     """MessageImagemapAction.
 
-    https://devdocs.line.me/en/#imagemap-message
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
     """
 
     def __init__(self, text=None, area=None, **kwargs):
@@ -151,7 +155,7 @@ class MessageImagemapAction(ImagemapAction):
 class ImagemapArea(Base):
     """ImagemapArea.
 
-    https://devdocs.line.me/en/#imagemap-area-object
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-area-object
 
     Defines the size of the full imagemap with the width as 1040px.
     The top left is used as the origin of the area.
@@ -172,3 +176,52 @@ class ImagemapArea(Base):
         self.y = y
         self.width = width
         self.height = height
+
+
+class Video(Base):
+    """Video.
+
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
+
+    Defines the properties of the video object in imagemap.
+    """
+
+    def __init__(self, original_content_url=None, preview_image_url=None,
+                 area=None, external_link=None, **kwargs):
+        """__init__ method.
+
+        :param str original_content_url: URL of the video file
+        :param str preview_image_url: URL of the preview image
+        :param area: Defined video area
+        :type area: :py:class:`linebot.models.imagemap.ImagemapArea`
+        :param external_link: Defined video external link
+        :type external_link: :py:class:`linebot.models.imagemap.ExternalLink`
+        :param kwargs:
+        """
+        super(Video, self).__init__(**kwargs)
+
+        self.original_content_url = original_content_url
+        self.preview_image_url = preview_image_url
+        self.area = self.get_or_new_from_json_dict(area, ImagemapArea)
+        self.external_link = self.get_or_new_from_json_dict(external_link, ExternalLink)
+
+
+class ExternalLink(Base):
+    """ExternalLink.
+
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
+
+    Defines URL and label of external link in video.
+    """
+
+    def __init__(self, link_uri=None, label=None, **kwargs):
+        """__init__ method.
+
+        :param str link_uri: Webpage URL
+        :param str label: Label
+        :param kwargs:
+        """
+        super(ExternalLink, self).__init__(**kwargs)
+
+        self.link_uri = link_uri
+        self.label = label
diff --git a/linebot/models/messages.py b/linebot/models/messages.py
index ace6e23..f541d29 100644
--- a/linebot/models/messages.py
+++ b/linebot/models/messages.py
@@ -41,7 +41,7 @@ class Message(with_metaclass(ABCMeta, Base)):
 class TextMessage(Message):
     """TextMessage.
 
-    https://devdocs.line.me/en/#text-message
+    https://developers.line.biz/en/reference/messaging-api/#wh-text
 
     Message object which contains the text sent from the source.
     """
@@ -62,67 +62,89 @@ class TextMessage(Message):
 class ImageMessage(Message):
     """ImageMessage.
 
-    https://devdocs.line.me/en/#image-message
+    https://developers.line.biz/en/reference/messaging-api/#wh-image
 
     Message object which contains the image content sent from the source.
     The binary image data can be retrieved with the Content API.
     """
 
-    def __init__(self, id=None, **kwargs):
+    def __init__(self, id=None, content_provider=None, **kwargs):
         """__init__ method.
 
         :param str id: Message ID
+        :param content_provider: ContentProvider object
+        :type content_provider:
+            :py:class:`linebot.models.messages.ContentProvider` | dict[str, str]
         :param kwargs:
         """
         super(ImageMessage, self).__init__(id=id, **kwargs)
 
         self.type = 'image'
+        self.content_provider = self.get_or_new_from_json_dict(
+            content_provider, ContentProvider
+        )
 
 
 class VideoMessage(Message):
     """VideoMessage.
 
-    https://devdocs.line.me/en/#video-message
+    https://developers.line.biz/en/reference/messaging-api/#wh-video
 
     Message object which contains the video content sent from the source.
     The binary video data can be retrieved with the Content API.
     """
 
-    def __init__(self, id=None, **kwargs):
+    def __init__(self, id=None, duration=None, content_provider=None, **kwargs):
         """__init__ method.
 
         :param str id: Message ID
+        :param long duration: Length of video file (milliseconds)
+        :param content_provider: ContentProvider object
+        :type content_provider:
+            :py:class:`linebot.models.messages.ContentProvider` | dict[str, str]
         :param kwargs:
         """
         super(VideoMessage, self).__init__(id=id, **kwargs)
 
         self.type = 'video'
+        self.duration = duration
+        self.content_provider = self.get_or_new_from_json_dict(
+            content_provider, ContentProvider
+        )
 
 
 class AudioMessage(Message):
     """AudioMessage.
 
-    https://devdocs.line.me/en/#audio-message
+    https://developers.line.biz/en/reference/messaging-api/#wh-audio
 
     Message object which contains the audio content sent from the source.
     The binary audio data can be retrieved with the Content API.
     """
 
-    def __init__(self, id=None, **kwargs):
+    def __init__(self, id=None, duration=None, content_provider=None, **kwargs):
         """__init__ method.
 
         :param str id: Message ID
+        :param long duration: Length of audio file (milliseconds)
+        :param content_provider: ContentProvider object
+        :type content_provider:
+            :py:class:`linebot.models.messages.ContentProvider` | dict[str, str]
         :param kwargs:
         """
         super(AudioMessage, self).__init__(id=id, **kwargs)
 
         self.type = 'audio'
+        self.duration = duration
+        self.content_provider = self.get_or_new_from_json_dict(
+            content_provider, ContentProvider
+        )
 
 
 class LocationMessage(Message):
     """LocationMessage.
 
-    https://devdocs.line.me/en/#location-message
+    https://developers.line.biz/en/reference/messaging-api/#wh-location
     """
 
     def __init__(self, id=None, title=None, address=None, latitude=None, longitude=None,
@@ -148,7 +170,7 @@ class LocationMessage(Message):
 class StickerMessage(Message):
     """StickerMessage.
 
-    https://devdocs.line.me/en/#sticker-message
+    https://developers.line.biz/en/reference/messaging-api/#wh-sticker
 
     Message object which contains the sticker data sent from the source.
     For a list of basic LINE stickers and sticker IDs, see sticker list.
@@ -172,7 +194,7 @@ class StickerMessage(Message):
 class FileMessage(Message):
     """FileMessage.
 
-    https://devdocs.line.me/en/#file-message
+    https://developers.line.biz/en/reference/messaging-api/#wh-file
 
     Message object which contains the file content sent from the source.
     The binary file data can be retrieved with the Content API.
@@ -191,3 +213,21 @@ class FileMessage(Message):
         self.type = 'file'
         self.file_size = file_size
         self.file_name = file_name
+
+
+class ContentProvider(Base):
+    """Content provider."""
+
+    def __init__(self, type=None, original_content_url=None, preview_image_url=None, **kwargs):
+        """__init__ method.
+
+        :param str type: Provider of the content. `line` or `external`.
+        :param str original_content_url: URL of the content.
+        :param str preview_image_url: URL of the preview image.
+        :param kwargs:
+        """
+        super(ContentProvider, self).__init__(**kwargs)
+
+        self.type = type
+        self.original_content_url = original_content_url
+        self.preview_image_url = preview_image_url
diff --git a/linebot/models/responses.py b/linebot/models/responses.py
index 8cd6e08..19b75f8 100644
--- a/linebot/models/responses.py
+++ b/linebot/models/responses.py
@@ -23,7 +23,7 @@ from .rich_menu import RichMenuSize, RichMenuArea
 class Profile(Base):
     """Profile.
 
-    https://devdocs.line.me/en/#bot-api-get-profile
+    https://developers.line.biz/en/reference/messaging-api/#get-profile
     """
 
     def __init__(self, display_name=None, user_id=None, picture_url=None,
@@ -47,7 +47,8 @@ class Profile(Base):
 class MemberIds(Base):
     """MemberIds.
 
-    https://devdocs.line.me/en/#get-group-room-member-ids
+    https://developers.line.biz/en/reference/messaging-api/#get-group-member-user-ids
+    https://developers.line.biz/en/reference/messaging-api/#get-room-member-user-ids
     """
 
     def __init__(self, member_ids=None, next=None, **kwargs):
@@ -69,7 +70,7 @@ class MemberIds(Base):
 class Content(object):
     """MessageContent.
 
-    https://devdocs.line.me/ja/#get-content
+    https://developers.line.biz/en/reference/messaging-api/#get-content
     """
 
     def __init__(self, response):
@@ -106,7 +107,6 @@ class Content(object):
 
         :param chunk_size: Chunk size
         :rtype: iterator
-        :return:
         """
         return self.response.iter_content(chunk_size=chunk_size)
 
@@ -150,3 +150,99 @@ class RichMenuResponse(Base):
                     self.get_or_new_from_json_dict(area, RichMenuArea)
                 )
         self.areas = new_areas
+
+
+class MessageQuotaResponse(Base):
+    """MessageQuotaResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-quota
+    """
+
+    def __init__(self, type=None, value=None, **kwargs):
+        """__init__ method.
+
+        :param str type: Quota limitation type
+        :param int value: The target limit for additional messages in the current month.
+            This property is returned when the type property has a value of limited.
+        :param kwargs:
+        """
+        super(MessageQuotaResponse, self).__init__(**kwargs)
+
+        self.type = type
+        self.value = value
+
+
+class MessageQuotaConsumptionResponse(Base):
+    """MessageQuotaConsumptionResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-consumption
+    """
+
+    def __init__(self, total_usage=None, **kwargs):
+        """__init__ method.
+
+        :param str total_usage: The number of sent messages in the current month
+        :param kwargs:
+        """
+        super(MessageQuotaConsumptionResponse, self).__init__(**kwargs)
+
+        self.total_usage = total_usage
+
+
+class MessageDeliveryBroadcastResponse(Base):
+    """MessageDeliveryBroadcastResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-number-of-broadcast-messages
+    """
+
+    def __init__(self, status=None, success=None, **kwargs):
+        """__init__ method.
+
+        :param str status: Status of the counting process.
+        :param int success: The number of messages sent with the Messaging API on the
+            date specified in date.
+        :param kwargs:
+        """
+        super(MessageDeliveryBroadcastResponse, self).__init__(**kwargs)
+
+        self.status = status
+        self.success = success
+
+
+class IssueLinkTokenResponse(Base):
+    """IssueLinkTokenResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#issue-link-token
+    """
+
+    def __init__(self, link_token=None, **kwargs):
+        """__init__ method.
+
+        :param str link_token: Link token.
+        :param kwargs:
+        """
+        super(IssueLinkTokenResponse, self).__init__(**kwargs)
+
+        self.link_token = link_token
+
+
+class IssueChannelTokenResponse(Base):
+    """IssueAccessTokenResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
+    """
+
+    def __init__(self, access_token=None, expires_in=None, token_type=None, **kwargs):
+        """__init__ method.
+
+        :param str access_token: Short-lived channel access token.
+        :param int expires_in: Time until channel access token expires in seconds
+            from time the token is issued.
+        :param str token_type: Bearer.
+        :param kwargs:
+        """
+        super(IssueChannelTokenResponse, self).__init__(**kwargs)
+
+        self.access_token = access_token
+        self.expires_in = expires_in
+        self.token_type = token_type
diff --git a/linebot/models/send_messages.py b/linebot/models/send_messages.py
index 855e03d..3caf0f1 100644
--- a/linebot/models/send_messages.py
+++ b/linebot/models/send_messages.py
@@ -43,7 +43,7 @@ class SendMessage(with_metaclass(ABCMeta, Base)):
 class TextSendMessage(SendMessage):
     """TextSendMessage.
 
-    https://devdocs.line.me/en/#text
+    https://developers.line.biz/en/reference/messaging-api/#text-message
     """
 
     def __init__(self, text=None, quick_reply=None, **kwargs):
@@ -63,7 +63,7 @@ class TextSendMessage(SendMessage):
 class ImageSendMessage(SendMessage):
     """ImageSendMessage.
 
-    https://devdocs.line.me/en/#image
+    https://developers.line.biz/en/reference/messaging-api/#image-message
     """
 
     def __init__(self, original_content_url=None, preview_image_url=None,
@@ -94,7 +94,7 @@ class ImageSendMessage(SendMessage):
 class VideoSendMessage(SendMessage):
     """VideoSendMessage.
 
-    https://devdocs.line.me/en/#video
+    https://developers.line.biz/en/reference/messaging-api/#video-message
     """
 
     def __init__(self, original_content_url=None, preview_image_url=None,
@@ -102,15 +102,9 @@ class VideoSendMessage(SendMessage):
         """__init__ method.
 
         :param str original_content_url: URL of video file.
-            HTTPS
-            mp4
-            Less than 1 minute
-            Max: 10 MB
+            HTTPS. mp4. Less than 1 minute. Max: 10 MB.
         :param str preview_image_url: URL of preview image.
-            HTTPS
-            JPEG
-            Max: 240 x 240
-            Max: 1 MB
+            HTTPS. JPEG. Max: 240 x 240. Max: 1 MB.
         :param quick_reply: QuickReply object
         :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
         :param kwargs:
@@ -125,17 +119,14 @@ class VideoSendMessage(SendMessage):
 class AudioSendMessage(SendMessage):
     """AudioSendMessage.
 
-    https://devdocs.line.me/en/#audio
+    https://developers.line.biz/en/reference/messaging-api/#audio-message
     """
 
     def __init__(self, original_content_url=None, duration=None, quick_reply=None, **kwargs):
         """__init__ method.
 
-        :param str original_content_url: URL of audio file.
-            HTTPS
-            m4a
-            Less than 1 minute
-            Max 10 MB
+        :param str original_content_url: URL of audio file. HTTPS.
+            m4a. Less than 1 minute. Max 10 MB.
         :param long duration: Length of audio file (milliseconds).
         :param quick_reply: QuickReply object
         :type quick_reply: T <= :py:class:`linebot.models.send_messages.QuickReply`
@@ -151,7 +142,7 @@ class AudioSendMessage(SendMessage):
 class LocationSendMessage(SendMessage):
     """LocationSendMessage.
 
-    https://devdocs.line.me/en/#location
+    https://developers.line.biz/en/reference/messaging-api/#location-message
     """
 
     def __init__(self, title=None, address=None, latitude=None, longitude=None,
@@ -178,7 +169,7 @@ class LocationSendMessage(SendMessage):
 class StickerSendMessage(SendMessage):
     """StickerSendMessage.
 
-    https://devdocs.line.me/en/#sticker
+    https://developers.line.biz/en/reference/messaging-api/#sticker-message
     """
 
     def __init__(self, package_id=None, sticker_id=None, quick_reply=None, **kwargs):
@@ -200,7 +191,7 @@ class StickerSendMessage(SendMessage):
 class QuickReply(with_metaclass(ABCMeta, Base)):
     """QuickReply.
 
-    https://developers.line.me/en/docs/messaging-api/using-quick-reply/
+    https://developers.line.me/en/docs/messaging-api/#quick-reply
     """
 
     def __init__(self, items=None, **kwargs):
@@ -224,7 +215,7 @@ class QuickReply(with_metaclass(ABCMeta, Base)):
 class QuickReplyButton(with_metaclass(ABCMeta, Base)):
     """QuickReplyButton.
 
-    https://developers.line.me/en/reference/messaging-api/#items-object
+    https://developers.line.me/en/reference/messaging-api/#quick-reply-button-object
     """
 
     def __init__(self, image_url=None, action=None, **kwargs):
diff --git a/linebot/models/sources.py b/linebot/models/sources.py
index c08a2c7..3671abd 100644
--- a/linebot/models/sources.py
+++ b/linebot/models/sources.py
@@ -46,7 +46,6 @@ class Source(with_metaclass(ABCMeta, Base)):
         'sender_id' is deprecated.
 
         :rtype: str
-        :return:
         """
         warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
         raise NotImplementedError
@@ -55,7 +54,7 @@ class Source(with_metaclass(ABCMeta, Base)):
 class SourceUser(Source):
     """SourceUser.
 
-    https://devdocs.line.me/en/#source-user
+    https://developers.line.biz/en/reference/messaging-api/#source-user
 
     JSON object which contains the source user of the event.
     """
@@ -78,7 +77,6 @@ class SourceUser(Source):
         'sender_id' is deprecated. Use 'user_id' instead.
 
         :rtype: str
-        :return:
         """
         warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
         return self.user_id
@@ -87,7 +85,7 @@ class SourceUser(Source):
 class SourceGroup(Source):
     """SourceGroup.
 
-    https://devdocs.line.me/en/#source-group
+    https://developers.line.biz/en/reference/messaging-api/#source-group
 
     JSON object which contains the source group of the event.
     """
@@ -112,7 +110,6 @@ class SourceGroup(Source):
         'sender_id' is deprecated. Use 'group_id' instead.
 
         :rtype: str
-        :return:
         """
         warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
         return self.group_id
@@ -121,7 +118,7 @@ class SourceGroup(Source):
 class SourceRoom(Source):
     """SourceRoom.
 
-    https://devdocs.line.me/en/#source-room
+    https://developers.line.biz/en/reference/messaging-api/#source-room
 
     JSON object which contains the source room of the event.
     """
@@ -146,7 +143,6 @@ class SourceRoom(Source):
         'sender_id' is deprecated. Use 'room_id' instead.
 
         :rtype: str
-        :return:
         """
         warnings.warn("'sender_id' is deprecated.", DeprecationWarning, stacklevel=2)
         return self.room_id
diff --git a/linebot/models/template.py b/linebot/models/template.py
index 0e8efb5..83b93c8 100644
--- a/linebot/models/template.py
+++ b/linebot/models/template.py
@@ -28,7 +28,7 @@ from .send_messages import SendMessage
 class TemplateSendMessage(SendMessage):
     """TemplateSendMessage.
 
-    https://devdocs.line.me/en/#template-messages
+    https://developers.line.biz/en/reference/messaging-api/#template-messages
 
     Template messages are messages with predefined layouts which you can customize.
     There are three types of templates available
@@ -73,7 +73,7 @@ class Template(with_metaclass(ABCMeta, Base)):
 class ButtonsTemplate(Template):
     """ButtonsTemplate.
 
-    https://devdocs.line.me/en/#buttons
+    https://developers.line.biz/en/reference/messaging-api/#buttons
 
     Template message with an image, title, text, and multiple action buttons.
     """
@@ -99,13 +99,7 @@ class ButtonsTemplate(Template):
             Specify one of the following values:
             rectangle: 1.51:1
             square: 1:1
-        :param str image_size: Size of the image.
-            Specify one of the following values:
-            cover: The image fills the entire image area.
-                Parts of the image that do not fit in the area are not displayed.
-            contain: The entire image is displayed in the image area.
-                A background is displayed in the unused areas to the left and right
-                of vertical images and in the areas above and below horizontal images.
+        :param str image_size: Size of the image. Specify ``cover`` or ``contain``.
         :param str image_background_color: Background color of image.
             Specify a RGB color value.
         :param actions: Action when tapped.
@@ -128,7 +122,7 @@ class ButtonsTemplate(Template):
 class ConfirmTemplate(Template):
     """ConfirmTemplate.
 
-    https://devdocs.line.me/en/#confirm
+    https://developers.line.biz/en/reference/messaging-api/#confirm
 
     Template message with two action buttons.
     """
@@ -153,7 +147,7 @@ class ConfirmTemplate(Template):
 class CarouselTemplate(Template):
     """CarouselTemplate.
 
-    https://devdocs.line.me/en/#carousel
+    https://developers.line.biz/en/reference/messaging-api/#carousel
 
     Template message with multiple columns which can be cycled like a carousel.
     """
@@ -166,16 +160,8 @@ class CarouselTemplate(Template):
             Max: 10
         :type columns: list[T <= :py:class:`linebot.models.template.CarouselColumn`]
         :param str image_aspect_ratio: Aspect ratio of the image.
-            Specify one of the following values:
-            rectangle: 1.51:1
-            square: 1:1
-        :param str image_size: Size of the image.
-            Specify one of the following values:
-            cover: The image fills the entire image area.
-                Parts of the image that do not fit in the area are not displayed.
-            contain: The entire image is displayed in the image area.
-                A background is displayed in the unused areas to the left and right
-                of vertical images and in the areas above and below horizontal images.
+            Specify ``rectangle`` or ``square``.
+        :param str image_size: Size of the image. Specify ``cover`` or ``contain``.
         :param kwargs:
         """
         super(CarouselTemplate, self).__init__(**kwargs)
@@ -196,7 +182,7 @@ class CarouselTemplate(Template):
 class ImageCarouselTemplate(Template):
     """ImageCarouselTemplate.
 
-    https://devdocs.line.me/en/#image-carousel
+    https://developers.line.biz/en/reference/messaging-api/#image-carousel
 
     Template message with multiple images columns which can be cycled like as carousel.
     """
@@ -225,7 +211,7 @@ class ImageCarouselTemplate(Template):
 class CarouselColumn(Base):
     """CarouselColumn.
 
-    https://devdocs.line.me/en/#column-object
+    https://developers.line.biz/en/reference/messaging-api/#column-object
     """
 
     def __init__(self, text=None, title=None, thumbnail_image_url=None,
@@ -237,12 +223,8 @@ class CarouselColumn(Base):
             Max: 60 characters (message with an image or title)
         :param str title: Title.
             Max: 40 characters
-        :param str thumbnail_image_url: Image URL.
-            HTTPS
-            JPEG or PNG
-            Aspect ratio: 1:1.51
-            Max width: 1024px
-            Max: 1 MB
+        :param str thumbnail_image_url: Image URL (HTTPS). JPEG or PNG. Aspect ration is 1:1.51.
+            Max width: 1024px, Max: 1 MB.
         :param str image_background_color: Background color of image.
             Specify a RGB color value.
         :param actions: Action when tapped.
@@ -262,20 +244,15 @@ class CarouselColumn(Base):
 class ImageCarouselColumn(Base):
     """ImageCarouselColumn.
 
-    https://devdocs.line.me/en/#column-object-for-image-carousel
+    https://developers.line.biz/en/reference/messaging-api/#column-object-for-image-carousel
     """
 
     def __init__(self, image_url=None, action=None, **kwargs):
         """__init__ method.
 
-        :param str image_url: Image URL.
-            HTTPS
-            JPEG or PNG
-            Aspect ratio: 1:1
-            Max width: 1024px
-            Max: 1 MB
-        :param action: Action when image is tapped
-            Max: 5
+        :param str image_url: Image URL (HTTPS). JPEG or PNG. Aspect ration is 1:1.
+            Max width: 1024px, Max: 1 MB.
+        :param action: Action when image is tapped. Max: 5.
         :type action: T <= :py:class:`linebot.models.actions.Action`
         :param kwargs:
         """
diff --git a/linebot/models/things.py b/linebot/models/things.py
new file mode 100644
index 0000000..04a1ac0
--- /dev/null
+++ b/linebot/models/things.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+"""linebot.models.things module."""
+
+from __future__ import unicode_literals
+
+from abc import ABCMeta
+
+from future.utils import with_metaclass
+
+from .base import Base
+
+
+class Things(with_metaclass(ABCMeta, Base)):
+    """Abstract Base Class of Things."""
+
+    def __init__(self, device_id=None, **kwargs):
+        """__init__ method.
+
+        :param str device_id: Device ID.
+        :param kwargs:
+        """
+        super(Things, self).__init__(**kwargs)
+        self.device_id = device_id
+
+
+class DeviceLink(Things):
+    """DeviceLink.
+
+    https://developers.line.biz/en/reference/messaging-api/#device-link-event
+
+    Indicates that a user linked a device with LINE.
+    """
+
+    def __init__(self, device_id=None, **kwargs):
+        """__init__ method.
+
+        :param str device_id: Device ID of the device that has been linked with LINE.
+        :param kwargs:
+        """
+        super(DeviceLink, self).__init__(device_id=device_id, **kwargs)
+
+        self.type = 'link'
+
+
+class DeviceUnlink(Things):
+    """DeviceUnlink.
+
+    https://developers.line.biz/en/reference/messaging-api/#device-unlink-event
+
+    Indicates that the user unlinked a device from LINE.
+    """
+
+    def __init__(self, device_id=None, **kwargs):
+        """__init__ method.
+
+        :param str device_id: Device ID of the device that was unlinked from LINE.
+        :param kwargs:
+        """
+        super(DeviceUnlink, self).__init__(device_id=device_id, **kwargs)
+
+        self.type = 'unlink'
+
+
+class ScenarioResult(Things):
+    """ScenarioResult.
+
+    https://developers.line.biz/en/reference/messaging-api/#scenario-result-event
+
+    Indicates that an automatic communication scenario has been executed.
+    """
+
+    def __init__(self, device_id=None, result=None, **kwargs):
+        """__init__ method.
+
+        :param str device_id: Device ID of the device that executed the scenario.
+        :param str result: ScenarioResultPayload object.
+        :param kwargs:
+        """
+        super(ScenarioResult, self).__init__(device_id=device_id, **kwargs)
+
+        self.type = 'scenarioResult'
+        self.result = self.get_or_new_from_json_dict(
+            result, ScenarioResultPayload
+        )
+
+
+class ScenarioResultPayload(Base):
+    """ScenarioResultPayload."""
+
+    def __init__(self, scenario_id=None, revision=None, start_time=None,
+                 result_code=None, end_time=None, action_results=None,
+                 ble_notification_payload=None, error_reason=None, **kwargs):
+        """__init__ method.
+
+        :param str scenario_id: Scenario ID executed.
+        :param long revision: Revision number.
+        :param long start_time: Timestamp for when execution of scenario
+            action started (milliseconds).
+        :param long end_time: Timestamp for when execution of scenario
+            was completed (milliseconds).
+        :param str result_code: Scenario execution completion status.
+        :param action_results: Array of actions specified in a scenario.
+        :type action_results: list[T <= :py:class:`linebot.models.things.ActionResult`]
+        :param str ble_notification_payload: Data contained in notification.
+        :param str error_reason: Error response.
+        :param kwargs:
+        """
+        super(ScenarioResultPayload, self).__init__(**kwargs)
+
+        self.scenario_id = scenario_id
+        self.revision = revision
+        self.start_time = start_time
+        self.end_time = end_time
+        self.result_code = result_code
+        self.action_results = [self.get_or_new_from_json_dict(it, ActionResult)
+                               for it in action_results]
+        self.ble_notification_payload = ble_notification_payload
+        self.error_reason = error_reason
+
+
+class ActionResult(Base):
+    """ActionResult.
+
+    Execution result of individual operations specified in action
+    """
+
+    def __init__(self, type=None, data=None, **kwargs):
+        """__init__ method.
+
+        :param str type: Type of the executed action.
+        :param str data: Base64-encoded binary data.
+        :param kwargs:
+        """
+        super(ActionResult, self).__init__(**kwargs)
+
+        self.type = type
+        self.data = data
diff --git a/linebot/utils.py b/linebot/utils.py
index cc09699..9d86036 100644
--- a/linebot/utils.py
+++ b/linebot/utils.py
@@ -30,7 +30,6 @@ def to_snake_case(text):
 
     :param str text:
     :rtype: str
-    :return:
     """
     s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text)
     return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
@@ -41,7 +40,6 @@ def to_camel_case(text):
 
     :param str text:
     :rtype: str
-    :return:
     """
     split = text.split('_')
     return split[0] + "".join(x.title() for x in split[1:])
diff --git a/linebot/webhook.py b/linebot/webhook.py
index 6253dcb..f9d1f0f 100644
--- a/linebot/webhook.py
+++ b/linebot/webhook.py
@@ -32,10 +32,12 @@ from .models.events import (
     PostbackEvent,
     BeaconEvent,
     AccountLinkEvent,
+    MemberJoinedEvent,
+    MemberLeftEvent,
+    ThingsEvent,
 )
 from .utils import LOGGER, PY3, safe_compare_digest
 
-
 if hasattr(hmac, "compare_digest"):
     def compare_digest(val1, val2):
         """compare_digest function.
@@ -71,7 +73,7 @@ else:
 class SignatureValidator(object):
     """Signature validator.
 
-    https://devdocs.line.me/en/#webhook-authentication
+    https://developers.line.biz/en/reference/messaging-api/#signature-validation
     """
 
     def __init__(self, channel_secret):
@@ -84,12 +86,9 @@ class SignatureValidator(object):
     def validate(self, body, signature):
         """Check signature.
 
-        https://devdocs.line.me/en/#webhook-authentication
-
         :param str body: Request body (as text)
         :param str signature: X-Line-Signature value (as text)
         :rtype: bool
-        :return: result
         """
         gen_signature = hmac.new(
             self.channel_secret,
@@ -98,10 +97,27 @@ class SignatureValidator(object):
         ).digest()
 
         return compare_digest(
-                signature.encode('utf-8'), base64.b64encode(gen_signature)
+            signature.encode('utf-8'), base64.b64encode(gen_signature)
         )
 
 
+class WebhookPayload(object):
+    """Webhook Payload.
+
+    https://developers.line.biz/en/reference/messaging-api/#request-body
+    """
+
+    def __init__(self, events=None, destination=None):
+        """__init__ method.
+
+        :param events: Information about the events.
+        :type events: list[T <= :py:class:`linebot.models.events.Event`]
+        :param str destination: User ID of a bot that should receive webhook events.
+        """
+        self.events = events
+        self.destination = destination
+
+
 class WebhookParser(object):
     """Webhook Parser."""
 
@@ -112,13 +128,15 @@ class WebhookParser(object):
         """
         self.signature_validator = SignatureValidator(channel_secret)
 
-    def parse(self, body, signature):
+    def parse(self, body, signature, as_payload=False):
         """Parse webhook request body as text.
 
         :param str body: Webhook request body (as text)
         :param str signature: X-Line-Signature value (as text)
+        :param bool as_payload: (optional) True to return WebhookPayload object.
         :rtype: list[T <= :py:class:`linebot.models.events.Event`]
-        :return:
+            | :py:class:`linebot.webhook.WebhookPayload`
+        :return: Events list, or WebhookPayload instance
         """
         if not self.signature_validator.validate(body, signature):
             raise InvalidSignatureError(
@@ -144,14 +162,26 @@ class WebhookParser(object):
                 events.append(BeaconEvent.new_from_json_dict(event))
             elif event_type == 'accountLink':
                 events.append(AccountLinkEvent.new_from_json_dict(event))
+            elif event_type == 'memberJoined':
+                events.append(MemberJoinedEvent.new_from_json_dict(event))
+            elif event_type == 'memberLeft':
+                events.append(MemberLeftEvent.new_from_json_dict(event))
+            elif event_type == 'things':
+                events.append(ThingsEvent.new_from_json_dict(event))
             else:
                 LOGGER.warn('Unknown event type. type=' + event_type)
 
-        return events
+        if as_payload:
+            return WebhookPayload(events=events, destination=body_json['destination'])
+        else:
+            return events
 
 
 class WebhookHandler(object):
-    """Webhook Handler."""
+    """Webhook Handler.
+
+    Please read https://github.com/line/line-bot-sdk-python#webhookhandler
+    """
 
     def __init__(self, channel_secret):
         """__init__ method.
@@ -188,7 +218,7 @@ class WebhookHandler(object):
         """[Decorator] Set default handler method.
 
         :rtype: func
-        :return:
+        :return: decorator
         """
         def decorator(func):
             self._default = func
@@ -202,9 +232,9 @@ class WebhookHandler(object):
         :param str body: Webhook request body (as text)
         :param str signature: X-Line-Signature value (as text)
         """
-        events = self.parser.parse(body, signature)
+        payload = self.parser.parse(body, signature, as_payload=True)
 
-        for event in events:
+        for event in payload.events:
             func = None
             key = None
 
@@ -226,8 +256,10 @@ class WebhookHandler(object):
                 args_count = self.__get_args_count(func)
                 if args_count == 0:
                     func()
-                else:
+                elif args_count == 1:
                     func(event)
+                else:
+                    func(event, payload.destination)
 
     def __add_handler(self, func, event, message=None):
         key = self.__get_handler_key(event, message=message)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index b76741f..8703151 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,6 +2,7 @@
 -r requirements-test.txt
 tox
 wheel
+sphinx>=2
 sphinx_rtd_theme
 pypandoc
 twine
diff --git a/tox.ini b/tox.ini
index 4367fb6..0815ab7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27, py34, py35, py36, py36-flake8-src, py36-flake8-other
+envlist = py27, py34, py35, py36, py37, py37-flake8-src, py37-flake8-other
 
 [testenv]
 deps =
@@ -8,21 +8,21 @@ deps =
     -r{toxinidir}/requirements-test.txt
 commands = py.test -v tests/
 
-[testenv:py36]
-basepython = python3.6
+[testenv:py37]
+basepython = python3.7
 deps = {[testenv]deps}
 commands = py.test -v --cov=linebot tests/
 
-[testenv:py36-flake8-src]
-basepython = python3.6
+[testenv:py37-flake8-src]
+basepython = python3.7
 deps =
     six
     flake8
     flake8-docstrings
 commands = flake8 linebot/
 
-[testenv:py36-flake8-other]
-basepython = python3.6
+[testenv:py37-flake8-other]
+basepython = python3.7
 deps =
     six
     flake8


Test patch:
diff --git a/tests/api/test_issue_channel_token.py b/tests/api/test_issue_channel_token.py
new file mode 100644
index 0000000..b0d5dbc
--- /dev/null
+++ b/tests/api/test_issue_channel_token.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import sys
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+
+PY3 = sys.version_info[0] == 3
+if PY3:
+    from urllib import parse
+else:
+    import urlparse as parse
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+        self.endpoint = LineBotApi.DEFAULT_API_ENDPOINT + '/v2/oauth/accessToken'
+        self.access_token = "W1TeHCgfH2Liwa....."
+        self.expires_in = 2592000
+        self.token_type = "Bearer"
+        self.client_id = 'client_id'
+        self.client_secret = 'client_secret'
+
+    @responses.activate
+    def test_issue_line_token(self):
+        responses.add(
+            responses.POST,
+            self.endpoint,
+            json={
+                "access_token": self.access_token,
+                "expires_in": self.expires_in,
+                "token_type": self.token_type
+            },
+            status=200
+        )
+
+        issue_access_token_response = self.tested.issue_channel_token(
+            self.client_id,
+            self.client_secret
+        )
+
+        request = responses.calls[0].request
+        self.assertEqual('POST', request.method)
+        self.assertEqual(self.endpoint, request.url)
+        self.assertEqual('application/x-www-form-urlencoded', request.headers['content-type'])
+        self.assertEqual(self.access_token, issue_access_token_response.access_token)
+        self.assertEqual(self.expires_in, issue_access_token_response.expires_in)
+        self.assertEqual(self.token_type, issue_access_token_response.token_type)
+
+        encoded_body = parse.parse_qs(request.body)
+        self.assertEqual('client_credentials', encoded_body['grant_type'][0])
+        self.assertEqual(self.client_id, encoded_body['client_id'][0])
+        self.assertEqual(self.client_secret, encoded_body['client_secret'][0])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_issue_link_token.py b/tests/api/test_issue_link_token.py
new file mode 100644
index 0000000..90517e0
--- /dev/null
+++ b/tests/api/test_issue_link_token.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+
+    @responses.activate
+    def test_issue_line_token(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/user_id/linkToken',
+            json={
+                "linkToken": "xxxx",
+            },
+            status=200
+        )
+
+        issue_link_token_response = self.tested.issue_link_token('user_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/user_id/linkToken')
+        self.assertEqual(issue_link_token_response.link_token, 'xxxx')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_revoke_channel_token.py b/tests/api/test_revoke_channel_token.py
new file mode 100644
index 0000000..64543ce
--- /dev/null
+++ b/tests/api/test_revoke_channel_token.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+        self.endpoint = LineBotApi.DEFAULT_API_ENDPOINT + '/v2/oauth/revoke'
+        self.access_token = "W1TeHCgfH2Liwa....."
+
+    @responses.activate
+    def test_issue_line_token(self):
+        responses.add(
+            responses.POST,
+            self.endpoint,
+            status=200
+        )
+
+        self.tested.revoke_channel_token(self.access_token)
+
+        request = responses.calls[0].request
+        self.assertEqual('POST', request.method)
+        self.assertEqual(self.endpoint, request.url)
+        self.assertEqual('application/x-www-form-urlencoded', request.headers['content-type'])
+        self.assertEqual('access_token={}'.format(self.access_token), request.body)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_rich_menu.py b/tests/api/test_rich_menu.py
index 3654a24..383758f 100644
--- a/tests/api/test_rich_menu.py
+++ b/tests/api/test_rich_menu.py
@@ -14,6 +14,7 @@
 
 from __future__ import unicode_literals, absolute_import
 
+import json
 import unittest
 
 import responses
@@ -283,6 +284,110 @@ class TestLineBotApi(unittest.TestCase):
         self.assertEqual(rich_menus[0].areas[0].action.type, 'postback')
         self.assertEqual(rich_menus[0].areas[0].action.data, 'action=buy&itemid=123')
 
+    @responses.activate
+    def test_link_rich_menu_to_users(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/bulk/link',
+            json={}, status=202
+        )
+
+        self.tested.link_rich_menu_to_users(['user_id1', 'user_id2'], 'rich_menu_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/bulk/link'
+        )
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                "richMenuId": "rich_menu_id",
+                "userIds": ["user_id1", "user_id2"],
+            }
+        )
+
+    @responses.activate
+    def test_unlink_rich_menu_to_users(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/bulk/unlink',
+            json={}, status=202
+        )
+
+        self.tested.unlink_rich_menu_from_users(['user_id1', 'user_id2'], 'rich_menu_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/richmenu/bulk/unlink'
+        )
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                "userIds": ["user_id1", "user_id2"],
+            }
+        )
+
+    @responses.activate
+    def test_set_default_rich_menu(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/all/richmenu/rich_menu_id',
+            json={}, status=200
+        )
+
+        self.tested.set_default_rich_menu('rich_menu_id')
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/all/richmenu/rich_menu_id'
+        )
+
+    @responses.activate
+    def test_get_default_rich_menu(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/all/richmenu',
+            json={"richMenuId": "richMenuId"}, status=200
+        )
+
+        result = self.tested.get_default_rich_menu()
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/all/richmenu'
+        )
+        self.assertEqual(result, "richMenuId")
+
+    @responses.activate
+    def test_cancel_default_rich_menu(self):
+        responses.add(
+            responses.DELETE,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/user/all/richmenu',
+            json={}, status=200
+        )
+
+        self.tested.cancel_default_rich_menu()
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'DELETE')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/all/richmenu'
+        )
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/api/test_send_audio_message.py b/tests/api/test_send_audio_message.py
index 9db1e52..24888a3 100644
--- a/tests/api/test_send_audio_message.py
+++ b/tests/api/test_send_audio_message.py
@@ -62,6 +62,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -85,6 +86,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -108,6 +110,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_image_message.py b/tests/api/test_send_image_message.py
index 0ef0115..1b59e3f 100644
--- a/tests/api/test_send_image_message.py
+++ b/tests/api/test_send_image_message.py
@@ -61,6 +61,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -84,6 +85,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -107,6 +109,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_imagemap_message.py b/tests/api/test_send_imagemap_message.py
index b94f315..bbac91d 100644
--- a/tests/api/test_send_imagemap_message.py
+++ b/tests/api/test_send_imagemap_message.py
@@ -24,7 +24,8 @@ from linebot import (
 )
 from linebot.models import (
     ImagemapSendMessage, BaseSize, URIImagemapAction,
-    ImagemapArea, MessageImagemapAction
+    ImagemapArea, MessageImagemapAction,
+    Video, ExternalLink
 )
 
 
@@ -36,6 +37,17 @@ class TestLineBotApi(unittest.TestCase):
             base_url='https://example.com/base',
             alt_text='this is an imagemap',
             base_size=BaseSize(height=1040, width=1040),
+            video=Video(
+                original_content_url='https://example.com/video.mp4',
+                preview_image_url='https://example.com/video_preview.jpg',
+                area=ImagemapArea(
+                    x=0, y=0, width=1040, height=585
+                ),
+                external_link=ExternalLink(
+                    link_uri='https://example.com/see_more.html',
+                    label='See More',
+                ),
+            ),
             actions=[
                 URIImagemapAction(
                     link_uri='https://example.com/',
@@ -60,6 +72,20 @@ class TestLineBotApi(unittest.TestCase):
                 "height": 1040,
                 "width": 1040
             },
+            "video": {
+                "originalContentUrl": "https://example.com/video.mp4",
+                "previewImageUrl": "https://example.com/video_preview.jpg",
+                "area": {
+                    "x": 0,
+                    "y": 0,
+                    "width": 1040,
+                    "height": 585
+                },
+                "externalLink": {
+                    "linkUri": "https://example.com/see_more.html",
+                    "label": "See More"
+                }
+            },
             "actions": [
                 {
                     "type": "uri",
@@ -103,6 +129,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -126,6 +153,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -149,6 +177,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_location_message.py b/tests/api/test_send_location_message.py
index 03fa5c9..c7f5d0c 100644
--- a/tests/api/test_send_location_message.py
+++ b/tests/api/test_send_location_message.py
@@ -65,6 +65,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -88,6 +89,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -111,6 +113,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_sticker_message.py b/tests/api/test_send_sticker_message.py
index 940c59e..323d9aa 100644
--- a/tests/api/test_send_sticker_message.py
+++ b/tests/api/test_send_sticker_message.py
@@ -60,6 +60,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -83,6 +84,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -106,6 +108,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_template_message.py b/tests/api/test_send_template_message.py
index c0e2e81..a6ab7ab 100644
--- a/tests/api/test_send_template_message.py
+++ b/tests/api/test_send_template_message.py
@@ -25,7 +25,7 @@ from linebot import (
 from linebot.models import (
     TemplateSendMessage, ButtonsTemplate,
     PostbackAction, MessageAction,
-    URIAction, DatetimePickerAction,
+    URIAction, AltUri, DatetimePickerAction,
     ConfirmTemplate, CarouselTemplate, CarouselColumn,
     ImageCarouselTemplate, ImageCarouselColumn
 )
@@ -51,7 +51,8 @@ class TestLineBotApi(unittest.TestCase):
                         label='message', text='message text'
                     ),
                     URIAction(
-                        label='uri', uri='http://example.com/'
+                        label='uri', uri='http://example.com/',
+                        alt_uri=AltUri(desktop="http://example.com/desktop")
                     )
                 ]
             )
@@ -81,7 +82,10 @@ class TestLineBotApi(unittest.TestCase):
                     {
                         "type": "uri",
                         "label": "uri",
-                        "uri": "http://example.com/"
+                        "uri": "http://example.com/",
+                        "altUri": {
+                            "desktop": "http://example.com/desktop"
+                        }
                     }
                 ]
             }
@@ -380,6 +384,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.button_message
             }
         )
@@ -403,6 +408,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.button_message
             }
         )
@@ -426,6 +432,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.confirm_message
             }
         )
@@ -450,6 +457,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.confirm_message
             }
         )
@@ -473,6 +481,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -496,6 +505,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -519,6 +529,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -542,6 +553,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
@@ -565,6 +577,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
@@ -588,6 +601,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
diff --git a/tests/api/test_send_text_message.py b/tests/api/test_send_text_message.py
index f64921d..1256324 100644
--- a/tests/api/test_send_text_message.py
+++ b/tests/api/test_send_text_message.py
@@ -54,6 +54,7 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -77,6 +78,7 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -100,6 +102,46 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
+                "messages": self.message
+            }
+        )
+
+    @responses.activate
+    def test_broadcast_text_message(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast',
+            json={}, status=200
+        )
+
+        self.tested.broadcast(self.text_message)
+
+        request = responses.calls[0].request
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast')
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                'notificationDisabled': False,
+                "messages": self.message
+            }
+        )
+
+        # call with notification_disable=True
+        self.tested.broadcast(self.text_message, notification_disabled=True)
+
+        request = responses.calls[1].request
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast')
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                'notificationDisabled': True,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_text_message_with_quick_reply.py b/tests/api/test_send_text_message_with_quick_reply.py
index 1ff8f5e..ae50260 100644
--- a/tests/api/test_send_text_message_with_quick_reply.py
+++ b/tests/api/test_send_text_message_with_quick_reply.py
@@ -81,6 +81,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": [
                     {
                         "type": "text",
diff --git a/tests/api/test_send_video_message.py b/tests/api/test_send_video_message.py
index 217b583..e3c6e02 100644
--- a/tests/api/test_send_video_message.py
+++ b/tests/api/test_send_video_message.py
@@ -61,6 +61,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -84,7 +85,8 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
-                "messages": self.message
+                "messages": self.message,
+                'notificationDisabled': False,
             }
         )
 
@@ -107,7 +109,8 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
-                "messages": self.message
+                "messages": self.message,
+                "notificationDisabled": False,
             }
         )
 
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index f26f1c5..57cf54c 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -24,10 +24,11 @@ from linebot import (
 from linebot.models import (
     MessageEvent, FollowEvent, UnfollowEvent, JoinEvent,
     LeaveEvent, PostbackEvent, BeaconEvent, AccountLinkEvent,
+    MemberJoinedEvent, MemberLeftEvent, ThingsEvent,
     TextMessage, ImageMessage, VideoMessage, AudioMessage,
     LocationMessage, StickerMessage, FileMessage,
-    SourceUser, SourceRoom, SourceGroup
-)
+    SourceUser, SourceRoom, SourceGroup,
+    DeviceLink, DeviceUnlink, ScenarioResult, ActionResult)
 
 
 class TestSignatureValidator(unittest.TestCase):
@@ -67,7 +68,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[0].source, SourceUser)
         self.assertEqual(events[0].source.type, 'user')
         self.assertEqual(events[0].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[0].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[0].message, TextMessage)
         self.assertEqual(events[0].message.id, '325708')
         self.assertEqual(events[0].message.type, 'text')
@@ -82,10 +82,14 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[1].source.type, 'room')
         self.assertEqual(events[1].source.room_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertEqual(events[1].source.user_id, None)
-        self.assertEqual(events[1].source.sender_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertIsInstance(events[1].message, ImageMessage)
         self.assertEqual(events[1].message.id, '325708')
         self.assertEqual(events[1].message.type, 'image')
+        self.assertEqual(events[1].message.content_provider.type, 'external')
+        self.assertEqual(events[1].message.content_provider.original_content_url,
+                         "https://example.com")
+        self.assertEqual(events[1].message.content_provider.preview_image_url,
+                         "https://example.com")
 
         # MessageEvent, SourceUser, VideoMessage
         self.assertIsInstance(events[2], MessageEvent)
@@ -95,10 +99,15 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[2].source, SourceUser)
         self.assertEqual(events[2].source.type, 'user')
         self.assertEqual(events[2].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[2].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[2].message, VideoMessage)
         self.assertEqual(events[2].message.id, '325708')
         self.assertEqual(events[2].message.type, 'video')
+        self.assertEqual(events[2].message.duration, 60000)
+        self.assertEqual(events[2].message.content_provider.type, 'external')
+        self.assertEqual(events[2].message.content_provider.original_content_url,
+                         "https://example.com")
+        self.assertEqual(events[2].message.content_provider.preview_image_url,
+                         "https://example.com")
 
         # MessageEvent, SourceUser, AudioMessage
         self.assertIsInstance(events[3], MessageEvent)
@@ -108,10 +117,13 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[3].source, SourceUser)
         self.assertEqual(events[3].source.type, 'user')
         self.assertEqual(events[3].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[3].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[3].message, AudioMessage)
         self.assertEqual(events[3].message.id, '325708')
         self.assertEqual(events[3].message.type, 'audio')
+        self.assertEqual(events[3].message.duration, 60000)
+        self.assertEqual(events[3].message.content_provider.type, 'external')
+        self.assertEqual(events[3].message.content_provider.original_content_url,
+                         "https://example.com")
 
         # MessageEvent, SourceUser, LocationMessage
         self.assertIsInstance(events[4], MessageEvent)
@@ -121,7 +133,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[4].source, SourceUser)
         self.assertEqual(events[4].source.type, 'user')
         self.assertEqual(events[4].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[4].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[4].message, LocationMessage)
         self.assertEqual(events[4].message.id, '325708')
         self.assertEqual(events[4].message.type, 'location')
@@ -138,7 +149,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[5].source, SourceUser)
         self.assertEqual(events[5].source.type, 'user')
         self.assertEqual(events[5].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[5].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertIsInstance(events[5].message, StickerMessage)
         self.assertEqual(events[5].message.id, '325708')
         self.assertEqual(events[5].message.type, 'sticker')
@@ -153,7 +163,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[6].source, SourceUser)
         self.assertEqual(events[6].source.type, 'user')
         self.assertEqual(events[6].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[6].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
 
         # UnfollowEvent, SourceUser
         self.assertIsInstance(events[7], UnfollowEvent)
@@ -162,7 +171,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[7].source, SourceUser)
         self.assertEqual(events[7].source.type, 'user')
         self.assertEqual(events[7].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[7].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
 
         # JoinEvent, SourceGroup
         self.assertIsInstance(events[8], JoinEvent)
@@ -173,7 +181,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[8].source.type, 'group')
         self.assertEqual(events[8].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[8].source.user_id, None)
-        self.assertEqual(events[8].source.sender_id, 'Ca56f94637cc4347f90a25382909b24b9')
 
         # LeaveEvent, SourceGroup
         self.assertIsInstance(events[9], LeaveEvent)
@@ -183,7 +190,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[9].source.type, 'group')
         self.assertEqual(events[9].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[9].source.user_id, None)
-        self.assertEqual(events[9].source.sender_id, 'Ca56f94637cc4347f90a25382909b24b9')
 
         # PostbackEvent, SourceUser
         self.assertIsInstance(events[10], PostbackEvent)
@@ -193,7 +199,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[10].source, SourceUser)
         self.assertEqual(events[10].source.type, 'user')
         self.assertEqual(events[10].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[10].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[10].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[10].postback.params, None)
 
@@ -205,7 +210,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[11].source, SourceUser)
         self.assertEqual(events[11].source.type, 'user')
         self.assertEqual(events[11].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[11].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[11].beacon.hwid, 'd41d8cd98f')
         self.assertEqual(events[11].beacon.type, 'enter')
         self.assertEqual(events[11].beacon.dm, None)
@@ -219,7 +223,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[12].source, SourceUser)
         self.assertEqual(events[12].source.type, 'user')
         self.assertEqual(events[12].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[12].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[12].beacon.hwid, 'd41d8cd98f')
         self.assertEqual(events[12].beacon.type, 'enter')
         self.assertEqual(events[12].beacon.dm, '1234567890abcdef')
@@ -233,7 +236,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[13].source, SourceUser)
         self.assertEqual(events[13].source.type, 'user')
         self.assertEqual(events[13].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[13].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[13].link.result, 'ok')
         self.assertEqual(events[13].link.nonce, 'Vb771wDYtXuammLszK6h9A')
 
@@ -246,7 +248,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[14].source.type, 'group')
         self.assertEqual(events[14].source.group_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertEqual(events[14].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[14].source.sender_id, 'Ca56f94637cc4347f90a25382909b24b9')
         self.assertIsInstance(events[14].message, TextMessage)
         self.assertEqual(events[14].message.id, '325708')
         self.assertEqual(events[14].message.type, 'text')
@@ -261,7 +262,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[15].source.type, 'room')
         self.assertEqual(events[15].source.room_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertEqual(events[15].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[15].source.sender_id, 'Ra8dbf4673c4c812cd491258042226c99')
         self.assertIsInstance(events[15].message, TextMessage)
         self.assertEqual(events[15].message.id, '325708')
         self.assertEqual(events[15].message.type, 'text')
@@ -275,7 +275,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[16].source, SourceUser)
         self.assertEqual(events[16].source.type, 'user')
         self.assertEqual(events[16].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[16].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[16].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[16].postback.params['date'], '2013-04-01')
 
@@ -287,7 +286,6 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[17].source, SourceUser)
         self.assertEqual(events[17].source.type, 'user')
         self.assertEqual(events[17].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[17].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[17].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[17].postback.params['time'], '10:00')
 
@@ -299,71 +297,156 @@ class TestWebhookParser(unittest.TestCase):
         self.assertIsInstance(events[18].source, SourceUser)
         self.assertEqual(events[18].source.type, 'user')
         self.assertEqual(events[18].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[18].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
         self.assertEqual(events[18].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[18].postback.params['datetime'], '2013-04-01T10:00')
 
-        # MessageEvent, SourceUser, FileMessage
-        self.assertIsInstance(events[19], MessageEvent)
+        # ThingsEvent, SourceUser, link
+        self.assertIsInstance(events[19], ThingsEvent)
         self.assertEqual(events[19].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
-        self.assertEqual(events[19].type, 'message')
+        self.assertEqual(events[19].type, 'things')
         self.assertEqual(events[19].timestamp, 1462629479859)
         self.assertIsInstance(events[19].source, SourceUser)
         self.assertEqual(events[19].source.type, 'user')
         self.assertEqual(events[19].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[19].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertIsInstance(events[19].message, FileMessage)
-        self.assertEqual(events[19].message.id, '325708')
-        self.assertEqual(events[19].message.type, 'file')
-        self.assertEqual(events[19].message.file_name, "file.txt")
-        self.assertEqual(events[19].message.file_size, 2138)
+        self.assertIsInstance(events[19].things, DeviceLink)
+        self.assertEqual(events[19].things.type, 'link')
+        self.assertEqual(events[19].things.device_id, 't2c449c9d1')
+
+        # MemberJoinedEvent
+        self.assertIsInstance(events[20], MemberJoinedEvent)
+        self.assertEqual(events[20].reply_token, '0f3779fba3b349968c5d07db31eabf65')
+        self.assertEqual(events[20].type, 'memberJoined')
+        self.assertEqual(events[20].timestamp, 1462629479859)
+        self.assertIsInstance(events[20].source, SourceGroup)
+        self.assertEqual(events[20].source.type, 'group')
+        self.assertEqual(events[20].source.group_id, 'C4af4980629...')
+        self.assertEqual(len(events[20].joined.members), 2)
+        self.assertIsInstance(events[20].joined.members[0], SourceUser)
+        self.assertEqual(events[20].joined.members[0].user_id, 'U4af4980629...')
+        self.assertEqual(events[20].joined.members[1].user_id, 'U91eeaf62d9...')
+
+        # MemberLeftEvent
+        self.assertIsInstance(events[21], MemberLeftEvent)
+        self.assertEqual(events[21].type, 'memberLeft')
+        self.assertEqual(events[21].timestamp, 1462629479960)
+        self.assertIsInstance(events[21].source, SourceGroup)
+        self.assertEqual(events[21].source.type, 'group')
+        self.assertEqual(events[21].source.group_id, 'C4af4980629...')
+        self.assertEqual(len(events[21].left.members), 2)
+        self.assertIsInstance(events[21].left.members[0], SourceUser)
+        self.assertEqual(events[21].left.members[0].user_id, 'U4af4980629...')
+        self.assertEqual(events[21].left.members[1].user_id, 'U91eeaf62d9...')
+
+        # ThingsEvent, SourceUser, unlink
+        self.assertIsInstance(events[22], ThingsEvent)
+        self.assertEqual(events[22].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[22].type, 'things')
+        self.assertEqual(events[22].timestamp, 1462629479859)
+        self.assertIsInstance(events[22].source, SourceUser)
+        self.assertEqual(events[22].source.type, 'user')
+        self.assertEqual(events[22].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertIsInstance(events[22].things, DeviceUnlink)
+        self.assertEqual(events[22].things.type, 'unlink')
+        self.assertEqual(events[22].things.device_id, 't2c449c9d1')
+
+        # MessageEvent, SourceUser, FileMessage
+        self.assertIsInstance(events[23], MessageEvent)
+        self.assertEqual(events[23].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[23].type, 'message')
+        self.assertEqual(events[23].timestamp, 1462629479859)
+        self.assertIsInstance(events[23].source, SourceUser)
+        self.assertEqual(events[23].source.type, 'user')
+        self.assertEqual(events[23].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[23].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertIsInstance(events[23].message, FileMessage)
+        self.assertEqual(events[23].message.id, '325708')
+        self.assertEqual(events[23].message.type, 'file')
+        self.assertEqual(events[23].message.file_name, "file.txt")
+        self.assertEqual(events[23].message.file_size, 2138)
+
+        # ThingsEvent, SourceUser, scenarioResult
+        self.assertIsInstance(events[24], ThingsEvent)
+        self.assertEqual(events[24].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[24].type, 'things')
+        self.assertEqual(events[24].timestamp, 1547817848122)
+        self.assertIsInstance(events[24].source, SourceUser)
+        self.assertEqual(events[24].source.type, 'user')
+        self.assertEqual(events[24].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertIsInstance(events[24].things, ScenarioResult)
+        self.assertEqual(events[24].things.type, 'scenarioResult')
+        self.assertEqual(events[24].things.device_id, 't2c449c9d1')
+        self.assertEqual(events[24].things.result.scenario_id, 'XXX')
+        self.assertEqual(events[24].things.result.revision, 2)
+        self.assertEqual(events[24].things.result.start_time, 1547817845950)
+        self.assertEqual(events[24].things.result.end_time, 1547817845952)
+        self.assertEqual(events[24].things.result.result_code, 'success')
+        self.assertEqual(events[24].things.result.ble_notification_payload, 'AQ==')
+        self.assertIsInstance(events[24].things.result.action_results[0], ActionResult)
+        self.assertEqual(events[24].things.result.action_results[0].type, 'binary')
+        self.assertEqual(events[24].things.result.action_results[0].data, '/w==')
+        self.assertIsInstance(events[24].things.result.action_results[1], ActionResult)
+        self.assertEqual(events[24].things.result.action_results[1].type, 'void')
 
 
 class TestWebhookHandler(unittest.TestCase):
     def setUp(self):
         self.handler = WebhookHandler('channel_secret')
-        self.calls = []
 
         @self.handler.add(MessageEvent, message=TextMessage)
-        def message_text(event):
-            self.calls.append('1 ' + event.type + '_' + event.message.type)
+        def message_text(event, destination):
+            self.assertEqual('message', event.type)
+            self.assertEqual('text', event.message.type)
+            self.assertEqual('U123', destination)
 
-        @self.handler.add(
-            MessageEvent, message=(ImageMessage, VideoMessage, AudioMessage))
+        @self.handler.add(MessageEvent,
+                          message=(ImageMessage, VideoMessage, AudioMessage))
         def message_content(event):
-            self.calls.append('2 ' + event.type + '_' + event.message.type)
+            self.assertEqual('message', event.type)
+            self.assertIn(
+                event.message.type,
+                ['image', 'video', 'audio']
+            )
 
         @self.handler.add(MessageEvent, message=StickerMessage)
         def message_sticker(event):
-            self.calls.append('3 ' + event.type + '_' + event.message.type)
+            self.assertEqual('message', event.type)
+            self.assertEqual('sticker', event.message.type)
 
         @self.handler.add(MessageEvent)
         def message(event):
-            self.calls.append(event.type + '_' + event.message.type)
+            self.assertEqual('message', event.type)
+            self.assertNotIn(
+                event.message.type,
+                ['text', 'image', 'video', 'audio', 'sticker']
+            )
 
         @self.handler.add(FollowEvent)
-        def follow(event):
-            self.calls.append('4 ' + event.type)
+        def follow(event, destination):
+            self.assertEqual('follow', event.type)
+            self.assertEqual('U123', destination)
 
         @self.handler.add(JoinEvent)
         def join(event):
-            self.calls.append('5 ' + event.type)
+            self.assertEqual('join', event.type)
 
         @self.handler.add(PostbackEvent)
         def postback(event):
-            self.calls.append('6 ' + event.type)
+            self.assertEqual('postback', event.type)
 
         @self.handler.add(BeaconEvent)
         def beacon(event):
-            self.calls.append('7 ' + event.type)
+            self.assertEqual('beacon', event.type)
 
         @self.handler.add(AccountLinkEvent)
         def account_link(event):
-            self.calls.append('8 ' + event.type)
+            self.assertEqual('accountLink', event.type)
 
         @self.handler.default()
         def default(event):
-            self.calls.append('default ' + event.type)
+            self.assertNotIn(
+                event.type,
+                ['message', 'follow', 'join', 'postback', 'beacon', 'accountLink']
+            )
 
     def test_handler(self):
         file_dir = os.path.dirname(__file__)
@@ -376,26 +459,6 @@ class TestWebhookHandler(unittest.TestCase):
 
         self.handler.handle(body, 'signature')
 
-        self.assertEqual(self.calls[0], '1 message_text')
-        self.assertEqual(self.calls[1], '2 message_image')
-        self.assertEqual(self.calls[2], '2 message_video')
-        self.assertEqual(self.calls[3], '2 message_audio')
-        self.assertEqual(self.calls[4], 'message_location')
-        self.assertEqual(self.calls[5], '3 message_sticker')
-        self.assertEqual(self.calls[6], '4 follow')
-        self.assertEqual(self.calls[7], 'default unfollow')
-        self.assertEqual(self.calls[8], '5 join')
-        self.assertEqual(self.calls[9], 'default leave')
-        self.assertEqual(self.calls[10], '6 postback')
-        self.assertEqual(self.calls[11], '7 beacon')
-        self.assertEqual(self.calls[12], '7 beacon')
-        self.assertEqual(self.calls[13], '8 accountLink')
-        self.assertEqual(self.calls[14], '1 message_text')
-        self.assertEqual(self.calls[15], '1 message_text')
-        self.assertEqual(self.calls[16], '6 postback')
-        self.assertEqual(self.calls[17], '6 postback')
-        self.assertEqual(self.calls[18], '6 postback')
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/text/webhook.json b/tests/text/webhook.json
index 01d8bad..5899f8a 100644
--- a/tests/text/webhook.json
+++ b/tests/text/webhook.json
@@ -1,4 +1,5 @@
 {
+  "destination": "U123",
   "events": [
     {
       "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
@@ -24,7 +25,12 @@
       },
       "message": {
         "id": "325708",
-        "type": "image"
+        "type": "image",
+        "contentProvider": {
+          "type": "external",
+          "originalContentUrl": "https://example.com",
+          "previewImageUrl": "https://example.com"
+        }
       }
     },
     {
@@ -37,7 +43,13 @@
       },
       "message": {
         "id": "325708",
-        "type": "video"
+        "type": "video",
+        "duration": 60000,
+        "contentProvider": {
+          "type": "external",
+          "originalContentUrl": "https://example.com",
+          "previewImageUrl": "https://example.com"
+        }
       }
     },
     {
@@ -50,7 +62,12 @@
       },
       "message": {
         "id": "325708",
-        "type": "audio"
+        "type": "audio",
+        "duration": 60000,
+        "contentProvider": {
+          "type": "external",
+          "originalContentUrl": "https://example.com"
+        }
       }
     },
     {
@@ -246,6 +263,73 @@
         }
       }
     },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "things",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "things": {
+        "deviceId": "t2c449c9d1",
+        "type": "link"
+      }
+    },
+    {
+      "replyToken": "0f3779fba3b349968c5d07db31eabf65",
+      "type": "memberJoined",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "group",
+        "groupId": "C4af4980629..."
+      },
+      "joined": {
+        "members": [
+          {
+            "type": "user",
+            "userId": "U4af4980629..."
+          },
+          {
+            "type": "user",
+            "userId": "U91eeaf62d9..."
+          }
+        ]
+      }
+    },
+    {
+      "type": "memberLeft",
+      "timestamp": 1462629479960,
+      "source": {
+        "type": "group",
+        "groupId": "C4af4980629..."
+      },
+      "left": {
+        "members": [
+          {
+            "type": "user",
+            "userId": "U4af4980629..."
+          },
+          {
+            "type": "user",
+            "userId": "U91eeaf62d9..."
+          }
+        ]
+      }
+    },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "things",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "things": {
+        "deviceId": "t2c449c9d1",
+        "type": "unlink"
+      }
+    },
     {
       "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
       "type": "message",
@@ -260,6 +344,36 @@
         "fileName": "file.txt",
         "fileSize": 2138
       }
+    },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "things",
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "timestamp": 1547817848122,
+      "things": {
+        "type": "scenarioResult",
+        "deviceId": "t2c449c9d1",
+        "result": {
+          "scenarioId": "XXX",
+          "revision": 2,
+          "startTime": 1547817845950,
+          "endTime": 1547817845952,
+          "resultCode": "success",
+          "bleNotificationPayload": "AQ==",
+          "actionResults": [
+            {
+              "type": "binary",
+              "data": "/w=="
+            },
+            {
+              "type": "void"
+            }
+          ]
+        }
+      }
     }
   ]
 }