Application Service API
The Matrix client-server API and server-server APIs provide the means to
implement a consistent self-contained federated messaging fabric.
However, they provide limited means of implementing custom server-side
behaviour in Matrix (e.g. gateways, filters, extensible hooks etc). The
Application Service API (AS API) defines a standard API to allow such
extensible functionality to be implemented irrespective of the
underlying homeserver implementation.
Application Services
Application services are passive and can only observe events from
homeserver. They can inject events into rooms they are participating in.
They cannot prevent events from being sent, nor can they modify the
content of the event being sent. In order to observe events from a
homeserver, the homeserver needs to be configured to pass certain types
of traffic to the application service. This is achieved by manually
configuring the homeserver with information about the application
service.
Registration
Previously, application services could register with a homeserver via
HTTP APIs. This was removed as it was seen as a security risk. A
compromised application service could re-register for a global *
regex
and sniff all traffic on the homeserver. To protect against this,
application services now have to register via configuration files which
are linked to the homeserver configuration file. The addition of
configuration files allows homeserver admins to sanity check the
registration for suspicious regex strings.
Application services register “namespaces” of user IDs, room aliases and
room IDs. These namespaces are represented as regular expressions. An
application service is said to be “interested” in a given event if one
of the IDs in the event match the regular expression provided by the
application service, such as the room having an alias or ID in the
relevant namespaces. Similarly, the application service is said to be
interested in a given event if one of the application service’s
namespaced users is the target of the event, or is a joined member of
the room where the event occurred.
An application service can also state whether they should be the only
ones who can manage a specified namespace. This is referred to as an
“exclusive” namespace. An exclusive namespace prevents humans and other
application services from creating/deleting entities in that namespace.
Typically, exclusive namespaces are used when the rooms represent real
rooms on another service (e.g. IRC). Non-exclusive namespaces are used
when the application service is merely augmenting the room itself (e.g.
providing logging or searching facilities). Namespaces are represented
by POSIX extended regular expressions and look like:
users:
- exclusive: true
regex: "@_irc_bridge_.*"
Application services may define the following namespaces (with none
being explicitly required):
Name |
Description |
users |
Events which are sent from certain users. |
aliases |
Events which are sent in rooms with certain room aliases. |
rooms |
Events which are sent in rooms with certain room IDs. |
Each individual namespace MUST declare the following fields:
Name |
Description |
exclusive |
Required A true or false value stating whether this application service has exclusive access to events within this namespace. |
regex |
Required A regular expression defining which values this namespace includes. |
Exclusive user and alias namespaces should begin with an underscore
after the sigil to avoid collisions with other users on the homeserver.
Application services should additionally attempt to identify the service
they represent in the reserved namespace. For example, @_irc_.*
would
be a good namespace to register for an application service which deals
with IRC.
The registration is represented by a series of key-value pairs, which
this specification will present as YAML. See below for the possible
options along with their explanation:
Name |
Description |
id |
Required A unique, user-defined ID of the application service which will never change. |
url |
Required The URL for the application service. May include a path after the domain name. Optionally set to null if no traffic is required. |
as_token |
Required A unique token for application services to use to authenticate requests to Homeservers. |
hs_token |
Required A unique token for Homeservers to use to authenticate requests to application services. |
sender_localpart |
Required The localpart of the user associated with the application service. |
namespaces |
Required A list of users , aliases and rooms namespaces that the application service controls. |
rate_limited |
Whether requests from masqueraded users are rate-limited. The sender is excluded. |
protocols |
The external protocols which the application service provides (e.g. IRC). |
An example registration file for an IRC-bridging application service is
below:
id: "IRC Bridge"
url: "http://127.0.0.1:1234"
as_token: "30c05ae90a248a4188e620216fa72e349803310ec83e2a77b34fe90be6081f46"
hs_token: "312df522183efd404ec1cd22d2ffa4bbc76a8c1ccf541dd692eef281356bb74e"
sender_localpart: "_irc_bot" # Will result in @_irc_bot:example.org
namespaces:
users:
- exclusive: true
regex: "@_irc_bridge_.*"
aliases:
- exclusive: false
regex: "#_irc_bridge_.*"
rooms: []
If the homeserver in question has multiple application services, each
as_token
and id
MUST be unique per application service as these are
used to identify the application service. The homeserver MUST enforce
this.
Homeserver -> Application Service API
Authorization
Homeservers MUST include a query parameter named access_token
containing the hs_token
from the application service’s registration
when making requests to the application service. Application services
MUST verify the provided access_token
matches their known hs_token
,
failing the request with an M_FORBIDDEN
error if it does not match.
Legacy routes
Previous drafts of the application service specification had a mix of
endpoints that have been used in the wild for a significant amount of
time. The application service specification now defines a version on all
endpoints to be more compatible with the rest of the Matrix
specification and the future.
Homeservers should attempt to use the specified endpoints first when
communicating with application services. However, if the application
service receives an HTTP status code that does not indicate success
(i.e.: 404, 500, 501, etc) then the homeserver should fall back to the
older endpoints for the application service.
The older endpoints have the exact same request body and response
format, they just belong at a different path. The equivalent path for
each is as follows:
/_matrix/app/v1/transactions/{txnId}
should fall back to
/transactions/{txnId}
/_matrix/app/v1/users/{userId}
should fall back to
/users/{userId}
/_matrix/app/v1/rooms/{roomAlias}
should fall back to
/rooms/{roomAlias}
/_matrix/app/v1/thirdparty/protocol/{protocol}
should fall back to
/_matrix/app/unstable/thirdparty/protocol/{protocol}
/_matrix/app/v1/thirdparty/user/{user}
should fall back to
/_matrix/app/unstable/thirdparty/user/{user}
/_matrix/app/v1/thirdparty/location/{location}
should fall back to
/_matrix/app/unstable/thirdparty/location/{location}
/_matrix/app/v1/thirdparty/user
should fall back to
/_matrix/app/unstable/thirdparty/user
/_matrix/app/v1/thirdparty/location
should fall back to
/_matrix/app/unstable/thirdparty/location
Homeservers should periodically try again for the newer endpoints
because the application service may have been updated.
Pushing events
The application service API provides a transaction API for sending a
list of events. Each list of events includes a transaction ID, which
works as follows:
Typical
HS ---> AS : Homeserver sends events with transaction ID T.
<--- : Application Service sends back 200 OK.
AS ACK Lost
HS ---> AS : Homeserver sends events with transaction ID T.
<-/- : AS 200 OK is lost.
HS ---> AS : Homeserver retries with the same transaction ID of T.
<--- : Application Service sends back 200 OK. If the AS had processed these
events already, it can NO-OP this request (and it knows if it is the
same events based on the transaction ID).
The events sent to the application service should be linearised, as if
they were from the event stream. The homeserver MUST maintain a queue of
transactions to send to the application service. If the application
service cannot be reached, the homeserver SHOULD backoff exponentially
until the application service is reachable again. As application
services cannot modify the events in any way, these requests can be
made without blocking other aspects of the homeserver. Homeservers MUST
NOT alter (e.g. add more) events they were going to send within that
transaction ID on retries, as the application service may have already
processed the events.
PUT
/_matrix/app/v1/transactions/{txnId}
This API is called by the homeserver when it wants to push an event
(or batch of events) to the application service.
Note that the application service should distinguish state events
from message events via the presence of a state_key
, rather than
via the event type.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
path parameters
Name |
Type |
Description |
txnId |
string |
Required: The transaction ID for this set of events. Homeservers generate
these IDs and they are used to ensure idempotency of requests. |
Request body
Name |
Type |
Description |
events |
[Event] |
Required: A list of events, formatted as per the Client-Server API. |
Request body example
{
"events": [
{
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@alice:example.org",
"type": "m.room.member",
"unsigned": {
"age": 1234
}
},
{
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "<b>This is an example text message</b>",
"msgtype": "m.text"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1234
}
}
]
}
Responses
Status |
Description |
200 |
The transaction was processed successfully. |
Querying
The application service API includes two querying APIs: for room aliases
and for user IDs. The application service SHOULD create the queried
entity if it desires. During this process, the application service is
blocking the homeserver until the entity is created and configured. If
the homeserver does not receive a response to this request, the
homeserver should retry several times before timing out. This should
result in an HTTP status 408 “Request Timeout” on the client which
initiated this request (e.g. to join a room alias).
Blocking the homeserver and expecting the application service to create
the entity using the client-server API is simpler and more flexible than
alternative methods such as returning an initial sync style JSON blob
and get the HS to provision the room/user. This also meant that there
didn’t need to be a “backchannel” to inform the application service
about information about the entity such as room ID to room alias
mappings.
GET
/_matrix/app/v1/users/{userId}
This endpoint is invoked by the homeserver on an application service to query
the existence of a given user ID. The homeserver will only query user IDs
inside the application service’s users
namespace. The homeserver will
send this request when it receives an event for an unknown user ID in
the application service’s namespace, such as a room invite.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
path parameters
Name |
Type |
Description |
userId |
string |
Required: The user ID being queried. |
Responses
Status |
Description |
200 |
The application service indicates that this user exists. The application
service MUST create the user using the client-server API. |
401 |
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response. |
403 |
The credentials supplied by the homeserver were rejected. |
404 |
The application service indicates that this user does not exist.
Optional error information can be included in the body of this response. |
401 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
403 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_FORBIDDEN"
}
404 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
GET
/_matrix/app/v1/rooms/{roomAlias}
This endpoint is invoked by the homeserver on an application service to query
the existence of a given room alias. The homeserver will only query room
aliases inside the application service’s aliases
namespace. The
homeserver will send this request when it receives a request to join a
room alias within the application service’s namespace.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
path parameters
Name |
Type |
Description |
roomAlias |
string |
Required: The room alias being queried. |
Responses
Status |
Description |
200 |
The application service indicates that this room alias exists. The
application service MUST have created a room and associated it with
the queried room alias using the client-server API. Additional
information about the room such as its name and topic can be set
before responding. |
401 |
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response. |
403 |
The credentials supplied by the homeserver were rejected. |
404 |
The application service indicates that this room alias does not exist.
Optional error information can be included in the body of this response. |
401 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
403 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_FORBIDDEN"
}
404 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
Third party networks
Application services may declare which protocols they support via their
registration configuration for the homeserver. These networks are
generally for third party services such as IRC that the application
service is managing. Application services may populate a Matrix room
directory for their registered protocols, as defined in the
Client-Server API Extensions.
Each protocol may have several “locations” (also known as “third party
locations” or “3PLs”). A location within a protocol is a place in the
third party network, such as an IRC channel. Users of the third party
network may also be represented by the application service.
Locations and users can be searched by fields defined by the application
service, such as by display name or other attribute. When clients
request the homeserver to search in a particular “network” (protocol),
the search fields will be passed along to the application service for
filtering.
GET
/_matrix/app/v1/thirdparty/location
Retrieve an array of third party network locations from a Matrix room
alias.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
query parameters
Name |
Type |
Description |
alias |
string |
The Matrix room alias to look up. |
Responses
Status |
Description |
200 |
All found third party locations. |
401 |
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response. |
403 |
The credentials supplied by the homeserver were rejected. |
404 |
No mappings were found with the given parameters. |
200 response
Array of Location
.
Location
Name |
Type |
Description |
alias |
string |
Required: An alias for a matrix room. |
fields |
object |
Required: Information used to identify this third party location. |
protocol |
string |
Required: The protocol ID that the third party location is a part of. |
Specification error: Example invalid or not present
401 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
403 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_FORBIDDEN"
}
404 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
GET
/_matrix/app/v1/thirdparty/location/{protocol}
Retrieve a list of Matrix portal rooms that lead to the matched third party location.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
path parameters
Name |
Type |
Description |
protocol |
string |
Required: The protocol ID. |
query parameters
Name |
Type |
Description |
fields... |
string |
One or more custom fields that are passed to the application
service to help identify the third party location. |
Responses
Status |
Description |
200 |
At least one portal room was found. |
401 |
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response. |
403 |
The credentials supplied by the homeserver were rejected. |
404 |
No mappings were found with the given parameters. |
200 response
Array of Location
.
Location
Name |
Type |
Description |
alias |
string |
Required: An alias for a matrix room. |
fields |
object |
Required: Information used to identify this third party location. |
protocol |
string |
Required: The protocol ID that the third party location is a part of. |
Specification error: Example invalid or not present
401 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
403 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_FORBIDDEN"
}
404 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
GET
/_matrix/app/v1/thirdparty/protocol/{protocol}
This API is called by the homeserver when it wants to present clients
with specific information about the various third party networks that
an application service supports.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
path parameters
Name |
Type |
Description |
protocol |
string |
Required: The protocol ID. |
Responses
Status |
Description |
200 |
The protocol was found and metadata returned. |
401 |
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response. |
403 |
The credentials supplied by the homeserver were rejected. |
404 |
No protocol was found with the given path. |
401 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
403 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_FORBIDDEN"
}
404 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
GET
/_matrix/app/v1/thirdparty/user
Retrieve an array of third party users from a Matrix User ID.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
query parameters
Name |
Type |
Description |
userid |
string |
The Matrix User ID to look up. |
Responses
Status |
Description |
200 |
An array of third party users. |
401 |
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response. |
403 |
The credentials supplied by the homeserver were rejected. |
404 |
No mappings were found with the given parameters. |
200 response
Array of User
.
User
Name |
Type |
Description |
fields |
object |
Required: Information used to identify this third party location. |
protocol |
string |
Required: The protocol ID that the third party location is a part of. |
userid |
string |
Required: A Matrix User ID represting a third party user. |
Specification error: Example invalid or not present
401 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
403 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
404 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
GET
/_matrix/app/v1/thirdparty/user/{protocol}
This API is called by the homeserver in order to retrieve a Matrix
User ID linked to a user on the third party network, given a set of
user parameters.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
path parameters
Name |
Type |
Description |
protocol |
string |
Required: The protocol ID. |
query parameters
Name |
Type |
Description |
fields... |
string |
One or more custom fields that are passed to the application
service to help identify the user. |
Responses
Status |
Description |
200 |
The Matrix User IDs found with the given parameters. |
401 |
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response. |
403 |
The credentials supplied by the homeserver were rejected. |
404 |
No users were found with the given parameters. |
200 response
Array of User
.
User
Name |
Type |
Description |
fields |
object |
Required: Information used to identify this third party location. |
protocol |
string |
Required: The protocol ID that the third party location is a part of. |
userid |
string |
Required: A Matrix User ID represting a third party user. |
Specification error: Example invalid or not present
401 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
403 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_FORBIDDEN"
}
404 response
Name |
Type |
Description |
errcode |
string |
Required: An error code. |
error |
string |
A human-readable error message. |
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
Client-Server API Extensions
Application services can use a more powerful version of the
client-server API by identifying itself as an application service to the
homeserver.
Endpoints defined in this section MUST be supported by homeservers in
the client-server API as accessible only by application services.
Identity assertion
The client-server API infers the user ID from the access_token
provided in every request. To avoid the application service from having
to keep track of each user’s access token, the application service
should identify itself to the Client-Server API by providing its
as_token
for the access_token
alongside the user the application
service would like to masquerade as.
Inputs:
- Application service token (
as_token
)
- User ID in the AS namespace to act as.
Notes:
- This applies to all aspects of the Client-Server API, except for
Account Management.
- The
as_token
is inserted into access_token
which is usually
where the client token is, such as via the query string or
Authorization
header. This is done on purpose to allow application
services to reuse client SDKs.
- The
access_token
should be supplied through the Authorization
header where possible to prevent the token appearing in HTTP request
logs by accident.
The application service may specify the virtual user to act as through
use of a user_id
query string parameter on the request. The user
specified in the query string must be covered by one of the application
service’s user
namespaces. If the parameter is missing, the homeserver
is to assume the application service intends to act as the user implied
by the sender_localpart
property of the registration.
An example request would be:
GET /_matrix/client/%CLIENT_MAJOR_VERSION%/account/whoami?user_id=@_irc_user:example.org
Authorization: Bearer YourApplicationServiceTokenHere
Timestamp massaging
Previous drafts of the Application Service API permitted application
services to alter the timestamp of their sent events by providing a ts
query parameter when sending an event. This API has been excluded from
the first release due to design concerns, however some servers may still
support the feature. Please visit issue
#1585 for more
information.
Server admin style permissions
The homeserver needs to give the application service full control over
its namespace, both for users and for room aliases. This means that the
AS should be able to create/edit/delete any room alias in its namespace,
as well as create/delete any user in its namespace. No additional API
changes need to be made in order for control of room aliases to be
granted to the AS. Creation of users needs API changes in order to:
- Work around captchas.
- Have a ‘passwordless’ user.
This involves bypassing the registration flows entirely. This is
achieved by including the as_token
on a /register
request, along
with a login type of m.login.application_service
to set the desired
user ID without a password.
POST /_matrix/client/%CLIENT_MAJOR_VERSION%/register
Authorization: Bearer YourApplicationServiceTokenHere
Content:
{
type: "m.login.application_service",
username: "_irc_example"
}
Application services which attempt to create users or aliases outside
of their defined namespaces will receive an error code M_EXCLUSIVE
.
Similarly, normal users who attempt to create users or aliases inside
an application service-defined namespace will receive the same
M_EXCLUSIVE
error code, but only if the application service has
defined the namespace as exclusive
.
Using /sync
and /events
Application services wishing to use /sync
or /events
from the
Client-Server API MUST do so with a virtual user (provide a user_id
via the query string). It is expected that the application service use
the transactions pushed to it to handle events rather than syncing with
the user implied by sender_localpart
.
Application service room directories
Application services can maintain their own room directories for their
defined third party protocols. These room directories may be accessed by
clients through additional parameters on the /publicRooms
client-server endpoint.
PUT
/_matrix/client/r0/directory/list/appservice/{networkId}/{roomId}
Updates the visibility of a given room on the application service’s room
directory.
This API is similar to the room directory visibility API used by clients
to update the homeserver’s more general room directory.
This API requires the use of an application service access token (as_token
)
instead of a typical client’s access_token. This API cannot be invoked by
users who are not identified as application services.
Rate-limited: |
No |
Requires authentication: |
Yes |
Request
Request parameters
path parameters
Name |
Type |
Description |
networkId |
string |
Required: The protocol (network) ID to update the room list for. This would
have been provided by the application service as being listed as
a supported protocol. |
roomId |
string |
Required: The room ID to add to the directory. |
Request body
Name |
Type |
Description |
visibility |
enum |
Required: Whether the room should be visible (public) in the directory
or not (private). One of: [public private] . |
Request body example
{
"visibility": "public"
}
Responses
Status |
Description |
200 |
The room’s directory visibility has been updated. |
Referencing messages from a third party network
Application services should include an external_url
in the content
of events it emits to indicate where the message came from. This
typically applies to application services that bridge other networks
into Matrix, such as IRC, where an HTTP URL may be available to
reference.
Clients should provide users with a way to access the external_url
if
it is present. Clients should additionally ensure the URL has a scheme
of https
or http
before making use of it.
The presence of an external_url
on an event does not necessarily mean
the event was sent from an application service. Clients should be wary
of the URL contained within, as it may not be a legitimate reference to
the event’s source.